算法与数据结构----哈希表

直接从计算机底层的数据结构算法角度来拆解哈希表。

哈希表的本质是:一个底层数组 + 一个数学计算函数。

它的核心目的是为了解决一个特定问题:如何通过非整数类型的"键"(比如字符串),以 O(1) 的时间复杂度直接定位到内存中的数据。

以下是它在代码底层的绝对真实运作过程:

1. 底层结构:数组

在计算机内存中,只有数组 能做到真正的 O(1) 极速访问。因为只要给定一个整数索引(如 index = 3),CPU 就能通过内存地址偏移量,一步直接读取出数据。 哈希表的底层,就是一个预先分配好固定大小的内存数组。假设我们初始化了一个长度为 5 的空数组: [ null, null, null, null, null ]

2. 核心引擎:哈希函数 (Hash Function)

我们需要把字符串的"键"(Key)转换成数组的"整数索引"。这个转换任务由哈希函数完成。 哈希函数是一个数学算法(如 MD5, SHA-1,或简单的质数相乘算法)。无论你输入什么字符串,它都会计算出一个固定长度的整数。

3. 数据存入的完整执行流

假设我们要往哈希表里存入一条数据:键为 "Alice",值为 25

  • 计算哈希值:"Alice" 输入哈希函数。假设算法计算出的哈希值是 842301

  • 取模运算(压缩索引): 我们的数组长度只有 5,放不下 842301 这么大的下标。所以用它对数组长度取模(求余数): 842301 % 5 = 1

  • 内存写入: 程序将键值对 ("Alice", 25) 存入底层数组索引为 1 的位置。

现在的底层数组变成了这样: [ null, ("Alice", 25), null, null, null ]

4. 数据读取的完整执行流

现在,我们要查找键为 "Alice" 的值。

  • 程序用同样的哈希函数重新计算 "Alice",必然再次得到 842301

  • 再次取模:842301 % 5 = 1

  • 程序直接访问底层数组的索引 1,读取出 25

这就是为什么哈希表不需要像 for 循环那样挨个对比数据。它通过一次数学运算,直接算出了数据在内存数组中的绝对位置。

5. 必然面临的技术难题:哈希冲突 (Collision)

数学上存在一种必然情况:不同的字符串,经过哈希函数计算并取模后,得到了相同的索引值

假设我们要存入另一条数据 ("Bob", 30),恰好它的哈希值取模后也是 1。但数组索引 1 的位置已经被 "Alice" 占用了。这叫做哈希冲突。

工程上的标准解决方案:链表法(Separate Chaining) 此时,哈希表底层的数组不再直接存储数据本身,而是存储内存指针 。这个指针指向一个链表(Linked List)

发生冲突后,底层数组的状态会变成这样:

Plaintext

复制代码
索引 0: null
索引 1: -> 节点("Alice", 25) -> 节点("Bob", 30) -> null
索引 2: null
索引 3: null
索引 4: null
  • 冲突后的查找逻辑: 当我们要找 "Bob" 时,程序计算出索引是 1。它来到索引 1 的链表头部,先对比第一个节点,发现键是 "Alice",不匹配;顺着指针走到下一个节点,发现键是 "Bob",字符串完全匹配,于是成功返回 30

总结

哈希表之所以快,是因为它在 99% 的情况下,都表现为一个直接通过数学公式算出物理地址的普通数组。只有在发生极少数的"哈希冲突"时,它才会退化成在极短的链表中进行顺序查找。

例题:

给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。

你可以假设每种输入只会对应一个答案,并且你不能使用两次相同的元素。

你可以按任意顺序返回答案。

答案:

cpp 复制代码
class Solution {
public:
    vector<int> twoSum(vector<int>& nums, int target) {
        unordered_map<int, int> hashtable;
        for (int i = 0; i < nums.size(); ++i) {
            auto it = hashtable.find(target - nums[i]);
            if (it != hashtable.end()) {
                return {it->second, i};
            }
            hashtable[nums[i]] = i;
        }
        return {};
    }
};

解释:

1. 哈希表的声明

复制代码
unordered_map<int, int> hashtable;
  • unordered_map:这是 C++ 标准模板库(STL)中提供的哈希表容器。它的底层实现是哈希表。

  • <int, int> :这是 C++ 的模板(Template)语法。尖括号内的参数指定了哈希表中存储的数据类型。第一个 int 代表键(Key)的类型是整数,第二个 int 代表值(Value)的类型也是整数。

  • hashtable:变量名。由于没有显式赋初值,C++ 会调用它的默认构造函数,在栈内存上初始化一个空的哈希表。

2. 循环与边界条件

复制代码
for (int i = 0; i < nums.size(); ++i) {
  • nums.size() :假设 nums 是一个 std::vector(C++ 的动态数组),.size() 是它的成员函数,用于返回数组中当前元素的个数。返回值类型通常是无符号整数(size_t)。

3. 类型推导与迭代器查找

复制代码
auto it = hashtable.find(target - nums[i]);
  • auto :这是 C++11 引入的类型推导 关键字。你不需要手动写出极其冗长的迭代器类型(如 std::unordered_map<int, int>::iterator),编译器会根据右侧表达式的返回值自动推断出 it 的正确类型。

  • hashtable.find(...):这是哈希表的成员函数,用于查找指定的键。

    • 如果找到了,它会返回一个指向该键值对的迭代器(Iterator)

    • 如果没找到,它会返回一个特殊的尾后迭代器(即 end())。

4. 迭代器有效性检查

复制代码
if (it != hashtable.end()) {
  • hashtable.end():返回一个指向哈希表"最后一个元素之后"的迭代器。它不指向任何实际数据,而是作为越界或"未找到"的标志。

  • != :这行代码的逻辑是:如果 find 返回的迭代器不等于 end(),说明在这个哈希表中成功找到了对应的键。

5. 迭代器解引用与列表初始化返回

复制代码
return {it->second, i};
  • it->second :在 C++ 的 unordered_map 中,迭代器指向的数据结构是一个键值对(std::pair)。

    • it->first 用于获取键(Key)。

    • it->second 用于获取值(Value)。这里获取的是之前存入哈希表的数组索引。

    • -> 是指针或迭代器的成员访问运算符。

  • { ..., ... } :这是 C++11 引入的统一初始化(Uniform Initialization)或称为大括号初始化。假设外层函数的返回值类型是 std::vector<int>,编译器会自动将这两个整数构造成一个 vector 并返回,无需显式写出 return vector<int>{...};

6. 重载下标运算符的插入/修改

复制代码
hashtable[nums[i]] = i;
  • [] 运算符 :C++ 的 unordered_map 对方括号运算符进行了重载。

    • 如果 nums[i] 这个键在哈希表中不存在 ,它会自动创建一个新的键值对,键为 nums[i],并将其值赋为等号右侧的 i

    • 如果 nums[i] 这个键已经存在 ,它会直接覆盖原有的值,将其更新为 i

7. 默认空返回

复制代码
return {};
  • {} :同样利用了 C++11 的统一初始化语法。这会构造一个空的容器(如一个空的 std::vector<int>)并返回。用于满足函数必须有返回值的语法要求。

慢动作回放(最直观的证明)

假设 nums = [2, 7, 11, 15]target = 9

第一轮循环: i = 0,当前数字 nums[0] = 2

  1. 我们要找的另一半是:9 - 2 = 7

  2. 去哈希表里 find(7),哈希表是空的,没找到。

  3. 执行 hashtable[nums[0]] = 0;

    • 此时哈希表里存入了一条数据:{Key: 2, Value: 0}

第二轮循环: i = 1,当前数字 nums[1] = 7

  1. 我们要找的另一半是:9 - 7 = 2

  2. 去哈希表里 find(2)找到了! 因为第一轮我们刚把 2 存进去。

  3. 此时 it 就指向了哈希表里的那条数据:{Key: 2, Value: 0}

  4. 所以:

    • it->first 就是 2

    • it->second 就是 0(这正是数字 2 当初在数组里的下标!)。

最后,代码执行 return {it->second, i};,也就等价于返回了 {0, 1}

相关推荐
不懒不懒1 小时前
Windows 深度学习环境配置(CUDA12.8 + cuDNN9.x + PyTorch)最简避坑指南(2026 最新)
人工智能·pytorch·深度学习
维基框架1 小时前
汽车座舱开发上云 Google的Arm实例解决了什么实际问题
arm开发·人工智能·汽车
Kobebryant-Manba1 小时前
Hugging Face中transformers库
人工智能·深度学习·机器学习·bert
萌动的小火苗2 小时前
嵌入式开发中的栈与队列:任务调度为什么依赖数据结构
数据结构·c++·单片机·嵌入式硬件
叩码以求索2 小时前
统计按位或能得到最大值的子集数目(一)
数据结构·算法
2601_957418802 小时前
AI美颜本地跑支持手机、电脑、苹果系统
人工智能·ai
AI_Auto2 小时前
工业与AI融合应用 | 四个实战用例!机械装备行业AI+数字孪生+机器人落地全景
大数据·人工智能·机器人·制造
Ww.xh2 小时前
AI助手高并发性能优化四策
人工智能
浩哥学JavaAI2 小时前
2026年最新AI agent面试(07)_大模型架构基础
人工智能·面试·架构