前言

哈希表是一种 键值对(key-value) 数据结构,具有高效的查找、插入和删除性能。它广泛用于需要快速访问数据的场景,例如字典、缓存、数据库索引等。


简介

散列表(Hash table,也叫哈希表),是根据键(Key)而直接访问在内存存储位置的数据结构。也就是说,它通过计算一个关于键值的函数,将所需查询的数据映射到表中一个位置来访问记录,这加快了查找速度。这个映射函数称做哈希函数,存放记录的数组称做哈希表。
一个哈希函数的好不好,取决于以下三点:

  • 哈希函数的定义域必须包括需要存储的全部关键码,而如果哈希表允许有m个地址时,其值域必须在0 到m-1之间
  • 哈希函数计算出来的地址能均匀分布在整个空间中
  • 哈希函数应该比较简单

哈希表的基本概念

  1. 键值对(key-value)存储:
    • 键(key):数据的唯一标识,类似索引。
    • 值(value):与键相关联的数据。
  2. 哈希函数(Hash Function):
    • 用于将键(key)映射到哈希表中的特定位置(索引)。
    • 理想的哈希函数应具有以下特点:
      • 快速计算。
      • 均匀分布,尽可能减少冲突(两个不同的键映射到相同的索引)。
  3. 哈希表的性能:
    • 查找、插入、删除的平均时间复杂度为 (O(1))
    • 在极端情况下(哈希冲突过多),时间复杂度可能退化为 (O(n))。

哈希表的实现原理

  1. 数组存储:
    • 哈希表底层通常是一个固定大小的数组,每个数组单元称为 桶(bucket)
  2. 哈希函数:
    • 根据键计算出数组的索引值:
  3. 哈希冲突(Collision):
    • 不同的键通过哈希函数映射到相同的索引时,称为 冲突
  4. 冲突解决策略:

    • 链地址法(Separate Chaining):
      • 每个桶使用链表存储多个键值对。
      • 插入冲突时,在存储数据后面加一个指针,指向后面冲突的数据,将新元素添加到链表的尾部。
      • ![[Pasted image 20241118171656.png]]
    • 开放地址法(Open Addressing):
      • 当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那 么可以把key存放到冲突位置中的“下一个” 空位置中去
      • 闭散列中主要处理方法有线性探测和二次探测

        线性探测:计算哈希地址,将数据存放在计算出来的哈希位置上,如果该位置有数据则往后查找第一个空闲位置插入。但是当我们的元素越多时,我们产生的哈希冲突的次数就会越多。
        二次探测:通过该位置的哈希冲突次数的平方来向后查找新的位置,将产生哈希冲突的数据分散,使不堆积在一起。
        以上两种方法都有很大的缺陷,就是空间利用率低。在这个基础上,引进一种新的技术来解决哈希冲突——开散列(链地址法)

  5. 扩容:

    • 当哈希表中的元素数量接近数组大小(负载因子达到阈值),会动态扩展哈希表的大小,并重新分布所有键值对(称为 rehash)。

哈希表的特点

优点:

  1. 快速查找:平均时间复杂度 (O(1))。
  2. 动态扩展:大多数哈希表会在容量接近满时自动扩展。
  3. 灵活性:可以存储复杂的数据结构(如对象、类等)。

缺点:

  1. 无法保持顺序:存储元素是无序的,无法按插入顺序访问。
  2. 哈希冲突:大量冲突会导致性能下降。
  3. 内存占用高:通常需要分配比实际存储元素更多的空间。
  4. 不支持范围查找:不像有序结构(如二叉搜索树)支持范围查找。

C++ 中的哈希表实现

标准库容器:

1. unordered_map

  • 用途:存储键值对,键唯一。
  • 时间复杂度:平均 (O(1)),最坏 (O(n))。
  • 底层实现:链地址法。
  • 示例:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    #include <unordered_map>
    #include <iostream>
    using namespace std;

    int main() {
    unordered_map<string, int> map;
    map["apple"] = 10;
    map["banana"] = 20;

    // 查找元素
    cout << "Value of apple: " << map["apple"] << endl;

    return 0;
    }
  • 输出:
    1
    Value of apple: 10

2. unordered_set

  • 用途:存储唯一键,无关联值。
  • 示例:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    #include <unordered_set>
    #include <iostream>
    using namespace std;

    int main() {
    unordered_set<int> set = {1, 2, 3, 4};

    // 查找元素
    if (set.find(2) != set.end()) {
    cout << "2 found in set" << endl;
    }

    return 0;
    }
  • 输出:
    1
    2 found in set

3. unordered_multimap 和 unordered_multiset

  • 允许存储重复的键。

哈希表的应用场景

  1. 快速查找:
    • 例如,电话号码簿、用户信息存储等。
  2. 计数问题:
    • 统计字符串中字母的出现频率。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      #include <unordered_map>
      #include <iostream>
      using namespace std;

      int main() {
      string str = "hello world";
      unordered_map<char, int> freq;

      for (char c : str) {
      freq[c]++;
      }

      for (auto& pair : freq) {
      cout << pair.first << ": " << pair.second << endl;
      }

      return 0;
      }
    • 输出:
      1
      2
      3
      4
      5
      6
      7
      h: 1
      e: 1
      l: 3
      o: 2
      w: 1
      r: 1
      d: 1
  3. 字母异位词分组:
    • 通过将字符串排序后的结果作为键,将字母异位词归为一组。
  4. 缓存(Cache):
    • 实现快速访问最近使用的数据(结合哈希表和链表实现 LRU 缓存)。

常见问题与优化

  1. 哈希冲突的解决:
    • 优化哈希函数,减少冲突。
    • 使用适当的负载因子。
  2. 内存消耗过多:
    • 如果数据规模较小,可以考虑有序映射(std::map)。
  3. 扩容和重哈希:
    • 如果数据量大,可以提前设置哈希表的初始大小以减少重哈希的开销。

哈希表 vs 其他数据结构

特性 哈希表(unordered_map 有序映射(map 动态数组(vector
查找/插入时间复杂度 (O(1)) (O(\log n)) (O(n))(非索引查找)
是否保持元素顺序
是否支持范围查询
内存使用 较高 较低 较低

哈希表相关容器的对比

容器 是否允许重复 是否有序 平均时间复杂度 备注
std::unordered_map O(1) 哈希表实现,键值对存储
std::unordered_set O(1) 哈希表实现,只存储键
std::unordered_multimap O(1) 哈希表实现,允许重复键
std::unordered_multiset O(1) 哈希表实现,允许重复元素
std::map O(log⁡n) 红黑树实现,按键有序存储
std::multimap O(log⁡n) 红黑树实现,允许重复键
std::set O(log⁡n) 红黑树实现,按键有序存储
std::multiset O(log⁡n) 红黑树实现,允许重复元素

推荐选择

  • 需要快速查找或插入:使用 std::unordered_mapstd::unordered_set
  • 需要排序功能:使用 std::mapstd::set
  • 允许重复键/值:使用 std::unordered_multimapstd::unordered_multiset

总结

哈希表是一种高效且实用的数据结构,具有快速查找、插入和删除的特点。C++ 提供了多种基于哈希表的标准容器(如 unordered_mapunordered_set),能够方便地解决实际问题。在选择数据结构时,需要根据具体应用场景权衡哈希表的优缺点。

参考链接

数据结构 5分钟带你搞定哈希表(建议收藏)!!!_哈希表怎么画-CSDN博客