C++哈希表

哈希表实现详解

1. 哈希概念

哈希(hash)又称散列,是一种组织数据的方式。本质就是通过哈希函数把关键字 Key 跟存储位置建立一个映射关系,查找时通过这个哈希函数计算出 Key 存储的位置,进行快速查找。

1.1 直接定址法

当关键字的范围比较集中时,直接定址法是非常简单高效的方法。比如一组关键字都在 [0,99] 之间,那么我们开一个 100 个数的数组,每个关键字的值直接就是存储位置的下标。

示例:统计字符串中第一个唯一字符

cpp 复制代码
class Solution {
public:
    int firstUniqChar(string s) {
        // 每个字母的ascii码 - 'a'的ascii码作为下标映射
        int count[26] = {0};
        
        for(auto ch : s) {
            count[ch - 'a']++;
        }
        
        for(size_t i = 0; i < s.size(); ++i) {
            if(count[s[i] - 'a'] == 1)
                return i;
        }
        return -1;
    }
};

直接定址法的缺点:当关键字的范围比较分散时,浪费内存甚至内存不够用。

1.2 哈希冲突

两个不同的 key 可能会映射到同一个位置,这叫做哈希冲突(哈希碰撞)。理想情况是找出一个好的哈希函数避免冲突,但实际中冲突不可避免,我们需要设计解决冲突的方案。

1.3 负载因子

负载因子 = N / M

其中 N 为哈希表中已存储的数据个数,M 为哈希表的大小。

  • 负载因子越大:哈希冲突概率越高,空间利用率越高
  • 负载因子越小:哈希冲突概率越低,空间利用率越低

1.4 关键字转整数

将关键字映射到数组位置,一般是整数好做映射计算。如果不是整数,需要想办法转换成整数。

2. 哈希函数

2.1 除法散列法(除留余数法)

公式h(key) = key % M

使用建议:M 取不太接近 2 的整数次幂的一个质数(素数)

实践中的变通:Java 的 HashMap 使用 2 的整数次幂作为 M,通过位运算提高效率,同时让 key 的所有位都参与计算以均匀分布。

2.2 乘法散列法(了解)

公式h(key) = floor(M × ((A × key) % 1.0))

其中 A ∈ (0,1),Knuth 建议取黄金分割点 A = (√5 - 1)/2 ≈ 0.6180339887

2.3 全域散列法(了解)

给散列函数增加随机性,防止恶意构造的数据集攻击。

公式h_{ab}(key) = ((a × key + b) % P) % M

  • P:足够大的质数
  • a ∈ [1, P-1],b ∈ [0, P-1]

3. 处理哈希冲突

3.1 开放定址法

所有元素都放到哈希表里,冲突时按规则找到下一个空位置存储。负载因子必须小于 1。

线性探测

从冲突位置开始,依次线性向后探测:

h(key, i) = (hash0 + i) % M

缺点:容易产生群集/堆积现象。

示例:{19, 30, 5, 36, 13, 20, 21, 12} 映射到 M=11

Key 哈希值
19 8
30 8(冲突)
5 5
36 3
13 2
20 9
21 10
12 1
二次探测

左右按二次方跳跃式探测:

h(key, i) = (hash0 ± i²) % M

可以一定程度改善线性探测的群集问题。

3.2 开放定址法代码实现

cpp 复制代码
namespace open_address {
    enum State { EXIST, EMPTY, DELETE };
    
    template<class K, class V>
    struct HashData {
        pair<K, V> _kv;
        State _state = EMPTY;
    };
    
    template<class K, class V, class Hash = HashFunc<K>>
    class HashTable {
    public:
        bool Insert(const pair<K, V>& kv) {
            if (Find(kv.first)) return false;
            
            // 负载因子 > 0.7 扩容
            if (_n * 10 / _tables.size() >= 7) {
                HashTable<K, V, Hash> newHT;
                newHT._tables.resize(__stl_next_prime(_tables.size() + 1));
                for (size_t i = 0; i < _tables.size(); i++) {
                    if (_tables[i]._state == EXIST) {
                        newHT.Insert(_tables[i]._kv);
                    }
                }
                _tables.swap(newHT._tables);
            }
            
            Hash hash;
            size_t hash0 = hash(kv.first) % _tables.size();
            size_t hashi = hash0;
            size_t i = 1;
            
            while (_tables[hashi]._state == EXIST) {
                hashi = (hash0 + i) % _tables.size(); // 线性探测
                ++i;
            }
            
            _tables[hashi]._kv = kv;
            _tables[hashi]._state = EXIST;
            ++_n;
            return true;
        }
        
        HashData<K, V>* Find(const K& key) {
            Hash hash;
            size_t hash0 = hash(key) % _tables.size();
            size_t hashi = hash0;
            size_t i = 1;
            
            while (_tables[hashi]._state != EMPTY) {
                if (_tables[hashi]._state == EXIST && 
                    _tables[hashi]._kv.first == key) {
                    return &_tables[hashi];
                }
                hashi = (hash0 + i) % _tables.size();
                ++i;
            }
            return nullptr;
        }
        
        bool Erase(const K& key) {
            HashData<K, V>* ret = Find(key);
            if (ret == nullptr) return false;
            ret->_state = DELETE;
            --_n;
            return true;
        }
        
    private:
        vector<HashData<K, V>> _tables;
        size_t _n = 0;  // 表中存储数据个数
    };
}

3.3 字符串哈希处理

当 key 为 string 类型时,需要将其转换为整数:

cpp 复制代码
template<class K>
struct HashFunc {
    size_t operator()(const K& key) {
        return (size_t)key;
    }
};

// string 特化版本 - BKDR哈希
template<>
struct HashFunc<string> {
    size_t operator()(const string& key) {
        size_t hash = 0;
        for (auto e : key) {
            hash *= 131;  // 质数,也可用31
            hash += e;
        }
        return hash;
    }
};

3.4 链地址法(哈希桶)

所有数据不再直接存储在哈希表中,哈希表中存储指针,冲突的数据链接成链表挂在对应位置。

优点

  • 负载因子可以大于 1
  • 解决冲突更简单

扩容策略:负载因子达到 1 时扩容

3.5 链地址法代码实现

cpp 复制代码
namespace hash_bucket {
    template<class K, class V>
    struct HashNode {
        pair<K, V> _kv;
        HashNode<K, V>* _next;
        
        HashNode(const pair<K, V>& kv)
            : _kv(kv), _next(nullptr) {}
    };
    
    template<class K, class V, class Hash = HashFunc<K>>
    class HashTable {
        typedef HashNode<K, V> Node;
        
    public:
        HashTable() {
            _tables.resize(__stl_next_prime(0), 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;
            }
        }
        
        bool Insert(const pair<K, V>& kv) {
            Hash hs;
            
            // 负载因子 == 1 扩容
            if (_n == _tables.size()) {
                HashTable<K, V, Hash> newHT;
                newHT._tables.resize(__stl_next_prime(_tables.size() + 1), nullptr);
                
                for (size_t i = 0; i < _tables.size(); i++) {
                    Node* cur = _tables[i];
                    while (cur) {
                        newHT.Insert(cur->_kv);
                        cur = cur->_next;
                    }
                }
                _tables.swap(newHT._tables);
            }
            
            size_t hashi = hs(kv.first) % _tables.size();
            
            // 检查是否已存在
            Node* cur = _tables[hashi];
            while (cur) {
                if (cur->_kv.first == kv.first)
                    return false;
                cur = cur->_next;
            }
            
            // 头插
            Node* newnode = new Node(kv);
            newnode->_next = _tables[hashi];
            _tables[hashi] = newnode;
            ++_n;
            return true;
        }
        
        Node* Find(const K& key) {
            Hash hs;
            size_t hashi = hs(key) % _tables.size();
            Node* cur = _tables[hashi];
            while (cur) {
                if (cur->_kv.first == key)
                    return cur;
                cur = cur->_next;
            }
            return nullptr;
        }
        
        bool Erase(const K& key) {
            Hash hs;
            size_t hashi = hs(key) % _tables.size();
            Node* prev = nullptr;
            Node* cur = _tables[hashi];
            
            while (cur) {
                if (cur->_kv.first == 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.6 质数表(用于扩容)

cpp 复制代码
inline unsigned long __stl_next_prime(unsigned long n) {
    static const int __stl_num_primes = 28;
    static const unsigned long __stl_prime_list[] = {
        53, 97, 193, 389, 769,
        1543, 3079, 6151, 12289, 24593,
        49157, 98317, 196613, 393241, 786433,
        1572869, 3145739, 6291469, 12582917, 25165843,
        50331653, 100663319, 201326611, 402653189, 805306457,
        1610612741, 3221225473, 4294967291
    };
    
    const unsigned long* first = __stl_prime_list;
    const unsigned long* last = __stl_prime_list + __stl_num_primes;
    const unsigned long* pos = lower_bound(first, last, n);
    return pos == last ? *(last - 1) : *pos;
}

4. 极端场景优化

如果某个桶特别长,Java 8 的 HashMap 采用的优化策略:

  • 当桶的长度超过阈值(8)时,将链表转换成红黑树
  • 查找效率从 O(n) 提升到 O(log n)

5. 总结

方法 优点 缺点 适用场景
直接定址法 简单高效 浪费内存 关键字范围集中
开放定址法 实现简单 产生堆积 负载因子小
链地址法 空间利用率高 需要额外指针 通用场景

选择建议

  • 实践中通常选择链地址法(哈希桶)
  • 哈希函数使用除留余数法,M 取质数
  • 字符串 key 使用 BKDR 哈希 转换
  • 负载因子控制在 0.7~1 之间
相关推荐
想学会c++2 小时前
单例模式笔记总结
c++·笔记·单例模式
阿旭超级学得完2 小时前
C++11(初始化)
java·开发语言·数据结构·c++·算法
云淡风轻~窗明几净2 小时前
关于角谷猜想的五行小猜想
数据结构·算法
洛水水2 小时前
设计模式入门:从设计原则到核心模式
c++·设计模式
Languorous.2 小时前
C++数据结构进阶|并查集(Union-Find)详解:从原理到面试实战
数据结构·c++·面试
不知名的老吴2 小时前
C++中emplace函数的不适场景总结(一)
java·开发语言·c++
Languorous.2 小时前
C++数据结构进阶|堆(Heap)详解:从手写实现到面试高频实战
数据结构·c++·面试
khalil10203 小时前
代码随想录算法训练营Day-49 图论01 | 图论理论基础、深搜理论基础、98. 所有可达路径、广搜理论基础
c++·算法·leetcode·深度优先·图论
Cinema KI3 小时前
Linux C/C++ 编译构建:GCC/G++ + Makefile 零基础完整教程
linux·c语言·c++