C/C++ 数据结构(十四)哈希表

本篇 核心知识点:哈希表核心理论(哈希函数、哈希冲突);冲突四大解决办法;C++11 unordered_map 底层;手写拉链哈希表;容器选型对比

一、哈希表(散列表)核心理论

1 哈希表概念

概念:利用哈希函数将键 Key 映射到一段连续数组下标,直接定位存储位置,实现近似 O (1) 增删查;类比字典拼音 / 部首索引,无需全局遍历。

特性:底层基础载体是连续数组(顺序存储);C++11 unordered_map/unordered_set 底层哈希表。

2 哈希函数

概念:输入键 Key,通过固定数学运算输出数组下标映射关系。

核心设计要求:运算速度快、冲突概率尽可能低。

主流哈希函数
(1)直接定址法

公式:hash(Key) = a * Key + b

特性:

  1. 一对一映射,完全无哈希冲突

  2. 缺点:需要极大连续数组空间,空间利用率极低;

  3. 适用:键值连续规整的场景。

(2)除留余数法(最常用)

公式:hash(Key) = Key % P

特性:

  1. P 建议选取小于数组长度的质数,降低冲突概率;

  2. 实现简单运算快,手写哈希表首选;

  3. 缺点:数据分布不均时冲突大量产生。

拓展:

1.数字分析法

​ 选取关键字中分布均匀、随机性强的几位数字作为哈希地址,避开重复集中的数字段,适合关键字位数固定、数字分布可预判的场景。

2.平方取中法

​ 将关键字做平方运算,截取平方结果中间若干位作为哈希值;平方后中间数字随机性更高,适合关键字分布零散、无明显规律的情况。

3.折叠法

​ 把关键字分割成长度相等的几段,将各段数值相加,取和的低位作为哈希地址,用于关键字位数远大于哈希表地址位数的场景。

4.随机数法

​ 借助随机函数,以关键字为参数生成随机数映射为哈希地址,随机性最好,适合关键字长度差异大、分布无规律的数据。

3 哈希冲突

概念

多个不同 Key,经过哈希函数计算得到同一个数组下标,称为哈希冲突。

影响因素
  1. 哈希函数质量;

  2. 哈希表数组长度;

  3. 装填因子 = 表内元素总数 / 数组总长度;装填因子越大,冲突概率越高。

4 四大哈希冲突解决方案

方案 1:开放定址法(原地探测)
概念

冲突后在当前数组向后寻找空下标,原地存放数据,不额外开辟内存。

细分:线性探测、二次探测、再哈希探测。

特性:无需额外链表,但删除操作会产生空位干扰查找,易堆积冲突。

方案 2:拉链法(链地址法,STL 标准实现)
概念

数组每个下标对应一条单向链表;同一哈希下冲突数据全部挂在对应链表尾部。

特性
  1. 空间利用率高,仅冲突节点额外分配;

  2. 增删查找逻辑清晰,是unordered_map底层方案;

  3. 链表过长会退化至 O (n) 遍历,需控制装填因子扩容。

手写拉链哈希表代码示例
复制代码
#include <iostream>
#include <list>
using namespace std;
// 哈希表容量
#define TABLE_SIZE 23
// 键值对结构体
struct Pair{
    int key;
    int val;
    Pair(int k,int v):key(k),val(v){}
};
// 拉链哈希表
class HashTable{
private:
    list<Pair> table[TABLE_SIZE];
    // 哈希函数:除留余数
    int hashFunc(int key){
        return key % TABLE_SIZE;
    }
public:
    // 插入
    void insert(int k, int v){
        int idx = hashFunc(k);
        table[idx].emplace_back(k,v);
    }
    // 查找
    bool find(int k, int& outVal){
        int idx = hashFunc(k);
        for(auto& p : table[idx]){
            if(p.key == k){
                outVal = p.val;
                return true;
            }
        }
        return false;
    }
    // 删除
    void erase(int k){
        int idx = hashFunc(k);
        for(auto it = table[idx].begin(); it != table[idx]; it++){
            if(it->key == k){
                table[idx].erase(it);
                return;
            }
        }
    }
};
方案 3 公共溢出区法

概念:主数组只存无冲突数据,冲突全部存入单独溢出数组;

缺点:大量冲突时溢出数组遍历效率极低,工程极少使用。

方案 4 再哈希法

概念:冲突时切换另一套哈希函数重新计算下标,多次计算直至空位;

缺点:多次哈希运算,查找速度下降。

二、STL 哈希容器 unordered_map /unordered_set

1 底层特性

  1. C++98 无哈希容器,C++11 新增;

  2. 底层采用

    拉链法哈希表,对比 map红黑树:

    容器 底层 有序性 平均复杂度 适用场景
    map 红黑树 自动升序 O (logn) 需要有序遍历、范围查找
    unordered_map 哈希表 无序 O (1) 海量数据单点快速查询

2 unordered_map 核心特性

  1. 存储pair<Key,Val>键值对,Key 唯一;

  2. push_back,仅insert/[]插入;

  3. 自定义 Key 需提供哈希函数重载,否则编译报错;

代码示例

复制代码
#include <unordered_map>
#include <iostream>
#include <string>
using namespace std;

int main(){
    unordered_map<string, int> equip;
    // 下标插入,键不存在自动创建
    equip["长剑"] = 120;
    // insert插入,重复键无效
    equip.insert(pair<string,int>("法杖",95));
    // 遍历
    for(auto& item : equip)
        cout << item.first << " 攻击:" << item.second << endl;
    // 查找
    if(equip.find("长剑") != equip.end())
        cout << "武器存在" << endl;
    return 0;
}