【算法训练营 · 二刷总结篇】链表、哈希表部分

文章目录

  • 链表部分
    • 核心知识点
    • 技巧方法
      • [✅ 技巧一:虚拟头节点法(★★★★★ 考察频率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缓存(后端专属高频模板,必背,面试常考))
  • 哈希表部分
    • 核心知识点
    • 技巧方法
    • 通用代码模板
      • [✔ 模板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缓存的双向链表)。

核心知识点

  1. 链表的本质:非连续的内存空间存储元素,每个节点由「数据域 + 指针域」组成,通过指针域连接成链,后端面试常考的节点结构:

    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; }
    }
  2. 链表的核心类型(后端面试高频问区别):

    • 单链表:只有next指针,只能单向遍历(最常考);
    • 双向链表:有prev+next指针,可双向遍历(LRU缓存核心);
    • 循环链表:尾节点next指向头节点(极少考,了解即可)。
  3. 链表的核心特性(对比数组,后端面试必问):

    • 访问效率:O(n)(必须遍历到目标节点),远低于数组的O(1);
    • 增删效率:O(1)(只需修改指针,无需移动元素),远高于数组的O(n);
    • 空间特性:无需连续内存,灵活但额外存储指针,空间开销略高。
  4. 链表的核心边界条件(所有链表题必处理,漏了直接扣分):

    • 空链表(head == null);
    • 单节点链表(head.next == null);
    • 环链表(尾节点不指向null,而是指向链表内节点);
    • 操作头节点/尾节点(比如删除头节点、在尾节点后插入)。
  5. 虚拟头节点(Dummy Node)的核心价值:统一头节点和非头节点的操作逻辑,避免单独处理头节点的边界问题(比如删除头节点时无需特殊判断),链表题90%都需要用虚拟头节点简化代码。

  6. 快慢指针的数学原理:

    • 找中点:快指针走2步,慢指针走1步,快指针到尾时,慢指针在中点;
    • 判环:快指针走2步,慢指针走1步,若相遇则有环;若快指针先到null则无环;
    • 找环入口:相遇后,慢指针回到头,快慢指针各走1步,再次相遇即为环入口(后端面试常问原理,要能解释)。
  7. 递归遍历的核心逻辑:把链表拆成「头节点 + 剩余链表」,递归处理剩余链表,再合并结果(比如反转链表的递归写法),递归的终止条件是「节点为null或节点.next为null」。

  8. 后端专属高频考点(链表与业务场景结合):

    • LRU缓存:双向链表(维护访问顺序)+ 哈希表(O(1)查找),核心操作是「移到头部」「删除尾部」「查找节点」;
    • HashMap的拉链法:解决哈希碰撞,链表长度超过8时转红黑树(JDK1.8),面试常问「为什么转红黑树」「链表长度阈值为什么是8」;
    • 链表的线程安全:ConcurrentHashMap的链表操作加锁(分段锁),避免并发修改异常。

技巧方法

链表的所有高频题,99%都能被以下5个技巧全覆盖

优先级排序:虚拟头节点法 > 快慢指针法 > 递归法 > 双指针遍历法 > 哈希表辅助法

✅ 技巧一:虚拟头节点法(★★★★★ 考察频率TOP1,链表的「万能简化技巧」,必考,占链表题60%+)

✔ 核心定位

虚拟头节点是链表二刷最需要优先掌握的技巧 ,没有之一!后端面试的链表题,绝大部分都需要用虚拟头节点简化代码,避免头节点的特殊处理,用了虚拟头节点,链表题的边界错误会减少80%

✔ 核心原理

创建一个「虚拟头节点(dummy)」,其next指向原链表的头节点,所有操作都基于虚拟头节点的next进行,最终返回dummy.next即为新链表的头节点。

✔ 适用场景(全覆盖,必记)
  • 删除链表节点(尤其是删除头节点);
  • 合并两个有序链表;
  • 链表分区(比如分隔链表);
  • 链表插入(尤其是在头节点前插入);
  • 反转链表的部分区间(比如反转链表II)。
✔ 核心思路(四步走,固化成习惯)
  1. 初始化:ListNode dummy = new ListNode(-1); dummy.next = head;
  2. 定义操作指针:ListNode cur = dummy;(cur始终指向「待操作节点的前一个节点」);
  3. 执行核心逻辑:遍历/判断/修改指针;
  4. 返回结果: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:找环的入口(后端面试常问原理)

✅ 核心思路:

  1. 快慢指针相遇后,slow回到头节点;
  2. 快慢指针各走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个有序链表;
  • 链表的深度遍历(比如回文链表的递归判断)。
✔ 核心思路(以反转链表为例)
  1. 终止条件:if (head == null || head.next == null) return head;(子链表只有一个节点,无需反转);
  2. 递归处理:ListNode newHead = reverseList(head.next);(反转剩余子链表);
  3. 合并结果:head.next.next = head; head.next = null;(将头节点接入反转后的子链表);
  4. 返回新头: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:判断回文链表

✅ 核心思路:

  1. 快慢指针找中点;
  2. 反转后半部分链表;
  3. 双指针分别遍历前半部分和反转后的后半部分,判断是否相等;
  4. 恢复原链表(可选,面试中提一句更加分);

✅ 高频题: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的哈希结构、数据库索引的哈希索引都基于哈希表思想。

核心知识点

  1. 哈希表的本质:通过哈希函数将「键(Key)」映射到「存储位置(索引)」,实现O(1)平均时间复杂度的查找/插入/删除,核心是「空间换时间」。

  2. 哈希函数的核心要求(Java HashMap实现):

    • 一致性:相同Key必须映射到相同索引;
    • 均匀性:不同Key尽量映射到不同索引,减少碰撞;
    • Java实现:hash(key) = (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16),再通过index = hash & (capacity-1)计算索引(capacity是2的幂,保证索引均匀)。
  3. 哈希碰撞的两种解决方式(后端面试必问):

    • 拉链法(Java HashMap/ConcurrentHashMap采用):相同索引的元素组成链表,JDK1.8中链表长度≥8时转为红黑树,长度≤6时转回链表;
    • 开放寻址法(ThreadLocalMap采用):冲突时按线性探测/二次探测寻找下一个空位置,内存利用率高但易堆积。
  4. Java核心哈希容器对比(选型是解题关键):

    容器 底层实现 允许null 线程安全 有序性 核心适用场景
    HashMap 数组+链表/红黑树 Key/Value都可 无序 高频查找、键值映射(算法题首选)
    HashSet 基于HashMap(Value为PRESENT) 元素可null 无序 快速去重、存在性判断
    LinkedHashMap HashMap+双向链表 Key/Value都可 插入/访问有序 LRU缓存、保留顺序场景
    ConcurrentHashMap 数组+链表/红黑树(CAS+分段锁) 不允许null 无序 并发场景(后端工程题)
  5. 哈希表的核心复杂度:

    • 平均时间复杂度:查找/插入/删除 O(1);
    • 最坏时间复杂度:O(n)(哈希函数极差,链表退化为线性表);
    • 空间复杂度:O(n)(存储所有元素)。
  6. HashMap的put方法流程(必背):

    ① 计算Key哈希值 → ② 计算索引(hash & (capacity-1))→ ③ 桶位为空则直接插入;非空则判断节点是否相等,相等更新Value,否则遍历链表/红黑树插入 → ④ 链表长度≥8转红黑树 → ⑤ 容量超阈值(capacity*0.75)则扩容(2倍)。

  7. HashMap扩容机制:

    • 触发条件:元素数 > 容量×负载因子(默认16×0.75=12);
    • 扩容规则:容量翻倍(2的幂),重新计算节点索引;
    • 为什么是2的幂:hash & (capacity-1) 等价于取模,位运算更快且索引更均匀。
  8. ConcurrentHashMap线程安全(JDK1.8):

    摒弃JDK1.7分段锁,改用「CAS + synchronized + 红黑树」,锁粒度缩小到桶位级,并发性能远高于Hashtable。

  9. 后端业务场景(面试加分项):

    • 缓存系统: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版)
  1. 初始化HashSet:Set<Integer> set = new HashSet<>();
  2. 遍历集合:
    • 存在性判断:if (set.contains(target)) return true;
    • 去重:if (!set.contains(cur)) set.add(cur);
  3. 输出结果。
高频题:LeetCode 217/219/349/242
Java避坑点
  • 不要用List.contains(O(n))替代HashSet(O(1));
  • 自定义对象作为Key需重写hashCode()equals()
  • 处理null值时先判空,避免空指针。

✅ 技巧二:哈希映射法(★★★★★ 占比30%+)

核心定位

利用HashMap键值对特性,建立「值→频率/索引/补数」映射,哈希表最核心的解题技巧。

适用场景
  • 频率统计(如"统计字符出现次数");
  • 索引映射(如"两数之和:值→索引");
  • 补数查找(如"两数之和:target-cur→索引")。
核心思路(Java版)
  1. 初始化HashMap:Map<Integer, Integer> map = new HashMap<>();
  2. 遍历集合:
    • 频率统计:map.put(cur, map.getOrDefault(cur, 0) + 1);
    • 补数查找:int complement = target - cur; if (map.containsKey(complement)) return 结果;
  3. 输出结果。
高频题: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版)
  1. 初始化:Map<Integer, Integer> preSumMap = new HashMap<>(); preSumMap.put(0, 1);(关键!处理从开头的子数组);
  2. 遍历数组计算curSum
    • int target = curSum - k; res += preSumMap.getOrDefault(target, 0);
    • preSumMap.put(curSum, preSumMap.getOrDefault(curSum, 0) + 1);
  3. 输出res。
高频题:LeetCode 560/974/525
Java避坑点
  • 必须初始化preSumMap.put(0, 1)
  • 前缀和用long存储,避免int溢出;
  • 支持负数Key,HashMap无需特殊处理。

✅ 技巧四:双哈希表法(★★★ 占比10%+)

核心定位

通过两个HashMap存储两组数据映射,解决「多组数据组合匹配」,如"四数相加II"将O(n⁴)优化到O(n²)。

核心思路(四数相加II为例)
  1. 第一个HashMap存储A+B的和→出现次数;
  2. 遍历C+D,查找-sumCD在第一个Map中的次数,累加结果。
高频题:LeetCode 454/205/290
Java避坑点
  • 避免暴力遍历四组数组;
  • 双向映射需检查「原→替」和「替→原」的唯一性。

✅ 技巧五:LinkedHashMap法(★★ 后端专属)

核心定位

利用LinkedHashMap的「访问有序」特性实现LRU缓存,后端面试必考。

核心思路
  1. 初始化LinkedHashMap时设置accessOrder=true
  2. 重写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;
    }
}
相关推荐
西瓜泡泡奶2 小时前
代码随想录算法Day13|(二叉树part3)110.平衡二叉树、257. 二叉树的所有路径、404.左叶子之和、222.完全二叉树的节点个数
数据结构·算法·二叉树·平衡二叉树·完全二叉树·二叉树路径·左叶子之和
Remember_9932 小时前
【LeetCode精选算法】前缀和专题一
java·开发语言·数据结构·算法·leetcode·eclipse
孞㐑¥2 小时前
算法—双指针
开发语言·c++·经验分享·笔记·算法
承渊政道2 小时前
C++学习之旅【C++List类介绍—入门指南与核心概念解析】
c语言·开发语言·c++·学习·链表·list·visual studio
多打代码2 小时前
2026.01.22 组合 &
算法·leetcode·深度优先
FJW0208142 小时前
Python排序算法
python·算法·排序算法
钮钴禄·爱因斯晨2 小时前
机器学习(二):KNN算法简介及API介绍(分类、回归)
人工智能·算法·机器学习·分类·回归
如此这般英俊2 小时前
第八章-排序
数据结构·算法·排序算法
ChoSeitaku2 小时前
31.C++进阶:⽤哈希表封装myunordered_map和 myunordered_set
c++·哈希算法·散列表