【C++】 unordered_map 与unordered_set

目录

    • [一、 容器简介](#一、 容器简介)
    • [二、 核心接口与使用示例](#二、 核心接口与使用示例)
      • [1. 容量与基础查询](#1. 容量与基础查询)
      • [2. 元素查找](#2. 元素查找)
      • [3. operator\[\] 的特殊机制](#3. operator[] 的特殊机制)
    • [三、 面试题实战](#三、 面试题实战)
      • [实战 1:](#实战 1:)
      • [实战 2:](#实战 2:)
    • [四、 哈希表底层原理](#四、 哈希表底层原理)
    • [五、 底层完整实现](#五、 底层完整实现)
      • [1. 基础组件与哈希函数](#1. 基础组件与哈希函数)
      • [2. 哈希桶与迭代器实现](#2. 哈希桶与迭代器实现)
      • [3. 封装 unordered_set](#3. 封装 unordered_set)
      • [4. 封装 unordered_map](#4. 封装 unordered_map)
    • [六、 扩展应用](#六、 扩展应用)
    • [七、 总结](#七、 总结)

一、 容器简介

在 C++98 中,STL 提供了底层为红黑树结构的关联式容器,查询效率为 O ( log ⁡ 2 N ) O(\log_2 N) O(log2N)。为了在海量数据下实现 O ( 1 ) O(1) O(1) 的常数级查找效率,C++11 引入了 4 个 unordered 系列关联式容器。

1. unordered_map

  • 它是存储 <key, value> 键值对的关联式容器,允许通过 keys 快速索引到对应的 value。
  • 在内部,容器没有对键值对按照任何特定的顺序排序。
  • 容器将相同哈希值的键值对放在相同的桶中,以此实现极致的查找速度。
  • 由于底层单链表结构限制,它的迭代器至少是前向迭代器(不支持反向遍历)。

2. unordered_set

  • 它是仅存储唯一关键码(Key)的关联式容器,是处理海量数据去重与极速存在性校验的绝佳利器。
  • unordered_map 机制相同,其内部元素同样无序,依靠哈希函数将关键码映射并存储到对应的哈希桶中。
  • 为了防止底层哈希映射位置被破坏,容器中的元素不能被修改(其迭代器本质上是 const 迭代器)。
  • 同样仅支持单向迭代,其插入、查找、删除的平均时间复杂度均能达到常数级别 O ( 1 ) O(1) O(1)。

二、 核心接口与使用示例

1. 容量与基础查询

  • empty():检测容器是否为空。
  • size():获取容器的有效元素个数。
cpp 复制代码
unordered_map<string, int> dict;
dict.insert({"apple", 1});
bool isEmpty = dict.empty(); // 返回 false
size_t count = dict.size();  // 返回 1

2. 元素查找

  • find(const K& key):返回 key 在哈希桶中的位置迭代器,若未找到则返回 end()
  • count(const K& key):返回关键码为 key 的键值对个数。因容器中 key 不可重复,故返回值最大为 1,常用于快速判断元素是否存在。
cpp 复制代码
unordered_set<int> us = {1, 2, 3};

// 使用 find 查找
auto it = us.find(2);
if (it != us.end()) {
    cout << "找到了: " << *it << endl;
}

// 使用 count 判断存在性
if (us.count(4) == 0) {
    cout << "元素 4 不存在" << endl;
}

3. operator\[\] 的特殊机制

unordered_map 提供了 operator[],其实际调用了底层的插入操作。用参数 key 与默认值构造键值对进行插入:

  • 若 key 不在容器中,插入成功并返回默认值的引用。
  • 若 key 已存在,插入失败,但会返回原来 key 对应的 value 引用。
cpp 复制代码
unordered_map<string, string> dict;
dict["insert"] = "插入"; // key不存在,插入 {"insert", ""},随后将其 value 修改为 "插入"
dict["insert"] = "覆盖"; // key已存在,直接返回 value 引用并修改为 "覆盖"

三、 面试题实战

实战 1:

重复 N 次的元素

给定一个数组,找出一个出现了特定次数的元素。利用 unordered_map 统计频次。

cpp 复制代码
class Solution {
public:
    int repeatedNTimes(vector<int>& A) {
        size_t N = A.size() / 2;
        unordered_map<int, int> m;
        for (auto e : A) {
            m[e]++; // 元素不存在则初始化为0后加1,存在则直接加1
        }
        for (auto& e : m) {
            if (e.second == N) return e.first;
        }
        return -1;
    }
};

实战 2:

两个数组的交集

求两个数组的交集并去重。利用 unordered_set 实现去重与快速匹配。

cpp 复制代码
class Solution {
public:
    vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
        unordered_set<int> s1;
        for (auto e : nums1) s1.insert(e);
            
        unordered_set<int> s2;
        for (auto e : nums2) s2.insert(e);
            
        vector<int> vRet;
        for (auto e : s1) {
            if (s2.find(e) != s2.end()) {
                vRet.push_back(e);
            }
        }
        return vRet;
    }
};

四、 哈希表底层原理

具体讲解哈希表

哈希表通过哈希函数将元素的关键码映射为存储位置。常见的哈希函数有除留余数法:Hash(key) = key % capacity

当不同关键字计算出相同的哈希地址时,即发生哈希冲突。解决冲突的方法有两种:

  1. 闭散列(开放定址法):寻找下一个空位置存放冲突元素(如线性探测)。其载荷因子必须严格限制在 0.7 - 0.8 以下,容易产生数据堆积,空间利用率低。
  2. 开散列(链地址法 / 哈希桶):将哈希地址相同的元素归于同一个桶,通过单链表链接。STL 主要采用开散列,其载荷因子可以达到 1.0,空间利用率和查找效率更优。

五、 底层完整实现

以下为基于开散列(哈希桶)完整封装 unordered_mapunordered_set 的核心代码。

1. 基础组件与哈希函数

cpp 复制代码
#pragma once
#include <iostream>
#include <vector>
#include <string>
using namespace std;

template<class K>
struct HashFunc {
    size_t operator()(const K& key) { return (size_t)key; }
};

// 针对 string 的哈希特化 (BKDR Hash)
template<>
struct HashFunc<string> {
    size_t operator()(const string& key) {
        size_t hash = 0;
        for (auto e : key) {
            hash *= 31;
            hash += e;
        }
        return hash;
    }
};

2. 哈希桶与迭代器实现

cpp 复制代码
namespace hash_bucket
{
    template<class T>
    struct HashNode {
        T _data;
        HashNode<T>* _next;
        HashNode(const T& data) :_data(data), _next(nullptr) {}
    };

    template<class K, class T, class KeyOfT, class Hash>
    class HashTable;

    // 迭代器实现
    template<class K, class T, class Ptr, class Ref, class KeyOfT, class Hash>
    struct HTIterator {
        typedef HashNode<T> Node;
        typedef HTIterator<K, T, Ptr, Ref, KeyOfT, Hash> Self;

        Node* _node;
        const HashTable<K, T, KeyOfT, Hash>* _pht;

        HTIterator(Node* node, const HashTable<K, T, KeyOfT, Hash>* pht)
            :_node(node), _pht(pht) {}

        Ref operator*() { return _node->_data; }
        Ptr operator->() { return &_node->_data; }
        bool operator!=(const Self& s) { return _node != s._node; }

        Self& operator++() {
            if (_node->_next) {
                _node = _node->_next;
            } else {
                KeyOfT kot;
                Hash hs;
                size_t hashi = hs(kot(_node->_data)) % _pht->_tables.size();
                ++hashi;
                while (hashi < _pht->_tables.size()) {
                    if (_pht->_tables[hashi]) break;
                    ++hashi;
                }
                if (hashi == _pht->_tables.size()) _node = nullptr;
                else _node = _pht->_tables[hashi];
            }
            return *this;
        }
    };

    // 哈希表主体
    template<class K, class T, class KeyOfT, class Hash>
    class HashTable {
        template<class K, class T, class Ptr, class Ref, class KeyOfT, class Hash>
        friend struct HTIterator;

        typedef HashNode<T> Node;
    public:
        typedef HTIterator<K, T, T*, T&, KeyOfT, Hash> Iterator;
        typedef HTIterator<K, T, const T*, const T&, KeyOfT, Hash> ConstIterator;

        HashTable() { _tables.resize(10, nullptr); }
        
        ~HashTable() {
            for (size_t i = 0; i < _tables.size(); i++) {
                Node* cur = _tables[i];
                while (cur) {
                    Node* next = cur->_next;
                    delete cur;
                    cur = next;
                }
                _tables[i] = nullptr;
            }
        }

        Iterator Begin() {
            if (_n == 0) return End();
            for (size_t i = 0; i < _tables.size(); i++) {
                if (_tables[i]) return Iterator(_tables[i], this);
            }
            return End();
        }
        Iterator End() { return Iterator(nullptr, this); }

        pair<Iterator, bool> Insert(const T& data) {
            KeyOfT kot;
            Iterator it = Find(kot(data));
            if (it != End()) return make_pair(it, false);

            Hash hs;
            if (_n == _tables.size()) {
                vector<Node*> newtables(_tables.size() * 2, nullptr);
                for (size_t i = 0; i < _tables.size(); i++) {
                    Node* cur = _tables[i];
                    while (cur) {
                        Node* next = cur->_next;
                        size_t hashi = hs(kot(cur->_data)) % newtables.size();
                        cur->_next = newtables[hashi];
                        newtables[hashi] = cur;
                        cur = next;
                    }
                    _tables[i] = nullptr;
                }
                _tables.swap(newtables);
            }

            size_t hashi = hs(kot(data)) % _tables.size();
            Node* newnode = new Node(data);
            newnode->_next = _tables[hashi];
            _tables[hashi] = newnode;
            ++_n;
            return make_pair(Iterator(newnode, this), true);
        }

        Iterator Find(const K& key) {
            KeyOfT kot;
            Hash hs;
            size_t hashi = hs(key) % _tables.size();
            Node* cur = _tables[hashi];
            while (cur) {
                if (kot(cur->_data) == key) return Iterator(cur, this);
                cur = cur->_next;
            }
            return End();
        }

        bool Erase(const K& key) {
            KeyOfT kot;
            Hash hs;
            size_t hashi = hs(key) % _tables.size();
            Node* prev = nullptr;
            Node* cur = _tables[hashi];
            while (cur) {
                if (kot(cur->_data) == key) {
                    if (prev == nullptr) _tables[hashi] = cur->_next;
                    else prev->_next = cur->_next;
                    delete cur;
                    --_n;
                    return true;
                }
                prev = cur;
                cur = cur->_next;
            }
            return false;
        }

    private:
        vector<Node*> _tables;
        size_t _n = 0;
    };
}

3. 封装 unordered_set

cpp 复制代码
namespace bit
{
    template<class K, class Hash = HashFunc<K>>
    class unordered_set {
        struct SetKeyOfT {
            const K& operator()(const K& key) { return key; }
        };
    public:
        typedef typename hash_bucket::HashTable<K, const K, SetKeyOfT, Hash>::Iterator iterator;
        typedef typename hash_bucket::HashTable<K, const K, SetKeyOfT, Hash>::ConstIterator const_iterator;

        iterator begin() { return _ht.Begin(); }
        iterator end() { return _ht.End(); }
        const_iterator begin() const { return _ht.Begin(); }
        const_iterator end() const { return _ht.End(); }

        pair<iterator, bool> insert(const K& key) { return _ht.Insert(key); }
        iterator Find(const K& key) { return _ht.Find(key); }
        bool Erase(const K& key) { return _ht.Erase(key); }

    private:
        hash_bucket::HashTable<K, const K, SetKeyOfT, Hash> _ht;
    };
}

4. 封装 unordered_map

cpp 复制代码
namespace bit
{
    template<class K, class V, class Hash = HashFunc<K>>
    class unordered_map {
        struct MapKeyOfT {
            const K& operator()(const pair<K, V>& kv) { return kv.first; }
        };
    public:
        typedef typename hash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT, Hash>::Iterator iterator;
        typedef typename hash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT, Hash>::ConstIterator const_iterator;

        iterator begin() { return _ht.Begin(); }
        iterator end() { return _ht.End(); }
        const_iterator begin() const { return _ht.Begin(); }
        const_iterator end() const { return _ht.End(); }

        pair<iterator, bool> insert(const pair<K, V>& kv) { return _ht.Insert(kv); }

        V& operator[](const K& key) {
            pair<iterator, bool> ret = _ht.Insert(make_pair(key, V()));
            return ret.first->second;
        }

        iterator Find(const K& key) { return _ht.Find(key); }
        bool Erase(const K& key) { return _ht.Erase(key); }

    private:
        hash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT, Hash> _ht;
    };
}

六、 扩展应用

在海量数据处理场景下,常规哈希表会面临内存不足的问题。此时可以使用哈希思想的延伸结构:

  1. 位图 (BitMap):使用二进制比特位代表数据是否存在,适用于海量整型数据的快速查找与去重。
  2. 布隆过滤器 (Bloom Filter):将哈希函数与位图结合,通过多个哈希函数将一个数据映射到位图中。适用于容忍一定误判率的海量字符串查重过滤场景,空间优势极大。

七、 总结

  • 若业务场景需要数据保持特定顺序输出,应选择基于红黑树的 map/set
  • 若核心需求为极限速度的增删查改,优先选用 unordered_map/unordered_set
  • STL 的底层通过仿函数提取器(KeyOfT)实现了 HashTable 泛型代码的复用,理解这一点有助于掌握 C++ 的泛型编程逻辑。
  • 在使用 operator[] 时需明确其底层包含插入逻辑,仅查询判断时应优先使用 find()count()
相关推荐
雪的季节2 小时前
人工智能 AI 5问
开发语言
李迟2 小时前
一种轻量级C++ CSV文件读写库的实现方案
开发语言·c++
小园子的小菜3 小时前
Java 并发编程:线程安全队列全解 —— 阻塞与非阻塞实现原理及源码深度剖析
java·开发语言
学无止境_永不停歇3 小时前
9. 缓冲区
linux·服务器·c++
Y3815326623 小时前
2026 年 7 月,SERP API 调用的稳定性实战:超时、重试、降级
开发语言·数据库·人工智能·php
Wang's Blog3 小时前
Go-Zero项目开发38:深入限流器实现与应用
开发语言·golang·go-zero
古道青阳3 小时前
Duilib 技术全景剖析
c++·windows
风吹心凉3 小时前
python3基础2026.7.28
开发语言·python
Rabitebla3 小时前
C++ 内存管理全面复习:从内存分布到 operator new/delete
java·c语言·开发语言·c++·算法·leetcode