哈希表可以理解为一个加强版的数组。

数组可以通过索引在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)?

虽然 LinkedHashMapHashMap 多维护了一个链表,但所有链表操作(插入、删除、移动)均通过指针直接修改,不涉及遍历,因此 不影响整体时间复杂度

  • 哈希表:负责快速定位节点(O(1))。
  • 双向链表:仅维护节点间的顺序关系(指针操作是 O(1))。
    适用场景
  • 需要按插入顺序或访问顺序遍历 Map。
  • 实现 LRU 缓存淘汰策略。
  • 需要同时高效查找和有序访问的数据结构。

用数组增强的hashmap
实际场景就是随机获取 key,同时保证 kv 对儿 O(1) 的操作复杂度,相当于给哈希表加一个 API。
先随机生成索引,再对数组末尾与索引交换并输出末尾,并反馈到哈希表。