1 概述

std::unordered_map 是 C++ 标准模板库(STL)中的一个关联容器,它存储的元素是键值对,且每个键在容器中唯一。这个容器的特点是它基于哈希表实现,因此具有非常快的查找、插入和删除操作的平均时间复杂度,即 O(1)。

std::unordered_map 与 std::map 的主要区别体现在以下几个方面:

(1)底层数据结构: std::unordered_map 底层是哈希表,通过哈希函数计算元素的存储位置。而 std::map 的底层则是红黑树,一种自平衡的二叉搜索树。

(2)性能: 由于哈希表的特性,std::unordered_map 在查找、插入和删除操作上的平均时间复杂度为O(1),这在处理大量数据时效率非常高。而 std::map 的查找、插入和删除操作的时间复杂度为 O(log n),其中 n 是元素的数量。因此,在大多数情况下,std::unordered_map 会比 std::map 更快。

(3)有序性: std::map 的元素会按照键的顺序进行排序,因此可以方便地进行有序性操作,如范围遍历和查找。而 std::unordered_map 不会保持元素的插入顺序,遍历时元素的顺序是不确定的。

(4)空间占用: 虽然 std::unordered_map 在查找、插入和删除操作上性能优越,但由于哈希表的特性,它在内存消耗上可能会稍大一些。而 std::map 由于需要维护红黑树的平衡,因此在空间占用上可能稍小一些。

std::unordered_map 的实现原理主要基于哈希表。哈希表通过哈希函数将键映射到数组的某个索引位置,从而快速找到对应的值。哈希表的每个槽位(bucket)可以存储一个链表或其他动态数据结构,用于处理哈希冲突(即不同键映射到同一索引位置的情况)。当进行查找、插入或删除操作时,std::unordered_map 首先使用哈希函数计算键的哈希值,然后定位到对应的槽位,最后在槽位中的链表或其他数据结构中查找、插入或删除元素。

2 声明与初始化

2.1 声明

首先,需要包含 <unordered_map> 头文件,然后声明一个 std::unordered_map 变量,并指定键和值的类型。

#include <unordered_map>  
  
std::unordered_map<KeyType, ValueType> mapName;

其中 KeyType 是键的类型,ValueType 是值的类型,mapName 是你为该 unordered_map 变量选择的名称。

2.2 初始化

std::unordered_map 可以通过多种方式初始化。以下是一些常见的初始化方法:

(1)默认初始化

可以声明一个 std::unordered_map 变量而不进行任何初始化,它会是空的。

std::unordered_map<std::string, int> myMap; // 默认初始化,空的unordered_map

(2)使用初始化列表

在声明时,可以使用初始化列表来插入一些初始元素。

std::unordered_map<std::string, int> myMap = {  
    {"apple", 5},  
    {"banana", 3},  
    {"orange", 2}  
};

(4)使用构造函数和迭代器范围

如果有另一个容器(如 std::pair 的数组或 std::vector),并且想要用它的内容来初始化 std::unordered_map,则可以使用构造函数和迭代器范围。

std::vector<std::pair<std::string, int>> vec = {  
    {"apple", 5},  
    {"banana", 3}  
};  
std::unordered_map<std::string, int> myMap(vec.begin(), vec.end());

3 插入元素

3.1 插入方法

(1)使用 insert 成员函数

insert 是 std::unordered_map 的一个成员函数,用于向容器中插入一个或多个元素。它有多种重载形式,以适应不同的插入需求。

单个元素插入

可以使用 insert 函数插入一个单独的键值对。这通常通过创建一个 std::pair 对象或使用 std::make_pair 函数来完成,然后将其作为 insert 函数的参数。

std::unordered_map<int, std::string> myMap;  
myMap.insert(std::make_pair(1, "one"));  
// 或者使用 std::pair 的构造函数  
myMap.insert(std::pair<int, std::string>(2, "two"));

从 C++11 开始,还可以使用列表初始化语法来更简洁地插入元素:

myMap.insert({3, "three"});

插入一个元素范围

可以使用 insert 函数插入一个元素范围,这通常是从另一个 unordered_map 或其他兼容的容器中复制元素。

std::unordered_map<int, std::string> otherMap = {{4, "four"}, {5, "five"}};  
myMap.insert(otherMap.begin(), otherMap.end());

这将把 otherMap 中的所有元素插入到 myMap 中。

(2)使用下标运算符 []

尽管下标运算符 [] 主要用于访问元素,但如果尝试访问一个不存在的键,它实际上会创建一个新的元素,并将其初始化为默认值。因此,也可以使用它来“插入”元素,尽管这不是其设计的主要目的,并且在键已存在时会替换现有值。

std::unordered_map<int, std::string> myMap;  
myMap[1] = "one"; // 如果键 1 不存在,则插入它

使用 [] 运算符插入元素时要小心,因为它会替换已存在的键的值,而不是仅仅插入新元素。

(3)使用 emplace 成员函数

emplace 是 C++11 引入的一个更高效的插入方法,它直接在容器中构造元素,避免了不必要的复制或移动操作。它接受构造元素所需的参数,并在容器内部直接构造元素。

std::unordered_map<int, std::string> myMap;  
myMap.emplace(1, "one"); // 直接在容器中构造键值对 (1, "one")

(4)插入的返回值

对于 insert 和 emplace 函数,如果插入成功(即键在插入前不存在于容器中),它们会返回一个指向新插入元素的迭代器。如果键已经存在,则 insert 不会插入新元素,并返回一个指向现有元素的迭代器。

3.2 时间复杂度

在平均情况下,std::unordered_map 的插入操作具有常数时间复杂度 O(1),因为哈希表允许直接定位到存储元素的位置。然而,在最坏情况下(例如哈希冲突很多时),插入操作的时间复杂度可能会退化到 O(n),其中 n 是容器中元素的数量。因此,虽然哈希表通常提供非常快的插入性能,但在设计哈希函数时需要小心以避免最坏情况的发生。

3.3 综合示例

下面是一个完整的示例,展示了如何使用 insert 和 emplace 函数向 std::unordered_map 中插入元素:

#include <iostream>  
#include <unordered_map>  
#include <string>  
  
int main() {  
    std::unordered_map<int, std::string> myMap;  
  
    // 使用 insert 插入元素  
    myMap.insert(std::make_pair(1, "one"));  
    myMap.insert({2, "two"});  
  
    // 使用 emplace 插入元素  
    myMap.emplace(3, "three");  
  
    // 遍历并打印元素  
    for (const auto& pair : myMap) {  
        std::cout << "Key: " << pair.first << ", Value: " << pair.second << std::endl;  
    }  
  
    return 0;  
}

上面代码的输出为:

Key: 1, Value: one
Key: 2, Value: two
Key: 3, Value: three

这个示例创建了一个 std::unordered_map,使用 insert 和 emplace 函数向其中插入了几个元素,然后遍历并打印了容器中的所有元素。

3.4 注意事项

  • 当使用 insert 或 emplace 函数时,如果插入的键已经存在于 std::unordered_map 中,则不会插入新的键值对,而是会保留原有的键值对不变。
  • 插入操作的性能受到哈希函数质量的影响。一个好的哈希函数应该能够均匀分布键到哈希表的各个桶中,从而避免哈希冲突。如果哈希函数设计得不好,可能导致性能下降,特别是在插入大量元素时。
  • std::unordered_map 不保证元素的顺序。每次插入元素时,它们可能会被放置到哈希表的任何位置,这取决于哈希函数和当前的哈希表状态。因此,如果你需要保持元素的插入顺序,应该考虑使用 std::map 或其他有序的容器。

4 访问元素

4.1 使用下标运算符 []

下标运算符 [] 是访问 std::unordered_map 中元素最常用的方法之一。通过提供键作为下标,可以获取或设置与该键关联的值。

std::unordered_map<int, std::string> myMap;  
myMap[1] = "one"; // 设置键为 1 的值为 "one"  
std::string value = myMap[1]; // 获取键为 1 的值,存储在 value 中

使用 [] 运算符的一个重要特点是,如果键不存在于 unordered_map 中,它会插入该键,并将其值初始化为默认值(对于基本数据类型,通常是 0 或空值)。因此,如果你只是想要检查一个键是否存在并获取其值,而不希望插入新键,那么使用 find 或 count 方法可能更为合适。

4.2 使用 at 成员函数

at 成员函数是另一种访问 std::unordered_map 中元素的方法。与 [] 运算符不同,at 不会插入新元素;如果键不存在,它会抛出一个 std::out_of_range 异常。

std::unordered_map<int, std::string> myMap;  
myMap[1] = "one"; // 设置键为 1 的值为 "one"  
std::string value = myMap.at(1); // 获取键为 1 的值,存储在 value 中  
// myMap.at(2); // 这会抛出 std::out_of_range 异常,因为键 2 不存在

由于 at 在键不存在时会抛出异常,因此它更适合于那些确定键一定存在的情况,或者想要通过异常处理机制来处理键不存在的情况。

4.3 使用 find 成员函数

find 成员函数用于在 std::unordered_map 中查找一个键,并返回一个指向该键对应元素的迭代器。如果键不存在,则返回 end() 迭代器。

std::unordered_map<int, std::string> myMap;  
myMap[1] = "one";  
auto it = myMap.find(1); // 查找键为 1 的元素  
if (it != myMap.end()) {  
    std::string value = it->second; // 获取键为 1 的值,存储在 value 中  
} else {  
    std::cout << "Key not found." << std::endl;  
}

使用 find 方法的一个优势是,它不会在键不存在时插入新元素或抛出异常。因此,它更适用于那些需要检查键是否存在,并且不希望改变 unordered_map 的情况。

4.4 访问键值对

一旦通过某种方式(如 []、at 或 find)获取了指向 unordered_map 中元素的迭代器或引用,那么就可以访问该键值对的键和值。对于迭代器 it,可以使用 it->first 来访问键,使用 it->second 来访问值。对于通过引用直接获取的元素,可以使用 .first 和 .second 成员来访问键和值。

std::unordered_map<int, std::string> myMap;  
myMap[1] = "one";  
auto it = myMap.find(1); // 查找键为 1 的元素  

int key = it->first; // 访问键  
std::string value = it->second; // 访问值  
  
// 或者通过引用直接访问  
const std::pair<int, std::string>& kvPair = *it;  
int key = kvPair.first; // 访问键  
std::string value = kvPair.second; // 访问值

5 删除元素

5.1 使用 erase 成员函数删除单个元素

erase 成员函数是 std::unordered_map 中用于删除元素的主要方法。它接受一个迭代器参数,指向要删除的元素,并返回指向下一个元素的迭代器。通过 find 成员函数,你可以获得要删除元素的迭代器,并将其传递给 erase。

std::unordered_map<int, std::string> myMap;  
myMap[1] = "one";  
myMap[2] = "two";  
myMap[3] = "three";  
  
// 使用 find 查找键为 2 的元素,并使用 erase 删除它  
auto it = myMap.find(2);  
if (it != myMap.end()) {  
    it = myMap.erase(it); // 删除元素,并返回指向下一个元素的迭代器  
}

注意:在调用 erase 之后,原来的迭代器 it 会失效,因此上面的代码将 erase 的返回值重新赋给 it,以便它可以继续用于遍历(如果有需要的话)。

5.2 使用 erase 成员函数删除指定范围内的元素

erase 成员函数还有一个重载版本,它接受两个迭代器参数,表示要删除的范围(包括起始迭代器指向的元素,但不包括结束迭代器指向的元素)。这可以用于删除多个连续的元素。

std::unordered_map<int, std::string> myMap;  
// 假设 myMap 中已经填充了一些元素  
  
// 删除键在 [2, 4) 范围内的所有元素(包括键为 2 的元素,但不包括键为 4 的元素)  
auto range_start = myMap.lower_bound(2); // 第一个不小于 2 的元素的迭代器  
auto range_end = myMap.upper_bound(4); // 第一个大于 4 的元素的迭代器  
myMap.erase(range_start, range_end);

5.3 使用 clear 成员函数删除所有元素

如果想要删除 std::unordered_map 中的所有元素,可以使用 clear 成员函数。这将移除容器中的所有键值对,并将容器的大小变为 0。

std::unordered_map<int, std::string> myMap;  
// 假设 myMap 中已经填充了一些元素  
  
myMap.clear(); // 删除所有元素,myMap 现在为空

5.4 遍历删除

(1)使用迭代器遍历并手动管理迭代器(推荐)

最安全的方法是使用迭代器遍历 unordered_map,并在删除元素后更新迭代器。一种常见的方法是使用 erase 的返回值,它返回指向下一个有效元素的迭代器。

#include <iostream>  
#include <unordered_map>  
#include <string>  

int main() {

	std::unordered_map<std::string, int> myMap = {
		{"apple", 5},
		{"banana", 6},
		{"orange", 2}
	};

	for (auto it = myMap.begin(); it != myMap.end(); ) {
		// 检查是否需要删除当前元素  
		if (it->second %2 == 0) {
			// 使用 erase 删除元素,并获取下一个有效元素的迭代器  
			it = myMap.erase(it);
		}
		else {
			// 否则,继续到下一个元素  
			++it;
		}
	}

	return 0;
}

这个例子使用了 it = myMap.erase(it); 来更新迭代器。如果当前元素满足删除条件,erase 会删除它并返回指向下一个元素的迭代器;否则,将迭代器递增到下一个元素。

(2)复制到临时容器并交换(如果删除大量元素,可能更高效)

如果预计会删除大量元素,一种更高效的方法可能是将不需要删除的元素复制到另一个 unordered_map 中,然后交换两个容器的内容。这样可以避免在原始容器中多次重新分配和哈希。

#include <iostream>  
#include <unordered_map>  
#include <string>  

int main() {

	std::unordered_map<std::string, int> myMap = {
		{"apple", 5},
		{"banana", 6},
		{"orange", 2}
	};

	std::unordered_map<std::string, int> tempMap;

	// 复制不需要删除的元素到临时容器  
	for (const auto& kvPair : myMap) {
		if (kvPair.second % 2 != 0) {
			tempMap.insert(kvPair);
		}
	}

	// 交换两个容器的内容  
	myMap.swap(tempMap);
	
	return 0;
}

这种方法的好处是它在删除大量元素时通常更高效,因为它避免了在原始容器中频繁地重新分配和哈希。然而,它也需要额外的内存来存储临时容器。

(3)注意事项

  • 当在循环中删除 std::unordered_map 的元素时,需要特别注意迭代器的有效性。一旦使用 erase 删除了一个元素,指向该元素的迭代器就会失效,因此不应再使用该迭代器。如果需要继续遍历容器,应该使用 erase 的返回值(指向下一个元素的迭代器)来更新你的迭代器。
  • erase 成员函数不会抛出异常(除非在自定义的哈希函数或相等性比较函数中发生了异常)。
  • erase 的时间复杂度通常与哈希表的性能相关,平均情况下是常数时间,但在最坏情况下可能是线性时间,这取决于哈希函数的质量和哈希表的当前状态。

6 使用自定义键类型

在 C++ 的 std::unordered_map 中,可以使用自定义键类型。当使用自定义类型作为键时,需要确保该类型可以正确地计算哈希值,并且提供了相等性比较的方式。这通常涉及到两个重要的方面:

  • 自定义哈希函数:需要提供一个哈希函数,它将自定义类型映射到一个 size_t 类型的哈希值。这可以通过定义一个函数对象(也称为仿函数或 Functor)来实现,该函数对象重载了 operator()。
  • 相等性比较:std::unordered_map 需要知道如何比较两个键是否相等。这通常是通过重载 operator== 来实现的,但也可以提供一个自定义的相等性比较函数对象。

下面是一个使用自定义键类型的 std::unordered_map 的示例:

#include <iostream>  
#include <unordered_map>  
#include <string>  
#include <functional> // 用于 std::hash 和 std::equal_to  
  
// 自定义键类型  
struct CustomKey {  
    std::string id;  
    int value;  
  
    // 重载 == 运算符以比较两个 CustomKey 对象是否相等  
    bool operator==(const CustomKey& other) const {  
        return id == other.id && value == other.value;  
    }  
};  
  
// 自定义哈希函数对象  
struct CustomKeyHash {  
    size_t operator()(const CustomKey& key) const {  
        // 使用 std::hash 特化版本来分别计算 id 和 value 的哈希值  
        // 然后将它们组合成一个最终的哈希值  
        size_t h1 = std::hash<std::string>()(key.id);  
        size_t h2 = std::hash<int>()(key.value);  
        return h1 ^ (h2 << 1); // 使用异或和左移来组合哈希值  
    }  
};  
  
int main() 
{  
    // 使用自定义键类型和哈希函数对象的 unordered_map  
    std::unordered_map<CustomKey, std::string, CustomKeyHash> myMap;  
  
    // 插入元素  
    myMap[{"key1", 10}] = "Value for key1";  
    myMap[{"key2", 20}] = "Value for key2";  
  
    // 查找元素  
    CustomKey searchKey = {"key1", 10};  
    auto it = myMap.find(searchKey);  
    if (it != myMap.end()) {  
        std::cout << "Found: " << it->second << std::endl;  
    } else {  
        std::cout << "Not found" << std::endl;  
    }  
  
    return 0;  
}

上面代码的输出为:

Found: Value for key1

这个例子定义了一个 CustomKey 结构体作为键类型,并重载了 operator== 来比较两个 CustomKey 对象是否相等。然后,定义了一个名为 CustomKeyHash 的自定义哈希函数对象,它接受一个 CustomKey 对象并返回一个 size_t 类型的哈希值。在哈希函数中,使用 std::hash 特化版本来分别计算 id 和 value 成员的哈希值,并将它们组合起来。

当创建 std::unordered_map 实例时,将 CustomKey 作为键类型,并传入 CustomKeyHash 作为哈希函数对象。这样,std::unordered_map 就知道如何计算 CustomKey 类型的哈希值,并使用它来存储和查找元素了。

注意:自定义哈希函数应该尽量使得不同的键产生不同的哈希值,以减少哈希冲突并提高性能。在实际应用中,可能需要根据键类型的特性来设计更复杂的哈希函数。此外,如果没有特殊的相等性比较需求,并且已经正确地重载了 operator==,那么通常不需要提供自定义的相等性比较函数对象,因为 std::unordered_map 会自动使用重载的 operator==。

03-18 16:23