哈希表有哪些算法?
一 概述
梳理哈希表相关的算法,按照基础、中级、高级进行分类,并提供了核心思想和算法描述。
二 基础算法
这类问题直接运用哈希表的基础特性(快速查找、去重、频率统计),通常只需一次或两次遍历即可解决。
1 两数之和
(1)核心思想:利用哈希表的O(1)查找特性,将“查找目标元素”转化为“验证互补元素是否存在”。
(2) 算法描述
a 初始化一个空哈希表,用于存储 {数组元素: 对应索引}。
b 遍历数组中的每一个元素 num 及其索引 i。
c 计算目标值 target 与 num 的差值 complement。
d 检查 complement 是否存在于哈希表中。
如果存在,则返回当前索引 i 和 complement 在哈希表中对应的索引。
如果不存在,则将当前 {num: i} 存入哈希表,继续遍历。
2 第一个只出现一次的字符
(1) 核心思想:使用哈希表进行频率统计,并通过二次遍历找到第一个满足条件的元素。
(2) 算法描述
a 初始化一个空哈希表,用于存储 {字符: 出现次数}。
b 第一次遍历字符串,对每个字符进行计数。
c 第二次遍历字符串,对于每个字符,检查其在哈希表中的计数。
d 返回第一个计数为1的字符。
3 两个数组的交集
(1) 核心思想:利用哈希集合(Set)的自动去重特性,进行高效的成员检查。
(2) 算法描述
a 将一个数组的所有元素放入一个哈希集合 set1,实现自动去重。
b 初始化一个结果集合 resultSet。
c 遍历第二个数组,检查每个元素是否存在于 set1 中。
d 如果存在,则将该元素加入 resultSet。
e 将 resultSet 转换为列表输出。
4 有效的字母异位词
(1) 核心思想:使用哈希表(或定长数组)统计字符频率,通过频率变化判断是否为异位词。
(2) 算法描述
a 如果两个字符串长度不等,直接返回 false。
b 初始化一个长度为26的数组(或哈希表),模拟从 'a' 到 'z' 的计数器。
c 遍历第一个字符串,将每个字符对应的计数器加1。
d 遍历第二个字符串,将每个字符对应的计数器减1。
e 最后检查所有计数器是否均为0。如果全是0,则是异位词;否则不是。
5 快乐数
(1)核心思想:使用哈希集合检测循环。如果某个中间结果重复出现,则说明进入循环,该数不是快乐数。
(2)算法描
a 初始化一个哈希集合 seen,用于记录已经出现过的数字。
b 当 n 不为1且不在 seen 集合中时,循环执行:
将 n 加入 seen。
将 n 替换为它每一位数字的平方和。
c 最终,如果 n 等于1,则是快乐数;否则不是。
三 中级算法
这类问题通常需要将哈希表与其他算法思想(如滑动窗口、前缀和、双向遍历等)结合,或解决更复杂的数据关系问题。
1 无重复字符的最长子串
(1) 核心思想:滑动窗口 + 哈希表。哈希表用于记录字符最近一次出现的索引,以便在遇到重复字符时快速收缩窗口左边界。
(2)算法描述:
a 初始化一个哈希表 charIndexMap,用于记录每个字符最新出现的索引。
b 使用左右指针 left 和 right 定义一个滑动窗口。
c 右指针不断向右移动,对于每个字符:
如果该字符已在哈希表中,并且其索引大于等于 left(即在当前窗口内),则将 left 指针移动到该字符旧索引的下一个位置。
更新该字符在哈希表中的索引为当前 right。
更新最大窗口长度。
2 字母异位词分组
(1)核心思想:使用规范化键(Canonical Key)。将异位词映射到同一个键上,这个键可以是排序后的字符串,也可以是字符频率统计的元组。
(2) 算法描述
a 初始化一个空字典 anagramMap,键是规范化表示,值是原字符串列表。
b 遍历字符串数组中的每一个字符串。
c 为当前字符串生成一个“键”:
方法一(排序):将字符串排序,排序后的字符串作为键。
方法二(计数):创建一个长度为26的数组统计字符频率,将该数组或其元组形式作为键。
d 将原始字符串加入到该键对应的列表中。
e 返回字典中所有值的集合。
3 最长连续序列
(1)核心思想:利用哈希集合进行O(1)存在性检查,并只从序列的起点开始扩展,避免重复计算。
(2) 算法描述
a 将数组所有数字存入一个哈希集合 numSet。
b 遍历哈希集合中的每个数字 num。
c 检查 num - 1 是否存在于集合中。如果不存在,说明 num 是一个连续序列的起点。
d 从该起点开始,不断检查 num + 1, num + 2 ... 是否存在,并计算当前序列长度。
e 更新遇到的最大序列长度。
4 复制带随机指针的链表
(1)核心思想:使用哈希表建立 原节点 -> 新节点 的映射关系,分两遍遍历完成复制。
(2)算法描述
a 第一遍遍历:复制所有节点,并建立原节点到新节点的映射,存入哈希表 nodeMap。此时先不处理 random 指针。
b 第二遍遍历:对于每个原节点,其新节点的 next 指向 nodeMap[原节点.next],其新节点的 random 指向 nodeMap[原节点.random]。
5 四数相加
(1)核心思想:分组 + 哈希表。将四数组问题转化为两数组问题。
(2)算法描述
a 初始化一个空字典 sumCount,用于记录前两个数组元素和及其出现次数。
b 遍历数组A和B,计算所有可能的 a + b,并在 sumCount 中记录每个和出现的次数。
c 初始化计数器 count = 0。
d 遍历数组C和D,计算所有可能的 c + d,并检查 -(c + d) 是否在 sumCount 中。如果存在,则 count 加上 sumCount[-(c + d)]。
e 返回 count。
四 高级算法
这类问题通常涉及复杂的数据结构设计,要求对哈希表的底层原理有深刻理解,并能将其与其他高级数据结构(如双向链表、堆、单调栈等)巧妙结合。
1 实现LRU缓存
(1)核心思想:哈希表 + 双向链表。哈希表保证O(1)的访问,双向链表维护访问顺序。
(2)算法描述
a 数据结构:
一个哈希表 cache:存储 {key: ListNode}。
一个双向链表:头节点后是最近使用的,尾节点前是最久未使用的。
b get(key) 操作:
b1 如果 key 不存在,返回-1。
b2 如果存在,通过哈希表定位到节点,将该节点移动到链表头部(标记为最近使用),返回节点值。
c put(key, value) 操作:
c1 如果 key 已存在,更新值,并将节点移动到头部。
c2 如果不存在:
创建新节点,加入哈希表,并添加到链表头部。
如果容量超限,则删除链表尾部的节点(最久未使用),并在哈希表中删除对应的键。
2 实现LFU缓存
(1)核心思想:多层哈希表 + 双向链表。需要维护一个“频率”维度,将相同频率的节点放在同一个双向链表中。
(2)算法描述:
a 核心数据结构:
keyToNode: {key: Node},用于O(1)访问节点。
freqToDict: {frequency: DoublyLinkedList},每个频率对应一个双向链表,头尾分别是最近和最久。
minFreq:记录当前最小频率,用于淘汰。
b get(key) 操作:
b1 如果不存在,返回-1。
b2 如果存在,增加节点的频率,将其从原频率链表移除,加入到新频率链表的头部。如果原链表为空且是 minFreq,则更新 minFreq。
c put(key, value) 操作:
c1 如果 key 存在,更新值,并执行一次 get 操作(以更新频率)。
c2 如果不存在:
如果容量已满,从 minFreq 对应的链表尾部淘汰一个节点。
创建新节点(频率为1),放入 freqToDict[1] 的头部,并更新 minFreq = 1。
3 全O(1)数据结构
(1)核心思想:与LFU类似,但操作更复杂。需要支持所有计数字符串的获取,以及递增、递减操作。
(2) 算法描述:
a 数据结构:
一个哈希表 keyCount:存储 {key: count}。
一个哈希表 countKeys:存储 {count: Set(keys)},即每个计数值对应的所有键的集合。
维护 minCount 和 maxCount。
b inc(key):
b1 增加 key 的计数 count。
b2 将其从 countKeys[oldCount] 中移除,加入到 countKeys[newCount] 中。
b3 更新 minCount 和 maxCount。
c dec(key):与 inc 对称,减少计数并移动集合,注意更新 minCount。
d getMaxKey():返回 countKeys[maxCount] 中的任意一个键。
e getMinKey():返回 countKeys[minCount] 中的任意一个键。
4 数据结构设计:O(1)时间插入、删除和获取随机元素
(1)核心思想:动态数组 + 哈希表。数组提供O(1)的随机访问,哈希表提供O(1)的查找和删除定位。
(2)算法描述:
a 数据结构:
一个动态数组 list 存储元素。
一个哈希表 valToIndex 存储 {元素值: 在数组中的索引}。
b insert(val):
b1 如果 val 已存在,返回 false。
b2 将 val 追加到 list 末尾,并在 valToIndex 中记录其索引。
c remove(val):
c1 如果 val 不存在,返回 false。
c2 从 valToIndex 中获取 val 的索引 idx。
c3 将数组末尾元素 lastElement 与 idx 位置的 val 交换。
c4 更新 valToIndex[lastElement] 为 idx。
c5 删除数组最后一个元素,并从 valToIndex 中删除 val。
d getRandom():
d1 在 [0, list.size()-1] 范围内生成一个随机索引,返回 list 中该索引对应的元素。
五 总结
哈希表算法分三级:基础算法直接应用,如两数之和、频率统计,体现空间换时间。中级算法需结合其他思想,如滑动窗口解无重复子串、分组哈希处理异位词。高级算法聚焦复杂结构设计,如哈希表配双向链表实现LRU/LFU缓存,考验系统设计能力。
