哈希表可以理解为一个加强版的数组。
数组可以通过索引在O(1)的时间复杂度内查找到对应元素,索引是一个非负整数。
哈希表是类似的,可以通过 key 在O(1)的时间复杂度内查找到这个 key 对应的 value。key 的类型可以是数字、字符串等多种类型。
其底层就是一个数组,先把key转化为索引再对其进行数组的操作
哈希函数作用是把任意长度的输入(key)转化成固定长度的输出(索引)。
如果两个不同的 key 通过哈希函数得到了相同的索引,怎么办呢?这种情况就叫做「哈希冲突」。
hash 函数相当于是把一个无穷大的空间映射到了一个有限的索引空间,所以必然会有不同的 key 映射到同一个索引上。
一种是拉链法,另一种是线性探查法(也经常被叫做开放寻址法)。
拉链法是重叠的索引值形成一串链表,线性探查法是向后查找到空位置。
那么为什么会频繁出现哈希冲突呢?两个原因呗:
1、哈希函数设计的不好,导致 key 的哈希值分布不均匀,很多 key 映射到了同一个索引上。
2、哈希表里面已经装了太多的 key-value 对了,这种情况下即使哈希函数再完美,也没办法避免哈希冲突。
负载因子的计算公式也很简单,就是 size / table.length。其中 size 是哈希表里面的 key-value 对的数量,table.length 是哈希表底层数组的容量。
用拉链法实现的哈希表,负载因子可以无限大,因为链表可以无限延伸;用线性探查法实现的哈希表,负载因子不会超过 1
双向链表加强哈希表
LinkedHashMap 的操作时间复杂度为 O(1),是因为它在 HashMap 的基础上 额外维护了一个双向链表,既保留了哈希表的高效查找特性,又实现了有序遍历。下面详细分析其实现原理和性能表现:
1. LinkedHashMap 的核心设计
(1) 底层结构 = 哈希表 + 双向链表
HashMap部分:使用数组 + 链表/红黑树存储键值对,保证get(key)和put(key, value)的 O(1) 平均时间复杂度。双向链表部分:按插入顺序(或访问顺序)连接所有节点,支持有序遍历。
2. 对比普通 HashMap
| 特性 | HashMap |
LinkedHashMap |
|---|---|---|
| 底层结构 | 数组 + 链表/红黑树 | 数组 + 链表/红黑树 + 双向链表 |
| 查找性能 | O(1) | O(1)(与 HashMap 相同) |
| 遍历顺序 | 无序 | 按插入顺序 或 访问顺序(LRU) |
| 额外内存开销 | 无 | 每个节点多存 2 个指针(before/after) |
3. 双向链表的作用
(1) 维护插入/访问顺序
- 默认模式(插入顺序):新节点追加到链表尾部,遍历时按插入顺序输出。
- 访问顺序模式(LRU):每次
get(key)或put(key, value)时,将节点移到链表尾部(最近使用)。
(2) 实现 LRU 缓存
通过重写 removeEldestEntry(),可以轻松实现固定大小的 LRU 缓存:
class LRUCache<K, V> extends LinkedHashMap<K, V> {
private final int capacity;
public LRUCache(int capacity) {
super(capacity, 0.75f, true); // 按访问顺序排序
this.capacity = capacity;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > capacity; // 容量超限时移除最久未使用的节点
}
}
4. 为什么仍然是 O(1)?
虽然 LinkedHashMap 比 HashMap 多维护了一个链表,但所有链表操作(插入、删除、移动)均通过指针直接修改,不涉及遍历,因此 不影响整体时间复杂度:
- 哈希表:负责快速定位节点(O(1))。
- 双向链表:仅维护节点间的顺序关系(指针操作是 O(1))。
适用场景: - 需要按插入顺序或访问顺序遍历 Map。
- 实现 LRU 缓存淘汰策略。
- 需要同时高效查找和有序访问的数据结构。
用数组增强的hashmap
实际场景就是随机获取 key,同时保证 kv 对儿 O(1) 的操作复杂度,相当于给哈希表加一个 API。
先随机生成索引,再对数组末尾与索引交换并输出末尾,并反馈到哈希表。
浙公网安备 33010602011771号