文章目录
- 链表部分
-
- 核心知识点
- 技巧方法
-
- [✅ 技巧一:虚拟头节点法(★★★★★ 考察频率TOP1,链表的「万能简化技巧」,必考,占链表题60%+)](#✅ 技巧一:虚拟头节点法(★★★★★ 考察频率TOP1,链表的「万能简化技巧」,必考,占链表题60%+))
-
- [✔ 核心定位](#✔ 核心定位)
- [✔ 核心原理](#✔ 核心原理)
- [✔ 适用场景(全覆盖,必记)](#✔ 适用场景(全覆盖,必记))
- [✔ 核心思路(四步走,固化成习惯)](#✔ 核心思路(四步走,固化成习惯))
- [✔ 高频经典题:LeetCode 203(移除链表元素)、21(合并两个有序链表)、82(删除排序链表中的重复元素II)、92(反转链表II)](#✔ 高频经典题:LeetCode 203(移除链表元素)、21(合并两个有序链表)、82(删除排序链表中的重复元素II)、92(反转链表II))
- [✔ 避坑点:](#✔ 避坑点:)
- [✅ 技巧二:快慢指针法(★★★★★ 考察频率TOP2,必考,占链表题20%+)](#✅ 技巧二:快慢指针法(★★★★★ 考察频率TOP2,必考,占链表题20%+))
-
- [✔ 核心定位](#✔ 核心定位)
- [✔ 核心原理](#✔ 核心原理)
- [✔ 四种核心应用场景(全覆盖,必背)](#✔ 四种核心应用场景(全覆盖,必背))
-
- [▶ 场景1:找链表的中点(归并排序/回文链表必备)](#▶ 场景1:找链表的中点(归并排序/回文链表必备))
- [▶ 场景2:判断链表是否有环](#▶ 场景2:判断链表是否有环)
- [▶ 场景3:找环的入口(后端面试常问原理)](#▶ 场景3:找环的入口(后端面试常问原理))
- [▶ 场景4:找倒数第k个节点(删除倒数第k个节点必备)](#▶ 场景4:找倒数第k个节点(删除倒数第k个节点必备))
- [✔ 避坑点:](#✔ 避坑点:)
- [✅ 技巧三:递归法(★★★★ 考察频率TOP3,高频必考,占链表题10%+)](#✅ 技巧三:递归法(★★★★ 考察频率TOP3,高频必考,占链表题10%+))
-
- [✔ 核心定位](#✔ 核心定位)
- [✔ 核心原理](#✔ 核心原理)
- [✔ 适用场景(一眼识别)](#✔ 适用场景(一眼识别))
- [✔ 核心思路(以反转链表为例)](#✔ 核心思路(以反转链表为例))
- [✔ 高频经典题:LeetCode 206(反转链表)、92(反转链表II)、24(两两交换链表中的节点)、23(合并K个升序链表)](#✔ 高频经典题:LeetCode 206(反转链表)、92(反转链表II)、24(两两交换链表中的节点)、23(合并K个升序链表))
- [✔ 避坑点:](#✔ 避坑点:)
- [✅ 技巧四:双指针遍历法(★★★ 考察频率TOP4,高频,占链表题5%+)](#✅ 技巧四:双指针遍历法(★★★ 考察频率TOP4,高频,占链表题5%+))
-
- [✔ 核心定位](#✔ 核心定位)
- [✔ 两种核心应用场景](#✔ 两种核心应用场景)
-
- [▶ 场景1:找两个链表的交点](#▶ 场景1:找两个链表的交点)
- [▶ 场景2:判断回文链表](#▶ 场景2:判断回文链表)
- [✔ 避坑点:](#✔ 避坑点:)
- [✅ 技巧五:哈希表辅助法(★★ 保底技巧,考察频率低,但必须会)](#✅ 技巧五:哈希表辅助法(★★ 保底技巧,考察频率低,但必须会))
-
- [✔ 核心定位](#✔ 核心定位)
- [✔ 适用场景:](#✔ 适用场景:)
- [✔ 避坑点:](#✔ 避坑点:)
- 通用代码模板
-
- [✔ 模板1:虚拟头节点(通用模板,必背,覆盖203/21/19/82)](#✔ 模板1:虚拟头节点(通用模板,必背,覆盖203/21/19/82))
- [✔ 模板2:快慢指针(找中点+判环+找环入口,必背,覆盖876/141/142/19)](#✔ 模板2:快慢指针(找中点+判环+找环入口,必背,覆盖876/141/142/19))
- [✔ 模板3:链表反转(迭代+递归双模板,必背,覆盖206/92)](#✔ 模板3:链表反转(迭代+递归双模板,必背,覆盖206/92))
- [✔ 模板4:双指针找链表交点(必背,覆盖160)](#✔ 模板4:双指针找链表交点(必背,覆盖160))
- [✔ 模板5:LRU缓存(后端专属高频模板,必背,面试常考)](#✔ 模板5:LRU缓存(后端专属高频模板,必背,面试常考))
- 哈希表部分
-
- 核心知识点
- 技巧方法
-
- [✅ 技巧一:快速查找/去重法(★★★★★ 占比40%+)](#✅ 技巧一:快速查找/去重法(★★★★★ 占比40%+))
-
- 核心定位
- 适用场景
- 核心思路(Java版)
- [高频题:LeetCode 217/219/349/242](#高频题:LeetCode 217/219/349/242)
- Java避坑点
- [✅ 技巧二:哈希映射法(★★★★★ 占比30%+)](#✅ 技巧二:哈希映射法(★★★★★ 占比30%+))
-
- 核心定位
- 适用场景
- 核心思路(Java版)
- [高频题:LeetCode 1/49/387/13](#高频题:LeetCode 1/49/387/13)
- Java避坑点
- [✅ 技巧三:前缀和+哈希表法(★★★★ 占比15%+)](#✅ 技巧三:前缀和+哈希表法(★★★★ 占比15%+))
-
- 核心定位
- 核心原理
- 核心思路(Java版)
- [高频题:LeetCode 560/974/525](#高频题:LeetCode 560/974/525)
- Java避坑点
- [✅ 技巧四:双哈希表法(★★★ 占比10%+)](#✅ 技巧四:双哈希表法(★★★ 占比10%+))
-
- 核心定位
- 核心思路(四数相加II为例)
- [高频题:LeetCode 454/205/290](#高频题:LeetCode 454/205/290)
- Java避坑点
- [✅ 技巧五:LinkedHashMap法(★★ 后端专属)](#✅ 技巧五:LinkedHashMap法(★★ 后端专属))
- 通用代码模板
-
- [✔ 模板1:快速查找/去重(HashSet)](#✔ 模板1:快速查找/去重(HashSet))
- [✔ 模板2:哈希映射(HashMap)](#✔ 模板2:哈希映射(HashMap))
- [✔ 模板3:前缀和+哈希表](#✔ 模板3:前缀和+哈希表)
- [✔ 模板4:双哈希表](#✔ 模板4:双哈希表)
- [✔ 模板5:LRU缓存(LinkedHashMap简化版+手动实现版)](#✔ 模板5:LRU缓存(LinkedHashMap简化版+手动实现版))
链表部分
链表是互联网大厂后端面试算法的S级必考模块(考察频率95%),也是后端技术栈的核心底层载体(JVM的GC链表、HashMap/ConcurrentHashMap的拉链法、Redis的链表结构、LRU缓存的双向链表)。
核心知识点
-
链表的本质:非连续的内存空间存储元素,每个节点由「数据域 + 指针域」组成,通过指针域连接成链,后端面试常考的节点结构:
java// Java 单链表节点定义(面试必写,要熟练到不用想) class ListNode { int val; ListNode next; ListNode() {} ListNode(int val) { this.val = val; } ListNode(int val, ListNode next) { this.val = val; this.next = next; } } -
链表的核心类型(后端面试高频问区别):
- 单链表:只有next指针,只能单向遍历(最常考);
- 双向链表:有prev+next指针,可双向遍历(LRU缓存核心);
- 循环链表:尾节点next指向头节点(极少考,了解即可)。
-
链表的核心特性(对比数组,后端面试必问):
- 访问效率:O(n)(必须遍历到目标节点),远低于数组的O(1);
- 增删效率:O(1)(只需修改指针,无需移动元素),远高于数组的O(n);
- 空间特性:无需连续内存,灵活但额外存储指针,空间开销略高。
-
链表的核心边界条件(所有链表题必处理,漏了直接扣分):
- 空链表(head == null);
- 单节点链表(head.next == null);
- 环链表(尾节点不指向null,而是指向链表内节点);
- 操作头节点/尾节点(比如删除头节点、在尾节点后插入)。
-
虚拟头节点(Dummy Node)的核心价值:统一头节点和非头节点的操作逻辑,避免单独处理头节点的边界问题(比如删除头节点时无需特殊判断),链表题90%都需要用虚拟头节点简化代码。
-
快慢指针的数学原理:
- 找中点:快指针走2步,慢指针走1步,快指针到尾时,慢指针在中点;
- 判环:快指针走2步,慢指针走1步,若相遇则有环;若快指针先到null则无环;
- 找环入口:相遇后,慢指针回到头,快慢指针各走1步,再次相遇即为环入口(后端面试常问原理,要能解释)。
-
递归遍历的核心逻辑:把链表拆成「头节点 + 剩余链表」,递归处理剩余链表,再合并结果(比如反转链表的递归写法),递归的终止条件是「节点为null或节点.next为null」。
-
后端专属高频考点(链表与业务场景结合):
- LRU缓存:双向链表(维护访问顺序)+ 哈希表(O(1)查找),核心操作是「移到头部」「删除尾部」「查找节点」;
- HashMap的拉链法:解决哈希碰撞,链表长度超过8时转红黑树(JDK1.8),面试常问「为什么转红黑树」「链表长度阈值为什么是8」;
- 链表的线程安全:ConcurrentHashMap的链表操作加锁(分段锁),避免并发修改异常。
技巧方法
链表的所有高频题,99%都能被以下5个技巧全覆盖。
优先级排序:虚拟头节点法 > 快慢指针法 > 递归法 > 双指针遍历法 > 哈希表辅助法
✅ 技巧一:虚拟头节点法(★★★★★ 考察频率TOP1,链表的「万能简化技巧」,必考,占链表题60%+)
✔ 核心定位
虚拟头节点是链表二刷最需要优先掌握的技巧 ,没有之一!后端面试的链表题,绝大部分都需要用虚拟头节点简化代码,避免头节点的特殊处理,用了虚拟头节点,链表题的边界错误会减少80%。
✔ 核心原理
创建一个「虚拟头节点(dummy)」,其next指向原链表的头节点,所有操作都基于虚拟头节点的next进行,最终返回dummy.next即为新链表的头节点。
✔ 适用场景(全覆盖,必记)
- 删除链表节点(尤其是删除头节点);
- 合并两个有序链表;
- 链表分区(比如分隔链表);
- 链表插入(尤其是在头节点前插入);
- 反转链表的部分区间(比如反转链表II)。
✔ 核心思路(四步走,固化成习惯)
- 初始化:
ListNode dummy = new ListNode(-1); dummy.next = head;; - 定义操作指针:
ListNode cur = dummy;(cur始终指向「待操作节点的前一个节点」); - 执行核心逻辑:遍历/判断/修改指针;
- 返回结果:
return dummy.next;(避免头节点被修改后丢失)。
✔ 高频经典题:LeetCode 203(移除链表元素)、21(合并两个有序链表)、82(删除排序链表中的重复元素II)、92(反转链表II)
✔ 避坑点:
- 操作指针cur必须从dummy开始,而非head;
- 遍历结束后,必须返回dummy.next,而非原head(原head可能被删除/修改);
- 不要忘记dummy节点的初始化,避免空指针异常。
✅ 技巧二:快慢指针法(★★★★★ 考察频率TOP2,必考,占链表题20%+)
✔ 核心定位
快慢指针是链表「找中点、判环、找环入口、找倒数第k个节点」的唯一最优解,时间复杂度O(n),空间复杂度O(1),后端面试中「环相关」「中点相关」的题目,面试官绝对期望你用快慢指针解法。
✔ 核心原理
定义两个指针(slow、fast),slow每次走1步,fast每次走2步(或k步),利用速度差实现「定位特定节点」的目标。
✔ 四种核心应用场景(全覆盖,必背)
▶ 场景1:找链表的中点(归并排序/回文链表必备)
✅ 核心思路:fast走2步,slow走1步,fast到尾时,slow在中点;
-
偶数长度:slow在「左中点」(比如链表[1,2,3,4],slow在2);若要找「右中点」,fast初始化为head.next;
✅ 高频题:LeetCode 876(链表的中间结点)、148(排序链表)。
▶ 场景2:判断链表是否有环
✅ 核心思路:fast走2步,slow走1步;若fast先到null,则无环;若快慢指针相遇,则有环;
✅ 高频题:LeetCode 141(环形链表)。
▶ 场景3:找环的入口(后端面试常问原理)
✅ 核心思路:
- 快慢指针相遇后,slow回到头节点;
- 快慢指针各走1步,再次相遇即为环入口;
✅ 原理(面试要能解释):设头到环入口距离为a,环入口到相遇点距离为b,相遇点到环入口距离为c,则 2*(a+b) = a + b + n*(b+c) → a = (n-1)*(b+c) + c,因此从头和相遇点各走1步会在入口相遇;
✅ 高频题:LeetCode 142(环形链表II)。
▶ 场景4:找倒数第k个节点(删除倒数第k个节点必备)
✅ 核心思路:fast先走k步,然后快慢指针各走1步,fast到尾时,slow在倒数第k个节点的前一个节点(配合虚拟头节点更方便 );
✅ 高频题:LeetCode 19(删除链表的倒数第N个结点)。
✔ 避坑点:
- 判环时,循环条件必须是
fast != null && fast.next != null(避免fast.next.next空指针); - 找倒数第k个节点时,fast先走k步前要判断k是否超过链表长度(避免空指针);
- 找环入口时,相遇后slow必须回到头节点,且快慢指针改为各走1步。
✅ 技巧三:递归法(★★★★ 考察频率TOP3,高频必考,占链表题10%+)
✔ 核心定位
递归法是「反转链表(尤其是递归反转)、合并链表、遍历链表」的优雅解法,后端面试中面试官常要求「用递归和迭代两种写法实现反转链表」,递归写法能体现你的逻辑思维能力。
✔ 核心原理
分治思想:把链表拆成「头节点 + 剩余子链表」,递归处理子链表,再将头节点接入处理后的子链表,终止条件是「节点为null或节点.next为null」。
✔ 适用场景(一眼识别)
- 反转链表(整体/部分);
- 合并k个有序链表;
- 链表的深度遍历(比如回文链表的递归判断)。
✔ 核心思路(以反转链表为例)
- 终止条件:
if (head == null || head.next == null) return head;(子链表只有一个节点,无需反转); - 递归处理:
ListNode newHead = reverseList(head.next);(反转剩余子链表); - 合并结果:
head.next.next = head; head.next = null;(将头节点接入反转后的子链表); - 返回新头:
return newHead;。
✔ 高频经典题:LeetCode 206(反转链表)、92(反转链表II)、24(两两交换链表中的节点)、23(合并K个升序链表)
✔ 避坑点:
- 递归终止条件必须包含「head.next == null」,否则会空指针;
- 反转链表的递归写法中,必须执行
head.next = null,避免形成环; - 递归深度不宜过深(链表长度超过1000会栈溢出),但面试题的链表长度通常较小,无需担心。
✅ 技巧四:双指针遍历法(★★★ 考察频率TOP4,高频,占链表题5%+)
✔ 核心定位
双指针遍历法是「找两个链表的交点、判断回文链表」的最优解,时间复杂度O(n),空间复杂度O(1),后端面试中「链表相交/回文」类题目必考。
✔ 两种核心应用场景
▶ 场景1:找两个链表的交点
✅ 核心思路:
- 指针p1遍历链表A,到尾后转到链表B;
- 指针p2遍历链表B,到尾后转到链表A;
- 若两指针相遇,则为交点;若都到null则无交点(原理:p1和p2走的总长度都是len(A)+len(B),必然同时到尾或相遇);
✅ 高频题:LeetCode 160(相交链表)。
▶ 场景2:判断回文链表
✅ 核心思路:
- 快慢指针找中点;
- 反转后半部分链表;
- 双指针分别遍历前半部分和反转后的后半部分,判断是否相等;
- 恢复原链表(可选,面试中提一句更加分);
✅ 高频题:LeetCode 234(回文链表)。
✔ 避坑点:
- 找链表交点时,不要用哈希表(空间复杂度O(n)),面试官期望O(1)空间解法;
- 判断回文链表时,反转后半部分后要注意奇数长度的中点节点无需比较。
✅ 技巧五:哈希表辅助法(★★ 保底技巧,考察频率低,但必须会)
✔ 核心定位
哈希表辅助法是「无法用指针技巧时的兜底解法」,比如「找环入口」「找重复节点」「找相交节点」,时间复杂度O(n),空间复杂度O(n),面试中优先用指针技巧,哈希表只作为「备选思路」。
✔ 适用场景:
- 找环入口(遍历链表,用哈希表存储节点,首次重复的节点即为环入口);
- 找相交链表(存储其中一个链表的所有节点,遍历另一个链表时判断是否存在);
- 找重复节点(比如删除排序链表中的重复元素)。
✔ 避坑点:
- 面试中若用哈希表解法,必须说明「这是兜底解法,最优解是指针法」,否则会减分;
- 哈希表存储的是「节点对象」,而非节点值(避免值相同但节点不同的情况)。
通用代码模板
✔ 模板1:虚拟头节点(通用模板,必背,覆盖203/21/19/82)
java
// Java版:虚拟头节点通用模板 - 删除/合并/插入链表节点
public ListNode removeElements(ListNode head, int val) {
// 1. 初始化虚拟头节点(核心)
ListNode dummy = new ListNode(-1);
dummy.next = head;
ListNode cur = dummy; // cur指向待操作节点的前一个节点
// 2. 遍历链表,执行核心逻辑
while (cur.next != null) {
if (cur.next.val == val) {
// 删除节点:跳过cur.next
cur.next = cur.next.next;
} else {
// 移动cur,继续遍历
cur = cur.next;
}
}
// 3. 返回新头节点(避免原head被删除)
return dummy.next;
}
✔ 模板2:快慢指针(找中点+判环+找环入口,必背,覆盖876/141/142/19)
java
// Java版:快慢指针 - 找环入口(覆盖判环+找入口,面试高频)
public ListNode detectCycle(ListNode head) {
if (head == null || head.next == null) return null;
ListNode slow = head, fast = head;
// 第一步:判环,快慢指针相遇
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
if (slow == fast) { // 有环
// 第二步:找入口,slow回到头,快慢各走1步
slow = head;
while (slow != fast) {
slow = slow.next;
fast = fast.next;
}
return slow;
}
}
return null; // 无环
}
✔ 模板3:链表反转(迭代+递归双模板,必背,覆盖206/92)
java
// Java版:迭代反转(面试首选,空间O(1),更实用)
public ListNode reverseList(ListNode head) {
ListNode pre = null; // 前驱节点
ListNode cur = head; // 当前节点
while (cur != null) {
ListNode nextTemp = cur.next; // 保存后继节点(核心,避免丢失)
cur.next = pre; // 反转指针
pre = cur; // 前驱节点后移
cur = nextTemp; // 当前节点后移
}
return pre; // 新头节点是pre
}
// Java版:递归反转(面试要求会写,体现逻辑能力)
public ListNode reverseListRecursion(ListNode head) {
// 终止条件:节点为null或只有一个节点
if (head == null || head.next == null) return head;
// 递归处理剩余链表
ListNode newHead = reverseListRecursion(head.next);
// 反转当前节点的指针
head.next.next = head;
head.next = null; // 避免形成环
return newHead;
}
//好理解的递归版本
class Solution {
public ListNode reverseList(ListNode head) {
//递归法
ListNode[] result = reverse(head);
if(result[1] != null) result[1].next = null;
return result[0];
}
//使用数组保存每层递归的添加指针,和列表的头节点
public ListNode[] reverse(ListNode node) {
if((node != null && node.next == null) || node == null) return new ListNode[]{node,node};
ListNode[] result = reverse(node.next);
result[1].next = node;
result[1] = node;
return result;
}
}
✔ 模板4:双指针找链表交点(必背,覆盖160)
java
// Java版:找两个链表的交点(O(1)空间,最优解)
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
if (headA == null || headB == null) return null;
ListNode p1 = headA, p2 = headB;
// 核心:p1和p2走的总长度都是len(A)+len(B)
while (p1 != p2) {
p1 = (p1 == null) ? headB : p1.next;
p2 = (p2 == null) ? headA : p2.next;
}
return p1; // 相遇则为交点,否则为null
}
✔ 模板5:LRU缓存(后端专属高频模板,必背,面试常考)
java
// Java版:LRU缓存(双向链表+哈希表,核心是LinkedHashMap简化版)
class LRUCache {
// 1. 定义双向链表节点
class DNode {
int key;
int value;
DNode prev;
DNode next;
public DNode() {}
public DNode(int _key, int _value) {key = _key; value = _value;}
}
// 2. 哈希表:key -> 节点(O(1)查找)
private Map<Integer, DNode> cache = new HashMap<>();
private int size; // 当前元素个数
private int capacity; // 缓存容量
private DNode head, tail; // 虚拟头/尾节点
// 3. 初始化LRU
public LRUCache(int capacity) {
this.size = 0;
this.capacity = capacity;
head = new DNode();
tail = new DNode();
head.next = tail;
tail.prev = head;
}
// 4. 获取节点(访问后移到头部)
public int get(int key) {
DNode node = cache.get(key);
if (node == null) return -1;
moveToHead(node); // 访问后移到头部
return node.value;
}
// 5. 放入节点(满了删除尾部,新节点移到头部)
public void put(int key, int value) {
DNode node = cache.get(key);
if (node == null) {
// 新建节点
DNode newNode = new DNode(key, value);
cache.put(key, newNode);
addToHead(newNode); // 移到头部
size++;
// 容量满了,删除尾部节点
if (size > capacity) {
DNode tailNode = removeTail();
cache.remove(tailNode.key);
size--;
}
} else {
// 更新值,移到头部
node.value = value;
moveToHead(node);
}
}
// 辅助方法:添加节点到头部
private void addToHead(DNode node) {
node.prev = head;
node.next = head.next;
head.next.prev = node;
head.next = node;
}
// 辅助方法:移除节点
private void removeNode(DNode node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}
// 辅助方法:移到头部(先移除,再添加)
private void moveToHead(DNode node) {
removeNode(node);
addToHead(node);
}
// 辅助方法:移除尾部节点(最久未使用)
private DNode removeTail() {
DNode res = tail.prev;
removeNode(res);
return res;
}
}
哈希表部分
哈希表是互联网大厂后端面试算法的S级必考模块(考察频率90%),更是后端技术栈的核心基石------HashMap/ConcurrentHashMap是缓存、负载均衡、分布式存储的核心容器,Redis的哈希结构、数据库索引的哈希索引都基于哈希表思想。
核心知识点
-
哈希表的本质:通过哈希函数将「键(Key)」映射到「存储位置(索引)」,实现O(1)平均时间复杂度的查找/插入/删除,核心是「空间换时间」。
-
哈希函数的核心要求(Java HashMap实现):
- 一致性:相同Key必须映射到相同索引;
- 均匀性:不同Key尽量映射到不同索引,减少碰撞;
- Java实现:
hash(key) = (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16),再通过index = hash & (capacity-1)计算索引(capacity是2的幂,保证索引均匀)。
-
哈希碰撞的两种解决方式(后端面试必问):
- 拉链法(Java HashMap/ConcurrentHashMap采用):相同索引的元素组成链表,JDK1.8中链表长度≥8时转为红黑树,长度≤6时转回链表;
- 开放寻址法(ThreadLocalMap采用):冲突时按线性探测/二次探测寻找下一个空位置,内存利用率高但易堆积。
-
Java核心哈希容器对比(选型是解题关键):
容器 底层实现 允许null 线程安全 有序性 核心适用场景 HashMap 数组+链表/红黑树 Key/Value都可 否 无序 高频查找、键值映射(算法题首选) HashSet 基于HashMap(Value为PRESENT) 元素可null 否 无序 快速去重、存在性判断 LinkedHashMap HashMap+双向链表 Key/Value都可 否 插入/访问有序 LRU缓存、保留顺序场景 ConcurrentHashMap 数组+链表/红黑树(CAS+分段锁) 不允许null 是 无序 并发场景(后端工程题) -
哈希表的核心复杂度:
- 平均时间复杂度:查找/插入/删除 O(1);
- 最坏时间复杂度:O(n)(哈希函数极差,链表退化为线性表);
- 空间复杂度:O(n)(存储所有元素)。
-
HashMap的put方法流程(必背):
① 计算Key哈希值 → ② 计算索引(hash & (capacity-1))→ ③ 桶位为空则直接插入;非空则判断节点是否相等,相等更新Value,否则遍历链表/红黑树插入 → ④ 链表长度≥8转红黑树 → ⑤ 容量超阈值(capacity*0.75)则扩容(2倍)。
-
HashMap扩容机制:
- 触发条件:元素数 > 容量×负载因子(默认16×0.75=12);
- 扩容规则:容量翻倍(2的幂),重新计算节点索引;
- 为什么是2的幂:
hash & (capacity-1)等价于取模,位运算更快且索引更均匀。
-
ConcurrentHashMap线程安全(JDK1.8):
摒弃JDK1.7分段锁,改用「CAS + synchronized + 红黑树」,锁粒度缩小到桶位级,并发性能远高于Hashtable。
-
后端业务场景(面试加分项):
- 缓存系统:HashMap/Redis Hash(快速查找);
- 负载均衡:一致性哈希(解决分布式缓存热点);
- 数据去重:HashSet(日志/用户ID去重);
- LRU缓存:LinkedHashMap(按访问顺序删除)。
| 容器类型 | 核心高频 API | 核心场景 |
|---|---|---|
| HashMap | put()/getOrDefault()/containsKey()/entrySet() |
键值映射、频率统计、补数查找 |
| HashSet | add()/contains()/remove() |
去重、存在性判断 |
| LinkedHashMap | 构造方法(accessOrder=true)+ removeEldestEntry() |
LRU 缓存、有序遍历 |
| ConcurrentHashMap | putIfAbsent()/get() |
并发场景、分布式缓存 |
技巧方法
哈希表99%的高频题可被以下5个技巧覆盖,记牢这些能快速匹配最优解法。
优先级:快速查找/去重法 > 哈希映射法 > 前缀和+哈希表法 > 双哈希表法 > LinkedHashMap法
✅ 技巧一:快速查找/去重法(★★★★★ 占比40%+)
核心定位
利用HashSet的O(1)查找特性,解决「存在性判断」「数据去重」,后端面试送分题。
适用场景
- 判断元素是否重复(如"数组中是否有重复元素");
- 数组/字符串去重、两数组交集。
核心思路(Java版)
- 初始化HashSet:
Set<Integer> set = new HashSet<>();; - 遍历集合:
- 存在性判断:
if (set.contains(target)) return true;; - 去重:
if (!set.contains(cur)) set.add(cur);;
- 存在性判断:
- 输出结果。
高频题:LeetCode 217/219/349/242
Java避坑点
- 不要用List.contains(O(n))替代HashSet(O(1));
- 自定义对象作为Key需重写
hashCode()和equals(); - 处理null值时先判空,避免空指针。
✅ 技巧二:哈希映射法(★★★★★ 占比30%+)
核心定位
利用HashMap键值对特性,建立「值→频率/索引/补数」映射,哈希表最核心的解题技巧。
适用场景
- 频率统计(如"统计字符出现次数");
- 索引映射(如"两数之和:值→索引");
- 补数查找(如"两数之和:target-cur→索引")。
核心思路(Java版)
- 初始化HashMap:
Map<Integer, Integer> map = new HashMap<>();; - 遍历集合:
- 频率统计:
map.put(cur, map.getOrDefault(cur, 0) + 1);; - 补数查找:
int complement = target - cur; if (map.containsKey(complement)) return 结果;;
- 频率统计:
- 输出结果。
高频题:LeetCode 1/49/387/13
Java避坑点
- 用
getOrDefault替代手动判空,简化代码; - 两数之和需「先查后存」,避免同一元素重复使用;
- Value初始值设为0,避免NullPointerException。
✅ 技巧三:前缀和+哈希表法(★★★★ 占比15%+)
核心定位
将「子数组和为k」问题从O(n²)优化到O(n),哈希表进阶技巧,中等难度题必考。
核心原理
前缀和preSum[i] = nums[0]+...+nums[i],子数组nums[j+1...i]和为k → preSum[j] = preSum[i] - k,用HashMap存储preSum值→出现次数。
核心思路(Java版)
- 初始化:
Map<Integer, Integer> preSumMap = new HashMap<>(); preSumMap.put(0, 1);(关键!处理从开头的子数组); - 遍历数组计算
curSum:int target = curSum - k; res += preSumMap.getOrDefault(target, 0);;preSumMap.put(curSum, preSumMap.getOrDefault(curSum, 0) + 1);;
- 输出res。
高频题:LeetCode 560/974/525
Java避坑点
- 必须初始化
preSumMap.put(0, 1); - 前缀和用long存储,避免int溢出;
- 支持负数Key,HashMap无需特殊处理。
✅ 技巧四:双哈希表法(★★★ 占比10%+)
核心定位
通过两个HashMap存储两组数据映射,解决「多组数据组合匹配」,如"四数相加II"将O(n⁴)优化到O(n²)。
核心思路(四数相加II为例)
- 第一个HashMap存储A+B的和→出现次数;
- 遍历C+D,查找
-sumCD在第一个Map中的次数,累加结果。
高频题:LeetCode 454/205/290
Java避坑点
- 避免暴力遍历四组数组;
- 双向映射需检查「原→替」和「替→原」的唯一性。
✅ 技巧五:LinkedHashMap法(★★ 后端专属)
核心定位
利用LinkedHashMap的「访问有序」特性实现LRU缓存,后端面试必考。
核心思路
- 初始化LinkedHashMap时设置
accessOrder=true; - 重写
removeEldestEntry,size>容量时返回true(自动删除最久未使用节点)。
通用代码模板
✔ 模板1:快速查找/去重(HashSet)
java
// LeetCode 217. 存在重复元素
public boolean containsDuplicate(int[] nums) {
Set<Integer> set = new HashSet<>();
for (int num : nums) {
if (set.contains(num)) {
return true;
}
set.add(num);
}
return false;
}
// LeetCode 349. 两个数组的交集
public int[] intersection(int[] nums1, int[] nums2) {
Set<Integer> set1 = new HashSet<>();
Set<Integer> resSet = new HashSet<>();
for (int num : nums1) set1.add(num);
for (int num : nums2) {
if (set1.contains(num)) resSet.add(num);
}
// 集合转数组(Java特有的处理)
int[] res = new int[resSet.size()];
int idx = 0;
for (int num : resSet) res[idx++] = num;
return res;
}
✔ 模板2:哈希映射(HashMap)
java
// LeetCode 1. 两数之和
public int[] twoSum(int[] nums, int target) {
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
int complement = target - nums[i];
if (map.containsKey(complement)) {
return new int[]{map.get(complement), i};
}
map.put(nums[i], i); // 先查后存,避免重复使用
}
throw new IllegalArgumentException("No solution");
}
// LeetCode 387. 字符串中的第一个唯一字符
public int firstUniqChar(String s) {
Map<Character, Integer> map = new HashMap<>();
// 统计频率
for (char c : s.toCharArray()) {
map.put(c, map.getOrDefault(c, 0) + 1);
}
// 找第一个频率为1的字符
for (int i = 0; i < s.length(); i++) {
if (map.get(s.charAt(i)) == 1) return i;
}
return -1;
}
✔ 模板3:前缀和+哈希表
java
// LeetCode 560. 和为K的子数组
public int subarraySum(int[] nums, int k) {
Map<Integer, Integer> preSumMap = new HashMap<>();
preSumMap.put(0, 1); // 核心初始化
int curSum = 0, res = 0;
for (int num : nums) {
curSum += num;
int target = curSum - k;
res += preSumMap.getOrDefault(target, 0);
preSumMap.put(curSum, preSumMap.getOrDefault(curSum, 0) + 1);
}
return res;
}
✔ 模板4:双哈希表
java
// LeetCode 454. 四数相加II
public int fourSumCount(int[] nums1, int[] nums2, int[] nums3, int[] nums4) {
Map<Integer, Integer> map1 = new HashMap<>();
// 统计A+B的和
for (int a : nums1) {
for (int b : nums2) {
int sumAB = a + b;
map1.put(sumAB, map1.getOrDefault(sumAB, 0) + 1);
}
}
int res = 0;
// 查找-(C+D)
for (int c : nums3) {
for (int d : nums4) {
int sumCD = c + d;
res += map1.getOrDefault(-sumCD, 0);
}
}
return res;
}
✔ 模板5:LRU缓存(LinkedHashMap简化版+手动实现版)
java
// 版本1:LinkedHashMap简化版(面试快速实现)
class LRUCache extends LinkedHashMap<Integer, Integer> {
private int capacity;
public LRUCache(int capacity) {
super(capacity, 0.75f, true); // accessOrder=true
this.capacity = capacity;
}
public int get(int key) {
return super.getOrDefault(key, -1);
}
public void put(int key, int value) {
super.put(key, value);
}
@Override
protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
return size() > capacity;
}
}
// 版本2:手动实现(HashMap+双向链表,体现底层原理)
class LRUCache {
class DNode {
int key, value;
DNode prev, next;
public DNode() {}
public DNode(int k, int v) {key = k; value = v;}
}
private Map<Integer, DNode> cache;
private int capacity, size;
private DNode head, tail;
public LRUCache(int capacity) {
this.cache = new HashMap<>();
this.capacity = capacity;
this.size = 0;
head = new DNode();
tail = new DNode();
head.next = tail;
tail.prev = head;
}
public int get(int key) {
DNode node = cache.get(key);
if (node == null) return -1;
moveToHead(node);
return node.value;
}
public void put(int key, int value) {
DNode node = cache.get(key);
if (node == null) {
DNode newNode = new DNode(key, value);
cache.put(key, newNode);
addToHead(newNode);
size++;
if (size > capacity) {
DNode tailNode = removeTail();
cache.remove(tailNode.key);
size--;
}
} else {
node.value = value;
moveToHead(node);
}
}
private void addToHead(DNode node) {
node.prev = head;
node.next = head.next;
head.next.prev = node;
head.next = node;
}
private void removeNode(DNode node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}
private void moveToHead(DNode node) {
removeNode(node);
addToHead(node);
}
private DNode removeTail() {
DNode res = tail.prev;
removeNode(res);
return res;
}
}