散列表


散列表(哈希表,Hash Table)介绍

散列表(Hash Table)是一种 基于哈希函数(Hash Function) 实现的 键值对(key-value)存储结构,支持 O(1) 级别的查找、插入和删除 操作,广泛用于 数据库索引、缓存系统、集合操作 等。


1. 为什么需要散列表?

(1) 线性查找 vs. 二分查找 vs. 平衡二叉树

数据结构 查找 插入 删除 适用场景
数组(无序) O(n) O(1) O(n) 小规模数据
数组(有序)+ 二分查找 O(log n) O(n) O(n) 静态数据
平衡二叉树(AVL/红黑树) O(log n) O(log n) O(log n) 动态数据
散列表(Hash Table) O(1) O(1) O(1) 快速查找

总结

  • 二叉搜索树(BST)适用于区间查找、排序查询
  • 哈希表适用于快速查找,不要求顺序存储

2. 散列表的基本原理

核心思想

  1. 哈希函数(Hash Function):将 key 映射到 哈希值(哈希桶索引)
  2. 冲突解决(Collision Resolution):
    • 拉链法(Separate Chaining)
    • 开放地址法(Open Addressing)
  3. 动态扩容(Resizing):
    • 负载因子(Load Factor) 超过阈值(如 0.75),进行 扩容(rehashing)

3. 哈希函数

哈希函数的作用是将 任意大小的 key 映射到固定范围的索引

(1) 常见哈希函数

  • 除留余数法(Modulo Method):

    int hash(int key, int size) {
        return key % size;
    }
  • 乘法散列法(Multiplication Method):

    int hash(int key, int size) {
        double A = 0.6180339887; // 黄金比例
        return (int)(size * (key * A - (int)(key * A)));
    }
  • 字符串哈希(BKDRHash):

    unsigned int BKDRHash(string str) {
        unsigned int seed = 131; // 31, 131, 1313...
        unsigned int hash = 0;
        for (char ch : str) {
            hash = hash * seed + ch;
        }
        return hash;
    }

4. 哈希冲突解决方案

(1) 拉链法(Separate Chaining)

  • 思路:每个哈希桶存储一个链表,发生冲突时,将新元素插入链表中。
  • 优点:避免哈希表扩容,适用于大规模数据。
  • 缺点:链表查找最坏 O(n)。
#include <iostream>
#include <vector>
#include <list>

using namespace std;

class HashTable {
private:
    vector<list<int>> table;
    int size;

    int hash(int key) {
        return key % size;
    }

public:
    HashTable(int s) : size(s) {
        table.resize(size);
    }

    void insert(int key) {
        int index = hash(key);
        table[index].push_back(key);
    }

    bool search(int key) {
        int index = hash(key);
        for (int num : table[index]) {
            if (num == key) return true;
        }
        return false;
    }
};

int main() {
    HashTable ht(10);
    ht.insert(15);
    ht.insert(25);
    cout << ht.search(15) << endl; // 输出 1
    cout << ht.search(20) << endl; // 输出 0
}

(2) 开放地址法(Open Addressing)

  • 思路:若发生冲突,则寻找 下一个空位(线性探测/二次探测/双重哈希)。
  • 优点:节省空间,无需额外链表。
  • 缺点:删除困难(可能需要懒惰删除)。
#include <iostream>
#include <vector>

using namespace std;

class OpenAddressingHashTable {
private:
    vector<int> table;
    int size;
    int EMPTY = -1;

    int hash(int key) {
        return key % size;
    }

public:
    OpenAddressingHashTable(int s) : size(s) {
        table.resize(size, EMPTY);
    }

    void insert(int key) {
        int index = hash(key);
        while (table[index] != EMPTY) {  // 线性探测
            index = (index + 1) % size;
        }
        table[index] = key;
    }

    bool search(int key) {
        int index = hash(key);
        int start = index;
        while (table[index] != EMPTY) {
            if (table[index] == key) return true;
            index = (index + 1) % size;
            if (index == start) break;
        }
        return false;
    }
};

int main() {
    OpenAddressingHashTable ht(10);
    ht.insert(15);
    ht.insert(25);
    cout << ht.search(15) << endl; // 输出 1
    cout << ht.search(20) << endl; // 输出 0
}

5. 动态扩容(Rehashing)

  • 当负载因子 > 0.75,进行扩容。
  • 扩容规则:
    1. 新建更大数组(通常为原来的 2 倍)
    2. 重新计算哈希值并插入新数组
void rehash() {
    vector<list<int>> oldTable = table;
    size *= 2;
    table.clear();
    table.resize(size);

    for (auto &bucket : oldTable) {
        for (int key : bucket) {
            insert(key);
        }
    }
}

6. 哈希表的实际应用

  1. 数据库索引(如 MySQL Hash Index)。
  2. 缓存系统(如 Redis)。
  3. 去重查重(如 unordered_set)。
  4. 字符串模式匹配(如 Rabin-Karp 算法)。

7. 哈希表 vs. 二叉搜索树(BST)

特性 哈希表(Hash Table) 二叉搜索树(BST)
查找 O(1) O(log n)
插入 O(1) O(log n)
删除 O(1) O(log n)
有序性 ❌(无序) ✅(有序)
区间查找 ❌(不支持) ✅(支持)
动态性 可能需要 Rehash 动态插入删除
适用场景 快速查找、集合 需要排序的场景

选择指南

  • 只需要快速查找 → 用 哈希表
  • 需要排序、区间查找 → 用 BST(红黑树)

总结

  • 哈希表的核心哈希函数 + 冲突解决
  • 拉链法适用于大规模数据,开放地址法适用于小规模数据
  • Redis、数据库索引、编译器符号表等广泛使用哈希表

哈希表是现代计算机科学 最重要的数据结构之一,掌握它能帮助你更高效地解决实际问题!


文章作者: Gustavo
版权声明: 本博客所有文章除特別声明外,均采用 CC BY-NC 4.0 许可协议。转载请注明来源 Gustavo !
评论
  目录