一、unordered_map简介
C++ STL中的unordered_map是基于哈希表(hash table)实现的关联容器。哈希表是一种将key-value映射存储的数据结构,它允许常数时间内进行插入、删除和查找操作。
unordered_map的每个元素包含一个key和value,元素之间没有顺序关系。因此,在进行查找操作时,需要通过哈希函数找到对应的桶,对桶中的元素进行搜索。因此,unordered_map的查找、插入、删除操作的时间复杂度都是O(1),相对于普通的红黑树实现的map,高效得多。
以下是unordered_map的常用操作:
// 声明一个unordered_map
std::unordered_map<int, std::string> umap;
// 插入key-value
umap.insert(std::make_pair(1, "value1"));
umap.insert({2, "value2"});
// 查找元素
if (umap.find(1) != umap.end()) {
std::cout << "Found element with key 1" << std::endl;
}
// 修改元素
umap[1] = "new value1";
// 删除元素
umap.erase(2);
// 遍历unordered_map
for (const auto& kv : umap) {
std::cout << kv.first << ": " << kv.second << std::endl;
}
二、unordered_map的内部实现原理
unordered_map的内部实现主要由哈希函数、哈希表、桶和节点组成。哈希函数用于将key映射到对应的桶中,每个桶保存对应的节点。节点包含key、value以及指向下一个节点的指针。
哈希函数的设计非常重要,它的优秀程度将直接影响unordered_map插入、查找、删除等操作的效率。常见的哈希函数有除留余数法、乘法散列法和随机旋转哈希等。具体实现中,STL使用除留余数法(%运算符)作为哈希函数的默认实现。下面是一个例子:
std::unordered_map<std::string, int> umap(8);
std::hash<std::string> hasher; // 哈希函数
std::size_t hash_val = hasher("hello world");
std::size_t bucket_num = hash_val % umap.bucket_count(); // 计算桶的索引
umap.insert(std::make_pair("hello world", 42));
插入元素时,将先使用哈希函数计算key的哈希值,然后将哈希值对桶的数量取模得到桶的索引。桶的数量最好选择一个质数,可以减少哈希冲突的概率。
如果桶中已经存在一个节点,这种情况称为哈希冲突。STL中使用链式法(chaining)解决哈希冲突,即将节点插入到桶对应链表的末尾。当节点数量太大时,桶中的链表可能会退化成一条很长的链表,时间复杂度可能退化到O(n)。
三、unordered_map的应用案例
unordered_map在实际应用中非常常用,尤其在需要高效的查找和插入操作时大受欢迎。下面是unordered_map的几个应用案例:
1、单词统计、列表去重
std::unordered_map<std::string, int> word_count;
std::vector<std::string> words = { "hello", "world", "hello", "foo", "bar", "foo" };
for (const auto& w : words) {
++word_count[w];
}
for (const auto& kv : word_count) {
std::cout << kv.first << ": " << kv.second << std::endl;
}
std::unordered_set<std::string> word_set(words.begin(), words.end());
for (const auto& w : word_set) {
std::cout << w << std::endl;
}
以上代码可以统计输入字符串中每个单词出现的次数,并去重输出。
2、图的邻接表表示
unordered_map可以用于实现图的邻接表(adjacency list)表示法,即用一个unordered_map保存每个节点以及与之相邻的节点集合。下面是一个例子:
std::unordered_map<int, std::vector<int>> adj_list;
adj_list[0] = { 1, 2 };
adj_list[1] = { 0, 2, 3 };
adj_list[2] = { 0, 1, 3 };
adj_list[3] = { 1, 2 };
以上代码可以实现一个无向图,每个节点都与若干个相邻节点相连。
3、LRU缓存
unordered_map可以用于实现LRU(Least Recently Used)缓存,即缓存数据时,如果缓存空间满了,则替换最近最少被使用的数据。可以使用两个数据结构:unordered_map和双向链表。unordered_map用于实现key到节点的映射,双向链表用于维护节点的顺序。
使用unordered_map保存key与链表中的节点的映射,使用双向链表维护节点顺序,链表中的元素按照访问时间从旧到新排序。以下是LRU缓存的具体实现:
class LRUCache {
public:
LRUCache(int capacity) : capacity_(capacity) {}
int get(int key) {
auto it = cache_.find(key);
if (it == cache_.end()) {
return -1;
}
touch(it->second);
return it->second.value;
}
void put(int key, int value) {
auto it = cache_.find(key);
if (it != cache_.end()) {
touch(it->second);
it->second.value = value;
} else {
if (cache_.size() == capacity_) {
evict();
}
Node* new_node = new Node(key, value);
cache_[key] = *new_node;
add(new_node);
}
}
private:
struct Node {
int key;
int value;
Node* prev;
Node* next;
Node(int key, int value) : key(key), value(value), prev(nullptr), next(nullptr) {}
};
void touch(Node& node) {
// 将node从链表中删除
node.prev->next = node.next;
node.next->prev = node.prev;
// 将node加入链表尾部
add(&node);
}
void evict() {
// 删除链表头部元素
Node* removed = head_.next;
head_.next = head_.next->next;
head_.next->prev = &head_;
cache_.erase(removed->key);
delete removed;
}
void add(Node* node) {
// 将node加入链表尾部
node->prev = tail_.prev;
node->next = &tail_;
tail_.prev->next = node;
tail_.prev = node;
}
int capacity_;
std::unordered_map<int, Node> cache_;
Node head_; // 哨兵节点,链表头部的前一个节点
Node tail_; // 哨兵节点,链表尾部的后一个节点
};
以上代码实现了一个带有get和put操作的LRU缓存,可以用于缓存key-value对,保持访问时间越新的元素越靠近链表尾部,访问时间越旧的元素越靠近链表头部。如果缓存空间满了,则将访问时间最早的元素删除。
原创文章,作者:GAWOH,如若转载,请注明出处:https://www.506064.com/n/318151.html
微信扫一扫
支付宝扫一扫