Java 数据结构与算法 (终极完整学习文档)

前言

本文为全网最完整 Java 数据结构体系 ,整合所有基础 + 补全遗漏难点 + 高级工程结构,全部对标 JDK 源码、LeetCode 刷题、大厂面试考点。无知识点遗漏,可直接作为背诵手册 + 刷题模板手册 + 面试手册

第一章 算法前置基础

数据结构:

1.1 时间/空间复杂度

1.1.1 复杂度量级(必背·优先级排序)

严格递增排序(算法性能从优到劣):O(1) < O(log n) < O(n) < O(n log n) < O(n²) < O(2ⁿ) < O(n!)

核心原则(面试必背) :时间复杂度描述的是数据量 n 趋近无穷大时的增长趋势,忽略常数、低次项、系数,只保留最高阶项,是Java算法性能、刷题取舍、工程选型的核心依据。

1. O(1) 常数级(最优)

  • 定义:执行次数与数据量 n 无关,固定次数执行

  • Java典型场景:数组随机访问、HashMap查询/插入、栈/队列头尾操作、变量赋值、数学运算

  • 数据量上限:无限制,百万/千万级数据依旧极速

  • 刷题特征:无循环、无递归、无遍历

2. O(log n) 对数级(极优·面试高频)

  • 定义:数据翻倍,执行次数仅+1,增长极其缓慢

  • 底层原理:每次操作舍弃一半数据(二分思想)

  • Java典型场景:二分查找、二叉搜索树查询、红黑树操作、堆的上浮/下沉、跳表查询

  • 数据量上限:n=10亿,仅需30次左右操作

  • 刷题特征:while循环不断折半、递归拆分问题规模减半

3. O(n) 线性级(优良·工业主流)

  • 定义:执行次数与数据量 n 成正比,数据翻倍,耗时翻倍

  • Java典型场景:数组/链表遍历、单循环处理、BFS、线性扫描、前缀和预处理

  • 数据量上限:Java刷题可稳跑 10^6 数据

  • 刷题特征:单层循环、一次遍历全量数据

4. O(n log n) 线性对数级(高效排序专属)

  • 定义 :线性遍历 + 对数拆分,综合效率极高,是通用排序最优下界

  • Java典型场景:快速排序、归并排序、堆排序、TreeMap遍历、TopK堆筛选

  • 数据量上限:Java刷题可稳跑 10^5 ~ 10^6 数据

  • 刷题特征:分治递归排序、堆结构批量处理数据

5. O(n²) 平方级(暴力低效)

  • 定义:双重循环嵌套,数据翻倍,耗时翻4倍

  • Java典型场景:冒泡/插入/选择排序、双重暴力枚举、二维数组全遍历

  • 数据量上限:仅支持 10^3 ~ 10^4 数据,超量必超时

  • 刷题禁忌:LeetCode中等及以上题目,禁止无脑双重循环,必须优化为O(n)/O(nlogn)

6. O(2ⁿ) 指数级(暴力穷举)

  • 定义:数据量每+1,耗时翻倍,爆炸式增长

  • Java典型场景:子集枚举、组合暴力回溯、未剪枝的递归枚举

  • 数据量上限:n≤20,超量直接超时,无工程使用价值

  • 优化方向:记忆化递归、DP动态规划剪枝

7. O(n!) 阶乘级(极差)

  • 定义:全排列枚举,增长速度全网最快

  • Java典型场景:全排列暴力枚举、无剪枝排列回溯

  • 数据量上限:n≤12,仅能用于教学演示,无任何工程价值

8. 附加:面试冷门常考复杂度

  • O(sqrt(n)) 开方级:质数枚举、暴力分段查找,性能介于O(logn)与O(n)之间

  • O(1)均摊:单次可能耗时,但多次操作平均为常数(ArrayList扩容、HashMap插入)

9. Java刷题复杂度取舍准则(必背)

  • n ≤ 1000:可接受 O(n²)

  • n ≤ 10^5:必须 O(n) / O(n log n)

  • n ≥ 10^6:只能 O(n) / O(log n) / O(1)

  • n ≥ 10^7:仅 O(1)、O(log n) 可通过

O(1) < O(log n) < O(n) < O(n log n) < O(n²) < O(2ⁿ) < O(n!)

场景适配(Java刷题/面试必记)

O(logn) 二分、树操作;O(nlogn) 排序、堆操作;O(n²) 暴力双重循环;O(2ⁿ) 回溯暴力枚举。

1.1.2 四类复杂度完整定义

  • 最好复杂度:最理想输入场景,参考价值极低,面试基本不考量

  • 最坏复杂度 :算法考试、面试、性能评估唯一标准,代表算法性能上限

  • 平均复杂度:所有输入概率加权平均值,理论性能指标

  • 均摊复杂度 :针对多次操作、少数耗时的特殊场景(Java高频),典型场景:ArrayList动态扩容、HashMap扩容、栈批量入栈、数组均摊删除,单次耗时操作分摊到多次普通操作,整体复杂度降级

1.1.3 递归复杂度主定理(Java 算法进阶·全网最全完整版·可直接面试默写)

一、递归

适用场景 :所有分治递归算法,快速求解时间复杂度,无需逐层展开递归树。

标准递推公式(必背):T(n) = aT(\frac{n}{b}) + f(n)

参数定义(面试常问)

  • a:每次递归拆分出的子问题个数(a ≥ 1)

  • b:每个子问题的规模缩小比例(b > 1)

  • f(n):当前层递归拆分、合并数据的非递归耗时(单层循环耗时)

  • 核心基准值n\^{log_b a} (子问题整体增长量级)

二、 主定理三大核心情况(100% 覆盖所有递归算法)

比较 f(n) 与 n^{log_b a} 的渐进量级大小,分三种情况:

情况一:子问题耗时主导(小于)

若 f(n) = O(n^{log_b a-\varepsilon}) \ (\varepsilon >0),存在常数差值,子问题更耗时

结论:T(n) = O(n^{log_b a})

Java典型例题:二叉树遍历、二分查找

情况二:两层耗时持平(等于·最常用)

若 f(n) = O(n^{log_b a} \cdot log^k n) \ (k\ge0),两层耗时持平

结论:T(n) = O(n^{log_b a} \cdot log^{k+1} n)

Java典型例题:归并排序、快速排序平均情况

情况三:当前层耗时主导(大于)

若 f(n)严格大于 n^{log_b a},且满足正则条件:a\cdot f(\frac{n}{b}) \le c\cdot f(n) \ (c<1)

结论:T(n) = O(f(n))

场景:单层合并耗时远大于子问题递归耗时

✅ 极简背诵口诀(刷题/面试秒判)

小取子、平乘log、大取父

  • f(n) 小:复杂度看子问题 n^{log_b a}

  • f(n) 平:复杂度多乘一个 logn

  • f(n) 大:复杂度直接看 f(n)

三、 Java高频算法 主定理演算实例(必背)

1. 二分查找

递归式:T(n)=T(n/2)+O(1)

a=1,b=2,log_2 1=0,n^0=1,f(n)=O(1) 持平

结果:O(logn)

2. 归并排序

递归式:T(n)=2T(n/2)+O(n)

a=2,b=2,log_2 2=1,n^1=n,f(n)=O(n) 持平

结果:O(nlogn)

3. 快速排序(平均)

递归式:T(n)=2T(n/2)+O(n)

同归并排序,结果:O(nlogn)

4. 二叉树全遍历

递归式:T(n)=2T(n/2)+O(1)

a=2,b=2,log_2 2=1,f(n)更小

结果:O(n)

四、 主定理不适用场景(面试冷门陷阱)

  • 子问题规模不相等(如:T(n)=T(n/2)+T(n/3)+n)

  • 递归非均匀拆分、存在随机递归、贪心递归

  • 此时需使用递归树法手动展开计算

五、 面试标准答题模板(直接背诵默写)

对于分治递归公式 T(n) = aT(n/b)+f(n),首先计算基准量级 n^{log_b a},对比单层耗时 f(n):若持平则多乘对数项;若子问题更慢则取子问题量级;若单层耗时更慢且满足正则条件,则取单层耗时。Java中归并、快排、二分、树遍历均可通过主定理快速判定复杂度。

1.1.4 Java专属空间复杂度细分(面试冷门高频·满分完整版)

常规算法空间复杂度只统计额外辅助空间,但Java面试、考研、大厂笔试会精细区分四类空间,也是递归算法、集合算法高频扣分点。

核心准则:算法空间复杂度只统计「算法主动开辟的额外空间」,原始输入空间不计入

1. 输入空间(不计入复杂度)

定义:题目传入的原始数据占用空间,由调用方传入,并非算法执行过程中主动开辟的空间。

Java场景:方法参数数组、链表、字符串、集合等原始入参。

判定规则:无论输入数据量多大,一律不参与空间复杂度计算

示例:遍历传入的int\[\] nums,数组本身空间不统计,仅统计方法内新建的变量。

2. 辅助空间(核心统计项·必考)

定义:算法执行中,为实现逻辑主动新建的临时空间,是空间复杂度的唯一核心依据。

细分Java场景:

  • 基础变量:int、long、boolean、指针引用等临时变量(O(1)常数空间)

  • 临时容器:新建List、Set、Map、临时数组、队列、栈

  • 缓存空间:前缀和数组、差分数组、DP状态数组、备忘录数组

分类:常数辅助空间O(1) (原地算法)、线性辅助空间O(n)、对数辅助空间O(logn)。

3. 递归栈空间(Java最易遗漏·面试重灾区)

定义:Java虚拟机栈(JVM Stack)在递归调用时,持续压入的栈帧空间,每一层递归对应一个栈帧,包含局部变量、返回地址、操作数栈。

核心特性:

  • 属于算法隐性辅助空间,递归算法必须统计,迭代算法无此项开销

  • 栈深度决定空间复杂度,极易触发 StackOverflowError

经典Java案例对比:

  • 二分递归:递归深度logn → 栈空间O(logn)

  • 二叉树递归遍历:平均logn、最坏n(斜树)→ 最坏空间O(n)

  • 普通数组递归枚举:深度n → 空间O(n)

面试标准答案:递归算法空间复杂度 = 最大递归深度

4. 堆内存空间(工程面试深挖)

定义:算法执行中通过 new 关键字在JVM堆内存开辟的对象与集合空间,由GC负责回收。

Java专属场景:新建List、HashMap、TreeNode、自定义对象、动态数组等。

与栈空间核心区别:

  • 栈空间:随方法结束自动释放,容量小、速度快、有深度限制

  • 堆空间:手动开辟、GC回收、容量大、无固定深度限制

刷题规则:堆中开辟的动态容器空间,全部计入辅助空间复杂度


⭐ 四大空间Java经典例题复盘(直接背面试答案)
  • 迭代反转链表:仅临时指针变量,无递归、无新建容器 → 辅助O(1)、栈O(1) → 总空间O(1)

  • 递归反转链表:无额外容器,但递归深度n → 栈空间O(n) → 总空间O(n)

  • 归并排序:新建临时数组O(n) + 递归栈O(logn) → 总空间O(n)(取最高阶)

  • 快速排序:无额外数组,仅递归栈O(logn)平均 / O(n)最坏 → 总空间O(logn)

  • 前缀和算法:新建前缀和数组O(n) → 总空间O(n)


⚠️ Java面试高频易错陷阱
  • 误区1:递归算法只算变量空间,忽略栈空间 → 直接判错

  • 误区2:把输入参数的数组、集合计入空间复杂度 → 扣分

  • 误区3:归并排序、快排混淆空间量级(归并看数组、快排看栈)

  • 误区4:认为O(1)原地算法无任何空间开销(只是无随n增长的空间,常数变量依旧存在)

✅ 面试标准答题模板(直接默写)

Java算法空间复杂度仅统计算法执行过程中主动开辟的额外辅助空间,不包含原始输入空间。非递归算法仅统计临时变量与堆容器空间;递归算法需要额外统计JVM递归栈帧空间,最终取所有空间的最高阶量级作为整体空间复杂度。

  • 输入空间:原始数据占用空间,不纳入算法空间复杂度统计

  • 辅助空间:算法临时变量、集合、数组等额外空间(统计核心)

  • 递归栈空间:Java虚拟机栈帧开销(递归算法必算,易遗漏)

  • 堆内存空间:new对象、集合容器占用,区别于栈空间

1.2 算法稳定性、原地性与容错性(Java面试必考)

  • 稳定排序:相等元素相对位置不变(归并、插入、冒泡),适合业务有序保留场景

  • 不稳定排序:快排、堆排、选择、希尔,效率更高但会打乱相等元素顺序

  • 原地算法:仅O(1)辅助空间,极低内存开销,适合大数据量处理

  • 自适应算法(新增):根据输入数据特性自动优化性能,Java Arrays.sort() 核心特性,有序数据下效率大幅提升

1.3 Java 算法通用核心技巧(刷题全覆盖·补全细节)

1.3.1 双指针体系(Java刷题万能模型·全网最全完整版)

核心定位 :双指针是Java算法通过率最高、适用性最广、替代暴力循环 的万能解题模型,核心思想:用两个指针的线性移动,将O(n²)暴力枚举优化为O(n)线性遍历,零额外空间或极低空间开销。

四大核心分类:同向双指针、左右双指针、快慢指针、滑动窗口指针,四类覆盖数组、字符串、链表所有高频题型。

一、同向双指针(指针同方向移动)

原理 :慢指针保存结果位置,快指针负责遍历筛选元素,二者同步向右移动,一次遍历完成数据筛选、去重、原地覆盖,典型原地O(1)空间算法

适用场景:有序数组去重、元素移除、数组合并、筛选合法元素、原地修改数组。

Java经典例题:LeetCode26有序数组去重、LeetCode27移除元素、有序数组合并。

java 复制代码
// 有序数组原地去重(同向双指针模板)
public int removeDuplicates(int[] nums) {
    if(nums.length == 0) return 0;
    // slow:合法元素末尾指针,fast:遍历指针
    int slow = 0;
    for(int fast = 1; fast < nums.length; fast++){
        // 发现新元素,慢指针前进并覆盖
        if(nums[fast] != nums[slow]){
            slow++;
            nums[slow] = nums[fast];
        }
    }
    return slow + 1;
}

核心特性:时间O(n)、空间O(1)、原地修改、无需额外容器。

二、左右双指针(对撞指针·有序专属)

原理 :左指针left从头部、右指针right从尾部,双向向中间收缩,根据大小判断逻辑移动指针,仅适用于有序数组/字符串

适用场景:有序数组两数之和、三数之和、反转数组、回文判断、区间收缩、最值查找。

Java经典例题:LeetCode1两数之和(有序)、LeetCode15三数之和、数组反转、验证回文字符串。

java 复制代码
// 左右对撞指针-有序两数之和模板
public int[] twoSum(int[] nums, int target) {
    int left = 0;
    int right = nums.length - 1;
    while(left < right){
        int sum = nums[left] + nums[right];
        if(sum == target){
            return new int[]{left + 1, right + 1};
        }else if(sum < target){
            // 和太小,左指针右移增大和
            left++;
        }else{
            // 和太大,右指针左移减小和
            right--;
        }
    }
    return new int[]{-1,-1};
}

核心特性:一次遍历完成查找,替代双重循环,O(n)时间、O(1)空间。

三、快慢指针(链表专属·面试必考)

原理 :快指针fast每次走2步,慢指针slow每次走1步,利用速度差实现链表定点查找、环判定,仅用于链式结构,数组不可用。

四大必考场景

  • 链表判环:快慢指针相遇则有环

  • 寻找环入口:相遇后慢指针从头、快慢同速遍历,相遇点即为入口

  • 寻找链表中点:快指针到底,慢指针停在中点(偶数取中左)

  • 查找倒数第k节点:快指针先行k步,快慢同速后移

java 复制代码
// 快慢指针判断链表是否有环
public boolean hasCycle(ListNode head) {
    ListNode slow = head;
    ListNode fast = head;
    while(fast != null && fast.next != null){
        slow = slow.next;
        fast = fast.next.next;
        if(slow == fast){
            return true;
        }
    }
    return false;
}

核心特性:无需额外集合记录节点,O(n)时间、O(1)空间,秒杀链表所有经典难题。

四、滑动窗口指针(子串/子数组万能)

原理:左右指针维护一个动态窗口 left, right,右指针扩窗口纳入元素,左指针缩窗口优化条件,动态满足题目约束。

分类:定长滑动窗口、不定长滑动窗口(最长/最短)。

适用场景:最长无重复子串、最小覆盖子串、滑动窗口最值、子数组和问题、字符串匹配。

Java经典例题:LeetCode3最长无重复子串、LeetCode76最小覆盖子串、LeetCode239滑动窗口最大值。

java 复制代码
// 不定长滑动窗口-最长无重复子串模板
public int lengthOfLongestSubstring(String s) {
    Set<Character> set = new HashSet<>();
    int left = 0;
    int maxLen = 0;
    for(int right = 0; right < s.length(); right++){
        // 出现重复,收缩左窗口
        while(set.contains(s.charAt(right))){
            set.remove(s.charAt(left));
            left++;
        }
        set.add(s.charAt(right));
        maxLen = Math.max(maxLen, right - left + 1);
    }
    return maxLen;
}

核心特性:左右指针仅前进不后退,整体O(n)时间,是字符串、子数组问题最优解。


⭐ 双指针四大模型选型口诀(刷题秒选)
  • 数组原地修改、去重筛选 → 同向双指针

  • 有序数组查找、求和、区间 → 左右对撞指针

  • 链表找中点、判环、倒数节点 → 快慢指针

  • 子串、子数组、连续区间最值 → 滑动窗口指针

⚠️ Java双指针高频易错点(面试扣分点)
  • 左右指针必须依赖有序条件,无序数组不可用对撞指针

  • 链表快慢指针循环条件必须判断 fast != null && fast.next != null,防止空指针异常

  • 滑动窗口必须先收缩左边界,再更新结果,顺序颠倒会出错

  • 同向双指针仅适用于原地覆盖场景,无法保留原始数据

✅ 面试标准答题模板(直接背诵)

双指针算法通过两个指针的线性移动,将传统暴力枚举的O(n²)复杂度优化至O(n)。根据场景分为四类:同向双指针用于数组原地筛选去重;左右对撞指针依托有序数组实现区间快速查找;快慢指针解决链表环与定点查找问题;滑动窗口指针动态维护连续区间,解决子串与子数组最值问题,整体空间开销极低,是Java刷题与工程优化的核心模型。

  • 快慢指针:链表专属,判环、找环入口、找中点、倒数第k节点

  • 左右指针:数组有序场景,两数之和、反转数组、二分边界收缩

  • 滑动窗口指针:定长/不定长窗口,解决子串、子数组最值、求和问题

  • 同向双指针:数组合并、去重、有序数组遍历

1.3.2 二分算法(三模板+二分答案·补齐边界·完整实战代码)

核心本质(必背) :二分算法不是只能找目标值,核心是不断缩小合法区间,排除无解区间 。所有二分问题本质只有两件事:区间收缩 + 边界判定

Java 铁律 :永远禁止 (left + right) / 2,整数溢出直接报错,统一使用 left + (right - left) / 2

适用场景:有序数组查找、最值判定、区间边界、二分答案求最值、单调条件判定。


一、模板一:左闭右闭 left, right(最通用·精准查找·面试首选)

适用场景 :数组中精准查找某个目标值,存在返回下标,不存在返回-1。

区间定义 :左右边界都为有效下标,left、right 指向当前有效查找区间。

java 复制代码
// 二分模板1:左闭右闭 [left, right] 精准查找
public int binarySearch(int[] nums, int target) {
    int left = 0;
    int right = nums.length - 1; // 右边界为最后一个有效下标
    
    // 区间有效则循环
    while (left <= right) {
        // 防整数溢出标准写法
        int mid = left + (right - left) / 2;
        if (nums[mid] == target) {
            // 找到目标直接返回
            return mid;
        } else if (nums[mid] < target) {
            // 目标在右侧,排除mid及左侧
            left = mid + 1;
        } else {
            // 目标在左侧,排除mid及右侧
            right = mid - 1;
        }
    }
    // 遍历完无结果
    return -1;
}

核心特征 :循环条件 left <= right、左右边界收缩均±1、精准匹配、无边界残留问题。


二、模板二:左闭右开 [left, right)(区间划分·边界查找)

适用场景 :查找左边界、右边界、首个大于/小于目标值,区间划分类题目。

区间定义:左边界有效,右边界无效,不包含right下标元素。

java 复制代码
// 二分模板2:左闭右开 [left, right) 查找左边界(首个>=target的位置)
public int searchLeftBound(int[] nums, int target) {
    int left = 0;
    int right = nums.length; // 右边界越界,为无效位置
    
    while (left < right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] >= target) {
            // 满足条件,向左收缩,保留mid位置
            right = mid;
        } else {
            // 不满足,向右排除mid
            left = mid + 1;
        }
    }
    // left == right,即为左边界位置
    return left;
}

核心特征 :循环条件 left < right、右边界不-1、天然适配边界查找、无需二次修正。


三、模板三:全开区间 (left, right)(高端复杂判定·进阶场景)

适用场景:复杂单调条件、二分答案极值、严格区间排除场景,刷题冷门但大厂面试必考。

区间定义:左右边界均无效,仅中间区间有效。

java 复制代码
// 二分模板3:全开区间 (left, right) 通用极值判定
public int searchOpenInterval(int[] nums, int target) {
    int left = -1;
    int right = nums.length;
    
    while (left + 1 < right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] < target) {
            left = mid;
        } else {
            right = mid;
        }
    }
    return right;
}

核心特征 :循环条件 left + 1 < right、边界无重复判定、适合无法取等的严格条件。


四、二分答案模板(刷题大杀器·最值问题通解)

核心思想 :不直接遍历求解,猜测答案 + 验证合法性,单调可行区间内找最大/最小合法值。

适用场景:求最小满足条件、最大满足条件、分割数组最值、能力检测类题目。

java 复制代码
/**
 * 二分答案通用模板
 * @param left 答案最小值下界
 * @param right 答案最大值上界
 * @return 最优合法答案
 */
public int binaryAnswer(int left, int right) {
    int ans = 0;
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (isValid(mid)) {
            // 满足条件,记录答案,尝试寻找更优解
            ans = mid;
            // 求最小值:right = mid - 1
            // 求最大值:left = mid + 1
            right = mid - 1;
        } else {
            // 不满足条件,调整区间
            left = mid + 1;
        }
    }
    return ans;
}

// 自定义合法性校验函数(根据题目修改)
private boolean isValid(int val) {
    // 此处替换为题目判定逻辑
    return true;
}

刷题口诀:满足存答案、缩区间找更优;不满足、反向缩区间。


五、二分模板选型对照表(刷题秒选)
  • 精准查找目标值 → 模板一 left,right

  • 找左边界/右边界/插入位置 → 模板二 [left,right)

  • 严格区间、复杂极值判定 → 模板三 (left,right)

  • 最值、能力检测、分割问题 → 二分答案模板


六、Java二分高频易错点(满分避坑)
  • 整数溢出 :必须用 left + (right - left) / 2,杜绝 (left+right)/2

  • 区间不匹配:循环条件必须和区间定义对应,闭区间用&le;、开区间用<

  • 边界收缩错误:已排除的区间必须±1,否则死循环

  • 无序数组二分 :二分仅适用于单调有序结构,无序数组直接失效

  • 二分答案漏存结果:必须用变量记录合法答案,不能直接返回left/right


七、面试标准答题话术(直接背诵)

二分算法基于单调有序特性,通过不断收缩有效查找区间,将O(n)线性查找优化为O(logn)对数级查找。主流分为三种区间模板,适配精准查找与边界查找场景;二分答案模板通过猜答案+合法性校验的思路,解决传统遍历无法高效求解的最值类问题。Java实现需规避整数溢出问题,严格匹配区间定义与循环条件,避免死循环与边界错误。

1.3.3 前缀和与差分(数组区间运算核心·Java刷题必通)

核心定位 :前缀和、差分是数组区间运算的一对万能互补工具 。专门解决两类高频痛点:大量区间查询求和大量区间批量修改,将暴力O(n)单次操作优化为O(1),是数组、矩阵刷题的基础核心,所有区间类题目优先想到该模型。

核心口诀查和用前缀,改区间用差分


一、一维前缀和(区间求和神器)
1. 原理定义

前缀和数组 preSumpreSum[i] 表示原数组前 i 个元素的累加和。采用下标从1开始的定义,规避边界判0问题,是Java刷题标准写法。

递推公式:preSumi = preSumi-1 + numsi-1

区间 [l, r](原数组下标,从0开始)求和公式:sum = preSumr+1 - preSuml

2. Java完整实战模板
java 复制代码
// 一维前缀和构建 + 区间查询
public class PreSum {
    // 构建前缀和数组
    public static int[] buildPreSum(int[] nums) {
        int n = nums.length;
        int[] preSum = new int[n + 1];
        // preSum[0] = 0,默认初始值,无需赋值
        for (int i = 1; i <= n; i++) {
            preSum[i] = preSum[i - 1] + nums[i - 1];
        }
        return preSum;
    }

    // 查询原数组 [l, r] 区间和(l、r为原数组下标,闭区间)
    public static int getRangeSum(int[] preSum, int l, int r) {
        return preSum[r + 1] - preSum[l];
    }

    // 测试
    public static void main(String[] args) {
        int[] nums = {1,2,3,4,5};
        int[] preSum = buildPreSum(nums);
        // 查询[1,3]区间和:2+3+4=9
        System.out.println(getRangeSum(preSum, 1, 3));
    }
}
3. 适用场景&刷题考点
  • 多次查询任意子数组区间和、子数组平均数

  • LeetCode高频题:和为K的子数组、连续子数组的最大和、前缀和求奇偶区间

  • 可结合哈希表优化,解决子数组计数、最值问题

4. 核心特性

预处理O(n),单次查询O(1),适合查询多、修改少的场景。


二、一维差分(区间批量修改神器)
1. 原理定义

差分数组 diff,专门用于区间批量加减。无需遍历区间,仅需两次操作即可完成整个区间数值修改,最后还原数组。同样采用1下标初始化,规避边界问题。

核心操作:对区间 [l, r] 所有元素加 val

  • diff[l] += val:区间起点生效

  • diff[r+1] -= val:区间终点后撤销生效

最后对差分数组求前缀和,即可得到修改后的原数组。

2. Java完整实战模板
java 复制代码
// 一维差分模板:区间批量修改
public class DiffArray {
    // 初始化差分数组
    public static int[] buildDiff(int[] nums) {
        int n = nums.length;
        int[] diff = new int[n + 2]; // 多开2位,防止r+1越界
        for (int i = 0; i < n; i++) {
            diff[i + 1] = nums[i] - nums[i - 1];
        }
        return diff;
    }

    // 区间[l,r] 所有元素 + val
    public static void rangeAdd(int[] diff, int l, int r, int val) {
        diff[l] += val;
        diff[r + 1] -= val;
    }

    // 还原为修改后的原数组
    public static int[] restoreArray(int[] diff, int n) {
        int[] res = new int[n];
        res[0] = diff[1];
        for (int i = 1; i < n; i++) {
            res[i] = res[i - 1] + diff[i + 1];
        }
        return res;
    }

    // 测试
    public static void main(String[] args) {
        int[] nums = {1,2,3,4,5};
        int n = nums.length;
        int[] diff = buildDiff(nums);
        // 对区间[1,3]全部+2
        rangeAdd(diff, 1, 3, 2);
        int[] res = restoreArray(diff, n);
        // 结果:[1,4,5,6,5]
    }
}
3. 适用场景&刷题考点
  • 频繁对数组区间进行批量加、批量减操作

  • 试卷覆盖、区间涂色、次数统计、差分计数问题

4. 核心特性

单次区间修改O(1),最终数组还原O(n),适合修改多、查询少的场景,完美互补前缀和。


三、二维前缀和(子矩阵求和核心)
1. 原理公式

针对二维矩阵,快速查询任意子矩阵和,依旧采用1下标规避边界问题。

构建公式:preij = prei-1j + preij-1 - prei-1j-1 + matrixi-1j-1

查询公式:以(x1,y1)为左上、(x2,y2)为右下的子矩阵 和

sum = prex2y2 - prex1-1y2 - prex2y1-1 + prex1-1y1-1

2. Java精简模板
java 复制代码
// 二维前缀和构建 & 子矩阵查询
public static int[][] build2DPresum(int[][] matrix) {
    int m = matrix.length, n = matrix[0].length;
    int[][] pre = new int[m + 1][n + 1];
    for (int i = 1; i <= m; i++) {
        for (int j = 1; j <= n; j++) {
            pre[i][j] = pre[i-1][j] + pre[i][j-1] - pre[i-1][j-1] + matrix[i-1][j-1];
        }
    }
    return pre;
}

// 查询子矩阵和
public static int get2DRangeSum(int[][] pre, int x1, int y1, int x2, int y2) {
    return pre[x2][y2] - pre[x1-1][y2] - pre[x2][y1-1] + pre[x1-1][y1-1];
}

四、二维差分(矩阵区间批量修改)

用于二维矩阵子矩阵批量加减,O(1)完成区间修改,O(nm)还原矩阵,是矩阵进阶刷题核心。

核心操作(子矩阵x1,y1-x2,y2加val)
  • diff[x1][y1] += val

  • diff[x1][y2+1] -= val

  • diff[x2+1][y1] -= val

  • diff[x2+1][y2+1] += val


五、高频易错点(Java刷题扣分点)
  • 下标混乱:必须统一前缀和/差分1下标、原数组0下标,杜绝边界越界

  • 差分越界:差分数组必须多开2位空间,避免r+1、x2+1下标溢出

  • 操作颠倒:前缀和先预处理再查询,差分先批量修改再还原数组

  • 场景误用:大量查询用前缀和,大量区间修改用差分,不可混用


六、面试标准背诵话术

前缀和与差分是数组区间运算的互补算法。前缀和通过O(n)预处理,实现任意区间和O(1)查询,适用于多查询少修改场景;差分数组通过两次标记实现O(1)区间批量修改,最后通过前缀和还原数组,适用于多修改少查询场景。二维前缀和与差分可拓展至矩阵区间运算,是数组、矩阵刷题的基础核心模型,Java实现统一采用1下标规避边界问题,有效减少越界与判空错误。

1.3.4 四大基础算法思想(完整版定义+判定条件+Java企业实战代码+真题)

核心总述(面试必背) :所有算法题目,100% 逃不出四大思想。刷题第一步不是写代码,而是先判定属于哪种算法思想。四大思想是所有复杂算法(双指针、二分、图论、树论、高级DP)的底层基石。

极速判定口诀

  • 能拆分子问题、子问题可合并 → 分治

  • 每一步只选局部最优、无后效性 → 贪心

  • 需要枚举所有组合/排列、尝试回溯撤销 → 回溯

  • 重复子问题、最优子结构、状态转移 → 动态规划


一、分治算法(Divide & Conquer)
1. 标准定义

分治即分而治之,将一个大规模复杂问题,递归拆分为若干个规模相同的独立子问题,求解所有子问题后,合并子问题结果得到原问题答案。

2. 三大核心步骤(固定模板)
  • 分(Divide):将原问题对半/均分拆分

  • 治(Conquer):递归求解子问题,子问题规模最小后直接返回结果

  • 合(Combine):合并所有子问题答案

3. 适用严格条件
  • 子问题与原问题结构完全一致

  • 所有子问题相互独立、无关联

  • 子问题解可合并为原问题解

4. Java经典企业实战代码(归并排序·标准分治模板)
java 复制代码
/**
 * 分治标准实战:归并排序(满分模板)
 * 分:数组对半拆分
 * 治:递归排序左右子数组
 * 合:有序数组合并
 */
public class DivideConquerDemo {
    public void mergeSort(int[] nums, int left, int right) {
        // 递归终止条件:区间只有一个元素,无需排序
        if (left >= right) {
            return;
        }
        // 1. 分:中点拆分
        int mid = left + (right - left) / 2;
        // 2. 治:递归求解左右子问题
        mergeSort(nums, left, mid);
        mergeSort(nums, mid + 1, right);
        // 3. 合:合并两个有序区间
        merge(nums, left, mid, right);
    }

    // 合并两个有序数组
    private void merge(int[] nums, int left, int mid, int right) {
        int[] temp = new int[right - left + 1];
        int i = left, j = mid + 1, k = 0;
        // 双指针合并有序区间
        while (i <= mid && j <= right) {
            temp[k++] = nums[i] <= nums[j] ? nums[i++] : nums[j++];
        }
        // 补全剩余元素
        while (i <= mid) temp[k++] = nums[i++];
        while (j <= right) temp[k++] = nums[j++];
        // 覆盖原数组
        System.arraycopy(temp, 0, nums, left, temp.length);
    }
}
5. 高频Java场景&真题
  • 算法:归并排序、快速排序、二分查找、二叉树遍历

  • 真题:逆序对统计、寻找数组第K大、合并有序数组

  • 工程:大数据分片处理、多线程任务拆分、日志分片解析


二、贪心算法(Greedy)
1. 标准定义

贪心算法每一步只做当前局部最优选择,不考虑未来、不回溯、不预判,通过持续局部最优累积得到全局最优解。

2. 两大硬性适用条件(缺一不可)
  • 最优子结构:局部最优可以推导全局最优

  • 无后效性:当前选择不会影响未来所有决策

致命误区:局部最优≠全局最优时,绝对不能用贪心(只能用DP)

3. Java企业实战代码(经典区间覆盖·活动选择)
java 复制代码
import java.util.Arrays;
import java.util.Comparator;

/**
 * 贪心经典实战:最多不重叠区间(LeetCode435)
 * 贪心策略:优先选结束时间最早的区间,留出更多后续空间
 */
public class GreedyDemo {
    public int eraseOverlapIntervals(int[][] intervals) {
        if (intervals.length == 0) return 0;
        // 1. 贪心预处理:按区间结束时间升序排序
        Arrays.sort(intervals, Comparator.comparingInt(a -> a[1]));
        
        int count = 1;
        int lastEnd = intervals[0][1];
        // 2. 逐一遍历,局部最优选择
        for (int i = 1; i < intervals.length; i++) {
            int start = intervals[i][0];
            // 当前区间不重叠,选择该区间(局部最优)
            if (start >= lastEnd) {
                count++;
                lastEnd = intervals[i][1];
            }
        }
        return intervals.length - count;
    }
}
4. 高频Java场景&真题
  • 算法:区间调度、零钱兑换(固定币种)、跳跃游戏、最大子数组贪心解

  • 工程:资源调度、任务优先级分配、流量限流策略、日志最优筛选


三、回溯算法(Backtracking)
1. 标准定义

回溯是深度优先枚举+状态撤销 的暴力搜索算法,通过递归遍历所有可能解,遇到不合法路径则回退上一层状态,继续尝试其他路径,本质是带剪枝的暴力枚举

2. 核心四步模板(万能通用)
  • 路径:当前已做出的选择

  • 选择列表:当前可选择的范围

  • 约束条件:筛选合法选择、剪枝无效路径

  • 回溯撤销:递归返回后,恢复状态,尝试下一个选择

3. Java企业实战代码(子集问题·回溯万能模板)
java 复制代码
import java.util.ArrayList;
import java.util.List;

/**
 * 回溯万能实战:子集问题(LeetCode78)
 * 覆盖所有排列、组合、子集通用逻辑
 */
public class BackTrackDemo {
    List<List<Integer>> res = new ArrayList<>();
    List<Integer> path = new ArrayList<>();

    public List<List<Integer>> subsets(int[] nums) {
        backtrack(nums, 0);
        return res;
    }

    // 回溯核心递归函数
    private void backtrack(int[] nums, int start) {
        // 1. 记录当前路径(所有节点都是合法解)
        res.add(new ArrayList<>(path));
        // 2. 遍历所有选择
        for (int i = start; i < nums.length; i++) {
            // 3. 做选择
            path.add(nums[i]);
            // 4. 递归深入
            backtrack(nums, i + 1);
            // 5. 回溯撤销(核心!恢复状态)
            path.remove(path.size() - 1);
        }
    }
}
4. 高频Java场景&真题
  • 算法:子集、组合、全排列、N皇后、迷宫搜索、括号生成

  • 优化核心:剪枝,剔除无效路径,将O(2ⁿ)暴力复杂度大幅优化

  • 工程:权限组合匹配、路径搜索、多条件枚举筛选


四、动态规划(Dynamic Programming·DP)
1. 标准定义

动态规划是优化后的递归 ,通过记忆化存储重复子问题 结果,避免重复计算,基于最优子结构推导状态转移,是解决最值、计数、状态推演的最优算法。

2. DP三大铁律(判定依据)
  • 重复子问题:递归过程中存在大量重复计算

  • 最优子结构:子问题最优解可推导全局最优解

  • 无后效性:当前状态仅依赖前置状态,与后续状态无关

3. DP五步解题法(刷题满分流程)

确定状态定义 → 推导状态转移方程 → 初始化边界 → 遍历顺序 → 结果输出

4. Java企业实战代码(经典爬楼梯·入门DP模板)
java 复制代码
/**
 * 动态规划实战:爬楼梯(LeetCode70)
 * 标准DP流程:状态定义+转移方程+边界初始化+遍历
 */
public class DpDemo {
    public int climbStairs(int n) {
        // 边界初始化
        if (n <= 2) return n;
        // 1. 状态定义:dp[i] 爬到第i阶的方法数
        int[] dp = new int[n + 1];
        // 2. 初始边界
        dp[1] = 1;
        dp[2] = 2;
        // 3. 状态转移:第i阶 = i-1阶走1步 + i-2阶走2步
        for (int i = 3; i <= n; i++) {
            dp[i] = dp[i - 1] + dp[i - 2];
        }
        return dp[n];
    }

    // 空间优化版:滚动数组O(1)空间
    public int climbStairsOpt(int n) {
        if (n <= 2) return n;
        int a = 1, b = 2, res = 0;
        for (int i = 3; i <= n; i++) {
            res = a + b;
            a = b;
            b = res;
        }
        return res;
    }
}
5. 高频Java场景&真题
  • 基础DP:爬楼梯、打家劫舍、最大子数组和

  • 进阶DP:背包问题、最长公共子序列、编辑距离

  • 工程:订单状态流转、风控评分计算、路径最优求解


五、四大算法思想终极选型对照表(刷题秒选)
算法思想 核心特征 时间复杂度 适用场景
分治 拆分独立子问题、合并结果 O(nlogn) 排序、二分、数据拆分
贪心 局部最优、无回溯、无预判 O(nlogn) 区间调度、资源最优分配
回溯 枚举所有解、状态回溯、可剪枝 O(2ⁿ)/O(n!) 排列组合、路径搜索、枚举类问题
动态规划 记忆化去重、状态转移、求最值 O(n²)/O(n) 最值、计数、状态推演、重复子问题

六、高频易错点(刷题扣分避坑)
  • 贪心误区:盲目局部最优,未验证无后效性,导致全局解错误

  • 分治误区:子问题不独立、不可合并,强行使用分治失效

  • 回溯误区:忘记状态撤销,导致路径污染、结果重复

  • DP误区:状态定义模糊、转移方程推导错误、边界初始化遗漏


七、面试标准背诵话术(直接默写)

算法四大核心思想包含分治、贪心、回溯与动态规划。分治通过拆分独立子问题、递归求解后合并结果,高效解决拆分类问题;贪心依靠每一步局部最优选择,在无后效性场景下快速得到全局最优解;回溯通过深度枚举+状态回退,适配所有排列组合、路径搜索类暴力场景,可通过剪枝优化效率;动态规划通过记忆化存储重复子问题结果,依托最优子结构完成状态转移,是解决最值与计数问题的核心优化思想,四大思想覆盖所有算法题型的底层逻辑。

  • 分治:分而治之,拆分问题+递归求解+合并结果(归并排序、快速排序、二叉树遍历)

  • 回溯:深度优先搜索+状态回退,解决组合、排列、子集、迷宫问题

  • 贪心:局部最优推导全局最优,无后效性(活动选择、零钱兑换、区间覆盖)

  • 动态规划:记忆化重复子问题、最优子结构,解决最值、计数、状态转移问题

1.4 Java 内存与算法性能底层原理(面试深挖·满分完整版)

本章节为Java算法面试高阶深挖考点,区别于通用算法理论,聚焦JVM内存模型、硬件缓存机制、存储结构底层差异、算法性能瓶颈根源,是大厂中高级面试、工程算法优化、刷题性能调优的核心加分点,所有知识点对标阿里、腾讯、字节面试真题。

1)顺序存储 vs 链式存储 底层核心差异(算法性能根源)

数组与链表是所有数据结构的底层基石,二者的性能差异本质是内存存储形式差异,直接决定算法选型、时间复杂度、硬件执行效率。

  • 顺序存储(数组/ArrayList):内存连续排布,元素地址依次递增,天然适配CPU缓存局部性,随机访问O(1)极速响应;但固定连续内存空间,插入、删除元素需要批量移位,产生O(n)时间开销,动态扩容会触发内存重分配与数组拷贝,存在短时性能抖动。

  • 链式存储(链表/LinkedList):内存离散分布,每个节点独立开辟堆空间,通过引用指针关联,无连续内存约束,增删仅需修改指针指向,无需移动元素,O(1)增删效率;但节点内存碎片化,无法命中CPU高速缓存,遍历必须逐节点寻址,缓存失效严重,整体遍历效率远低于数组,且存在大量指针引用开销。

面试核心结论:查询、遍历优先用数组,频繁头尾增删优先用链表;大数据量遍历场景,数组性能碾压链表,哪怕链表算法时间复杂度更低,实际执行效率仍不如数组。

2)Java 栈内存 vs 堆内存(算法空间复杂度核心依据)

算法执行的所有内存开销,最终都落在JVM栈内存与堆内存,二者的特性直接决定空间复杂度判定、递归栈溢出、算法内存开销、GC压力

  • JVM栈内存(线程私有) :存放方法局部变量、基本数据类型、方法栈帧、递归栈帧、对象引用地址;空间固定且极小,默认栈深度有限,执行速度极快,方法执行结束后内存自动释放,无GC开销。递归算法的空间复杂度核心由栈帧深度决定,深层递归会触发StackOverflowError,是Java算法独有报错场景。

  • JVM堆内存(线程共享):存放所有new出来的对象、数组、集合容器(List/Map/Set)、TreeNode等自定义数据结构;空间极大,支持海量数据存储,但内存开辟速度慢,依赖GC回收内存,频繁创建销毁对象会产生大量GC垃圾,导致算法吞吐量下降、性能抖动。所有算法主动开辟的动态容器空间,全部计入堆内存开销,是空间复杂度核心统计项。

高频易错点:刷题时仅统计堆内存辅助空间+递归栈空间,方法内临时基本变量属于常数栈空间,不计入渐进式空间复杂度。

3)CPU缓存局部性原理(Java算法工程优化核心)

算法的理论时间复杂度不等于实际执行效率,CPU缓存局部性是Java工程算法优化的底层核心,也是大厂面试深挖的高频考点,分为时间局部性与空间局部性。

(1)两大局部性核心原理
  • 空间局部性:CPU加载数据时,会一次性加载当前数据及相邻内存数据到高速缓存,后续访问相邻内存数据可直接读取缓存,无需访问低速主存。数组连续内存完美适配该特性,链表离散内存完全无法利用。

  • 时间局部性:近期被访问的数据,大概率会被再次访问,CPU会缓存该数据,重复访问极速响应。循环遍历、重复查询的算法可充分利用该特性。

(2)算法实战优化规则
  • 一维数组必须顺序遍历,禁止随机跳跃遍历,避免缓存失效;

  • 二维数组必须行优先遍历(Java数组行连续),列遍历会彻底失效缓存,性能暴跌数倍;

  • 同等时间复杂度下,连续内存结构(数组)执行效率 > 离散结构(链表、哈希表);

  • 大数据量算法优先采用数组实现,舍弃链表、集合嵌套结构,减少缓存失效。

4)Java算法核心性能瓶颈底层解析(面试必问)

(1)扩容性能瓶颈(ArrayList/HashMap核心痛点)

Java动态集合均存在扩容机制,扩容并非简单扩空间,而是新建更大容量数组 + 全量数据拷贝,会产生高额内存开销与时间开销,触发GC,是工程算法性能抖动的核心原因。

  • ArrayList:默认容量10,1.5倍扩容,每次扩容需数组拷贝,海量数据频繁新增会多次扩容,性能极差;

  • HashMap:默认容量16,2倍扩容,扩容需重新哈希散列、迁移数据,开销远大于ArrayList;

  • 工程优化方案:提前预估数据量,初始化集合时指定初始容量,彻底避免动态扩容。

(2)内存碎片化瓶颈

链表、频繁new小对象、临时集合创建,会产生大量离散内存碎片,导致JVM内存利用率下降,GC频繁触发Full GC,大幅降低算法吞吐量。

(3)引用访问开销

链表、树形结构、嵌套集合需要通过引用寻址,每次访问都需要从栈引用定位堆对象,存在二次寻址开销,远大于数组直接内存访问。

5)递归 vs 迭代 底层性能差异(面试高频对比)

递归与迭代功能等价,但底层内存与性能天差地别,是算法选型的核心依据。

  • 递归:代码简洁、逻辑清晰,无需手动维护循环状态;但持续占用栈帧,栈深度过大触发栈溢出,频繁方法调用存在方法压栈、出栈开销,性能较差,仅适用于树、分治等逻辑复杂的场景。

  • 迭代:仅占用少量栈变量,无栈溢出风险,无方法调用开销,可复用内存空间,性能极致;但代码冗余,需要手动维护遍历状态,是大数据量算法的首选实现方式。

工程选型准则:小数据量、面试刷题可用递归简化代码;大数据量、线上工程代码必须迭代实现,规避栈溢出与性能损耗。

6)Java专属算法内存优化方案(工程实战)

  • 原地算法优先:优先使用O(1)辅助空间的原地修改算法(同向双指针、原地反转),减少堆内存开辟,降低GC压力;

  • 滚动数组优化DP:多维DP数组替换为滚动变量/一维数组,舍弃无效历史状态,将O(n)空间压缩为O(1);

  • 预初始化集合容量:所有ArrayList、HashMap初始化指定预估容量,杜绝动态扩容开销;

  • 复用临时对象:循环、算法迭代中避免频繁new对象,复用已有临时变量,减少内存碎片;

  • 递归迭代转换:深度递归算法手动转为迭代+栈模拟,规避栈溢出,提升执行效率。

7)面试真题标准答题模板(直接默写满分)

Java算法性能不仅取决于理论时间复杂度,更受JVM内存模型与硬件缓存机制影响。顺序存储数组内存连续,适配CPU缓存局部性,随机访问与遍历效率极高,但增删存在移位开销;链式存储内存离散,增删高效但缓存失效严重,遍历性能差。算法内存开销分为栈内存与堆内存,栈内存用于存放局部变量与递归栈帧,深度递归易触发栈溢出;堆内存用于存放动态集合与对象,是算法辅助空间与GC压力的核心来源。工程优化核心为规避集合扩容、利用CPU缓存、递归转迭代、原地空间优化,在理论复杂度基础上最大化算法实际执行效率。

1.5 算法通用易错点(Java专属避坑·完整版)

本节汇总Java独有的算法刷题、面试、工程实战易错点,区别于通用算法误区,全部为高频扣分、超时、报错核心问题,配套标准规避方案,可直接背诵落地,彻底解决Java算法实战各类隐性bug。

1. 递归栈溢出问题(Java专属致命报错)

核心误区:默认递归可无限深度执行,忽略JVM栈内存限制。Java虚拟机栈空间狭小,默认栈深度仅几千层,深度递归(如n>1000的数组递归、斜树遍历)必然触发 StackOverflowError。 规避方案:大数据量场景优先将递归转为迭代实现;树递归控制遍历深度,复杂分治算法增加深度判定阈值。

2. 整数溢出高危陷阱(刷题第一报错点)

核心误区:所有数值运算直接使用int类型,忽略Java int取值范围(-2³¹~2³¹-1)。二分求和、数组累加、阶乘、幂运算、mid计算极易溢出,导致数值负数、结果错乱、死循环。

规避方案:涉及累加、最值、中间计算的变量统一用 long 类型;二分mid严格使用 left + (right - left) / 2,杜绝 (left + right) / 2

3. 空指针异常NPE(Java算法高频坑)

核心误区:链表、二叉树、集合遍历未做空判断,直接调用属性/方法。链表节点next、二叉树左右子树、空集合直接遍历,是刷题和面试最常见报错。

规避方案:遍历前先判空,递归终止条件优先拦截null节点;使用迭代遍历链式结构时,循环条件前置非空判定;集合操作先判断 isEmpty()

4. 集合动态扩容性能与数据错乱坑

核心误区:循环内频繁新增元素、未初始化集合容量,触发多次扩容拷贝。ArrayList 1.5倍扩容、HashMap 2倍扩容会新建数组、拷贝全量数据、HashMap重新哈希迁移元素,造成严重性能损耗,海量数据场景直接超时。

规避方案:提前预估数据量,初始化时指定集合初始容量;高频新增场景复用集合对象,避免循环内频繁new集合。

5. 数组下标越界(边界判定核心坑)

核心误区:二分区间、滑动窗口、前缀和、差分算法下标混乱,混淆原数组0下标与前缀和/差分1下标;循环终止条件错误,导致访问不存在的下标。

规避方案:统一编码规范:原数组0下标、前缀和/差分1下标;严格匹配区间定义与循环条件,开区间用<、闭区间用<=;差分数组默认多开2位空间,防止r+1、x2+1越界。

6. 递归回溯状态污染(结果重复/数据错乱)

核心误区:回溯算法做完选择后,忘记撤销状态,导致路径污染、结果重复、子集/排列答案错误。

规避方案:严格遵循「做选择→递归→撤销选择」三步模板,集合、路径变量必须回溯还原,保证每一层递归状态独立。

7. 无序数组滥用有序算法(逻辑失效坑)

核心误区:不判断数组单调性,直接对无序数组使用二分、左右对撞双指针,算法逻辑完全失效,结果全部错误。

规避方案:二分、左右指针、单调栈、有序区间类算法,必须前置判定数据是否单调有序,无序数据优先使用遍历、滑动窗口、暴力枚举优化。

8. 空间复杂度统计误区(面试扣分重灾区)

核心误区:统计空间复杂度时,误将输入参数数组/集合计入空间开销;忽略递归栈隐性空间;混淆栈内存与堆内存统计规则。

规避方案:仅统计算法主动开辟的辅助空间、堆容器空间、递归栈空间;原始输入数据一律不计入,最终取最高阶量级。

9. 集合遍历增删报错(并发修改异常)

核心误区:普通for/foreach遍历ArrayList、HashMap时,直接增删元素,

触发 ConcurrentModificationException

规避方案:遍历中删除元素使用迭代器Iterator;新增元素提前预处理,遍历过程仅做查询与统计,不修改集合结构。

10. 浮点数精度丢失(最值/比较坑)

核心误区:使用double/float做等值判断、数值比较,Java浮点类型存在二进制精度丢失,导致正常相等数值判定为不相等。

规避方案:浮点比较通过极小误差值判定,禁止直接 == 判断;算法数值计算优先使用整数运算,规避浮点精度问题。

11. 滑动窗口顺序错误(结果漏算/错算)

核心误区:滑动窗口算法先更新结果、再收缩左边界,导致窗口条件未合规就统计数据,答案错误。

规避方案:固定执行顺序:右指针扩窗口→收缩左边界保证窗口合法→更新最值/统计结果。

12. 二分答案漏存合法结果(极值求解失效)

核心误区:二分答案题型直接返回left/right,未用变量记录合法答案,边界偏移导致最优解丢失。

规避方案:所有二分答案题目,必须单独定义ans变量存储每一次合法值,最终返回最优记录结果。

13. 链表遍历空指针循环坑

核心误区:快慢指针、链表遍历循环条件缺失,仅判断fast!=null,未判断fast.next!=null,导致fast.next.next空指针报错。

规避方案:链表快慢指针固定循环条件:fast != null && fast.next != null

14. 原地算法误区(数据覆盖丢失)

核心误区:滥用同向双指针等原地覆盖算法,未备份原始数据,导致后续需要原始数据的逻辑失效。

规避方案:仅在无需保留原数组数据的场景使用原地算法,需要原始数据时提前拷贝备份。

15. 贪心算法全局最优误区

核心误区:盲目使用局部最优推导全局最优,未验证无后效性,导致算法逻辑不成立、答案错误。

规避方案:无法确定无后效性、局部最优无法推导全局最优的场景,优先使用动态规划替代贪心。

16.Java算法避坑通用准则(直接背诵)

Java算法实战需兼顾语法特性、JVM内存机制、数据结构底层、算法逻辑四大维度。数值运算防溢出、链式结构防空指针、递归遍历防栈溢出、集合操作防扩容与并发修改、边界判定防下标越界,严格遵循各类算法模板执行顺序与适用条件,区分通用算法逻辑与Java独有特性,即可规避99%的刷题报错与面试扣分点。

第二章 线性数据结构(JDK 全部底层)

2.1 数组(Java 最核心结构·面试/刷题/工程全覆盖)

核心定位 :数组是Java所有线性数据结构的基石,是唯一支持随机访问的基础结构,适配CPU缓存局部性,遍历性能碾压所有链式结构,90%以上的刷题算法、工程数据存储、集合底层优化均基于数组实现,是Java算法体系的核心根基。

2.1.1 核心本质与底层特性(面试必背)

  • 内存连续性 :Java静态数组在堆内存中开辟连续内存空间,元素地址连续递增,首个元素地址为数组基地址,可通过偏移量精准定位任意元素,实现O(1)随机访问。

  • 固定长度特性:静态数组初始化后长度不可修改,内存空间固定,不存在碎片问题,但无法动态适配数据量增减,是静态数组的核心短板。

  • 类型统一性:数组所有元素必须为同一数据类型(基本类型/引用类型),内存占用均匀,无类型转换开销,读写效率极高。

  • 缓存友好性:完美契合CPU空间局部性原理,顺序遍历可批量命中高速缓存,大数据量遍历性能远超链表、哈希表等离散结构。

  • 下标特性:统一0下标起始,下标范围 0, length-1,越界直接抛出ArrayIndexOutOfBoundsException(Java高频运行时异常)。

2.1.2 Java数组完整分类(静态+动态·全覆盖)

(1).静态数组(基本数组·JDK原生)

初始化时固定长度,内存一次性分配,全程不可扩容缩容,分为基本类型数组与引用类型数组。

定义与初始化标准写法

java 复制代码
// 1. 先声明、后初始化(默认初始值)
int[] nums = new int[5]; // 基本类型默认0,boolean默认false,引用类型默认null

// 2. 声明并直接赋值(长度由元素个数决定)
int[] arr = {1,2,3,4,5};

// 3. 标准完整写法(面试规范)
String[] strs = new String[]{"Java","数据结构","算法"};

静态数组默认初始值(面试高频)

  • 基本数值类型(byte/short/int/long/float/double):默认 0 / 0.0

  • 布尔类型 boolean:默认 false

  • 引用类型(String/对象/数组):默认 null

(2).动态数组(ArrayList·工程核心)

JDK封装的动态扩容数组,底层基于静态数组实现,解决静态数组长度固定的痛点,是Java工程开发、刷题最常用的数组结构。

核心底层规则(必背面试点)

  • 底层存储:transient Object[] elementData 静态数组

  • 默认容量:空参初始化默认容量10(JDK1.8)

  • 扩容机制:当前容量不足时,1.5倍扩容(oldCapacity + (oldCapacity >> 1))

  • 扩容流程:新建更大容量数组 → 数组拷贝(System.arraycopy)→ 替换原数组 → 原数组GC回收

  • 空数组优化:JDK1.7+ 空实例共享空数组常量,减少内存开销

2.1.3 核心时间复杂度(精准对标刷题/面试)

  • 随机访问(根据下标查值):O(1),直接内存偏移量定位,极速响应

  • 顺序遍历:O(n),缓存友好,实际执行效率优于同复杂度链表遍历

  • 尾部增删:O(1)(无扩容场景),仅需赋值/删除末尾元素,无需移位

  • 头部/中间增删:O(n),需批量移动后续所有元素,产生大量数据拷贝开销

  • 查找最值/包含元素:无序数组O(n),有序数组可二分优化为O(logn)

2.1.4 一维数组 vs 二维数组(Java专属)

(1). 一维数组

最基础线性结构,单一连续内存空间,适配所有一维遍历、区间运算、双指针、二分算法,是前缀和、差分算法的核心载体。

(2). 二维数组(不规则数组·Java独有)

Java二维数组非纯矩阵结构 ,本质是「数组的数组」,每一行数组可独立分配内存,支持不规则行列(各行长度不一致),区别于C/C++固定矩阵数组。

java 复制代码
// Java不规则二维数组(合法)
int[][] diffArr = new int[3][];
diffArr[0] = new int[2];
diffArr[1] = new int[5];
diffArr[2] = new int[3];

核心特性:行内存连续、行与行之间内存离散;遍历必须行优先(适配CPU缓存),列优先遍历缓存彻底失效,性能暴跌。

2.1.5 数组核心算法模型(刷题全覆盖)

所有数组刷题题型,全部基于以下核心模型,无额外难点:

  • 双指针体系:同向去重、左右对撞求和、快慢指针筛选、滑动窗口区间最值(数组最优优化模型,O(n)替代O(n²)暴力)

  • 二分查找:有序数组精准查找、边界查找、二分答案极值求解

  • 前缀和/差分:区间求和、批量区间修改,O(n)预处理+O(1)单次操作

  • 原地算法:数组反转、元素移除、有序数组合并,O(1)空间优化

  • 排序算法:各类排序底层载体,排序后可适配所有有序类算法

  • 哈希映射:数组元素频次统计、两数之和、重复元素判定

2.1.6 Java数组高频易错点(刷题扣分避坑)

  • 下标越界:严格区分0下标原数组、1下标前缀和/差分数组,杜绝r+1、mid越界

  • ArrayList扩容超时:海量数据循环新增未指定初始容量,多次扩容拷贝导致超时,工程必须预分配容量

  • 二维数组遍历低效:违规列优先遍历,缓存失效,大数据量性能暴跌

  • 静态数组不可变:混淆静态数组与ArrayList,试图修改静态数组长度,引发编译异常

  • 数组拷贝误区 :直接赋值 arr1 = arr2 为引用传递,修改一方影响另一方,深拷贝必须使用Arrays.copyOf、System.arraycopy

  • 空数组判断遗漏:未前置判断数组长度为0,直接遍历导致下标越界

2.1.7 数组工具类核心方法(Java工程/刷题必备)

java.util.Arrays 专属工具类,覆盖数组所有常用操作,刷题快速上手、工程简化代码:

java 复制代码
import java.util.Arrays;

public class ArrayUtilDemo {
    public static void main(String[] args) {
        int[] nums = {3,1,4,2,5};
        
        // 1. 数组排序(底层优化快排)
        Arrays.sort(nums);
        
        // 2. 数组拷贝(深拷贝,独立内存)
        int[] copyArr = Arrays.copyOf(nums, nums.length);
        
        // 3. 数组填充默认值
        int[] newArr = new int[5];
        Arrays.fill(newArr, 0);
        
        // 4. 数组转字符串(快速打印)
        System.out.println(Arrays.toString(nums));
        
        // 5. 二分查找(有序数组专属,返回下标)
        int index = Arrays.binarySearch(nums, 3);
        
        // 6. 数组比较(逐元素比对)
        boolean isEqual = Arrays.equals(nums, copyArr);
    }
}

2.1.8 工程级数组优化方案(大厂实战)

  • 预初始化容量:ArrayList、动态数组场景,提前预估数据量,初始化指定容量,彻底避免动态扩容开销

  • 优先使用静态数组:固定数据量场景,舍弃ArrayList,使用静态数组减少对象开销与GC压力

  • 行优先遍历:二维数组强制行优先遍历,最大化利用CPU缓存,提升3-10倍遍历性能

  • 原地算法优先:数组修改场景优先使用双指针原地操作,减少额外数组开辟,降低空间复杂度

  • 避免频繁数组拷贝:批量操作复用原数组,减少System.arraycopy高频调用

2.1.9 面试标准满分答题模板(直接默写)

Java数组分为静态数组与动态ArrayList,静态数组内存连续、长度固定、支持O(1)随机访问,适配CPU缓存局部性,遍历性能优异,但增删效率低且无法动态扩容;ArrayList底层基于静态数组实现,支持1.5倍动态扩容,解决了静态数组长度固定的短板。Java二维数组为不规则数组,是数组的数组,行列内存独立。数组核心优势为随机访问、缓存友好,适用于遍历、查询、区间运算等场景,头部/中间增删为性能短板,工程中可通过预分配容量、原地算法、行优先遍历实现性能优化,是Java数据结构与算法的核心底层载体。

( 1 ). 特性

  • 连续内存、随机访问 O(1)

  • 插入删除 O(n)

  • CPU 缓存友好(局部性原理)

( 2 ). Java 特有拓展

  • 静态数组 / 动态数组 ArrayList

  • ArrayList 扩容:默认10,1.5倍扩容

  • 滚动数组、前缀和、差分、稀疏矩阵

2.2 链表(Java 面试重中之重·全覆盖补完整版)

核心定位(面试必背) :链表是Java线性结构核心,完美弥补数组增删低效的短板,是指针操作、递归思维、双指针算法的核心载体。区别于数组连续内存,链表采用离散内存+引用指针寻址,增删效率极高,但随机访问能力缺失,是笔试刷题高频题型、大厂面试必考底层知识点,也是LinkedList、哈希表链式寻址的底层基础。

2.2.1 链表核心底层特性(数组全方位对比)

链表所有性能、场景、算法选型差异,均源于底层内存结构,核心特性对标数组,面试高频对比考点:

  • 内存结构:离散非连续内存,每个节点独立开辟堆空间,通过next/prev引用指针关联,无内存连续性约束

  • 访问特性:无随机访问能力,仅支持从头节点遍历查找,查询时间复杂度O(n)

  • 增删特性:仅需修改指针指向,无需移动元素,已知节点位置下增删O(1),无数组批量拷贝开销

  • 空间特性:无内存碎片浪费,支持动态扩容缩容,无需提前预设容量;但每个节点存在指针引用开销,内存占用高于数组

  • 缓存特性:内存离散排布,无法命中CPU高速缓存,批量遍历效率远低于数组

选型铁律(刷题/工程):频繁查询、批量遍历优先用数组;频繁头尾/中间增删、动态不确定数据量优先用链表。

2.2.2 四大链表完整分类(定义+特性+适用场景)

(1)单向链表(单链表·刷题核心·企业实战完整版)

结构定义:每个节点仅包含值和后继指针next,仅能从头部向后遍历,无前置节点引用,尾节点next为null。

核心特性:结构最简单、内存开销最小;仅支持正向遍历,反向操作需遍历回溯,增删前置节点效率低。

适用场景:单向遍历、简单数据存储、哈希表冲突链、刷题90%链表题型载体。

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; }
}

/**
 * 单向链表企业实战工具类
 * 包含:链表构建、增删改查、遍历、反转、去重、倒数K节点、链表合并、判空、长度计算
 * 适配LeetCode刷题、业务数据封装、接口数据组装场景
 */
public class SingleLinkedListDemo {

    /**
     * 1. 尾插法构建链表(企业最常用,顺序贴合输入)
     * @param arr 源数组
     * @return 链表头节点
     */
    public static ListNode buildLinkedList(int[] arr) {
        // 虚拟头结点,统一操作边界
        ListNode dummy = new ListNode(-1);
        ListNode cur = dummy;
        for (int val : arr) {
            cur.next = new ListNode(val);
            cur = cur.next;
        }
        return dummy.next;
    }

    /**
     * 2. 遍历打印链表(调试/日志输出必备)
     * @param head 链表头节点
     */
    public static void printLinkedList(ListNode head) {
        StringBuilder sb = new StringBuilder();
        ListNode cur = head;
        while (cur != null) {
            sb.append(cur.val).append(" -> ");
            cur = cur.next;
        }
        sb.append("null");
        System.out.println("链表遍历结果:" + sb);
    }

    /**
     * 3. 获取链表长度
     * @param head 链表头节点
     * @return 链表节点个数
     */
    public static int getLength(ListNode head) {
        int len = 0;
        ListNode cur = head;
        while (cur != null) {
            len++;
            cur = cur.next;
        }
        return len;
    }

    /**
     * 4. 链表头部插入节点
     * @param head 原头节点
     * @param val 插入值
     * @return 新头节点
     */
    public static ListNode addFirst(ListNode head, int val) {
        ListNode newNode = new ListNode(val);
        newNode.next = head;
        return newNode;
    }

    /**
     * 5. 链表尾部插入节点
     * @param head 头节点
     * @param val 插入值
     * @return 原头节点
     */
    public static ListNode addLast(ListNode head, int val) {
        ListNode newNode = new ListNode(val);
        // 空链表直接返回新节点
        if (head == null) {
            return newNode;
        }
        ListNode cur = head;
        while (cur.next != null) {
            cur = cur.next;
        }
        cur.next = newNode;
        return head;
    }

    /**
     * 6. 指定下标插入节点(从0开始)
     * @param head 头节点
     * @param index 插入下标
     * @param val 插入值
     * @return 新头节点
     */
    public static ListNode addByIndex(ListNode head, int index, int val) {
        int len = getLength(head);
        // 下标合法性校验
        if (index < 0 || index > len) {
            throw new IllegalArgumentException("下标越界,插入失败");
        }
        // 虚拟头结点统一处理头部插入场景
        ListNode dummy = new ListNode(-1);
        dummy.next = head;
        ListNode cur = dummy;
        // 移动到插入位置前一个节点
        for (int i = 0; i < index; i++) {
            cur = cur.next;
        }
        // 插入节点
        ListNode newNode = new ListNode(val);
        newNode.next = cur.next;
        cur.next = newNode;
        return dummy.next;
    }

    /**
     * 7. 删除指定下标节点
     * @param head 头节点
     * @param index 删除下标
     * @return 新头节点
     */
    public static ListNode removeByIndex(ListNode head, int index) {
        int len = getLength(head);
        if (index < 0 || index >= len) {
            throw new IllegalArgumentException("下标越界,删除失败");
        }
        ListNode dummy = new ListNode(-1);
        dummy.next = head;
        ListNode cur = dummy;
        // 找到待删除节点前驱
        for (int i = 0; i < index; i++) {
            cur = cur.next;
        }
        // 跳过待删除节点
        cur.next = cur.next.next;
        return dummy.next;
    }

    /**
     * 8. 删除指定值的所有节点(刷题高频:移除链表元素)
     * @param head 头节点
     * @param val 待删除值
     * @return 新头节点
     */
    public static ListNode removeAllVal(ListNode head, int val) {
        ListNode dummy = new ListNode(-1);
        dummy.next = head;
        ListNode cur = dummy;
        while (cur.next != null) {
            if (cur.next.val == val) {
                // 匹配值,删除节点
                cur.next = cur.next.next;
            } else {
                // 不匹配,后移遍历
                cur = cur.next;
            }
        }
        return dummy.next;
    }

    /**
     * 9. 有序链表去重(LeetCode83 刷题真题)
     * @param head 有序链表头节点
     * @return 去重后链表
     */
    public static ListNode distinctLinkedList(ListNode head) {
        if (head == null || head.next == null) {
            return head;
        }
        ListNode cur = head;
        while (cur.next != null) {
            if (cur.val == cur.next.val) {
                // 重复则删除后继节点
                cur.next = cur.next.next;
            } else {
                cur = cur.next;
            }
        }
        return head;
    }

    /**
     * 10. 迭代反转链表(工程最优,O(1)空间)
     * @param head 原链表头节点
     * @return 反转后新头节点
     */
    public static ListNode reverseLinkedList(ListNode head) {
        ListNode pre = null;
        ListNode cur = head;
        while (cur != null) {
            // 保存后继节点,防止链表断裂
            ListNode temp = cur.next;
            // 反转指针
            cur.next = pre;
            // 指针后移
            pre = cur;
            cur = temp;
        }
        return pre;
    }

    /**
     * 11. 获取链表倒数第k个节点(快慢指针·企业高频)
     * @param head 头节点
     * @param k 倒数位数
     * @return 目标节点
     */
    public static ListNode getLastKNode(ListNode head, int k) {
        if (head == null || k <= 0) {
            return null;
        }
        ListNode fast = head;
        ListNode slow = head;
        // 快指针先行k步
        for (int i = 0; i < k; i++) {
            // 防止k大于链表长度
            if (fast == null) {
                return null;
            }
            fast = fast.next;
        }
        // 快慢指针同步后移
        while (fast != null) {
            fast = fast.next;
            slow = slow.next;
        }
        return slow;
    }

    /**
     * 12. 合并两个有序单向链表(贪心·面试必考)
     * @param l1 有序链表1
     * @param l2 有序链表2
     * @return 合并后有序链表
     */
    public static ListNode mergeTwoSortedList(ListNode l1, ListNode l2) {
        ListNode dummy = new ListNode(-1);
        ListNode cur = dummy;
        // 双指针遍历合并
        while (l1 != null && l2 != null) {
            if (l1.val <= l2.val) {
                cur.next = l1;
                l1 = l1.next;
            } else {
                cur.next = l2;
                l2 = l2.next;
            }
            cur = cur.next;
        }
        // 拼接剩余节点
        cur.next = l1 == null ? l2 : l1;
        return dummy.next;
    }

    // 企业实战测试主方法
    public static void main(String[] args) {
        // 1. 构建链表
        int[] arr = {1,2,2,3,4,4,5};
        ListNode head = buildLinkedList(arr);
        System.out.println("初始链表:");
        printLinkedList(head);

        // 2. 链表去重
        ListNode distinctHead = distinctLinkedList(head);
        System.out.println("去重后链表:");
        printLinkedList(distinctHead);

        // 3. 链表反转
        ListNode reverseHead = reverseLinkedList(distinctHead);
        System.out.println("反转后链表:");
        printLinkedList(reverseHead);

        // 4. 获取倒数第3个节点
        ListNode lastKNode = getLastKNode(reverseHead, 3);
        System.out.println("倒数第3个节点值:" + lastKNode.val);

        // 5. 节点新增与删除
        ListNode addHead = addLast(reverseHead, 6);
        System.out.println("尾部新增节点后:");
        printLinkedList(addHead);

        ListNode removeHead = removeAllVal(addHead, 2);
        System.out.println("删除值为2的节点后:");
        printLinkedList(removeHead);
    }
}

企业实战核心说明

结构定义:每个节点仅包含值和后继指针next,仅能从头部向后遍历,无前置节点引用,尾节点next为null。

核心特性:结构最简单、内存开销最小;仅支持正向遍历,反向操作需遍历回溯,增删前置节点效率低。

适用场景:单向遍历、简单数据存储、哈希表冲突链、刷题90%链表题型载体、业务简单有序数据流转、日志链路存储、数据单向迭代处理。

企业编码规范

  • 统一使用虚拟头结点,规避空链表、头节点操作边界问题,代码统一规范,无冗余判空逻辑;

  • 所有方法独立解耦,单一方法只做一件事,适配工程复用、单元测试;

  • 增加参数合法性校验,规避下标越界、空指针等线上异常;

  • 优先迭代实现,避免递归栈溢出,适配大数据量链表场景,线上稳定性更强。

(2)双向链表(双链表·工程核心·企业实战完整版)

结构定义:节点包含值、后继next、前驱prev指针,可正向/反向双向遍历,首尾节点互相关联(JDK LinkedList优化)。相较于单链表,解决了前驱节点查找、中间节点增删低效的问题,是Java LinkedList底层核心实现结构。

核心特性:遍历灵活,已知任意节点可快速访问前后节点,中间节点增删效率极致;额外prev指针占用少量内存,空间开销略高于单链表;支持双向迭代,适配复杂业务数据遍历场景。

适用场景:Java LinkedList底层、LRU缓存实现、双向遍历、频繁中间增删场景、队列双端操作、业务数据双向回溯查询。

java 复制代码
// 双向链表标准节点(企业通用定义)
class DoubleListNode {
    int val;
    DoubleListNode prev;
    DoubleListNode next;

    // 无参构造
    public DoubleListNode() {}
    // 单值构造
    public DoubleListNode(int val) {
        this.val = val;
    }
    // 全参构造
    public DoubleListNode(int val, DoubleListNode prev, DoubleListNode next) {
        this.val = val;
        this.prev = prev;
        this.next = next;
    }
}

/**
 * 双向链表企业实战工具类
 * 全覆盖:构建、增删改查、双向遍历、头尾操作、节点清空、长度统计、边界防护
 * 适配:工程业务开发、LeetCode刷题、面试手写、LRU底层封装
 * 编码规范:空值校验、边界拦截、无内存泄漏、指针完整复位
 */
public class DoubleLinkedListDemo {
    // 虚拟头尾节点(工程最优解,彻底统一所有边界)
    private DoubleListNode dummyHead;
    private DoubleListNode dummyTail;
    // 链表有效节点长度
    private int size;

    // 初始化双向空链表(首尾虚拟节点对接,闭环边界)
    public DoubleLinkedListDemo() {
        dummyHead = new DoubleListNode();
        dummyTail = new DoubleListNode();
        dummyHead.next = dummyTail;
        dummyTail.prev = dummyHead;
        size = 0;
    }

    /**
     * 1. 获取链表有效长度
     */
    public int getSize() {
        return size;
    }

    /**
     * 2. 判断链表是否为空
     */
    public boolean isEmpty() {
        return size == 0;
    }

    /**
     * 3. 尾部插入节点(高频业务)
     */
    public void addLast(int val) {
        DoubleListNode newNode = new DoubleListNode(val);
        // 绑定新节点前驱:尾部虚拟节点的前驱
        newNode.prev = dummyTail.prev;
        // 绑定新节点后继:尾部虚拟节点
        newNode.next = dummyTail;
        // 原尾节点后继指向新节点
        dummyTail.prev.next = newNode;
        // 尾部虚拟节点前驱指向新节点
        dummyTail.prev = newNode;
        size++;
    }

    /**
     * 4. 头部插入节点
     */
    public void addFirst(int val) {
        DoubleListNode newNode = new DoubleListNode(val);
        newNode.next = dummyHead.next;
        newNode.prev = dummyHead;
        dummyHead.next.prev = newNode;
        dummyHead.next = newNode;
        size++;
    }

    /**
     * 5. 指定下标插入节点(0起始)
     */
    public void addByIndex(int index, int val) {
        // 下标合法性校验
        if (index < 0 || index > size) {
            throw new IllegalArgumentException("下标越界,插入失败");
        }
        // 定位到插入位置的后一个节点
        DoubleListNode cur = getNodeByIndex(index);
        DoubleListNode newNode = new DoubleListNode(val);
        // 指针绑定
        newNode.prev = cur.prev;
        newNode.next = cur;
        cur.prev.next = newNode;
        cur.prev = newNode;
        size++;
    }

    /**
     * 6. 根据下标获取节点(内部核心工具方法)
     */
    private DoubleListNode getNodeByIndex(int index) {
        // 二分优化遍历:靠前下标从头遍历,靠后下标从尾遍历
        DoubleListNode cur;
        if (index < size / 2) {
            cur = dummyHead.next;
            for (int i = 0; i < index; i++) {
                cur = cur.next;
            }
        } else {
            cur = dummyTail;
            for (int i = size; i > index; i--) {
                cur = cur.prev;
            }
        }
        return cur;
    }

    /**
     * 7. 根据下标获取节点值
     */
    public int getValByIndex(int index) {
        if (index < 0 || index >= size) {
            throw new IllegalArgumentException("下标越界,查询失败");
        }
        return getNodeByIndex(index).val;
    }

    /**
     * 8. 根据下标修改节点值
     */
    public void updateValByIndex(int index, int newVal) {
        if (index < 0 || index >= size) {
            throw new IllegalArgumentException("下标越界,修改失败");
        }
        getNodeByIndex(index).val = newVal;
    }

    /**
     * 9. 删除指定下标节点
     */
    public void removeByIndex(int index) {
        if (index < 0 || index >= size) {
            throw new IllegalArgumentException("下标越界,删除失败");
        }
        DoubleListNode delNode = getNodeByIndex(index);
        // 跳过待删除节点,双向指针重绑定
        delNode.prev.next = delNode.next;
        delNode.next.prev = delNode.prev;
        // 断开删除节点指针,避免内存泄漏
        delNode.prev = null;
        delNode.next = null;
        size--;
    }

    /**
     * 10. 删除首个指定值的节点
     */
    public void removeFirstVal(int val) {
        DoubleListNode cur = dummyHead.next;
        while (cur != dummyTail) {
            if (cur.val == val) {
                // 重绑定指针
                cur.prev.next = cur.next;
                cur.next.prev = cur.prev;
                // 断开指针
                cur.prev = null;
                cur.next = null;
                size--;
                return;
            }
            cur = cur.next;
        }
    }

    /**
     * 11. 删除所有指定值的节点(刷题高频)
     */
    public void removeAllVal(int val) {
        DoubleListNode cur = dummyHead.next;
        while (cur != dummyTail) {
            // 提前保存后继节点,防止删除后断链
            DoubleListNode nextNode = cur.next;
            if (cur.val == val) {
                cur.prev.next = cur.next;
                cur.next.prev = cur.prev;
                cur.prev = null;
                cur.next = null;
                size--;
            }
            cur = nextNode;
        }
    }

    /**
     * 12. 正向遍历(从头至尾)
     */
    public void printForward() {
        StringBuilder sb = new StringBuilder();
        DoubleListNode cur = dummyHead.next;
        sb.append("正向遍历:");
        while (cur != dummyTail) {
            sb.append(cur.val).append(" <-> ");
            cur = cur.next;
        }
        sb.append("null");
        System.out.println(sb);
    }

    /**
     * 13. 反向遍历(从尾至头,双向链表独有特性)
     */
    public void printBackward() {
        StringBuilder sb = new StringBuilder();
        DoubleListNode cur = dummyTail.prev;
        sb.append("反向遍历:");
        while (cur != dummyHead) {
            sb.append(cur.val).append(" <-> ");
            cur = cur.prev;
        }
        sb.append("null");
        System.out.println(sb);
    }

    /**
     * 14. 清空链表
     */
    public void clear() {
        dummyHead.next = dummyTail;
        dummyTail.prev = dummyHead;
        size = 0;
    }

    // 企业实战测试主方法
    public static void main(String[] args) {
        DoubleLinkedListDemo list = new DoubleLinkedListDemo();

        // 批量新增节点
        list.addLast(1);
        list.addLast(2);
        list.addLast(3);
        list.addFirst(0);
        System.out.println("初始链表长度:" + list.getSize());
        list.printForward();
        list.printBackward();

        // 下标插入、修改
        list.addByIndex(2, 99);
        list.updateValByIndex(3, 100);
        System.out.println("插入修改后:");
        list.printForward();

        // 删除节点
        list.removeByIndex(1);
        list.removeAllVal(99);
        System.out.println("删除节点后:");
        list.printForward();

        // 判空、清空
        System.out.println("链表是否为空:" + list.isEmpty());
        list.clear();
        System.out.println("清空后长度:" + list.getSize());
    }
}

企业核心编码规范(面试加分点)

  • 虚拟首尾双节点:区别于单链表单虚拟头结点,双向链表采用首尾双虚拟节点,天然闭环,彻底规避头尾节点增删的边界判断,代码零冗余、无空指针风险,与JDK LinkedList底层设计思想一致。

  • 双向指针完整复位:删除节点时强制清空被删节点的prev、next指针,杜绝内存泄漏,符合工程内存优化规范。

  • 遍历二分优化:根据下标位置选择从头/从尾遍历,将单次查询时间优化至O(n/2),大数据量下性能显著优于纯从头遍历。

  • 全量参数校验:所有下标操作前置合法性校验,杜绝下标越界、空链表操作等线上异常。

  • 方法单一职责:增删改查、遍历、清空功能完全解耦,支持业务独立复用,适配工程分层开发。

双向链表高频面试考点

  • 优势:支持双向遍历、中间节点增删O(1)(已知节点前提下),弥补单链表前驱操作低效缺陷;

  • 劣势:每个节点多存储一个前驱指针,内存开销更大,指针操作逻辑更复杂;

  • JDK LinkedList核心原理:基于双向链表实现,无扩容机制,头尾操作高效,随机查询低效;

  • LRU缓存核心依赖:利用双向链表有序性+快速增删特性,实现热点数据快速更新与淘汰。

结构定义:尾节点next指针指向头节点,无null尾节点,形成闭环;双向循环链表首尾节点互相指向。

核心特性:支持无限循环遍历,无首尾边界区分,适合环形业务场景。

适用场景:约瑟夫环问题、定时任务轮询、环形缓冲区、并发队列。

(3)循环链表(单向+双向·企业实战完整代码)

结构定义:尾节点next指针指向头节点,无null尾节点,形成闭环;双向循环链表首尾节点互相指向,彻底消除空边界,支持无限循环遍历。

核心特性:无首尾null边界、可循环遍历、天然闭环结构;单向循环链表实现简单,双向循环链表操作更灵活,适配环形轮询场景。

适用场景:约瑟夫环问题、定时任务轮询、环形缓冲区、并发轮询队列、游戏角色回合调度。

一、单向循环链表(企业实战完整版代码)

核心特点:尾节点.next = 头节点,无null节点,统一循环遍历逻辑,无需判空收尾。

java 复制代码
// 单向循环链表节点定义
class CircleSingleNode {
    int val;
    CircleSingleNode next;

    public CircleSingleNode() {}
    public CircleSingleNode(int val) {
        this.val = val;
    }
}

/**
 * 单向循环链表 企业实战工具类
 * 全覆盖:初始化、增删改查、循环遍历、判空、长度统计、闭环维护
 * 适配:约瑟夫环、轮询任务、刷题环形场景
 */
public class CircleSingleLinkedList {
    // 头节点
    private CircleSingleNode head;
    // 链表有效长度
    private int size;

    // 初始化空循环链表
    public CircleSingleLinkedList() {
        head = null;
        size = 0;
    }

    // 获取链表长度
    public int getSize() {
        return size;
    }

    // 判断链表是否为空
    public boolean isEmpty() {
        return size == 0;
    }

    /**
     * 尾部插入节点(循环链表核心插入)
     */
    public void addLast(int val) {
        CircleSingleNode newNode = new CircleSingleNode(val);
        // 空链表:自身闭环
        if (isEmpty()) {
            head = newNode;
            newNode.next = head;
            size++;
            return;
        }
        // 非空链表:找到尾节点,重构闭环
        CircleSingleNode cur = head;
        while (cur.next != head) {
            cur = cur.next;
        }
        cur.next = newNode;
        newNode.next = head;
        size++;
    }

    /**
     * 头部插入节点
     */
    public void addFirst(int val) {
        CircleSingleNode newNode = new CircleSingleNode(val);
        if (isEmpty()) {
            head = newNode;
            newNode.next = head;
            size++;
            return;
        }
        // 找到尾节点,重构闭环
        CircleSingleNode cur = head;
        while (cur.next != head) {
            cur = cur.next;
        }
        newNode.next = head;
        cur.next = newNode;
        head = newNode;
        size++;
    }

    /**
     * 指定下标插入节点(0起始)
     */
    public void addByIndex(int index, int val) {
        if (index < 0 || index > size) {
            throw new IllegalArgumentException("下标越界,插入失败");
        }
        if (index == 0) {
            addFirst(val);
            return;
        }
        CircleSingleNode newNode = new CircleSingleNode(val);
        CircleSingleNode cur = head;
        // 找到插入位置前驱节点
        for (int i = 0; i < index - 1; i++) {
            cur = cur.next;
        }
        newNode.next = cur.next;
        cur.next = newNode;
        size++;
    }

    /**
     * 删除指定下标节点
     */
    public void removeByIndex(int index) {
        if (isEmpty() || index < 0 || index >= size) {
            throw new IllegalArgumentException("下标越界或链表为空,删除失败");
        }
        // 删除头节点
        if (index == 0) {
            // 仅有一个节点
            if (size == 1) {
                head = null;
                size = 0;
                return;
            }
            // 找到尾节点重构闭环
            CircleSingleNode cur = head;
            while (cur.next != head) {
                cur = cur.next;
            }
            head = head.next;
            cur.next = head;
            size--;
            return;
        }
        // 删除中间/尾部节点
        CircleSingleNode cur = head;
        for (int i = 0; i < index - 1; i++) {
            cur = cur.next;
        }
        cur.next = cur.next.next;
        size--;
    }

    /**
     * 删除指定值的所有节点
     */
    public void removeAllVal(int val) {
        if (isEmpty()) return;
        CircleSingleNode cur = head;
        CircleSingleNode pre = null;
        int count = 0;

        // 循环遍历,避免死循环(遍历次数等于节点数)
        while (count < size) {
            if (cur.val == val) {
                // 删除头节点
                if (pre == null) {
                    if (size == 1) {
                        head = null;
                        size = 0;
                        return;
                    }
                    // 找到尾节点重构闭环
                    CircleSingleNode tail = head;
                    while (tail.next != head) {
                        tail = tail.next;
                    }
                    head = head.next;
                    tail.next = head;
                    cur = head;
                } else {
                    // 删除中间/尾部节点
                    pre.next = cur.next;
                    cur = pre.next;
                }
                size--;
            } else {
                pre = cur;
                cur = cur.next;
            }
            count++;
        }
    }

    /**
     * 循环遍历打印(可控遍历,避免死循环)
     */
    public void printCircleList() {
        if (isEmpty()) {
            System.out.println("空循环链表");
            return;
        }
        StringBuilder sb = new StringBuilder();
        CircleSingleNode cur = head;
        int count = 0;
        // 遍历所有节点后终止,杜绝死循环
        while (count < size) {
            sb.append(cur.val).append(" -> ");
            cur = cur.next;
            count++;
        }
        sb.append("(回到头节点)");
        System.out.println(sb);
    }

    /**
     * 清空链表
     */
    public void clear() {
        head = null;
        size = 0;
    }

    // 企业测试主方法
    public static void main(String[] args) {
        CircleSingleLinkedList list = new CircleSingleLinkedList();
        // 批量新增
        list.addLast(1);
        list.addLast(2);
        list.addLast(3);
        list.addFirst(0);
        System.out.println("初始循环链表:");
        list.printCircleList();

        // 下标插入、删除
        list.addByIndex(2, 99);
        System.out.println("下标2插入99后:");
        list.printCircleList();

        list.removeByIndex(1);
        System.out.println("删除下标1节点后:");
        list.printCircleList();

        // 删除指定值
        list.removeAllVal(99);
        System.out.println("删除值为99节点后:");
        list.printCircleList();

        System.out.println("链表长度:" + list.getSize());
    }
}
二、双向循环链表(JDK进阶思想·企业最优代码)

核心特点:首尾节点双向互指,形成完整闭环,支持双向循环遍历,中间增删O(1)效率,无任何空指针边界,是环形业务最优结构。

java 复制代码
// 双向循环链表节点定义
class CircleDoubleNode {
    int val;
    CircleDoubleNode prev;
    CircleDoubleNode next;

    public CircleDoubleNode() {}
    public CircleDoubleNode(int val) {
        this.val = val;
    }
}

/**
 * 双向循环链表 企业实战完整版
 * 优势:双向循环、零空边界、增删高效、适配轮询/缓存场景
 * 编码规范:闭环永久维护、指针完整复位、无内存泄漏、边界全覆盖
 */
public class CircleDoubleLinkedList {
    // 虚拟头节点(统一边界,永久闭环)
    private CircleDoubleNode dummy;
    private int size;

    // 初始化双向循环空链表
    public CircleDoubleLinkedList() {
        dummy = new CircleDoubleNode();
        // 自闭环,彻底消除null边界
        dummy.prev = dummy;
        dummy.next = dummy;
        size = 0;
    }

    public int getSize() {
        return size;
    }

    public boolean isEmpty() {
        return size == 0;
    }

    /**
     * 尾部插入
     */
    public void addLast(int val) {
        CircleDoubleNode newNode = new CircleDoubleNode(val);
        // 插入虚拟头前驱位置(尾部)
        newNode.prev = dummy.prev;
        newNode.next = dummy;
        dummy.prev.next = newNode;
        dummy.prev = newNode;
        size++;
    }

    /**
     * 头部插入
     */
    public void addFirst(int val) {
        CircleDoubleNode newNode = new CircleDoubleNode(val);
        newNode.next = dummy.next;
        newNode.prev = dummy;
        dummy.next.prev = newNode;
        dummy.next = newNode;
        size++;
    }

    /**
     * 指定下标插入
     */
    public void addByIndex(int index, int val) {
        if (index < 0 || index > size) {
            throw new IllegalArgumentException("下标越界");
        }
        CircleDoubleNode cur = getNodeByIndex(index);
        CircleDoubleNode newNode = new CircleDoubleNode(val);
        newNode.prev = cur.prev;
        newNode.next = cur;
        cur.prev.next = newNode;
        cur.prev = newNode;
        size++;
    }

    /**
     * 根据下标获取节点(内部工具方法)
     */
    private CircleDoubleNode getNodeByIndex(int index) {
        CircleDoubleNode cur;
        // 二分优化遍历
        if (index < size / 2) {
            cur = dummy.next;
            for (int i = 0; i < index; i++) {
                cur = cur.next;
            }
        } else {
            cur = dummy.prev;
            for (int i = size - 1; i > index; i--) {
                cur = cur.prev;
            }
        }
        return cur;
    }

    /**
     * 删除指定下标节点
     */
    public void removeByIndex(int index) {
        if (index < 0 || index >= size) {
            throw new IllegalArgumentException("下标越界");
        }
        CircleDoubleNode delNode = getNodeByIndex(index);
        // 重绑定双向指针,维护闭环
        delNode.prev.next = delNode.next;
        delNode.next.prev = delNode.prev;
        // 清空指针,防止内存泄漏
        delNode.prev = null;
        delNode.next = null;
        size--;
    }

    /**
     * 删除所有指定值节点
     */
    public void removeAllVal(int val) {
        CircleDoubleNode cur = dummy.next;
        while (cur != dummy) {
            CircleDoubleNode nextNode = cur.next;
            if (cur.val == val) {
                cur.prev.next = cur.next;
                cur.next.prev = cur.prev;
                cur.prev = null;
                cur.next = null;
                size--;
            }
            cur = nextNode;
        }
    }

    /**
     * 正向循环遍历
     */
    public void printForward() {
        if (isEmpty()) {
            System.out.println("空双向循环链表");
            return;
        }
        StringBuilder sb = new StringBuilder();
        CircleDoubleNode cur = dummy.next;
        sb.append("正向循环:");
        while (cur != dummy) {
            sb.append(cur.val).append(" <-> ");
            cur = cur.next;
        }
        sb.append("回到起点");
        System.out.println(sb);
    }

    /**
     * 反向循环遍历
     */
    public void printBackward() {
        if (isEmpty()) return;
        StringBuilder sb = new StringBuilder();
        CircleDoubleNode cur = dummy.prev;
        sb.append("反向循环:");
        while (cur != dummy) {
            sb.append(cur.val).append(" <-> ");
            cur = cur.prev;
        }
        sb.append("回到起点");
        System.out.println(sb);
    }

    /**
     * 清空链表,重置闭环
     */
    public void clear() {
        dummy.next = dummy;
        dummy.prev = dummy;
        size = 0;
    }

    // 企业测试主方法
    public static void main(String[] args) {
        CircleDoubleLinkedList list = new CircleDoubleLinkedList();
        list.addLast(10);
        list.addLast(20);
        list.addFirst(5);
        list.addByIndex(2, 15);

        list.printForward();
        list.printBackward();

        list.removeByIndex(1);
        list.removeAllVal(20);
        System.out.println("删除节点后:");
        list.printForward();
        System.out.println("链表长度:" + list.getSize());
    }
}

三、循环链表企业核心考点&易错点
  • 闭环维护核心:所有增删操作必须重构首尾闭环,杜绝链表断裂、死循环、空指针异常

  • 遍历避坑:循环链表禁止无条件while遍历,必须通过节点数量/虚拟节点终止,防止无限循环

  • 单/双向选型:简单轮询选单向循环链表,频繁双向操作、中间增删选双向循环链表

  • 工程优化:双向循环链表优先使用虚拟节点闭环,彻底统一空链表、首尾节点操作逻辑

  • 经典算法应用:约瑟夫环问题首选单向循环链表,通过循环遍历+节点删除快速求解

四、面试满分答题模板

循环链表分为单向与双向循环链表,核心是尾节点与头节点闭环绑定,消除普通链表的null边界。单向循环链表结构简单,适合单向轮询遍历场景;双向循环链表支持双向遍历,中间节点增删效率O(1),闭环结构稳定性更强。工程实现中通过虚拟节点优化边界逻辑,所有操作严格维护闭环结构,遍历通过计数/虚拟节点终止避免死循环,广泛应用于定时轮询、环形缓冲区、约瑟夫环等环形场景。

结构定义:尾节点next指针指向头节点,无null尾节点,形成闭环;双向循环链表首尾节点互相指向。

核心特性:支持无限循环遍历,无首尾边界区分,适合环形业务场景。

适用场景:约瑟夫环问题、定时任务轮询、环形缓冲区、并发队列。

(4)跳表(SkipList·大厂进阶·完整实战)

结构定义 :跳表是基于有序单向链表 优化的多层索引有序结构,核心是通过空间换时间 ,为底层原始有序链表构建多层稀疏索引链表,实现快速二分式查找,完美解决普通有序链表查询O(n)的低效问题,底层本质:有序链表 + 多层分层索引

核心复杂度(面试必背)

  • 时间复杂度:查询/插入/删除 O(logn),媲美红黑树

  • 空间复杂度:O(n),多层索引占用额外空间

  • 对比优势:实现简单、无红黑树复杂的旋转变色平衡操作、并发友好

核心特性

  • 全局有序:底层链表始终保持元素有序,天然支持有序遍历

  • 分层索引:上层索引稀疏、下层索引密集,查询从上至下逐层匹配、快速缩小范围

  • 随机层高:通过随机算法决定新增节点的层数,无需人工平衡,天然维持结构均衡

  • 无平衡开销:相较于红黑树,无旋转、变色、修复平衡的复杂逻辑,工程维护成本极低

工程&面试核心场景

  • Redis 核心底层:ZSet有序集合底层完全基于跳表实现,支持有序范围查询、排名查询

  • Java 并发有序容器:ConcurrentSkipListMap、ConcurrentSkipListSet 底层核心结构

  • 高频有序数据场景:海量有序数据查询、范围遍历、排名统计,替代平衡树简化实现

  • 刷题场景:有序数据高效查找、区间最值、排名问题

一、跳表核心原理(逐层查找逻辑)
  1. 跳表由多层单向有序链表组成,最底层为存储全量数据的原始链表,上层均为索引链表;

  2. 查询时从最高层索引开始遍历,找到当前层最后一个小于目标值的节点,下沉至下一层继续查找;

  3. 逐层缩小查询范围,直至下沉到最底层原始链表,精准定位目标节点;

  4. 插入/删除节点时,通过随机算法生成节点层高,同步更新对应层级索引,维持跳表结构完整性。

二、Java企业实战完整版代码(可直接面试手写、工程复用)

实现功能:随机层高、节点插入、精准查询、删除节点、逐层打印、有序遍历,完全对标Redis ZSet简易实现,适配面试手写规范。

java 复制代码
import java.util.Random;

/**
 * 跳表(SkipList)企业实战完整版
 * 适配:大厂面试手写、算法刷题、Redis ZSet底层原理复刻
 * 核心特性:O(logn)查增删、随机层高、分层索引、全局有序
 */
public class SkipList {
    // 跳表最大层数(工程常规取值32,满足千万级数据)
    private static final int MAX_LEVEL = 32;
    // 随机层高概率因子(Redis标准0.25)
    private static final double PROBABILITY = 0.25;
    // 跳表当前有效最高层数
    private int currentLevel;
    // 顶层虚拟头节点(统一所有层级边界)
    private SkipNode head;
    // 随机数对象(生成层高)
    private final Random random;

    // 跳表节点定义
    private static class SkipNode {
        // 节点存储值
        int val;
        // 每层对应的后继节点数组(下标对应层数)
        SkipNode[] next;

        public SkipNode(int val, int level) {
            this.val = val;
            this.next = new SkipNode[level];
        }
    }

    // 初始化跳表
    public SkipList() {
        currentLevel = 1;
        // 初始化最大层数虚拟头节点
        head = new SkipNode(-1, MAX_LEVEL);
        random = new Random();
    }

    /**
     * 核心工具:随机生成节点层高(核心平衡机制)
     * 概率0.25逐层递增,保证高层节点稀疏、底层密集
     */
    private int randomLevel() {
        int level = 1;
        while (random.nextDouble() < PROBABILITY && level < MAX_LEVEL) {
            level++;
        }
        return level;
    }

    /**
     * 查找目标值节点
     * @param target 目标值
     * @return 存在返回true,不存在返回false
     */
    public boolean search(int target) {
        SkipNode cur = head;
        // 从最高层逐层向下查找
        for (int i = currentLevel - 1; i >= 0; i--) {
            // 当前层向后遍历,找到最后一个小于target的节点
            while (cur.next[i] != null && cur.next[i].val < target) {
                cur = cur.next[i];
            }
        }
        // 下沉到最底层,判断后继节点是否为目标值
        cur = cur.next[0];
        return cur != null && cur.val == target;
    }

    /**
     * 插入节点(自动生成层高、更新多层索引)
     * @param num 待插入数值
     */
    public void add(int num) {
        // 1. 初始化每层需要更新的前驱节点数组
        SkipNode[] update = new SkipNode[MAX_LEVEL];
        SkipNode cur = head;

        // 2. 从高层到低层,记录每层插入位置的前驱节点
        for (int i = currentLevel - 1; i >= 0; i--) {
            while (cur.next[i] != null && cur.next[i].val < num) {
                cur = cur.next[i];
            }
            update[i] = cur;
        }

        // 3. 随机生成当前节点层高
        int newLevel = randomLevel();

        // 4. 如果新节点层高超过当前最大层数,更新高层前驱节点
        if (newLevel > currentLevel) {
            for (int i = currentLevel; i < newLevel; i++) {
                update[i] = head;
            }
            currentLevel = newLevel;
        }

        // 5. 逐层插入新节点,重构索引链表
        SkipNode newNode = new SkipNode(num, newLevel);
        for (int i = 0; i < newLevel; i++) {
            newNode.next[i] = update[i].next[i];
            update[i].next[i] = newNode;
        }
    }

    /**
     * 删除指定值节点
     * @param num 待删除数值
     * @return 删除成功返回true,节点不存在返回false
     */
    public boolean erase(int num) {
        // 1. 记录每层删除位置的前驱节点
        SkipNode[] update = new SkipNode[MAX_LEVEL];
        SkipNode cur = head;

        for (int i = currentLevel - 1; i >= 0; i--) {
            while (cur.next[i] != null && cur.next[i].val < num) {
                cur = cur.next[i];
            }
            update[i] = cur;
        }

        // 2. 判断节点是否存在
        cur = cur.next[0];
        if (cur == null || cur.val != num) {
            return false;
        }

        // 3. 逐层删除节点,重构索引
        for (int i = 0; i < currentLevel; i++) {
            if (update[i].next[i] != cur) {
                break;
            }
            update[i].next[i] = cur.next[i];
        }

        // 4. 更新当前最大层数(清除空高层)
        while (currentLevel > 1 && head.next[currentLevel - 1] == null) {
            currentLevel--;
        }
        return true;
    }

    /**
     * 逐层打印跳表结构(直观展示分层索引特性)
     */
    public void printSkipList() {
        for (int i = currentLevel - 1; i >= 0; i--) {
            StringBuilder sb = new StringBuilder();
            sb.append("第").append(i + 1).append("层索引:");
            SkipNode cur = head.next[i];
            while (cur != null) {
                sb.append(cur.val).append(" -> ");
                cur = cur.next[i];
            }
            sb.append("null");
            System.out.println(sb);
        }
    }

    // 企业测试主方法
    public static void main(String[] args) {
        SkipList skipList = new SkipList();

        // 批量插入数据
        int[] nums = {1, 3, 5, 7, 9, 11, 15};
        for (int num : nums) {
            skipList.add(num);
        }

        System.out.println("===== 跳表分层结构 =====");
        skipList.printSkipList();

        // 测试查询
        System.out.println("\n查询数值7:" + skipList.search(7));
        System.out.println("查询数值10:" + skipList.search(10));

        // 测试删除
        System.out.println("\n删除数值5:" + skipList.erase(5));
        System.out.println("===== 删除后分层结构 =====");
        skipList.printSkipList();
    }
}
三、核心编码规范(面试加分·对标Redis源码)
  • 固定概率层高:采用Redis标准0.25概率生成随机层高,保证跳表平衡度,规避极端倾斜场景

  • 分层前驱记录:增删节点时提前记录全层前驱节点,一次遍历完成多层索引更新,效率最优

  • 虚拟头节点统一边界:全局唯一虚拟头节点,彻底规避每层链表空边界判断,代码零冗余

  • 自动缩层机制:删除节点后自动清理空高层,实时维护跳表有效层数,减少无效遍历

  • 层数上限约束:固定32层最大层数,适配千万级数据量,防止层高无限增长溢出

四、跳表高频面试考点(满分必背)
  • 为什么跳表可以替代平衡树? 跳表通过随机层高实现概率平衡,无需红黑树的旋转、变色、修复平衡等复杂操作,代码更简洁、读写性能稳定、并发锁粒度更小,适合海量有序数据场景

  • 跳表为什么是O(logn)? 每层索引筛选一半左右数据,逐层折半缩小范围,和二分思想一致,概率上稳定维持logn级复杂度

  • Redis为什么用跳表不用红黑树? 跳表支持高效范围查询、有序遍历、排名统计,且迭代遍历性能优于红黑树,更适配ZSet的有序范围操作场景

  • 跳表优缺点:优点是有序高效、实现简单、并发友好、支持范围查询;缺点是相较于平衡树,空间开销更大,存在概率性最坏O(n)复杂度(工程几乎不会触发)

五、面试标准满分答题模板

跳表是基于有序单向链表优化的分层索引结构,核心通过空间换时间+随机层高概率平衡,将普通链表O(n)的查增删复杂度优化为稳定O(logn)。跳表由多层有序索引链表和底层全量数据链表组成,查询从上至下逐层下沉缩小范围,增删通过随机层高更新多层索引维持结构平衡。相较于红黑树,跳表无复杂平衡操作、实现简单、范围查询与有序遍历性能更优,是Redis ZSet和Java并发有序容器的核心底层结构,广泛应用于海量有序数据处理场景。

2.2.3 Java链表核心必考技巧(刷题/面试满分模板)

1)虚拟头结点技巧(统一边界·杜绝空指针)

核心作用 :解决链表头部节点单独删除/修改的边界问题,统一所有节点操作逻辑,无需单独判断头结点为空场景,是链表刷题万能前置操作

适用场景:链表删除、合并、排序、去重、反转等所有修改类操作。

java 复制代码
// 虚拟头结点通用模板
ListNode dummy = new ListNode(-1);
dummy.next = head;
ListNode cur = dummy;
// 后续统一通过cur遍历操作,无需区分头结点
2)快慢指针四大必考场景(链表专属)

通过fast、slow指针速度差(fast走2步、slow走1步),解决链表所有定点、环形问题,O(n)时间、O(1)空间,无额外容器开销。

  • 判环:快慢指针相遇则链表有环,无环则fast率先遍历到null

  • 找环入口:相遇后slow重置到头节点,快慢同速遍历,相遇点即为环入口

  • 找中点:fast遍历结束,slow停在链表中点(偶数长度取左中点)

  • 查找倒数第k节点:fast先行k步,之后快慢同速后移,slow即为目标节点

java 复制代码
// 快慢指针找链表中点(最常用)
public ListNode findMiddle(ListNode head) {
    ListNode slow = head, fast = head;
    while (fast != null && fast.next != null) {
        slow = slow.next;
        fast = fast.next.next;
    }
    return slow;
}
3)链表反转(递归+迭代双模板·必背)

链表反转是所有链表题型的基础,迭代为工程最优、递归为面试必考,双模板全覆盖。

java 复制代码
// 迭代版反转(O(1)空间·工程首选)
public ListNode reverseList(ListNode head) {
    ListNode pre = null, cur = head;
    while (cur != null) {
        ListNode temp = cur.next; // 保存后继节点
        cur.next = pre;            // 反转指针
        pre = cur;                 // pre后移
        cur = temp;                // cur后移
    }
    return pre;
}

// 递归版反转(面试手写·简洁直观)
public ListNode reverseRecur(ListNode head) {
    if (head == null || head.next == null) return head;
    ListNode newHead = reverseRecur(head.next);
    head.next.next = head;
    head.next = null;
    return newHead;
}
4)链表合并(有序合并·高频真题)

合并两个有序链表,链表经典贪心题型,虚拟头结点+单次遍历完成。

java 复制代码
// 合并两个有序链表
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
    ListNode dummy = new ListNode(-1);
    ListNode cur = dummy;
    while (l1 != null && l2 != null) {
        if (l1.val <= l2.val) {
            cur.next = l1;
            l1 = l1.next;
        } else {
            cur.next = l2;
            l2 = l2.next;
        }
        cur = cur.next;
    }
    // 拼接剩余节点
    cur.next = l1 == null ? l2 : l1;
    return dummy.next;
}

2.2.4 JDK LinkedList 底层详解(面试深挖)

底层结构:双向循环链表(JDK优化版),无容量限制,无需动态扩容

核心优势:头尾增删O(1)时间复杂度,支持双向遍历,无数组拷贝开销

核心短板:随机访问慢,get(index)需要遍历寻址,效率极低;内存占用高

底层节点:私有静态内部类Node,包含item值、prev前驱、next后继

工程特性:非线程安全,多线程并发修改会触发ConcurrentModificationException

高频面试对比:ArrayList适合大量查询、遍历;LinkedList适合大量头尾增删,绝对不适合随机访问场景。

2.2.5 链表高频面试真题清单(全覆盖)

  • 基础题型:反转链表、移除链表元素、链表去重、两两交换链表节点

  • 进阶题型:环形链表判定、环入口查找、链表中点、倒数第k个节点

  • 压轴题型:合并K个有序链表、链表排序、回文链表、链表相交、奇偶链表

2.2.6 链表高频易错点(面试扣分避坑)

  • 空指针异常:遍历、取next节点前必须判空,循环条件优先拦截null节点

  • 快慢指针条件错误 :必须写 fast != null && fast.next != null,缺失任一条件触发NPE

  • 递归栈溢出:超长链表递归反转会触发StackOverflowError,大数据量优先迭代

  • 指针污染:链表操作未保存后继节点,导致链表断裂、节点丢失

  • 无虚拟头结点:头结点单独处理,代码冗余且容易出现边界bug

2.2.7 面试标准满分答题模板(直接默写)

Java链表分为单向、双向、循环链表和跳表四大类型,核心底层为离散内存+引用指针寻址,区别于数组连续内存结构。链表优势是动态扩容、头尾增删效率O(1),无需批量移动元素;短板是无随机访问能力、CPU缓存不友好、遍历效率低。刷题核心技巧为虚拟头结点统一边界、快慢指针解决环形与定点问题、迭代/递归实现链表反转与合并。JDK LinkedList基于双向链表实现,适合频繁增删场景,不适合随机查询,工程中需根据业务场景与数组灵活选型。

2.3 栈 Stack(LIFO 后进先出)

核心定义(面试必背) :栈是受限线性表 ,遵循 LIFO(Last In First Out 后进先出) 原则,仅允许在一端(栈顶)进行插入(入栈)、删除(出栈)操作,另一端(栈底)固定封闭,不允许直接操作。属于典型的单向操作受限数据结构。

核心复杂度 :栈顶增删、查询栈顶元素均为 O(1),无遍历开销,性能极致高效。

2.3.1 Java 栈体系与 JDK 底层选型(工程规范)

一、传统 Stack 类(废弃不推荐)

JDK 原生 java.util.Stack 继承自 Vector,底层为动态数组,存在严重缺陷:方法同步加锁、并发性能差、扩容效率低、设计老旧,工程开发与面试均禁止使用,仅做历史了解。

二、官方推荐实现(企业标准)

JDK 官方推荐使用 Deque<E> 双端队列实现栈,完全替代老旧 Stack 类,性能更高、功能更全、无锁并发友好。

  • 首选实现:ArrayDeque:基于动态数组,栈操作效率最高,无扩容冗余开销,单线程首选

  • 备选实现:LinkedList:基于链表,适合高频入栈出栈、无扩容场景,内存动态分配

  • 并发栈:ConcurrentLinkedDeque:多线程并发场景安全栈实现

Java 标准栈定义代码(必背模板)

java 复制代码
// 官方标准栈实现(替代原生Stack,工程通用)
Deque<Integer> stack = new ArrayDeque<>();

2.3.2 栈核心 API(面试/刷题标准)

统一使用 Deque 栈专属方法,杜绝 Vector 老旧方法,刷题零报错、面试零扣分。

  • push(E e):栈顶入栈,添加元素

  • pop():栈顶出栈,删除并返回栈顶元素,栈空抛异常

  • peek():获取栈顶元素,不删除,栈空抛异常

  • isEmpty():判断栈是否为空,刷题高频判空条件

  • size():获取栈内元素个数

刷题避坑准则:栈空时禁止调用 pop()/peek(),必须先通过 isEmpty() 判断,杜绝空指针/栈空异常。

2.3.3 栈核心分类与底层原理

一、静态栈(数组栈)

基于固定/动态数组实现,底层连续内存,CPU 缓存友好、访问速度快、无节点指针开销,ArrayDeque 底层即为动态数组栈,自动扩容无上限。

二、动态栈(链表栈)

基于链表节点实现,动态分配内存,无扩容机制、无内存浪费,适合元素数量波动极大的场景,LinkedList 栈为典型实现。

三、JVM 虚拟机栈(面试深挖)

JVM 栈是线程私有内存空间,每个方法调用对应一个栈帧入栈,方法执行完毕栈帧出栈,递归深度过大将触发StackOverflowError,是递归算法空间复杂度的核心统计项。

2.3.4 栈四大核心应用场景(全覆盖)

1)基础刷题场景(入门高频)

括号匹配:有效括号、括号嵌套校验、最长有效括号

表达式求值:中缀转后缀、后缀表达式计算、四则运算解析

字符串逆序、单词反转:利用后进先出特性实现翻转

进制转换:十进制转二进制/十六进制

2)算法递归辅助场景(核心底层)

迭代版 DFS 深度优先搜索:替代递归栈,避免栈溢出

二叉树非递归遍历:前/中/后序迭代遍历全靠栈实现回溯算法

迭代优化:手动栈保存状态,替代递归

3)单调栈(刷题大杀器·中等题高频)

核心原理:栈内元素保持严格递增/递减顺序,通过栈的有序性,一次遍历求解区间最值、边界问题,将暴力 O(n²) 优化为 O(n)。

两大类型:单调递增栈、单调递减栈

必考经典题型

单调递增栈:柱状图中最大矩形、每日温度、下一个更大元素

单调递减栈:接雨水、上一个更大元素、区间最小值统计

单调栈通用刷题模板(直接默写)

java 复制代码
// 单调栈通用模板(以单调递增栈为例)
public void monotonicStack(int[] nums) {
    Deque<Integer> stack = new ArrayDeque<>();
    // 遍历所有元素
    for (int i = 0; i < nums.length; i++) {
        // 栈不为空,且破坏递增规则,弹出栈顶并计算结果
        while (!stack.isEmpty() && nums[i] < nums[stack.peek()]) {
            int topIdx = stack.pop();
            // 基于栈顶下标计算区间、最值、距离等结果
        }
        // 当前下标入栈,维持栈单调特性
        stack.push(i);
    }
}

4)工程进阶场景(面试深挖)

最小栈/最大栈:O(1) 时间复杂度获取栈内最值

撤销/回退机制:编辑器撤销操作、页面路由回退、事务回滚

函数调用栈:JVM 方法执行、递归调用、栈帧管理

2.3.5 高频专项:最小栈(面试手撕必考题)

需求 :实现一个栈,支持 push、pop、top、getMin 操作,且 getMin() O(1) 时间复杂度。核心思路:双栈辅助,数据栈存元素,最小栈同步存最小值。

java 复制代码
/**
 * 最小栈 企业面试满分实现
 * 所有操作 O(1) 时间复杂度
 */
public class MinStack {
    // 主数据栈
    private final Deque<Integer> dataStack;
    // 最小值辅助栈
    private final Deque<Integer> minStack;

    public MinStack() {
        dataStack = new ArrayDeque<>();
        minStack = new ArrayDeque<>();
    }

    // 入栈:同步更新最小栈
    public void push(int val) {
        dataStack.push(val);
        // 最小栈为空或当前值小于等于栈顶,入栈(等于也要入,保证弹出同步)
        if (minStack.isEmpty() || val <= minStack.peek()) {
            minStack.push(val);
        }
    }

    // 出栈:最小值同步弹出
    public void pop() {
        int top = dataStack.pop();
        if (top == minStack.peek()) {
            minStack.pop();
        }
    }

    // 获取栈顶
    public int top() {
        return dataStack.peek();
    }

    // O(1) 获取最小值
    public int getMin() {
        return minStack.peek();
    }
}

2.3.6 栈高频易错点(面试扣分避坑)

  • 栈空异常:pop、peek 操作前必须判空,刷题90%栈类报错源于此

  • 原生Stack禁用:老旧Stack类线程低效、性能差,工程一律用Deque

  • 单调栈判等错误:最值问题必须包含等于条件,否则会漏算边界、结果错误

  • 递归栈溢出:大数据量递归优先手动栈迭代,避免JVM栈帧溢出

  • 最小栈重复值遗漏:相等最小值必须重复入栈,弹出时才能同步更新最小值

2.3.7 面试标准满分答题模板(直接默写)

栈是后进先出的受限线性表,仅支持栈顶增删查操作,核心时间复杂度O(1)。Java工程中废弃原生Stack类,统一使用Deque接口下的ArrayDeque实现栈功能,性能更优。栈核心应用包含基础括号匹配、表达式求值、迭代DFS遍历、二叉树非递归遍历;进阶核心为单调栈,通过维持栈内元素单调性,一次遍历解决区间最值、边界查找等难题,将暴力算法优化为线性时间复杂度。最小栈通过双栈设计,实现O(1)获取最值,是面试高频手撕题型。同时JVM虚拟机栈基于栈结构实现方法调用与栈帧管理,递归算法的栈空间复杂度由最大递归深度决定。

2.4 队列 Queue(FIFO 先进先出)

核心定义(面试必背) :队列是受限线性表 ,严格遵循 FIFO(First In First Out 先进先出) 原则,仅允许在队尾(rear)插入元素(入队)、队首(front)删除元素(出队),两端操作严格受限,不支持中间位置增删改查。

核心复杂度 :常规队列首尾增删操作均为 O(1),遍历查询为 O(n);优先队列、单调队列根据结构复杂度不同。

核心作用:解耦、异步、削峰、任务排队、层级遍历,是BFS、滑动窗口、线程池、消息队列的底层核心结构。

2.4.1 Java 队列完整体系(工程分级·全覆盖)

Java队列核心顶层接口为 java.util.Queue,继承Collection接口,分为普通队列、双端队列、优先队列、并发阻塞队列四大类,适配不同刷题与工程场景。

一、普通非阻塞队列(单线程刷题首选)
  • ArrayDeque(数组双端队列·首选) :基于动态循环数组,无容量限制、无锁、性能极致,首尾操作O(1),单线程队列/栈场景最优替代方案,刷题首选

  • LinkedList(链式队列):基于双向链表,动态扩容无内存浪费,适合元素数量波动极大场景,首尾增删高效,随机访问低效

  • PriorityQueue(优先队列·堆结构):底层完全二叉树(小顶堆默认),元素自动排序,用于TopK、最值筛选场景,无序入队、有序出队

二、双端队列 Deque(万能结构)

定义:Deque 是双向队列,支持首尾双向入队、出队、查询,可同时充当栈、普通队列、双端队列,是Java最灵活的线性结构。

核心优势:兼顾栈LIFO、队列FIFO特性,一套结构适配多种场景,工程与刷题优先使用。

三、并发阻塞队列(多线程工程必备)
  • ArrayBlockingQueue:有界数组阻塞队列,固定容量,生产消费同步

  • LinkedBlockingQueue:无界/有界链式阻塞队列,线程池默认队列

  • PriorityBlockingQueue:并发优先队列,带排序功能

  • SynchronousQueue:同步队列,无容量,一对一生产消费

2.4.2 队列核心API(刷题/工程标准·零报错)

区分抛异常返回空值两套API,刷题优先使用安全API,避免异常报错。

1)安全API(推荐·刷题首选)
  • offer(E e):队尾入队,成功返回true,队列满返回false(不抛异常)

  • poll():队首出队并返回元素,队空返回null(不抛异常)

  • peek():获取队首元素不删除,队空返回null(不抛异常)

  • isEmpty():判断队列是否为空

  • size():获取队列元素个数

2)风险API(禁止刷题使用)
  • add()/remove()/element():队列满/空时直接抛异常,刷题极易报错,仅工程特殊场景使用

2.4.3 三大核心队列 实战完整代码

一、普通队列(ArrayDeque)标准实战模板

适用于BFS遍历、任务排队、普通先进先出场景,单线程性能最优。

java 复制代码
import java.util.Deque;
import java.util.ArrayDeque;

/**
 * 普通队列(ArrayDeque)实战模板
 * 标准FIFO先进先出,刷题通用
 */
public class QueueDemo {
    public static void main(String[] args) {
        // 初始化标准队列(官方首选)
        Deque<Integer> queue = new ArrayDeque<>();

        // 1. 入队 offer(安全不抛异常)
        queue.offer(1);
        queue.offer(2);
        queue.offer(3);
        queue.offer(4);

        // 2. 获取队首元素(不删除)
        System.out.println("队首元素:" + queue.peek()); // 输出1

        // 3. 遍历出队(标准FIFO)
        System.out.print("队列出队顺序:");
        while (!queue.isEmpty()) {
            // 出队并删除队首元素
            Integer val = queue.poll();
            System.out.print(val + " ");
        }
        // 输出:1 2 3 4
    }
}
二、双端队列 Deque 实战模板(首尾双向操作)

支持首尾入队、首尾出队,可模拟栈、队列、双向队列,适配滑动窗口、边界操作场景。

java 复制代码
import java.util.Deque;
import java.util.ArrayDeque;

/**
 * 双端队列 Deque 实战模板
 * 首尾双向操作,万能线性结构
 */
public class DequeDemo {
    public static void main(String[] args) {
        Deque<Integer> deque = new ArrayDeque<>();

        // 尾部入队(队列模式)
        deque.offerLast(10);
        deque.offerLast(20);
        // 头部入队
        deque.offerFirst(5);

        System.out.println("队首:" + deque.peekFirst()); // 5
        System.out.println("队尾:" + deque.peekLast());  // 20

        // 头部出队
        deque.pollFirst();
        // 尾部出队
        deque.pollLast();

        System.out.println("剩余元素:" + deque.peek()); // 10
    }
}
三、优先队列 PriorityQueue 实战模板(TopK专属)

底层最小堆,默认升序排序,可自定义比较器实现最大堆,用于最值筛选、TopK问题。

java 复制代码
import java.util.PriorityQueue;
import java.util.Arrays;
import java.util.List;

/**
 * 优先队列(堆)实战模板
 * 最小堆、最大堆、TopK问题全覆盖
 */
public class PriorityQueueDemo {
    public static void main(String[] args) {
        int[] nums = {3, 1, 4, 2, 5, 9, 7};

        // 1. 默认最小堆(升序)
        PriorityQueue<Integer> minHeap = new PriorityQueue<>();
        for (int num : nums) {
            minHeap.offer(num);
        }
        System.out.print("最小堆出队顺序:");
        while (!minHeap.isEmpty()) {
            System.out.print(minHeap.poll() + " ");
        }

        // 2. 自定义最大堆(降序)
        PriorityQueue<Integer> maxHeap = new PriorityQueue<>((a, b) -> b - a);
        for (int num : nums) {
            maxHeap.offer(num);
        }
        System.out.print("\n最大堆出队顺序:");
        while (!maxHeap.isEmpty()) {
            System.out.print(maxHeap.poll() + " ");
        }

        // 3. 经典TopK问题:获取前3大元素
        List<Integer> topK = getTopK(nums, 3);
        System.out.println("\n前3大元素:" + topK);
    }

    /**
     * TopK 核心模板:小顶堆求前K大
     */
    public static List<Integer> getTopK(int[] nums, int k) {
        PriorityQueue<Integer> heap = new PriorityQueue<>();
        for (int num : nums) {
            heap.offer(num);
            // 堆容量超过k,弹出最小值,保留最大k个
            if (heap.size() > k) {
                heap.poll();
            }
        }
        return new java.util.ArrayList<>(heap);
    }
}

2.4.4 单调队列(刷题大杀器·滑动窗口专属)

核心定义 :基于双端队列实现,队列内元素下标/数值严格单调递增/递减,一次遍历 O(n) 求解滑动窗口最值,是LeetCode困难题高频考点。

核心原理:入队时移除破坏单调性的队尾元素,出队时移除超出窗口范围的队首元素,始终维持队列单调有序。

单调队列完整实战模板(滑动窗口最大值)
java 复制代码
import java.util.Deque;
import java.util.ArrayDeque;

/**
 * 单调队列模板:滑动窗口最大值
 * 时间复杂度O(n),秒杀LeetCode239
 */
public class MonotonicQueue {
    public int[] maxSlidingWindow(int[] nums, int k) {
        if (nums == null || nums.length == 0 || k == 0) {
            return new int[0];
        }
        int n = nums.length;
        int[] res = new int[n - k + 1];
        // 队列存储元素下标,维持队内数值单调递减
        Deque<Integer> deque = new ArrayDeque<>();
        int index = 0;

        for (int i = 0; i < n; i++) {
            // 1. 队尾维护单调递减:移除所有比当前元素小的尾部元素
            while (!deque.isEmpty() && nums[i] > nums[deque.peekLast()]) {
                deque.pollLast();
            }
            // 当前下标入队
            deque.offerLast(i);

            // 2. 移除窗口外过期元素(队首超出左边界)
            while (deque.peekFirst() <= i - k) {
                deque.pollFirst();
            }

            // 3. 窗口形成后,记录最大值(队首为当前窗口最大值)
            if (i >= k - 1) {
                res[index++] = nums[deque.peekFirst()];
            }
        }
        return res;
    }

    // 测试
    public static void main(String[] args) {
        MonotonicQueue mq = new MonotonicQueue();
        int[] nums = {1,3,-1,-3,5,3,6,7};
        int[] res = mq.maxSlidingWindow(nums, 3);
        // 输出:[3,3,5,5,6,7]
        for (int num : res) {
            System.out.print(num + " ");
        }
    }
}

2.4.5 队列核心高频应用场景

  • BFS广度优先遍历:二叉树层序遍历、图的层序遍历、最短路径问题(队列专属核心场景)

  • 滑动窗口算法:单调队列求解窗口最值、区间极值问题

  • TopK最值筛选:优先队列堆排序,高效筛选海量数据最值

  • 任务排队机制:线程池任务队列、消息队列、异步任务处理

  • 缓冲区限流:流量削峰、数据缓冲、有序消费

  • 多源BFS:矩阵最远/最近距离、洪水填充问题

2.4.6 队列高频易错点(面试/刷题避坑)

  • API混用报错:禁止使用add/remove,优先offer/poll/peek,杜绝空队列异常

  • 优先队列误区:默认小顶堆,求最大值需手动反转比较器,队列遍历无序,仅出队有序

  • 单调队列下标混淆:队列存储下标而非数值,用于判断窗口边界、维持单调性

  • 窗口过期元素未清理:必须先移除窗口外队首元素,再更新结果,否则取值错误

  • ArrayDeque不支持null:入队null元素直接抛空指针,刷题需提前判空

2.4.7 面试标准满分答题模板(直接默写)

队列是遵循FIFO先进先出的受限线性表,仅支持队尾入队、队首出队操作,常规首尾操作时间复杂度O(1)。Java工程与刷题优先使用ArrayDeque实现普通队列,性能优于LinkedList;优先队列PriorityQueue基于堆实现,可完成有序出队与TopK最值筛选;单调队列基于双端队列实现,通过维持队内元素单调性,线性时间解决滑动窗口最值难题。队列核心应用为BFS层序遍历、任务排队、流量削峰、窗口极值计算,多线程场景下使用阻塞队列保证线程安全,是算法刷题与工程开发的基础核心结构。

第三章 树形数据结构(Java 集合底层全在这里)

3.1 普通二叉树(完整理论+全网最全实战代码)

核心定义(面试必背):二叉树是每个节点最多拥有两个子节点(左子节点、右子节点)的树形非线性数据结构,子树严格区分左右,不可互换。核心特性:递归定义、天然分治结构,所有二叉树题目均可通过「递归分解子问题」解决,是树形算法的基础核心。

基础节点定义(全局通用模板)

java 复制代码
// 二叉树标准节点类(刷题/面试通用)
public class TreeNode {
    // 节点值
    int val;
    // 左子节点
    TreeNode left;
    // 右子节点
    TreeNode right;

    // 空构造
    TreeNode() {}
    // 单值构造
    TreeNode(int val) { this.val = val; }
    // 全参数构造
    TreeNode(int val, TreeNode left, TreeNode right) {
        this.val = val;
        this.left = left;
        this.right = right;
    }
}

3.1.1 二叉树四大遍历(全覆盖:递归+迭代+Morris)

遍历核心口诀:前序(根左右)、中序(左根右)、后序(左右根)、层序(从上到下、从左到右)

一、递归版遍历(简洁·面试默写首选)

递归遍历核心:依托二叉树递归定义,先处理当前根节点,再递归遍历左右子树,代码极简,适合快速刷题。

java 复制代码
import java.util.ArrayList;
import java.util.List;

// 二叉树递归遍历全套模板
public class TreeRecurTraverse {
    List<Integer> res = new ArrayList<>();

    // 1. 前序遍历:根 → 左 → 右
    public List<Integer> preOrder(TreeNode root) {
        if (root == null) return res;
        res.add(root.val);
        preOrder(root.left);
        preOrder(root.right);
        return res;
    }

    // 2. 中序遍历:左 → 根 → 右(BST专属有序遍历)
    public List<Integer> inOrder(TreeNode root) {
        if (root == null) return res;
        inOrder(root.left);
        res.add(root.val);
        inOrder(root.right);
        return res;
    }

    // 3. 后序遍历:左 → 右 → 根
    public List<Integer> postOrder(TreeNode root) {
        if (root == null) return res;
        postOrder(root.left);
        postOrder(root.right);
        res.add(root.val);
        return res;
    }
}
二、迭代版遍历(栈实现·工程首选·无递归栈溢出)

核心原理:手动模拟JVM递归栈,用Deque栈保存节点,规避大数据量递归栈溢出问题,面试高频手撕考点。

java 复制代码
import java.util.ArrayList;
import java.util.Deque;
import java.util.ArrayDeque;
import java.util.List;

// 二叉树迭代遍历全套模板
public class TreeIterTraverse {

    // 前序迭代:根左右
    public List<Integer> preOrderIter(TreeNode root) {
        List<Integer> res = new ArrayList<>();
        if (root == null) return res;
        Deque<TreeNode> stack = new ArrayDeque<>();
        stack.push(root);
        while (!stack.isEmpty()) {
            TreeNode cur = stack.pop();
            res.add(cur.val);
            // 栈后进先出,先压右、后压左
            if (cur.right != null) stack.push(cur.right);
            if (cur.left != null) stack.push(cur.left);
        }
        return res;
    }

    // 中序迭代:左根右(最常考)
    public List<Integer> inOrderIter(TreeNode root) {
        List<Integer> res = new ArrayList<>();
        Deque<TreeNode> stack = new ArrayDeque<>();
        TreeNode cur = root;
        while (cur != null || !stack.isEmpty()) {
            // 一直遍历到最左节点
            while (cur != null) {
                stack.push(cur);
                cur = cur.left;
            }
            cur = stack.pop();
            res.add(cur.val);
            // 遍历右子树
            cur = cur.right;
        }
        return res;
    }

    // 后序迭代:左右根
    public List<Integer> postOrderIter(TreeNode root) {
        List<Integer> res = new ArrayList<>();
        if (root == null) return res;
        Deque<TreeNode> stack = new ArrayDeque<>();
        TreeNode pre = null;
        while (root != null || !stack.isEmpty()) {
            while (root != null) {
                stack.push(root);
                root = root.left;
            }
            root = stack.peek();
            // 右子树为空或已遍历完毕,才可访问根节点
            if (root.right == null || root.right == pre) {
                res.add(root.val);
                pre = root;
                stack.pop();
                root = null;
            } else {
                root = root.right;
            }
        }
        return res;
    }
}
三、层序遍历 BFS(队列实现·必考)

核心原理:借助队列FIFO特性,逐层遍历节点,可获取树的高度、每层节点数据,是二叉树层次类问题通解。

java 复制代码
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;

// 二叉树层序遍历(完整分层结果)
public class TreeBFS {
    public List<List<Integer>> levelOrder(TreeNode root) {
        List<List<Integer>> res = new ArrayList<>();
        if (root == null) return res;
        Queue<TreeNode> queue = new LinkedList<>();
        queue.offer(root);

        while (!queue.isEmpty()) {
            // 当前层节点数量
            int levelSize = queue.size();
            List<Integer> levelList = new ArrayList<>();
            for (int i = 0; i < levelSize; i++) {
                TreeNode cur = queue.poll();
                levelList.add(cur.val);
                // 左右子节点入队
                if (cur.left != null) queue.offer(cur.left);
                if (cur.right != null) queue.offer(cur.right);
            }
            res.add(levelList);
        }
        return res;
    }
}
四、Morris遍历(O(1)空间·进阶面试考点)

核心优势:无需栈、无需递归,仅用临时指针实现遍历,空间复杂度O(1),是二叉树最优遍历方式,大厂进阶面试必考。核心原理:利用叶子节点空指针,建立临时回溯路径。

java 复制代码
import java.util.ArrayList;
import java.util.List;

// Morris遍历 前/中/后序 完整版
public class TreeMorris {
    List<Integer> res = new ArrayList<>();

    // Morris前序遍历
    public List<Integer> morrisPre(TreeNode root) {
        res.clear();
        TreeNode cur = root, mostRight;
        while (cur != null) {
            mostRight = cur.left;
            // 存在左子树
            if (mostRight != null) {
                // 找到左子树最右节点
                while (mostRight.right != null && mostRight.right != cur) {
                    mostRight = mostRight.right;
                }
                // 未建立临时路径,访问当前节点
                if (mostRight.right == null) {
                    mostRight.right = cur;
                    res.add(cur.val);
                    cur = cur.left;
                } else {
                    // 已遍历完毕,拆除临时路径
                    mostRight.right = null;
                    cur = cur.right;
                }
            } else {
                // 无左子树,直接访问当前节点
                res.add(cur.val);
                cur = cur.right;
            }
        }
        return res;
    }

    // Morris中序遍历
    public List<Integer> morrisIn(TreeNode root) {
        res.clear();
        TreeNode cur = root, mostRight;
        while (cur != null) {
            mostRight = cur.left;
            if (mostRight != null) {
                while (mostRight.right != null && mostRight.right != cur) {
                    mostRight = mostRight.right;
                }
                if (mostRight.right == null) {
                    mostRight.right = cur;
                    cur = cur.left;
                } else {
                    mostRight.right = null;
                    res.add(cur.val);
                    cur = cur.right;
                }
            } else {
                res.add(cur.val);
                cur = cur.right;
            }
        }
        return res;
    }
}

3.1.2 普通二叉树高频手撕真题代码(全覆盖)

1)二叉树最大深度(基础必背)
java 复制代码
// 递归分治:左右子树最大深度+1
public int maxDepth(TreeNode root) {
    if (root == null) return 0;
    int leftDepth = maxDepth(root.left);
    int rightDepth = maxDepth(root.right);
    return Math.max(leftDepth, rightDepth) + 1;
}
2)判断对称二叉树(面试高频)
java 复制代码
public boolean isSymmetric(TreeNode root) {
    return check(root.left, root.right);
}

// 递归校验:左左=右右、左右=右左
private boolean check(TreeNode l, TreeNode r) {
    if (l == null && r == null) return true;
    if (l == null || r == null) return false;
    return l.val == r.val && check(l.left, r.right) && check(l.right, r.left);
}
3)二叉树直径(中等高频)
java 复制代码
public class TreeDiameter {
    int maxLen = 0;
    public int diameterOfBinaryTree(TreeNode root) {
        depth(root);
        return maxLen;
    }

    // 递归求深度,同时更新最大直径
    private int depth(TreeNode root) {
        if (root == null) return 0;
        int left = depth(root.left);
        int right = depth(root.right);
        // 直径=左深度+右深度
        maxLen = Math.max(maxLen, left + right);
        return Math.max(left, right) + 1;
    }
}
4)二叉树最大路径和(压轴高频)
java 复制代码
public class MaxPathSum {
    int maxSum = Integer.MIN_VALUE;
    public int maxPathSum(TreeNode root) {
        dfs(root);
        return maxSum;
    }

    // 返回当前节点可贡献的最大路径和
    private int dfs(TreeNode root) {
        if (root == null) return 0;
        // 负数路径直接舍弃
        int left = Math.max(0, dfs(root.left));
        int right = Math.max(0, dfs(root.right));
        // 更新全局最大路径和
        maxSum = Math.max(maxSum, left + right + root.val);
        // 向父节点贡献单边最大值
        return Math.max(left, right) + root.val;
    }
}
5)翻转二叉树(面试经典)
java 复制代码
public TreeNode invertTree(TreeNode root) {
    if (root == null) return null;
    // 交换左右子树
    TreeNode temp = root.left;
    root.left = invertTree(root.right);
    root.right = invertTree(temp);
    return root;
}

3.1.3 二叉树高频易错点(刷题避坑)

  • 递归终止条件:必须优先判root==null,空树直接返回,杜绝空指针异常

  • 后序遍历逻辑:必须先遍历左右子树,再处理根节点,适合求深度、路径、最值问题

  • 层序遍历边界:必须提前记录每层节点数,不可直接遍历队列,否则无法分层

  • 负数路径舍弃:最大路径和问题中,子路径和为负数时直接丢弃,不贡献上层节点

  • Morris临时路径:遍历完毕必须拆除临时指针,避免破坏原树结构

3.1.4 面试满分答题模板

普通二叉树是每个节点最多包含左右两个子节点的非线性递归结构,核心解题思想为分治递归,将整树问题拆解为左右子树子问题求解。主流遍历方式分为四类:递归遍历代码简洁、适合快速刷题;迭代遍历基于栈/队列实现,规避递归栈溢出,适配大数据量场景;层序BFS可实现分层遍历,解决树高度、层级最值问题;Morris遍历通过临时指针回溯,实现O(1)极致空间复杂度。二叉树高频题型均围绕遍历、深度、路径、对称、翻转展开,核心解题思路为递归分治+状态更新,是树形进阶结构(BST、红黑树、堆)的基础。

3.2 BST 二叉搜索树(完整版理论+全实战代码+面试满分考点)

核心定义(面试必背) :二叉搜索树(BST,Binary Search Tree)是满足有序递归特性的特殊二叉树,是TreeMap/TreeSet底层核心结构。

核心性质左子树所有节点值 < 根节点值 < 右子树所有节点值,且左右子树也必须满足该规则,无重复节点(默认去重)。

核心专属特性 :BST 中序遍历结果严格升序有序,这是BST所有算法的核心突破口,也是普通二叉树不具备的独有特性。

时间复杂度:理想平衡状态下,增删查均为 O(logn);最坏斜树状态下退化为 O(n),这也是红黑树等平衡二叉树诞生的核心原因。


3.2.1 BST 五大核心基础操作(全套可默写代码)

包含:查询节点、插入节点、删除节点、找最小值、找最大值,覆盖BST所有基础场景,其中删除节点为面试最高频难点

java 复制代码
/**
 * 二叉搜索树 BST 全套基础操作
 * 查询/插入/删除/最值查找 全覆盖
 */
public class BSTOperation {

    // BST 标准节点(复用全局TreeNode)
    static class TreeNode {
        int val;
        TreeNode left, right;
        TreeNode(int val) { this.val = val; }
    }

    // 1. 查找目标节点(BST专属二分查找)
    public TreeNode searchBST(TreeNode root, int val) {
        // 空节点或找到目标,直接返回
        if (root == null || root.val == val) {
            return root;
        }
        // 目标值更小,递归左子树
        return val < root.val ? searchBST(root.left, val) : searchBST(root.right, val);
    }

    // 2. 插入节点(BST标准插入,不重复)
    public TreeNode insertBST(TreeNode root, int val) {
        // 找到空位置,新建节点插入
        if (root == null) {
            return new TreeNode(val);
        }
        // 小于根节点插左子树,大于插右子树,重复值不处理
        if (val < root.val) {
            root.left = insertBST(root.left, val);
        } else if (val > root.val) {
            root.right = insertBST(root.right, val);
        }
        return root;
    }

    // 3. BST 删除节点(面试核心难点,三种情况全覆盖)
    public TreeNode deleteBST(TreeNode root, int val) {
        if (root == null) {
            return null;
        }
        // 递归查找待删除节点
        if (val < root.val) {
            root.left = deleteBST(root.left, val);
        } else if (val > root.val) {
            root.right = deleteBST(root.right, val);
        } else {
            // 找到待删除节点,分三种情况处理
            // 情况1:叶子节点(无左右孩子),直接删除
            if (root.left == null && root.right == null) {
                return null;
            }
            // 情况2:只有单个孩子,直接用子节点替代当前节点
            else if (root.left == null) {
                return root.right;
            } else if (root.right == null) {
                return root.left;
            }
            // 情况3:左右孩子都存在(最难)
            // 规则:找【右子树最小节点】(后继节点)替换当前节点,再删除后继节点
            TreeNode minRight = getMinNode(root.right);
            root.val = minRight.val;
            // 递归删除被替换的后继节点
            root.right = deleteBST(root.right, minRight.val);
        }
        return root;
    }

    // 4. 获取BST最小值(最左叶子节点)
    public TreeNode getMinNode(TreeNode root) {
        while (root.left != null) {
            root = root.left;
        }
        return root;
    }

    // 5. 获取BST最大值(最右叶子节点)
    public TreeNode getMaxNode(TreeNode root) {
        while (root.right != null) {
            root = root.right;
        }
        return root;
    }
}

3.2.2 BST 删除节点三大核心场景(面试必背解析)

  • 场景一:删除叶子节点:节点无左右子树,直接置空删除,无结构变动

  • 场景二:删除单分支节点:仅左孩子/仅右孩子,直接用唯一子节点替代当前节点,维持BST有序性

  • 场景三:删除双分支节点(高频考点) :左右子树均存在,采用后继替换法后继节点:右子树的最小值节点(保证大于左子树所有值、小于右子树剩余值)

  • 前驱节点:左子树的最大值节点(等价替换方案,面试通用)

  • 核心逻辑:替换值 + 递归删除后继,完美保留BST有序特性


3.2.3 BST 高频手撕真题代码(面试刷题全覆盖)

1)验证二叉搜索树(LeetCode98 必考)

核心思路:依托中序遍历有序特性,校验遍历序列是否严格递增;或递归传递上下界约束。

java 复制代码
import java.util.Stack;

public class ValidBST {
    // 递归上下界法(最优解)
    public boolean isValidBST(TreeNode root) {
        // 用Long规避int极值边界问题
        return check(root, Long.MIN_VALUE, Long.MAX_VALUE);
    }

    private boolean check(TreeNode root, long min, long max) {
        if (root == null) return true;
        // 当前节点超出边界,不是BST
        if (root.val <= min || root.val >= max) return false;
        // 左子树上限为当前值,右子树下限为当前值,递归校验
        return check(root.left, min, root.val) && check(root.right, root.val, max);
    }

    // 中序迭代法(验证有序性)
    public boolean isValidBSTInOrder(TreeNode root) {
        Stack<TreeNode> stack = new Stack<>();
        long preVal = Long.MIN_VALUE;
        while (root != null || !stack.isEmpty()) {
            while (root != null) {
                stack.push(root);
                root = root.left;
            }
            root = stack.pop();
            // 中序遍历必须严格递增
            if (root.val <= preVal) return false;
            preVal = root.val;
            root = root.right;
        }
        return true;
    }
}
2)BST 第K小元素(LeetCode230 高频)

核心思路:BST中序遍历升序,遍历第k个节点即为答案,可提前终止遍历优化效率。

java 复制代码
public class KthSmallestBST {
    int count = 0;
    int res = 0;

    public int kthSmallest(TreeNode root, int k) {
        inOrder(root, k);
        return res;
    }

    private void inOrder(TreeNode root, int k) {
        if (root == null) return;
        // 左子树优先遍历
        inOrder(root.left, k);
        // 计数,找到第k个直接终止
        count++;
        if (count == k) {
            res = root.val;
            return;
        }
        inOrder(root.right, k);
    }
}
3)有序数组构建平衡BST(LeetCode108)

核心思路:有序数组中点为根,左右区间递归构建子树,天然平衡。

java 复制代码
public class SortedArrayToBST {
    public TreeNode sortedArrayToBST(int[] nums) {
        return build(nums, 0, nums.length - 1);
    }

    private TreeNode build(int[] nums, int left, int right) {
        if (left > right) return null;
        // 取中点,保证平衡
        int mid = left + (right - left) / 2;
        TreeNode root = new TreeNode(nums[mid]);
        root.left = build(nums, left, mid - 1);
        root.right = build(nums, mid + 1, right);
        return root;
    }
}

3.2.4 BST 高频易错点(面试扣分避坑)

  • 有序性误区 :BST要求全局有序,不仅是父子节点有序,子树所有节点都要满足大小关系

  • 删除节点误区:双孩子节点不能直接删除,必须用后继/前驱替换,否则破坏BST结构

  • 边界值溢出:校验BST时,必须用Long存储极值,避免int极值边界判断错误

  • 重复值处理:标准BST不存储重复值,插入重复值需直接跳过,不可覆盖

  • 最坏复杂度遗漏:非平衡BST会退化为链表,复杂度O(n),工程必须用平衡BST(红黑树)


3.2.5 面试满分答题模板(直接默写)

二叉搜索树BST是满足左子树所有节点值<根节点<右子树所有节点值的特殊二叉树,核心特性为中序遍历严格升序有序。基础操作包含查询、插入、最值查找和删除,其中删除节点分为叶子节点、单孩子节点、双孩子节点三种场景,双孩子节点需通过后继节点替换法维持有序性。理想平衡BST增删查复杂度为O(logn),最坏斜树状态退化为O(n)。BST是Java TreeMap、TreeSet的底层结构,核心用于有序数据存储、区间查询、最值筛选,工程中为规避退化问题,衍生出红黑树等平衡二叉搜索树。

3.3 平衡树(Java 集合底层核心·完整版原理+面试考点+实战代码)

核心定位(面试必背) :普通BST会因插入删除退化为链表,时间复杂度从O(logn)劣化为O(n)。平衡树是自平衡的二叉搜索树 ,通过严格平衡规则+旋转修复,保证树高始终维持在O(logn),增删查操作稳定高效,是Java TreeMap、TreeSet的底层核心实现,也是大厂面试高频手撕考点。

平衡树核心分类:AVL树(严格平衡)、红黑树(近似平衡,工程主流),Java集合仅采用红黑树,AVL树多用于理论面试考察。


3.3.1 AVL 树(严格平衡二叉搜索树)

1. 核心定义

AVL树是首个自平衡二叉搜索树 ,在BST基础上增加严格平衡约束:任意节点的左右子树高度差(平衡因子)绝对值 ≤ 1,否则判定为失衡,必须通过旋转修复平衡。

核心参数:平衡因子 = 左子树高度 - 右子树高度,取值仅为 -1、0、1。

优缺点 :平衡精度极高、查询效率最优;但插入删除极易失衡,需要频繁旋转维护,修改性能差,无工程落地,仅面试理论考察

2. 四大失衡场景+旋转修复(必背)

所有AVL树失衡仅分为四种场景,对应四种固定旋转操作,全覆盖所有失衡修复:

  • LL型(左左失衡) :左子树的左子节点插入导致失衡 →单次右旋

  • RR型(右右失衡) :右子树的右子节点插入导致失衡 →单次左旋

  • LR型(左右失衡) :左子树的右子节点插入导致失衡 → 先左旋、后右旋

  • RL型(右左失衡) :右子树的左子节点插入导致失衡 → 先右旋、后左旋

3. AVL树完整实战代码(可直接默写)
java 复制代码
/**
 * AVL树完整实现(平衡因子+四大旋转+插入修复)
 * 严格平衡二叉搜索树,面试手撕专用
 */
public class AVLTree {
    // 树节点定义
    static class TreeNode {
        int val;
        TreeNode left;
        TreeNode right;
        int height; // 记录当前节点子树高度,平衡核心参数

        public TreeNode(int val) {
            this.val = val;
            this.height = 1; // 新节点默认高度为1
        }
    }

    // 获取节点高度(空节点高度为0)
    private int getHeight(TreeNode node) {
        return node == null ? 0 : node.height;
    }

    // 计算平衡因子
    private int getBalanceFactor(TreeNode node) {
        return node == null ? 0 : getHeight(node.left) - getHeight(node.right);
    }

    // 右旋(修复LL失衡)
    private TreeNode rightRotate(TreeNode y) {
        TreeNode x = y.left;
        TreeNode temp = x.right;

        // 旋转核心逻辑
        x.right = y;
        y.left = temp;

        // 更新高度(先更新子节点,再更新根节点)
        y.height = Math.max(getHeight(y.left), getHeight(y.right)) + 1;
        x.height = Math.max(getHeight(x.left), getHeight(x.right)) + 1;
        return x;
    }

    // 左旋(修复RR失衡)
    private TreeNode leftRotate(TreeNode x) {
        TreeNode y = x.right;
        TreeNode temp = y.left;

        // 旋转核心逻辑
        y.left = x;
        x.right = temp;

        // 更新高度
        x.height = Math.max(getHeight(x.left), getHeight(x.right)) + 1;
        y.height = Math.max(getHeight(y.left), getHeight(y.right)) + 1;
        return y;
    }

    // AVL插入节点+自动平衡修复
    public TreeNode insert(TreeNode root, int val) {
        // 1. 常规BST插入
        if (root == null) {
            return new TreeNode(val);
        }
        if (val < root.val) {
            root.left = insert(root.left, val);
        } else if (val > root.val) {
            root.right = insert(root.right, val);
        } else {
            // 重复值不插入
            return root;
        }

        // 2. 更新当前节点高度
        root.height = Math.max(getHeight(root.left), getHeight(root.right)) + 1;

        // 3. 计算平衡因子,判断失衡类型并修复
        int balance = getBalanceFactor(root);

        // LL型失衡:右旋
        if (balance > 1 && val < root.left.val) {
            return rightRotate(root);
        }

        // RR型失衡:左旋
        if (balance < -1 && val > root.right.val) {
            return leftRotate(root);
        }

        // LR型失衡:先左后右
        if (balance > 1 && val > root.left.val) {
            root.left = leftRotate(root.left);
            return rightRotate(root);
        }

        // RL型失衡:先右后左
        if (balance < -1 && val < root.right.val) {
            root.right = rightRotate(root.right);
            return leftRotate(root);
        }

        // 未失衡,直接返回
        return root;
    }
}
4. AVL树高频考点总结
  • 平衡因子绝对值严格≤1,树高稳定O(logn),查询效率高于红黑树

  • 四大旋转场景必须精准区分,双旋转针对交叉型失衡

  • 短板:插入删除一次失衡就需要旋转修复,维护成本极高,不适合频繁修改场景

  • 工程无应用,仅用于面试平衡树基础原理考察


3.3.2 红黑树(Java核心·TreeMap/TreeSet底层·满分完整版)

核心定位(面试重中之重) :红黑树是近似平衡的二叉搜索树 ,放弃AVL树的严格平衡,通过5条颜色规则实现低维护成本的自平衡,增删查均稳定O(logn),是Java集合、C++ STL、Go Tree的底层核心,面试必考原理+手撕。

1. 红黑树五大铁律(必背满分)
    1. 每个节点只能是红色黑色
    1. 根节点必须是黑色
    1. 所有**叶子节点(NIL空节点)**均为黑色
    1. 红色节点的两个子节点必须是黑色(红节点不能相邻)
    1. 任意节点到其所有叶子节点的路径,黑色节点数量相同(黑高一致)
2. 核心特性(面试高频问答)
  • 平衡本质:最长路径长度 ≤ 最短路径长度的2倍,实现近似平衡

  • 时间复杂度:增删查稳定 O(logn),不受数据有序/无序影响

  • 优势:相比AVL树,旋转次数极少,插入删除仅需最多2次旋转、删除最多3次旋转,修改性能极强,适配工程高频读写场景

  • 黑高:从根到叶子的黑色节点总数,是红黑树平衡的核心判定依据

3. 红黑树三大修复手段

红黑树失衡不依赖高度,依赖颜色违规,修复仅需三种操作,组合解决所有异常场景:

  • 变色:修改节点红黑颜色,解决相邻红节点、黑高不一致问题(最常用)

  • 左旋:针对右子树偏重失衡,同AVL左旋逻辑

  • 右旋:针对左子树偏重失衡,同AVL右旋逻辑

4. 红黑树完整Java实战代码(极简可默写版)
java 复制代码
/**
 * 红黑树完整实现(Java TreeMap底层核心)
 * 包含节点定义、旋转、插入、颜色修复核心逻辑
 */
public class RedBlackTree {
    // 定义节点颜色常量
    private static final boolean RED = true;
    private static final boolean BLACK = false;

    // 红黑树节点
    static class TreeNode {
        int val;
        TreeNode left, right, parent;
        boolean color; // true=红色,false=黑色

        public TreeNode(int val) {
            this.val = val;
            this.color = RED; // 新节点默认红色(减少黑高破坏概率)
            this.left = null;
            this.right = null;
            this.parent = null;
        }
    }

    private TreeNode root;

    // 判断节点颜色(空节点默认黑色)
    private boolean getColor(TreeNode node) {
        return node == null ? BLACK : node.color;
    }

    // 左旋
    private void leftRotate(TreeNode x) {
        TreeNode y = x.right;
        TreeNode temp = y.left;

        // 1. y左子树挂载到x右节点
        x.right = temp;
        if (temp != null) {
            temp.parent = x;
        }

        // 2. x父节点挂载到y
        y.parent = x.parent;
        if (x.parent == null) {
            root = y;
        } else if (x == x.parent.left) {
            x.parent.left = y;
        } else {
            x.parent.right = y;
        }

        // 3. x作为y左子树
        y.left = x;
        x.parent = y;
    }

    // 右旋
    private void rightRotate(TreeNode y) {
        TreeNode x = y.left;
        TreeNode temp = x.right;

        // 1. x右子树挂载到y左节点
        y.left = temp;
        if (temp != null) {
            temp.parent = y;
        }

        // 2. y父节点挂载到x
        x.parent = y.parent;
        if (y.parent == null) {
            root = x;
        } else if (y == y.parent.left) {
            y.parent.left = x;
        } else {
            y.parent.right = x;
        }

        // 3. y作为x右子树
        x.right = y;
        y.parent = x;
    }

    // 插入后颜色平衡修复(核心)
    private void insertFix(TreeNode node) {
        // 父节点为红色时,触发违规修复
        while (node.parent != null && getColor(node.parent) == RED) {
            // 父节点是祖父节点左子树
            if (node.parent == node.parent.parent.left) {
                TreeNode uncle = node.parent.parent.right;

                // 场景1:叔叔节点为红色,直接变色修复
                if (getColor(uncle) == RED) {
                    node.parent.color = BLACK;
                    uncle.color = BLACK;
                    node.parent.parent.color = RED;
                    node = node.parent.parent;
                } else {
                    // 场景2:叔叔节点为黑色
                    if (node == node.parent.right) {
                        // LR场景:先左旋
                        node = node.parent;
                        leftRotate(node);
                    }
                    // LL场景:右旋+变色
                    node.parent.color = BLACK;
                    node.parent.parent.color = RED;
                    rightRotate(node.parent.parent);
                }
            } else {
                // 父节点是祖父节点右子树(对称逻辑)
                TreeNode uncle = node.parent.parent.left;

                // 场景1:叔叔红色,变色修复
                if (getColor(uncle) == RED) {
                    node.parent.color = BLACK;
                    uncle.color = BLACK;
                    node.parent.parent.color = RED;
                    node = node.parent.parent;
                } else {
                    // 场景2:叔叔黑色
                    if (node == node.parent.left) {
                        // RL场景:先右旋
                        node = node.parent;
                        rightRotate(node);
                    }
                    // RR场景:左旋+变色
                    node.parent.color = BLACK;
                    node.parent.parent.color = RED;
                    leftRotate(node.parent.parent);
                }
            }
        }
        // 根节点强制置黑
        root.color = BLACK;
    }

    // 红黑树插入节点
    public void insert(int val) {
        TreeNode newNode = new TreeNode(val);
        if (root == null) {
            root = newNode;
            root.color = BLACK;
            return;
        }

        // 1. 常规BST插入
        TreeNode cur = root;
        TreeNode parent = null;
        while (cur != null) {
            parent = cur;
            if (val < cur.val) {
                cur = cur.left;
            } else if (val > cur.val) {
                cur = cur.right;
            } else {
                return; // 重复值不插入
            }
        }

        // 绑定父子关系
        newNode.parent = parent;
        if (val < parent.val) {
            parent.left = newNode;
        } else {
            parent.right = newNode;
        }

        // 2. 插入后平衡修复
        insertFix(newNode);
    }
}
5. 红黑树插入核心考点
  • 新节点默认红色:若为黑色会直接破坏黑高规则,触发全局修复,红色仅可能触发红节点相邻违规,修复成本更低

  • 优先变色、其次旋转:变色无性能损耗,旋转有结构开销,工程优先轻量化修复

  • 插入仅3种违规场景,全部通过「变色+旋转」组合修复,最多2次旋转即可平衡

  • 根节点最终强制置黑,兜底满足五大规则

6. 红黑树 vs AVL树(面试必背对比)

|------|-------------|------------------------------|
| 对比维度 | AVL树 | 红黑树 |
| 平衡程度 | 严格平衡(高度差≤1) | 近似平衡(最长路径≤2倍最短路径) |
| 维护成本 | 高,频繁旋转 | 低,极少旋转 |
| 查询性能 | 略优(树更矮) | 略差 |
| 修改性能 | 差 | 优异(工程首选) |
| 工程应用 | 无实际落地 | Java TreeMap/TreeSet、C++ STL |

7. 面试满分答题模板(直接默写)

平衡树是解决普通BST退化问题的自平衡有序树,核心分为AVL树和红黑树。AVL树基于高度差实现严格平衡,通过四种旋转修复失衡,查询性能优异,但修改维护成本极高,无工程应用。红黑树是Java集合底层核心,通过五条颜色规则实现近似平衡,放弃严格平衡换取极低的维护成本,插入删除仅需少量变色和旋转操作,增删查稳定O(logn),适配工程高频读写场景,是TreeMap、TreeSet有序存储、有序查询的底层支撑。

3.3.3 B / B+ / B* 树(数据库索引核心·完整原理+面试考点+实战模拟代码)

核心定位(面试必背) :B/B+/B*树是多路平衡查找树 ,专为磁盘IO场景 设计(MySQL、文件系统索引底层)。二叉树树高过高,磁盘多次IO查询效率极低,而多路平衡树通过多分支、低树高特性,大幅减少磁盘读写次数,是磁盘索引的专属数据结构。三者层层迭代优化,B+树为目前数据库主流,B*树为进阶优化版本。

核心设计思想 :磁盘IO耗时远大于内存计算,尽可能降低树高、减少磁盘访问次数,所有节点一次磁盘页加载完成读写。


一、基础通用规则(B系列树统一约束)

定义m阶B树(多路平衡树通用标准):

  • 每个节点最多包含 m-1 个关键字、m 个子节点

  • 除根节点外,所有非叶子节点关键字数量 ≥ ⌈m/2⌉ - 1

  • 所有叶子节点在同一层,天然完全平衡

  • 节点内关键字严格升序有序,满足左小右大规则


二、B树(多路平衡查找树·基础版)
1. 核心结构特性
  • 关键字与数据共存:所有节点(叶子+非叶子)同时存储「索引关键字+完整数据」

  • 分支规则:k个关键字对应k+1个子节点,区间拆分精准

  • 查询特性:数据分散在所有层级,查询命中即终止,最短路径快、最长路径慢

  • IO次数:树高略高,磁盘IO次数多于B+树

2. 优缺点&适用场景
  • 优点:单点查询最快,命中高层节点直接返回数据,无需遍历到叶子

  • 缺点:范围查询极差,需要跨节点多次遍历、多次磁盘IO;节点存储数据量大,单页存储索引少,树高更高

  • 场景:少量单点精准查询,无工程主流应用,仅理论基础

3. Java极简模拟实战代码(m阶B树查询核心)
java 复制代码
import java.util.ArrayList;
import java.util.List;

/**
 * m阶B树 极简模拟实现(核心查询逻辑)
 * 适配理解多路平衡树查找原理,贴合磁盘索引查询逻辑
 */
public class BTree {
    // B树阶数
    private final int m;
    // 根节点
    private BTreeNode root;

    // B树节点结构
    static class BTreeNode {
        // 节点内关键字(有序)
        List<Integer> keys;
        // 子节点集合
        List<BTreeNode> children;
        // 是否为叶子节点
        boolean isLeaf;

        public BTreeNode(boolean isLeaf) {
            this.keys = new ArrayList<>();
            this.children = new ArrayList<>();
            this.isLeaf = isLeaf;
        }
    }

    // 初始化m阶B树
    public BTree(int m) {
        this.m = m;
        this.root = new BTreeNode(true);
    }

    // 核心:B树精准查询
    public Integer search(int key) {
        return search(root, key);
    }

    private Integer search(BTreeNode node, int key) {
        // 遍历当前节点关键字,查找匹配值
        int i = 0;
        while (i < node.keys.size() && key > node.keys.get(i)) {
            i++;
        }
        // 命中当前节点关键字,直接返回数据
        if (i < node.keys.size() && key == node.keys.get(i)) {
            return key;
        }
        // 叶子节点未命中,查询失败
        if (node.isLeaf) {
            return null;
        }
        // 递归查询对应子节点
        return search(node.children.get(i), key);
    }
}

三、B+树(MySQL索引主流·重点必学)

核心定义(面试满分) :B+树是B树的优化升级版,非叶子节点仅存索引关键字、不存数据,所有完整数据仅存储在叶子节点 ,且叶子节点通过双向链表串联,是MySQL InnoDB聚簇索引底层唯一实现

1. 核心独有特性(区别B树)
  • 数据分区存储:非叶子节点纯索引(轻量化),叶子节点存全量数据+有序链表

  • 关键字冗余:上层节点关键字会在下层叶子节点重复出现,保证索引全覆盖

  • 叶子链表串联 :所有叶子节点有序双向链表,范围查询O(k)极致高效

  • 树高更低:非叶子节点极简,单磁盘页可存储海量索引,树高通常2-3层

2. 核心优势(MySQL选型原因)
  • 极低磁盘IO:树高极低,查询最多3次磁盘IO

  • 范围查询无敌:链表遍历无需回溯,适配数据库区间查询、排序、分页

  • 查询性能稳定:所有查询都从根走到叶子,耗时均匀,无极端差异

  • 磁盘利用率高:非叶子节点无冗余数据,索引密度极高

3. Java极简模拟实战代码(B+树查询+范围查询)
java 复制代码
import java.util.ArrayList;
import java.util.List;

/**
 * m阶B树 极简模拟实现(核心查询逻辑)
 * 适配理解多路平衡树查找原理,贴合磁盘索引查询逻辑
 */
public class BTree {
    // B树阶数
    private final int m;
    // 根节点
    private BTreeNode root;

    // B树节点结构
    static class BTreeNode {
        // 节点内关键字(有序)
        List<Integer> keys;
        // 子节点集合
        List<BTreeNode> children;
        // 是否为叶子节点
        boolean isLeaf;

        public BTreeNode(boolean isLeaf) {
            this.keys = new ArrayList<>();
            this.children = new ArrayList<>();
            this.isLeaf = isLeaf;
        }
    }

    // 初始化m阶B树
    public BTree(int m) {
        this.m = m;
        this.root = new BTreeNode(true);
    }

    // 核心:B树精准查询
    public Integer search(int key) {
        return search(root, key);
    }

    private Integer search(BTreeNode node, int key) {
        // 遍历当前节点关键字,查找匹配值
        int i = 0;
        while (i < node.keys.size() && key > node.keys.get(i)) {
            i++;
        }
        // 命中当前节点关键字,直接返回数据
        if (i < node.keys.size() && key == node.keys.get(i)) {
            return key;
        }
        // 叶子节点未命中,查询失败
        if (node.isLeaf) {
            return null;
        }
        // 递归查询对应子节点
        return search(node.children.get(i), key);
    }
}

四、B*树(B+树进阶优化版)

核心定位 :B*树是B+树的空间优化版本,解决B+树节点拆分频繁、空间利用率低的问题,多用于文件系统索引(NTFS、FAT32),数据库极少使用。

1. 核心优化特性(区别B+树)
  • 空间利用率提升:B+树节点满1/2即拆分,B*树节点满2/3才拆分,大幅减少节点分裂次数

  • 非叶子节点链表串联 :不仅叶子节点有双向链表,非叶子节点也串联,索引遍历效率更高

  • 优先填充、后拆分:节点空间不足时,优先向兄弟节点借关键字,无法填充再拆分,减少树结构变动

2. 优缺点&适用场景
  • 优点:空间利用率极高、节点拆分IO开销少、树结构更稳定

  • 缺点:结构复杂、插入删除逻辑繁琐、范围查询提升有限

  • 场景:文件系统索引(静态多读少改场景),不适合数据库高频增删改场景

3. B*树核心优化逻辑代码片段(节点扩容优化)
java 复制代码
import java.util.ArrayList;
import java.util.List;

/**
 * B+树 核心模拟实现(贴合MySQL索引)
 * 核心特性:非叶子存索引、叶子存数据、叶子链表有序
 */
public class BPlusTree {
    private final int m;
    private BPlusTreeNode root;
    // 叶子节点链表头节点(用于范围查询)
    private BPlusTreeNode leafHead;

    static class BPlusTreeNode {
        List<Integer> keys;
        List<BPlusTreeNode> children;
        // 叶子节点存储真实数据,非叶子节点无效
        List<Integer> values;
        boolean isLeaf;
        // 叶子节点双向链表指针
        BPlusTreeNode prev, next;

        public BPlusTreeNode(boolean isLeaf) {
            this.keys = new ArrayList<>();
            this.children = new ArrayList<>();
            this.values = new ArrayList<>();
            this.isLeaf = isLeaf;
            this.prev = null;
            this.next = null;
        }
    }

    public BPlusTree(int m) {
        this.m = m;
        this.root = new BPlusTreeNode(true);
        this.leafHead = root;
    }

    // 精准查询(MySQL单点查询底层逻辑)
    public Integer search(int key) {
        BPlusTreeNode cur = root;
        // 递归下沉到叶子节点
        while (!cur.isLeaf) {
            int i = 0;
            while (i < cur.keys.size() && key > cur.keys.get(i)) i++;
            cur = cur.children.get(i);
        }
        // 叶子节点匹配数据
        for (int i = 0; i < cur.keys.size(); i++) {
            if (cur.keys.get(i) == key) {
                return cur.values.get(i);
            }
        }
        return null;
    }

    // 核心优势:范围查询(MySQL区间查询底层)
    public List<Integer> rangeSearch(int leftKey, int rightKey) {
        List<Integer> res = new ArrayList<>();
        BPlusTreeNode cur = leafHead;
        // 遍历所有叶子链表节点
        while (cur != null) {
            for (int i = 0; i < cur.keys.size(); i++) {
                int key = cur.keys.get(i);
                if (key >= leftKey && key <= rightKey) {
                    res.add(cur.values.get(i));
                }
                if (key > rightKey) return res;
            }
            cur = cur.next;
        }
        return res;
    }
}

五、三树终极对比(面试必背表格)

|--------|------------|---------------|------------|
| 对比维度 | B树 | B+树 | B*树 |
| 数据存储位置 | 所有节点 | 仅叶子节点 | 仅叶子节点 |
| 节点链表 | 无 | 仅叶子节点串联 | 叶子+非叶子均串联 |
| 拆分阈值 | 1/2满拆分 | 1/2满拆分 | 2/3满拆分(优化) |
| 单点查询 | 最优(可能高层命中) | 稳定(统一到叶子) | 稳定 |
| 范围查询 | 极差 | 最优(链表遍历) | 优秀 |
| 空间利用率 | 低 | 中 | 极高 |
| 工程应用 | 无 | MySQL索引、数据库主流 | 文件系统索引 |


六、面试满分答题模板(直接默写)

B、B+、B*树是专为磁盘IO优化的多路平衡查找树,核心通过降低树高减少磁盘读写次数。B树所有节点共存索引与数据,单点查询最优但范围查询低效,无工程应用;

B+树将索引与数据分离,非叶子节点纯索引、叶子节点存全量数据且通过双向链表串联,范围查询性能极致、树高极低,是MySQL InnoDB索引的底层实现;

B*树在B+树基础上优化空间利用率,提高节点拆分阈值、新增非叶子节点链表,减少拆分IO开销,多用于文件系统索引。

三者迭代优化核心方向:降低树高、优化空间、适配磁盘读写特性

磁盘数据库结构:MySQL 索引为 B+树

3.4 堆(PriorityQueue 底层·完整实战版)

核心定位(面试/刷题必考) :堆是基于完全二叉树实现的动态有序结构,底层采用数组顺序存储,无链表指针开销,是Java PriorityQueue优先队列的唯一底层实现。核心优势:可在O(logn)时间内快速获取/删除全局最值,是TopK问题、区间最值、贪心算法的核心数据结构。

核心特性必背

  • 结构约束:严格遵循完全二叉树规则,节点从左至右依次填充,无空缺节点

  • 有序约束:仅保证根节点为全局最值,子节点之间无序,区别于二叉搜索树

  • 存储方式:纯数组存储,下标映射二叉树父子关系,无额外空间开销

  • 时间复杂度:插入、删除、堆调整均稳定 O(logn),获取最值 O(1)

3.4.1 堆核心分类与下标映射公式

1. 两大核心堆类型
  • 小顶堆(默认) :任意父节点值 ≤ 子节点值,根节点为全局最小值,Java PriorityQueue默认实现小顶堆

  • 大顶堆:任意父节点值 ≥ 子节点值,根节点为全局最大值,需自定义比较器实现

2. 数组下标映射公式(刷题/手写必背)

假设当前节点下标为 i(数组从0开始):

  • 父节点下标:(i - 1) / 2

  • 左子节点下标:2 * i + 1

  • 右子节点下标:2 * i + 2

数组从1开始可简化计算,Java原生堆底层默认0下标,刷题统一适配0下标公式。

3.4.2 堆核心底层操作(上浮+下沉·手写源码)

堆的所有功能(插入、删除、初始化)均依赖上浮(shiftUp) 和**下沉(shiftDown)**两大核心操作,是PriorityQueue底层核心逻辑。

1. 上浮操作(新增节点、向上调整)

核心场景:堆尾部新增节点,向上比对父节点,交换位置直至满足堆规则,维护堆有序性。

java 复制代码
/**
 * 小顶堆-上浮操作
 * @param heap 堆数组
 * @param index 新增节点下标
 */
private void shiftUp(int[] heap, int index) {
    // 循环比对父节点,到达根节点终止
    while (index > 0) {
        // 计算父节点下标
        int parentIndex = (index - 1) / 2;
        // 小顶堆:当前节点大于父节点,无需上浮,直接终止
        if (heap[index] >= heap[parentIndex]) {
            break;
        }
        // 交换父子节点位置
        int temp = heap[index];
        heap[index] = heap[parentIndex];
        heap[parentIndex] = temp;
        // 指针上移,继续比对上层节点
        index = parentIndex;
    }
}
2. 下沉操作(删除根节点、向下调整)

核心场景:删除根节点后,将尾部节点移至根位置,向下比对子节点,交换位置直至满足堆规则。

java 复制代码
/**
 * 小顶堆-下沉操作
 * @param heap 堆数组
 * @param size 当前堆有效大小
 * @param index 需要下沉的节点下标
 */
private void shiftDown(int[] heap, int size, int index) {
    while (true) {
        // 默认当前节点为最小值
        int minIndex = index;
        int left = 2 * index + 1;
        int right = 2 * index + 2;

        // 左子节点更小,更新最小值下标
        if (left < size && heap[left] < heap[minIndex]) {
            minIndex = left;
        }
        // 右子节点更小,更新最小值下标
        if (right < size && heap[right] < heap[minIndex]) {
            minIndex = right;
        }

        // 当前节点就是最小值,无需下沉
        if (minIndex == index) {
            break;
        }

        // 交换当前节点与最小子节点
        int temp = heap[index];
        heap[index] = heap[minIndex];
        heap[minIndex] = temp;

        // 指针下移,继续下沉
        index = minIndex;
    }
}

3.4.3 完整手写堆实现(新增/删除/建堆/遍历)

java 复制代码
/**
 * 完整手写小顶堆(对标Java PriorityQueue底层)
 * 包含:建堆、新增元素、删除堆顶、获取堆顶、堆排序全套逻辑
 */
public class MinHeap {
    private int[] heap;
    private int size; // 堆当前有效元素个数
    private static final int DEFAULT_CAPACITY = 16;

    // 初始化空堆
    public MinHeap() {
        heap = new int[DEFAULT_CAPACITY];
        size = 0;
    }

    // 数组批量建堆(O(n)最优建堆算法)
    public MinHeap(int[] nums) {
        size = nums.length;
        heap = new int[size];
        System.arraycopy(nums, 0, heap, 0, size);
        // 从最后一个非叶子节点开始下沉建堆
        for (int i = (size - 2) / 2; i >= 0; i--) {
            shiftDown(heap, size, i);
        }
    }

    // 新增元素到堆中
    public void add(int val) {
        // 简易扩容(对标JDK动态扩容机制)
        if (size >= heap.length) {
            int[] newHeap = new int[heap.length * 2];
            System.arraycopy(heap, 0, newHeap, 0, size);
            heap = newHeap;
        }
        heap[size++] = val;
        // 尾部元素上浮调整
        shiftUp(heap, size - 1);
    }

    // 删除并返回堆顶最小值
    public int poll() {
        if (size == 0) {
            throw new RuntimeException("堆为空");
        }
        int minVal = heap[0];
        // 尾部元素覆盖堆顶
        heap[0] = heap[--size];
        // 堆顶下沉调整
        shiftDown(heap, size, 0);
        return minVal;
    }

    // 获取堆顶最小值(不删除)
    public int peek() {
        if (size == 0) {
            throw new RuntimeException("堆为空");
        }
        return heap[0];
    }

    // 判断堆是否为空
    public boolean isEmpty() {
        return size == 0;
    }

    // 上浮、下沉核心方法
    private void shiftUp(int[] heap, int index) {
        while (index > 0) {
            int parentIndex = (index - 1) / 2;
            if (heap[index] >= heap[parentIndex]) break;
            // 交换父子节点
            int temp = heap[index];
            heap[index] = heap[parentIndex];
            heap[parentIndex] = temp;
            index = parentIndex;
        }
    }

    private void shiftDown(int[] heap, int size, int index) {
        while (true) {
            int minIndex = index;
            int left = 2 * index + 1;
            int right = 2 * index + 2;

            if (left < size && heap[left] < heap[minIndex]) minIndex = left;
            if (right < size && heap[right] < heap[minIndex]) minIndex = right;
            if (minIndex == index) break;

            // 交换节点
            int temp = heap[index];
            heap[index] = heap[minIndex];
            heap[minIndex] = temp;
            index = minIndex;
        }
    }

    // 堆排序(升序)
    public void heapSort() {
        int len = size;
        for (int i = len - 1; i > 0; i--) {
            // 堆顶与末尾元素交换
            int temp = heap[0];
            heap[0] = heap[i];
            heap[i] = temp;
            // 剩余元素重新下沉调整
            shiftDown(heap, i, 0);
        }
    }

    // 获取堆数组元素
    public int[] getHeapArray() {
        return heap;
    }
}

3.4.4 Java PriorityQueue 实战用法(大小顶堆+刷题模板)

1. 默认小顶堆用法
java 复制代码
import java.util.PriorityQueue;

// 默认小顶堆:堆顶为最小值
public class MinHeapDemo {
    public static void main(String[] args) {
        PriorityQueue<Integer> minHeap = new PriorityQueue<>();
        // 新增元素
        minHeap.add(5);
        minHeap.add(2);
        minHeap.add(8);
        minHeap.add(1);

        // 遍历输出(从小到大)
        while (!minHeap.isEmpty()) {
            System.out.print(minHeap.poll() + " "); // 输出:1 2 5 8
        }
    }
}
2. 自定义大顶堆实现
java 复制代码
import java.util.PriorityQueue;
import java.util.Comparator;

// 自定义比较器实现大顶堆:堆顶为最大值
public class MaxHeapDemo {
    public static void main(String[] args) {
        // 倒序比较,构建大顶堆
        PriorityQueue<Integer> maxHeap = new PriorityQueue<>((a, b) -> b - a);
        maxHeap.add(5);
        maxHeap.add(2);
        maxHeap.add(8);
        maxHeap.add(1);

        // 遍历输出(从大到小)
        while (!maxHeap.isEmpty()) {
            System.out.print(maxHeap.poll() + " "); // 输出:8 5 2 1
        }
    }
}

3.4.5 面试高频:TopK 问题万能模板

核心解题思路:求前K大用小顶堆、求前K小用大顶堆,时间复杂度O(nlogk),优于全局排序O(nlogn),适配大数据量场景。

java 复制代码
import java.util.PriorityQueue;
import java.util.ArrayList;
import java.util.List;

public class TopKDemo {
    // 数组前 K 大元素
    public List<Integer> topKLarge(int[] nums, int k) {
        // 小顶堆,堆内始终保留K个最大元素
        PriorityQueue<Integer> minHeap = new PriorityQueue<>();
        for (int num : nums) {
            minHeap.add(num);
            // 超过K个元素,弹出最小值
            if (minHeap.size() > k) {
                minHeap.poll();
            }
        }
        return new ArrayList<>(minHeap);
    }

    // 数组前 K 小元素
    public List<Integer> topKSmall(int[] nums, int k) {
        // 大顶堆,堆内始终保留K个最小元素
        PriorityQueue<Integer> maxHeap = new PriorityQueue<>((a, b) -> b - a);
        for (int num : nums) {
            maxHeap.add(num);
            if (maxHeap.size() > k) {
                maxHeap.poll();
            }
        }
        return new ArrayList<>(maxHeap);
    }
}

3.4.6 堆核心考点&面试易错点

  • 建堆复杂度:批量数组建堆为O(n),逐个插入建堆为O(nlogn)(面试高频考点)

  • 堆有序特性:仅根节点最值,整体无序,不能直接遍历得到有序数组,需配合堆排序

  • PriorityQueue 线程不安全 :多线程场景需使用PriorityBlockingQueue

  • 阈值规则:无红黑树转换阈值,纯堆结构,始终保持完全二叉树形态

  • 刷题选型:最值动态筛选、TopK、贪心区间最值、任务调度优先选用堆

3.4.7 面试满分答题模板

堆是基于完全二叉树、数组存储的动态有序数据结构,Java中由PriorityQueue实现,默认小顶堆。核心通过上浮、下沉两大操作维护堆特性,保证根节点始终为全局最值,增删查操作稳定O(logn)。堆主要用于TopK最值筛选、堆排序、贪心算法优化,相比全局排序,TopK堆解法可将时间复杂度优化至O(nlogk),是大数据量最值问题的最优解。大顶堆需通过自定义比较器实现,堆结构线程不安全,多线程场景需使用并发优先队列。

3.5 高级多叉树(面试进阶·全覆盖·源码+刷题+面试考点)

核心定位 :高级多叉树区别于普通二叉树,以多分支、字符映射、连通性判定、字符串匹配、编码压缩为核心能力,是Java笔试压轴、大厂进阶面试高频考点,广泛应用于海量字符串处理、连通性问题、文本检索、数据压缩等场景。

核心包含四大必考结构:Trie字典树、AC自动机、哈夫曼树、并查集。

3.5.1 Trie 字典树(字符串前缀树·高频刷题/面试)

核心定义(面试必背) :Trie字典树是多叉有序树 ,专门用于存储、检索字符串前缀,核心思想是公共前缀共享节点,规避重复字符存储,大幅节省空间,将字符串查找复杂度从O(n)降至O(len)(len为字符串长度)。

核心特性

  • 根节点为空,所有子节点存储单个字符

  • 相同前缀字符串共享路径,仅后缀分支区分

  • 节点标记结束位,区分「前缀」和「完整单词」

  • 仅适配字符集场景(小写字母、数字、ASCII字符)

1. 适用场景
  • 字符串前缀匹配、自动补全、输入法联想

  • 词频统计、重复单词去重、敏感词初步筛查

  • LeetCode高频题:单词搜索、前缀匹配、最短前缀

2. Java 完整手写模板(标准版·可直接默写)
java 复制代码
/**
 * 标准版Trie字典树(26个小写字母)
 * 包含:插入、查询完整单词、查询前缀、词频统计
 */
public class Trie {
    // 字典树节点
    static class TrieNode {
        // 26个小写字母多叉分支
        TrieNode[] children = new TrieNode[26];
        // 标记是否为单词结尾
        boolean isEnd;
        // 拓展:词频统计
        int count;
    }

    private final TrieNode root;

    public Trie() {
        root = new TrieNode();
    }

    // 插入单词
    public void insert(String word) {
        TrieNode cur = root;
        for (char c : word.toCharArray()) {
            int idx = c - 'a';
            // 无当前字符分支则新建节点
            if (cur.children[idx] == null) {
                cur.children[idx] = new TrieNode();
            }
            cur = cur.children[idx];
        }
        // 标记单词结束,词频+1
        cur.isEnd = true;
        cur.count++;
    }

    // 查询完整单词是否存在
    public boolean search(String word) {
        TrieNode cur = root;
        for (char c : word.toCharArray()) {
            int idx = c - 'a';
            if (cur.children[idx] == null) {
                return false;
            }
            cur = cur.children[idx];
        }
        // 必须是单词结尾,不能是单纯前缀
        return cur.isEnd;
    }

    // 查询是否存在指定前缀
    public boolean startsWith(String prefix) {
        TrieNode cur = root;
        for (char c : prefix.toCharArray()) {
            int idx = c - 'a';
            if (cur.children[idx] == null) {
                return false;
            }
            cur = cur.children[idx];
        }
        return true;
    }

    // 查询单词出现次数
    public int getWordCount(String word) {
        TrieNode cur = root;
        for (char c : word.toCharArray()) {
            int idx = c - 'a';
            if (cur.children[idx] == null) {
                return 0;
            }
            cur = cur.children[idx];
        }
        return cur.count;
    }
}
3. 面试高频易错点
  • 必须区分前缀完整单词,仅遍历到末尾不够,需判断isEnd标记

  • 基础版仅支持小写字母,大小写/数字场景需扩容节点数组

  • 海量短文本场景性能远超HashMap,长文本优势不明显

4. 面试满分答题模板

Trie字典树是基于多叉树的字符串检索结构,通过公共前缀共享节点实现空间压缩,核心支持字符串插入、前缀查询、完整单词匹配。相较于哈希表,字典树可高效实现前缀模糊匹配,适配自动补全、词频统计、敏感词过滤场景,时间复杂度仅与字符串长度相关,无哈希碰撞问题,是字符串专项刷题的核心结构。


3.5.2 AC自动机(多模式串匹配·大厂笔试压轴)

核心定位 :AC自动机 = Trie字典树 + KMP失败指针 + BFS层序遍历,是多模式串匹配最优解,解决KMP单次仅能匹配一个模式串的痛点,可一次性在文本中匹配海量关键词,时间复杂度线性O(n)。

工程场景:敏感词批量过滤、文本检索、日志关键词匹配、输入法词库匹配。

1. 核心三大组件
  • Trie树:存储所有待匹配的模式串

  • Fail失败指针:对标KMP的next数组,匹配失败时跳转至最长有效后缀,避免回溯文本

  • 遍历匹配:文本线性遍历,结合Fail指针无缝跳转匹配

2. Java 极简实战模板(可直接刷题)
java 复制代码
import java.util.LinkedList;
import java.util.Queue;

/**
 * AC自动机 多模式串敏感词匹配模板
 */
public class ACAutomaton {
    // AC自动机节点
    static class ACNode {
        ACNode[] children = new ACNode[26];
        ACNode fail; // 失败指针
        boolean isEnd; // 单词结束标记
    }

    private final ACNode root;

    public ACAutomaton() {
        root = new ACNode();
    }

    // 1. 构建Trie树,插入所有模式串
    public void insert(String word) {
        ACNode cur = root;
        for (char c : word.toCharArray()) {
            int idx = c - 'a';
            if (cur.children[idx] == null) {
                cur.children[idx] = new ACNode();
            }
            cur = cur.children[idx];
        }
        cur.isEnd = true;
    }

    // 2. BFS构建失败指针(核心)
    public void buildFail() {
        Queue<ACNode> queue = new LinkedList<>();
        // 初始化第一层节点,失败指针指向根节点
        for (int i = 0; i < 26; i++) {
            if (root.children[i] != null) {
                root.children[i].fail = root;
                queue.offer(root.children[i]);
            }
        }

        // 层序遍历构建所有节点失败指针
        while (!queue.isEmpty()) {
            ACNode curNode = queue.poll();
            for (int i = 0; i < 26; i++) {
                ACNode child = curNode.children[i];
                if (child != null) {
                    // 寻找父节点失败指针的子节点
                    ACNode failNode = curNode.fail;
                    while (failNode != null && failNode.children[i] == null) {
                        failNode = failNode.fail;
                    }
                    // 赋值失败指针
                    child.fail = (failNode == null) ? root : failNode.children[i];
                    // 继承后缀匹配结果
                    child.isEnd |= child.fail.isEnd;
                    queue.offer(child);
                }
            }
        }
    }

    // 3. 文本匹配:检测是否包含任意模式串(敏感词)
    public boolean match(String text) {
        ACNode cur = root;
        for (char c : text.toCharArray()) {
            int idx = c - 'a';
            // 匹配失败,跳转失败指针
            while (cur != null && cur.children[idx] == null) {
                cur = cur.fail;
            }
            cur = (cur == null) ? root : cur.children[idx];
            // 匹配到任意关键词直接返回
            if (cur.isEnd) {
                return true;
            }
        }
        return false;
    }
}
3. 面试核心考点
  • 核心优化:通过Fail指针消除文本回溯,实现一次遍历完成多串匹配

  • 与KMP区别:KMP单模式串匹配,AC自动机多模式串批量匹配

  • 时间复杂度:文本遍历O(n),预处理Trie+Fail指针O(m)(m为所有模式串总长度)


3.5.3 哈夫曼树(最优二叉树·编码压缩必考)

核心定义 :哈夫曼树是带权路径长度最短的多叉二叉树,基于贪心算法构建,所有节点均为带权节点,无度为1的节点,叶子节点带权重,核心用于数据压缩编码。

1. 核心必背概念
  • 路径长度:节点到根节点的路径边数

  • 带权路径长度(WPL):叶子节点权重 × 路径长度

  • 哈夫曼树特性:所有节点WPL总和最小,是最优编码树

2. 构建规则(贪心)
  1. 初始化:将所有权重节点作为独立叶子节点;

  2. 每次选取权重最小的两个节点,构建新父节点,父节点权重为两节点权重和;

  3. 将新节点加入节点集合,重复操作直至仅剩一个节点,即为哈夫曼树根。

3. 工程应用
  • 哈夫曼编码:无损数据压缩(图片、文本、压缩包底层)

  • 高频字符短编码、低频字符长编码,最大化压缩率

4. 面试满分答题模板

哈夫曼树是基于贪心策略构建的最优带权二叉树,通过不断合并最小权重节点,实现整树带权路径长度最小。基于哈夫曼树生成的哈夫曼编码,无前缀冲突、编码长度自适应,高频字符占用更少存储空间,是无损数据压缩的核心底层结构,无固定编码表,完全根据数据权重动态生成。


3.5.4 并查集(连通性万能结构·笔试高频)

核心定位(刷题/面试必背) :并查集(Disjoint Set Union,DSU)是处理动态连通性问题的多叉森林结构,

核心能力:快速判断两点是否连通、合并两个集合、统计连通分量,是图论最小生成树、连通块判定、朋友圈问题的万能模板。

两大核心优化:路径压缩、按秩合并(近乎O(1)均摊复杂度)

1. 核心适配场景
  • 无向图连通块判定、岛屿数量、朋友圈问题

  • 最小生成树Kruskal算法核心组件

  • 动态合并集合、查询连通关系、判环

2. 标准版+优化版 Java 完整模板(可直接默写)
java 复制代码
/**
 * 并查集 完整版(路径压缩+按秩合并·最优均摊复杂度)
 * 支持:连通查询、集合合并、连通分量统计
 */
public class UnionFind {
    // 父节点数组
    private final int[] parent;
    // 秩:记录树的深度(用于按秩合并)
    private final int[] rank;
    // 连通分量数量
    private int count;

    // 初始化并查集
    public UnionFind(int n) {
        parent = new int[n];
        rank = new int[n];
        count = n;
        // 初始每个节点自成一个集合
        for (int i = 0; i < n; i++) {
            parent[i] = i;
            rank[i] = 1;
        }
    }

    // 查找根节点 + 路径压缩(核心优化1)
    public int find(int x) {
        if (parent[x] != x) {
            // 递归路径压缩,所有节点直接指向根
            parent[x] = find(parent[x]);
        }
        return parent[x];
    }

    // 合并两个集合 + 按秩合并(核心优化2)
    public void union(int x, int y) {
        int fx = find(x);
        int fy = find(y);
        // 已连通无需合并
        if (fx == fy) return;

        // 小树合并到大树,控制树高
        if (rank[fx] < rank[fy]) {
            parent[fx] = fy;
        } else {
            parent[fy] = fx;
            // 树高相同时,合并后根节点秩+1
            if (rank[fx] == rank[fy]) {
                rank[fx]++;
            }
        }
        // 连通分量-1
        count--;
    }

    // 判断两点是否连通
    public boolean isConnected(int x, int y) {
        return find(x) == find(y);
    }

    // 获取连通分量总数
    public int getCount() {
        return count;
    }
}
3. 进阶:带权并查集(食物链/偏移量·大厂压轴)

普通并查集仅记录连通关系,带权并查集可记录节点与根节点的相对关系(偏移量、权重),适配食物链、奇偶关系、相对大小等带约束连通问题。

java 复制代码
/**
 * 带权并查集(偏移量权重·适配食物链、奇偶约束问题)
 */
public class WeightedUnionFind {
    private final int[] parent;
    // weight[i]:i节点到父节点的权重偏移量
    private final int[] weight;

    public WeightedUnionFind(int n) {
        parent = new int[n];
        weight = new int[n];
        for (int i = 0; i < n; i++) {
            parent[i] = i;
            weight[i] = 0;
        }
    }

    // 路径压缩+权重更新
    public int find(int x) {
        if (parent[x] != x) {
            int originParent = parent[x];
            parent[x] = find(parent[x]);
            // 累加权重,更新为到根节点的总偏移量
            weight[x] += weight[originParent];
        }
        return parent[x];
    }

    // 带权重合并:x与y满足 x - y = val
    public void union(int x, int y, int val) {
        int fx = find(x);
        int fy = find(y);
        if (fx != fy) {
            parent[fx] = fy;
            // 权重公式推导:weight[x] - weight[y] = val
            weight[fx] = weight[y] + val - weight[x];
        }
    }
}
4. 面试核心考点&易错点
  • 路径压缩:彻底扁平化树结构,单次查询近乎O(1)

  • 按秩合并:限制树高,避免极端链式结构,双重优化后均摊复杂度极低

  • 普通并查集无权重,仅判断连通;带权并查集可处理约束关系

  • 初始化必须每个节点指向自身,否则连通判定失效

5. 面试满分答题模板

并查集是基于多叉森林的连通性管理结构,核心通过find查找根节点、union合并集合两大操作处理动态连通性问题。通过路径压缩扁平化树结构、按秩合并控制树高,将均摊时间复杂度优化至近似O(1)。普通并查集适用于连通块判定、最小生成树Kruskal算法;带权并查集可拓展处理带相对约束的连通问题,是图论刷题和大厂面试的核心基础结构。

3.6 区间高级树结构(大厂进阶·压轴必考)

区间高级树结构是Java算法刷题、大厂笔试面试压轴核心考点 ,专门解决普通前缀和、差分无法处理的动态区间修改、动态区间最值、历史版本查询问题。

核心包含三大结构:树状数组(轻量化区间工具)、线段树(全能区间神器)、主席树(可持久化区间进阶),三者层层递进,覆盖99%区间类高阶算法题型。

3.6.1 树状数组(FenwickTree / BIT)

核心定位(必背) :轻量化区间树结构,代码极简、常数极小、效率极高,专门解决单点更新、区间求和,也可拓展实现区间更新、单点查询,是线段树的轻量化替代品。无法处理区间最值、复杂懒标记操作,适合简单区间动态运算场景。

核心原理:基于二进制低位1(LowBit)拆分下标,通过树状分层存储前缀和,舍弃复杂区间划分,用极简迭代实现上下更新与查询,时间复杂度稳定 O(logn),常数远小于线段树。

1. 核心LowBit公式(刷题必背)

lowbit(x) = x \\\& -x,作用:取出数字二进制最低位的1及后面所有0,是树状数组迭代的核心依据。

2. 两大基础模板
(1)单点更新 + 区间求和(最常用)
java 复制代码
/**
 * 树状数组:单点更新、区间求和
 * 适用场景:单点修改数值、查询任意前缀和/区间和
 */
public class FenwickTree {
    private int[] tree;
    private int n;

    // 初始化
    public FenwickTree(int size) {
        this.n = size;
        tree = new int[n + 1]; // 统一1下标,规避边界问题
    }

    // 低位运算核心
    private int lowbit(int x) {
        return x & -x;
    }

    // 单点更新:下标idx位置 + val
    public void update(int idx, int val) {
        while (idx <= n) {
            tree[idx] += val;
            idx += lowbit(idx);
        }
    }

    // 查询前缀和 [1, idx]
    public int queryPrefix(int idx) {
        int res = 0;
        while (idx > 0) {
            res += tree[idx];
            idx -= lowbit(idx);
        }
        return res;
    }

    // 查询区间和 [l, r]
    public int queryRange(int l, int r) {
        return queryPrefix(r) - queryPrefix(l - 1);
    }
}
(2)区间更新 + 单点查询(差分拓展版)
java 复制代码
/**
 * 树状数组拓展:区间更新、单点查询
 * 适用场景:对[l,r]批量加val,查询某一点具体数值
 */
public class RangeAddPointQueryBIT {
    private int[] tree;
    private int n;

    public RangeAddPointQueryBIT(int size) {
        this.n = size;
        tree = new int[n + 1];
    }

    private int lowbit(int x) {
        return x & -x;
    }

    // 差分思想:区间[l,r] + val
    public void rangeAdd(int l, int r, int val) {
        update(l, val);
        update(r + 1, -val);
    }

    private void update(int idx, int val) {
        while (idx <= n) {
            tree[idx] += val;
            idx += lowbit(idx);
        }
    }

    // 单点查询:获取idx位置数值
    public int queryPoint(int idx) {
        int res = 0;
        while (idx > 0) {
            res += tree[idx];
            idx -= lowbit(idx);
        }
        return res;
    }
}
3. 进阶:区间更新 + 区间求和模板

基于差分树状数组双数组实现,支持任意区间加减、任意区间求和,覆盖绝大多数中等区间题型。

java 复制代码
/**
 * 终极树状数组:区间更新 + 区间求和
 * 公式推导:sum(1~x) = x * query(B1,x) - query(B2,x)
 */
public class FullFenwickTree {
    private int[] B1, B2;
    private int n;

    public FullFenwickTree(int size) {
        this.n = size;
        B1 = new int[n + 1];
        B2 = new int[n + 1];
    }

    private int lowbit(int x) {
        return x & -x;
    }

    private void update(int[] tree, int idx, int val) {
        while (idx <= n) {
            tree[idx] += val;
            idx += lowbit(idx);
        }
    }

    private int query(int[] tree, int idx) {
        int res = 0;
        while (idx > 0) {
            res += tree[idx];
            idx -= lowbit(idx);
        }
        return res;
    }

    // 区间[l,r] 批量加val
    public void rangeAdd(int l, int r, int val) {
        update(B1, l, val);
        update(B1, r + 1, -val);
        update(B2, l, val * (l - 1));
        update(B2, r + 1, -val * r);
    }

    // 查询前缀和 [1,idx]
    public int queryPrefix(int idx) {
        return idx * query(B1, idx) - query(B2, idx);
    }

    // 查询区间和 [l,r]
    public int queryRange(int l, int r) {
        return queryPrefix(r) - queryPrefix(l - 1);
    }
}
4. 核心适用场景&刷题考点
  • 基础场景:动态单点修改、区间求和、数组前缀统计

  • 高频真题:数组逆序对统计、频繁区间增量更新、区间和查询

  • 优势:代码短、速度快、无递归开销,常数级性能优于线段树

  • 局限:不支持区间最值修改、区间最值查询,复杂区间操作必须用线段树

5. 面试易错点&满分答题模板

易错点:必须使用1下标、LowBit运算不可颠倒、区间更新双数组公式不可记错、越界需判断r+1是否超数组长度。

答题模板:树状数组是基于二进制拆分的轻量化区间树结构,通过LowBit机制实现O(logn)的更新与查询复杂度,支持单点/区间更新、区间求和等场景。相较于线段树,代码更简洁、运行常数更小,适合简单动态区间运算,但不支持区间最值、复杂区间修改,是区间刷题的基础优选结构。

3.6.2 线段树(SegmentTree·全能区间神器)

核心定位(大厂必考) :区间算法万能结构 ,无场景短板,支持任意区间修改、区间求和、区间最值、区间计数,搭配懒标记(Lazy)实现O(logn)均摊复杂度,是解决所有复杂区间问题的终极方案,笔试压轴题核心考点。

核心原理:将原数组递归二分拆分,构建完全二叉树,每个节点存储对应区间的统计信息,通过懒标记延迟更新,避免重复遍历区间,大幅优化效率。

1. 核心特性
  • 全覆盖场景:区间加减、区间赋值、区间求和、区间最大/最小值

  • 核心优化:懒标记(Lazy)延迟更新,杜绝暴力遍历区间

  • 时间复杂度:构建O(n)、更新/查询O(logn)

  • 短板:代码量大于树状数组,常数开销略高

2. 完整版模板(区间加减+区间求和+懒标记)
java 复制代码
/**
 * 万能线段树:区间加减、区间求和、懒标记延迟更新
 * 全覆盖中等难度区间题型
 */
public class SegmentTree {
    private int[] tree;    // 存储区间和
    private int[] lazy;    // 懒标记:延迟更新增量
    private int n;

    // 初始化线段树,适配原数组
    public SegmentTree(int[] nums) {
        this.n = nums.length;
        tree = new int[4 * n];  // 开4倍空间杜绝越界(行业通用写法)
        lazy = new int[4 * n];
        build(0, 0, n - 1, nums);
    }

    // 构建线段树
    private void build(int node, int l, int r, int[] nums) {
        if (l == r) {
            tree[node] = nums[l];
            return;
        }
        int mid = l + (r - l) / 2;
        build(2 * node + 1, l, mid, nums);
        build(2 * node + 2, mid + 1, r, nums);
        // 合并左右子区间和
        tree[node] = tree[2 * node + 1] + tree[2 * node + 2];
    }

    // 懒标记下传:将延迟增量下发到子节点
    private void pushDown(int node, int l, int r) {
        if (lazy[node] == 0) return;
        int mid = l + (r - l) / 2;
        int left = 2 * node + 1;
        int rightNode = 2 * node + 2;

        // 更新左子区间
        tree[left] += lazy[node] * (mid - l + 1);
        lazy[left] += lazy[node];
        // 更新右子区间
        tree[rightNode] += lazy[node] * (r - mid);
        lazy[rightNode] += lazy[node];

        // 清空当前节点懒标记
        lazy[node] = 0;
    }

    // 区间更新:[ul, ur] 全部 + val
    public void rangeUpdate(int ul, int ur, int val) {
        update(0, 0, n - 1, ul, ur, val);
    }

    private void update(int node, int l, int r, int ul, int ur, int val) {
        // 无交集,直接返回
        if (ur < l || ul > r) return;
        // 完全覆盖,延迟更新
        if (ul <= l && r <= ur) {
            tree[node] += val * (r - l + 1);
            lazy[node] += val;
            return;
        }
        // 部分覆盖,先下传懒标记,再递归更新
        pushDown(node, l, r);
        int mid = l + (r - l) / 2;
        update(2 * node + 1, l, mid, ul, ur, val);
        update(2 * node + 2, mid + 1, r, ul, ur, val);
        // 合并子节点结果
        tree[node] = tree[2 * node + 1] + tree[2 * node + 2];
    }

    // 区间查询:查询[ql,qr]区间和
    public int rangeQuery(int ql, int qr) {
        return query(0, 0, n - 1, ql, qr);
    }

    private int query(int node, int l, int r, int ql, int qr) {
        if (qr < l || ql > r) return 0;
        if (ql <= l && r <= qr) return tree[node];
        // 查询前必须下传懒标记,保证数据最新
        pushDown(node, l, r);
        int mid = l + (r - l) / 2;
        int leftSum = query(2 * node + 1, l, mid, ql, qr);
        int rightSum = query(2 * node + 2, mid + 1, r, ql, qr);
        return leftSum + rightSum;
    }
}
3. 拓展:区间最值线段树模板
java 复制代码
/**
 * 区间最值线段树(区间最大值查询+区间更新)
 */
public class MaxSegmentTree {
    private int[] tree;
    private int[] lazy;
    private int n;
    private final int MIN_VAL = Integer.MIN_VALUE;

    public MaxSegmentTree(int[] nums) {
        this.n = nums.length;
        tree = new int[4 * n];
        lazy = new int[4 * n];
        build(0, 0, n - 1, nums);
    }

    private void build(int node, int l, int r, int[] nums) {
        if (l == r) {
            tree[node] = nums[l];
            return;
        }
        int mid = l + (r - l) / 2;
        build(2*node+1, l, mid, nums);
        build(2*node+2, mid+1, r, nums);
        tree[node] = Math.max(tree[2*node+1], tree[2*node+2]);
    }

    private void pushDown(int node) {
        if (lazy[node] == 0) return;
        int left = 2 * node + 1;
        int rightNode = 2 * node + 2;
        tree[left] += lazy[node];
        lazy[left] += lazy[node];
        tree[rightNode] += lazy[node];
        lazy[rightNode] += lazy[node];
        lazy[node] = 0;
    }

    // 区间批量加val
    public void rangeAdd(int ul, int ur, int val) {
        update(0, 0, n-1, ul, ur, val);
    }

    private void update(int node, int l, int r, int ul, int ur, int val) {
        if (ur < l || ul > r) return;
        if (ul <= l && r <= ur) {
            tree[node] += val;
            lazy[node] += val;
            return;
        }
        pushDown(node);
        int mid = l + (r - l) / 2;
        update(2*node+1, l, mid, ul, ur, val);
        update(2*node+2, mid+1, r, ul, ur, val);
        tree[node] = Math.max(tree[2*node+1], tree[2*node+2]);
    }

    // 查询区间最大值
    public int queryMax(int ql, int qr) {
        return query(0, 0, n-1, ql, qr);
    }

    private int query(int node, int l, int r, int ql, int qr) {
        if (qr < l || ql > r) return MIN_VAL;
        if (ql <= l && r <= qr) return tree[node];
        pushDown(node);
        int mid = l + (r - l) / 2;
        return Math.max(query(2*node+1, l, mid, ql, qr), query(2*node+2, mid+1, r, ql, qr));
    }
}
4. 核心考点&易错陷阱
  • 空间开辟:必须开4倍原数组空间,防止递归越界

  • 懒标记规则:先下传、再递归、最后合并,顺序错误直接答案错误

  • 边界判定:区间交集判断必须严谨,无交集直接截断递归

  • 场景区分:求和线段树统计区间元素和,最值线段树统计区间极值,不可混用

5. 面试满分答题模板

线段树是基于二分递归拆分的全能区间结构,通过将数组分层构建二叉树,实现区间信息的高效维护。依托懒标记延迟更新机制,将区间修改、区间查询的时间复杂度优化至O(logn),支持区间加减、区间赋值、区间求和、区间最值等全场景区间操作,是复杂区间算法的核心解决方案,唯一短板是代码量较大、运行常数略高于树状数组。

3.6.3 主席树(可持久化线段树·大厂压轴)

核心定位(顶级考点) :主席树 = 可持久化 + 权值线段树 ,是线段树的进阶升级版,核心解决历史版本查询、区间第K大/第K小问题,是大厂笔试、算法竞赛压轴必考结构,普通线段树无法实现历史版本回溯。

核心特性:每次修改不覆盖原树节点,仅新建变更节点,复用不变节点,完整保留每一次修改后的数组版本,空间复用率极高。

1. 核心适配场景
  • 经典真题:静态区间第K大、区间第K小

  • 历史版本数组回溯、不同版本区间信息对比

  • 离线海量区间极值查询、有序区间统计

2. 极简核心原理
  1. 对原数组数值离散化(压缩值域,适配大范围数值);

  2. 构建权值线段树,统计每个数值出现次数;

  3. 每次更新新建节点,保留历史版本树根;

  4. 通过两个版本树根差值,计算区间数值分布,二分查找第K极值。

3. Java 极简刷题模板(区间第K小)
java 复制代码
import java.util.*;

/**
 * 主席树:静态区间第K小模板
 * 核心:可持久化权值线段树 + 离散化
 */
public class PersistentSegmentTree {
    // 树节点:左孩子、右孩子、区间数值个数
    static class Node {
        int lc, rc, cnt;
        Node() { lc = rc = cnt = 0; }
    }

    private Node[] tree;
    private int[] root; // 记录每个版本的根节点
    private int[] nums, sorted;
    private int n, tot;

    public PersistentSegmentTree(int[] arr) {
        this.n = arr.length;
        this.nums = arr;
        this.tot = 0;
        tree = new Node[n * 20]; // 开20倍空间适配可持久化
        root = new int[n + 1];
        // 离散化
        sorted = nums.clone();
        Arrays.sort(sorted);
        // 初始化空树
        tree[0] = new Node();
        // 构建每个版本的主席树
        for (int i = 1; i <= n; i++) {
            root[i] = update(root[i-1], 1, n, getRank(nums[i-1]));
        }
    }

    // 获取数值离散化后的排名
    private int getRank(int x) {
        return Arrays.binarySearch(sorted, x) + 1;
    }

    // 可持久化更新:不覆盖原节点,新建节点
    private int update(int pre, int l, int r, int val) {
        int cur = ++tot;
        tree[cur] = new Node();
        tree[cur].cnt = tree[pre].cnt + 1;
        if (l == r) return cur;
        int mid = l + (r - l) / 2;
        if (val <= mid) {
            tree[cur].lc = update(tree[pre].lc, l, mid, val);
            tree[cur].rc = tree[pre].rc;
        } else {
            tree[cur].rc = update(tree[pre].rc, mid+1, r, val);
            tree[cur].lc = tree[pre].lc;
        }
        return cur;
    }

    // 查询区间[l,r]第K小
    public int queryKth(int l, int r, int k) {
        return query(root[l-1], root[r], 1, n, k);
    }

    private int query(int lRoot, int rRoot, int l, int r, int k) {
        if (l == r) return sorted[l-1];
        int mid = l + (r - l) / 2;
        // 左右区间数值个数差值
        int leftCnt = tree[tree[rRoot].lc].cnt - tree[tree[lRoot].lc].cnt;
        if (leftCnt >= k) {
            return query(tree[lRoot].lc, tree[rRoot].lc, l, mid, k);
        } else {
            return query(tree[lRoot].rc, tree[rRoot].rc, mid+1, r, k - leftCnt);
        }
    }
}
4. 面试核心考点&区别对比
  • 可持久化核心:只改变更节点,复用不变节点,保留所有历史版本

  • 与普通线段树区别:普通线段树覆盖更新、无历史版本;主席树保留全量版本

  • 离散化必要性:解决数值范围过大、数组无法开辟的问题

  • 时间复杂度:构建O(nlogn)、查询O(logn)

5. 面试满分答题模板

主席树是可持久化权值线段树,通过节点复用技术保留每一次数组修改的历史版本,无需重复开辟全量空间,空间利用率极高。核心通过离散化压缩值域,基于版本差值实现静态区间第K大/第K小查询,是普通线段树无法实现的高阶区间功能,主要用于大厂算法压轴题、海量区间统计与历史版本回溯场景。

3.6.4 三大区间树结构终极选型(刷题秒选口诀)

  • 简单区间求和、单点修改 → 树状数组(代码最短、速度最快)

  • 复杂区间修改、区间最值、全能区间操作 → 线段树(全场景适配)

  • 区间第K极值、历史版本查询 → 主席树(唯一解)

3.6.5 大厂面试终极对比总结

树状数组轻量化、常数最优,适配基础动态区间求和场景;线段树功能全面,依托懒标记解决各类复杂区间修改与查询,是工程与刷题主力结构;主席树基于可持久化特性,突破普通线段树版本限制,解决高阶区间极值问题,三者层层递进,覆盖所有Java区间类算法考点。

  • 树状数组 FenwickTree:单点更新、区间求和、逆序对

  • 线段树:懒标记、区间修改、区间最值

  • 主席树(可持久化线段树):区间第 K 大

第四章 哈希结构(Java 集合核心灵魂·面试满分完整版)

核心总述(必背) :哈希表是Java集合框架的核心底层结构,核心思想是通过哈希函数将任意对象映射为数组下标,实现O(1)均摊复杂度的增删改查。

Java哈希家族核心包含HashMap、HashSet、LinkedHashMap三大核心类,底层依托「数组+链表+红黑树」结构,完美平衡查询效率与冲突处理能力,是面试最高频、源码考察最深、刷题最常用的核心结构。

核心优势:均摊O(1)读写性能,远优于链表、数组;

核心痛点 :哈希冲突、扩容开销、线程不安全;核心应用:去重、计数、缓存、键值映射、刷题哈希查表优化。

4.1 Java 哈希家族全体系详解(底层关联+区别)

4.1.1 HashMap(核心键值映射)

底层结构(JDK1.8 终极版)数组 + 单向链表 + 红黑树

数组为主体容器,链表处理少量哈希冲突,红黑树优化大量冲突下的查询性能,彻底解决JDK1.7纯链表结构的O(n)查询瓶颈。

4.1.2 HashSet(纯去重容器)

底层本质完全基于HashMap实现,无任何自研逻辑

HashSet存储的元素作为HashMap的key,所有value统一为静态空Object常量,依托HashMap的key唯一性实现元素去重。

核心特性:无序、不可重复、允许存null元素、线程不安全。

4.1.3 LinkedHashMap(有序哈希·缓存核心)

底层结构 :继承HashMap,在原有哈希结构基础上,新增双向链表串联所有节点

两大有序模式

  • 插入有序(默认):元素存储顺序与插入顺序一致

  • 访问有序:被访问元素移动到链表尾部,天然实现LRU缓存淘汰逻辑

核心价值:Java原生LRU缓存底层实现,无需手动维护链表顺序,工程缓存场景首选。

4.1.4 哈希家族核心区别(面试必问)

结构 存储结构 有序性 可重复性 底层依赖 核心场景
HashMap 键值对存储 无序 Key唯一,Value可重复 自研哈希结构 键值映射、计数、缓存基础
HashSet 单元素存储 无序 元素不可重复 HashMap 数组去重、存在性判断
LinkedHashMap 键值对存储 插入/访问有序 Key唯一 HashMap+双向链表 有序缓存、LRU本地缓存

4.2 HashMap 必考底层源码细节(JDK1.8 面试满分版)

4.2.1 核心常量与参数(必背)

  • 默认初始容量 DEFAULT_CAPACITY:16(必须为2的幂次,哈希取模优化核心)

  • 最大容量 MAX_CAPACITY:2^30

  • 默认负载因子 DEFAULT_LOAD_FACTOR:0.75(空间与性能最优平衡点)

  • 链表转红黑树阈值 TREEIFY_THRESHOLD:8(链表节点数≥8,触发树化)

  • 红黑树退链表阈值 UNTREEIFY_THRESHOLD:6(节点数≤6,退化为链表)

  • 最小树化容量 MIN_TREEIFY_CAPACITY:64(数组容量<64,即使链表长度8也不树化,优先扩容)

4.2.2 哈希扰动函数(核心原理·面试高频)

问题痛点:原生hashCode()仅返回对象哈希值,若高位差异大、低位一致,会导致哈希冲突严重、数组下标分布不均。

JDK1.8 扰动源码

java 复制代码
static final int hash(Object key) {
    int h;
    // 核心:高16位 ^ 低16位,保留高位特征,均匀散列
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

核心作用 :将哈希值高位信息扩散到低位,让哈希值分布更均匀,大幅减少哈希冲突,null键固定映射下标0。

下标计算规则tab[i = (n - 1) & hash],利用2的幂次特性,位运算替代取模,效率极高。

4.2.3 哈希冲突解决方案(Java专属)

Java 采用 链地址法(数组+链表+红黑树),区别于线性探测、二次探测、双重哈希:

  • 少量冲突(节点数<8):单向链表挂载,结构简单、开销小

  • 大量冲突(节点数≥8 & 容量≥64):链表转红黑树,查询复杂度从O(n)优化为O(logn)

  • 节点减少(节点数≤6):红黑树退化为链表,降低树结构维护开销

4.2.4 扩容机制(核心难点)

1. 扩容触发条件

数组元素个数 > 容量 × 负载因子(默认16×0.75=12,元素超12触发扩容)

2. 扩容规则

每次扩容为原容量2倍,始终保持容量为2的幂次,保证下标位运算有效性。

3. 重哈希优化(JDK1.8 重大改进)

无需重新计算哈希值,仅通过哈希值高位判断节点新下标:

  • 哈希值高位为0:新下标 = 原下标

  • 哈希值高位为1:新下标 = 原下标 + 原容量

大幅简化扩容迁移逻辑,提升扩容效率。

4.2.5 JDK1.7 与 JDK1.8 核心区别(面试必背)

  • 结构差异:1.7 纯数组+链表;1.8 数组+链表+红黑树

  • 插入方式:1.7 头插法;1.8 尾插法

  • 线程安全问题:1.7 头插法扩容易产生循环链表,触发死循环;1.8 尾插法规避循环链表问题,但依旧线程不安全

  • 扰动函数:1.7 扰动次数多、逻辑复杂;1.8 简化为一次高低位异或,效率更高

4.3 哈希核心高频考点(面试扣分点避坑)

4.3.1 hashCode() 与 equals() 重写规则(必考)

核心契约

  1. 两个对象equals()相等,hashCode()必须相等

  2. 两个对象hashCode()相等,equals()不一定相等(哈希冲突)

重写规范 :自定义对象作为HashMap的Key,必须同时重写hashCode()和equals(),否则会出现「内容相同但哈希不同,重复存储」的bug。

4.3.2 线程安全问题(高频面试)

HashMap线程不安全场景:多线程put、扩容、迁移数据时,会出现数据覆盖、数据丢失、并发空指针等问题。

线程安全替代方案

  • Hashtable:全方法加锁,并发性能极低(淘汰)

  • Collections.synchronizedMap:普通同步包装,锁粒度大

  • ConcurrentHashMap:分段锁+CAS,高并发首选(Java并发核心)

4.3.3 关键误区澄清

  • 误区1:负载因子越大越好 → 错误,负载因子过大哈希冲突变多、查询变慢;过小空间浪费严重

  • 误区2:链表长度到8一定树化 → 错误,需同时满足数组容量≥64,否则只扩容不树化

  • 误区3:HashMap有序 → 错误,仅LinkedHashMap支持有序,HashMap完全无序

  • 误区4:可以存重复key → 错误,put重复key会覆盖原有value

4.4 哈希冲突四大解决方案(笔试面试全覆盖+Java实战代码)

核心前置定义 :哈希冲突指不同key经过哈希函数计算后,得到相同数组下标,导致元素存储位置重叠。工业界与笔试主流共四种解决方案,Java HashMap 选用链地址法,其余三种为笔试常考、面试对比必背方案,下面附全套原理、优劣解析、可直接运行实战代码。

4.4.1 链地址法(Java 采用·工业主流)

冲突节点挂载在数组下标对应的链表/红黑树中,不占用其他下标空间,支持批量冲突处理,是工业界主流方案。JDK1.8 优化为「数组+链表+红黑树」,平衡低冲突开销与高冲突查询性能。

1. 核心优缺点
  • 优点:无哈希堆积、冲突容忍度高、删除操作简单、空间利用率高

  • 缺点:需要额外链表/树结构存储节点,存在少量对象开销

2. Java 实战模拟代码(简易HashMap冲突处理)
java 复制代码
import java.util.LinkedList;

/**
 * 链地址法 哈希冲突解决模拟(Java HashMap核心原理)
 */
public class ChainHashDemo {
    // 哈希数组 + 链表实现链地址法
    private LinkedList<Node>[] table;
    private static final int DEFAULT_CAP = 16;

    // 键值对节点
    static class Node {
        String key;
        int val;
        public Node(String key, int val) {
            this.key = key;
            this.val = val;
        }
    }

    // 初始化哈希表
    public ChainHashDemo() {
        table = new LinkedList[DEFAULT_CAP];
        for (int i = 0; i < DEFAULT_CAP; i++) {
            table[i] = new LinkedList<>();
        }
    }

    // 简易哈希函数
    private int hash(String key) {
        return key == null ? 0 : (key.hashCode() & (DEFAULT_CAP - 1));
    }

    // 插入元素,处理哈希冲突
    public void put(String key, int val) {
        int idx = hash(key);
        LinkedList<Node> list = table[idx];
        // 存在相同key则覆盖
        for (Node node : list) {
            if (node.key.equals(key)) {
                node.val = val;
                return;
            }
        }
        // 冲突直接挂载链表尾部
        list.add(new Node(key, val));
    }

    // 查询元素
    public int get(String key) {
        int idx = hash(key);
        LinkedList<Node> list = table[idx];
        for (Node node : list) {
            if (node.key.equals(key)) {
                return node.val;
            }
        }
        return -1;
    }

    public static void main(String[] args) {
        ChainHashDemo hashMap = new ChainHashDemo();
        // 制造哈希冲突(不同key,相同哈希下标)
        hashMap.put("abc", 100);
        hashMap.put("def", 200);
        System.out.println(hashMap.get("abc"));
        System.out.println(hashMap.get("def"));
    }
}

4.4.2 线性探测法(笔试高频·开放定址法核心)

冲突时向后遍历数组,找到第一个空位置插入,缺点是容易产生哈希堆积 ,查询效率持续下降。冲突探测公式:newIdx = (oldIdx + i) % cap(i为探测次数,从1开始递增)。

1. 核心优缺点
  • 优点:无需额外空间、结构简单、查询速度快(无链表遍历)

  • 缺点:严重哈希堆积、删除元素易产生空洞、冲突越多效率越低

2. Java 实战代码
java 复制代码
/**
 * 线性探测法 解决哈希冲突
 * 冲突后依次向后探测空位插入
 */
public class LinearProbeHashDemo {
    private String[] table;
    private static final int CAP = 16;
    // 标记已删除空位,避免查询截断
    private static final String DEL_FLAG = "DEL";

    public LinearProbeHashDemo() {
        table = new String[CAP];
    }

    private int hash(String key) {
        return key == null ? 0 : (key.hashCode() & (CAP - 1));
    }

    // 插入:冲突线性后探
    public void put(String key) {
        int idx = hash(key);
        // 探测空位:为空或已删除则插入
        while (table[idx] != null && !DEL_FLAG.equals(table[idx])) {
            idx = (idx + 1) % CAP;
        }
        table[idx] = key;
    }

    // 查询
    public boolean contains(String key) {
        int idx = hash(key);
        int start = idx;
        while (table[idx] != null) {
            if (key.equals(table[idx])) {
                return true;
            }
            idx = (idx + 1) % CAP;
            // 遍历一圈终止,避免死循环
            if (idx == start) break;
        }
        return false;
    }

    // 删除(标记删除,不直接置空)
    public void remove(String key) {
        int idx = hash(key);
        int start = idx;
        while (table[idx] != null) {
            if (key.equals(table[idx])) {
                table[idx] = DEL_FLAG;
                return;
            }
            idx = (idx + 1) % CAP;
            if (idx == start) break;
        }
    }

    public static void main(String[] args) {
        LinearProbeHashDemo hash = new LinearProbeHashDemo();
        hash.put("123");
        hash.put("456");
        System.out.println(hash.contains("123"));
        hash.remove("123");
        System.out.println(hash.contains("123"));
    }
}

4.4.3 二次探测法(优化线性探测·笔试冷门)

基于二次函数偏移寻找空位置,缓解线性探测的堆积问题,但仍存在聚集风险。探测公式:newIdx = (oldIdx + i²) % capnewIdx = (oldIdx - i²) % cap,双向探测规避单侧堆积。

1. 核心优缺点
  • 优点:大幅缓解线性探测的哈希堆积、冲突分散更均匀

  • 缺点:无法完全杜绝聚集、代码复杂度更高、可能存在探测不到空位的情况

2. Java 实战代码
java 复制代码
/**
 * 二次探测法 解决哈希冲突
 * 采用双向二次偏移探测,优化堆积问题
 */
public class QuadraticProbeHashDemo {
    private String[] table;
    private static final int CAP = 17; // 素数容量,提升探测成功率
    private static final String DEL_FLAG = "DEL";

    public QuadraticProbeHashDemo() {
        table = new String[CAP];
    }

    private int hash(String key) {
        return key == null ? 0 : Math.abs(key.hashCode()) % CAP;
    }

    // 二次探测插入
    public boolean put(String key) {
        int idx = hash(key);
        // 先查询是否已存在
        if (contains(key)) return true;

        // 正向二次探测 1²,2²,3²...
        for (int i = 1; i < CAP / 2; i++) {
            int newIdx = (idx + i * i) % CAP;
            if (table[newIdx] == null || DEL_FLAG.equals(table[newIdx])) {
                table[newIdx] = key;
                return true;
            }
        }
        // 反向二次探测
        for (int i = 1; i < CAP / 2; i++) {
            int newIdx = (idx - i * i + CAP) % CAP;
            if (table[newIdx] == null || DEL_FLAG.equals(table[newIdx])) {
                table[newIdx] = key;
                return true;
            }
        }
        return false; // 表满插入失败
    }

    public boolean contains(String key) {
        int idx = hash(key);
        for (int i = 0; i < CAP / 2; i++) {
            int newIdx = (idx + i * i) % CAP;
            if (key.equals(table[newIdx])) return true;
            newIdx = (idx - i * i + CAP) % CAP;
            if (key.equals(table[newIdx])) return true;
        }
        return false;
    }

    public static void main(String[] args) {
        QuadraticProbeHashDemo hash = new QuadraticProbeHashDemo();
        hash.put("a");
        hash.put("b");
        System.out.println(hash.contains("a"));
    }
}

4.4.4 双重哈希法(最优开放定址·竞赛常用)

冲突时使用第二个哈希函数重新计算下标,散列效果好、冲突率极低,实现复杂度较高。探测公式:newIdx = (hash1(key) + i * hash2(key)) % cap,双哈希函数彻底打散冲突数据。

1. 核心优缺点
  • 优点:无哈希堆积、冲突概率极低、散列均匀性最优

  • 缺点:需要设计两个优质哈希函数、计算开销略大、实现复杂

2. Java 实战代码
java 复制代码
/**
 * 双重哈希法 解决哈希冲突
 * 双哈希函数动态计算探测下标,冲突率最低
 */
public class DoubleHashDemo {
    private String[] table;
    private static final int CAP = 19; // 素数容量
    private static final String DEL_FLAG = "DEL";

    public DoubleHashDemo() {
        table = new String[CAP];
    }

    // 第一个哈希函数:基础散列
    private int hash1(String key) {
        return key == null ? 0 : Math.abs(key.hashCode()) % CAP;
    }

    // 第二个哈希函数:增量散列(必须与容量互质)
    private int hash2(String key) {
        return key == null ? 1 : 7 - (Math.abs(key.hashCode()) % 7);
    }

    // 双重哈希插入
    public boolean put(String key) {
        if (contains(key)) return true;
        int h1 = hash1(key);
        int h2 = hash2(key);

        // 最多遍历数组长度次,避免死循环
        for (int i = 0; i < CAP; i++) {
            int newIdx = (h1 + i * h2) % CAP;
            if (table[newIdx] == null || DEL_FLAG.equals(table[newIdx])) {
                table[newIdx] = key;
                return true;
            }
        }
        return false;
    }

    public boolean contains(String key) {
        int h1 = hash1(key);
        int h2 = hash2(key);
        for (int i = 0; i < CAP; i++) {
            int newIdx = (h1 + i * h2) % CAP;
            if (key.equals(table[newIdx])) return true;
            if (table[newIdx] == null) break;
        }
        return false;
    }

    public static void main(String[] args) {
        DoubleHashDemo hash = new DoubleHashDemo();
        hash.put("test1");
        hash.put("test2");
        System.out.println(hash.contains("test1"));
    }
}

4.4.5 四大方案终极对比(面试必背)

|-------|------------|---------------|--------------|--------------|
| 解决方案 | 核心原理 | 优点 | 缺点 | 适用场景 |
| 链地址法 | 冲突节点链表/树挂载 | 无堆积、增删高效、容忍度高 | 有少量对象开销 | Java集合、工业主流 |
| 线性探测法 | 顺序向后空位探测 | 无额外空间、实现简单 | 严重哈希堆积、删除有空洞 | 笔试基础考题、简易哈希表 |
| 二次探测法 | 二次偏移双向探测 | 缓解堆积、散列更均匀 | 仍有聚集风险、复杂度高 | 优化型开放定址场景 |
| 双重哈希法 | 双哈希函数动态探测 | 冲突率极低、无堆积 | 双哈希设计繁琐、计算慢 | 算法竞赛、高精度哈希场景 |

4.4.6 面试满分答题模板(实战版)

哈希冲突主流有四种解决方案:

  1. 链地址法,通过链表/红黑树挂载冲突节点,无哈希堆积、增删高效,是Java HashMap采用的工业方案,适配高并发、大数据量场景;

  2. 线性探测法,冲突后顺序向后探测空位,实现简单但易产生哈希堆积、查询效率衰减;

  3. 二次探测法,通过二次函数双向偏移探测,优化线性探测的堆积问题,但无法完全杜绝聚集;

  4. 双重哈希法,依托双哈希函数动态计算下标,冲突率最低、散列效果最优,缺点是实现复杂度较高。笔试重点考察四种方案的原理对比,面试重点考察Java链地址法的底层优化与冲突处理机制。

4.5 刷题万能哈希模板(可直接默写)

4.5.1 数组计数/去重模板(HashSet)

java 复制代码
// 数组去重、统计元素是否存在
public Set<Integer> deduplicate(int[] nums) {
    Set<Integer> set = new HashSet<>();
    for (int num : nums) {
        set.add(num);
    }
    return set;
}

4.5.2 元素频次统计模板(HashMap)

java 复制代码
// 统计数组元素出现频次
public Map<Integer, Integer> countFrequency(int[] nums) {
    Map<Integer, Integer> map = new HashMap<>();
    for (int num : nums) {
        map.put(num, map.getOrDefault(num, 0) + 1);
    }
    return map;
}

4.5.3 两数之和经典哈希模板(O(n)最优解)

java 复制代码
public int[] twoSum(int[] nums, int target) {
    Map<Integer, Integer> map = new HashMap<>();
    for (int i = 0; i < nums.length; i++) {
        int diff = target - nums[i];
        if (map.containsKey(diff)) {
            return new int[]{map.get(diff), i};
        }
        map.put(nums[i], i);
    }
    return new int[]{-1, -1};
}

4.6 面试标准满分答题模板(直接背诵)

Java哈希结构核心以HashMap为核心,底层采用数组+链表+红黑树的复合结构,通过哈希扰动函数优化哈希值分布,依托链地址法解决哈希冲突。默认容量16、负载因子0.75,链表长度达8且数组容量超64时树化,节点少于6时退化为链表,平衡读写性能与空间开销。HashSet基于HashMap实现元素去重,LinkedHashMap通过双向链表实现有序存储与LRU缓存能力。HashMap线程不安全,高并发场景需使用ConcurrentHashMap,自定义对象作为键必须重写hashCode和equals方法,保证哈希映射准确性。

第五章 图论体系(Java 笔试/大厂压轴·全覆盖完整版)

核心总述(面试必背):图论是Java算法笔试、大厂面试压轴核心,属于高阶必考模块。

所有图论题目核心分为四大类:图的存储与遍历、最短路问题、最小生成树、有向图进阶拓扑与强连通。所有复杂图论题,均可拆解为「建图+遍历/松弛/集合合并」三大基础操作,本文补齐全网最全Java图论模板,适配笔试刷题、面试手撕、算法竞赛基础场景。

图的核心分类(前置认知)

  • 按方向:有向图、无向图

  • 按权重:带权图、无权图

  • 按稠密:稠密图(邻接矩阵)、稀疏图(邻接表·Java主流)

  • 特殊图:二分图、有向无环图DAG、连通图、强连通图

5.1 图的存储方式(Java 选型+实战代码)

5.1.1 邻接表(Sparse图·Java首选)

适用场景:稀疏图(绝大多数刷题/笔试场景),节点多、边少,空间利用率极高,支持快速遍历邻边。

实现方式List<List<Integer>> 无权图、List<List<Edge>> 带权图

Edge边类定义:存储目标节点、边权重

java 复制代码
// 带权图边实体
static class Edge {
    int to;     // 目标节点
    int weight; // 边权重
    public Edge(int to, int weight) {
        this.to = to;
        this.weight = weight;
    }
}

// 1. 无权图邻接表构建
public static List<List<Integer>> buildUnGraph(int n, int[][] edges) {
    List<List<Integer>> graph = new ArrayList<>();
    for (int i = 0; i <= n; i++) { // 节点从1开始
        graph.add(new ArrayList<>());
    }
    for (int[] edge : edges) {
        int u = edge[0], v = edge[1];
        graph.get(u).add(v);
        graph.get(v).add(u); // 无向图双向建边
    }
    return graph;
}

// 2. 带权图邻接表构建
public static List<List<Edge>> buildWeightGraph(int n, int[][] edges) {
    List<List<Edge>> graph = new ArrayList<>();
    for (int i = 0; i <= n; i++) {
        graph.add(new ArrayList<>());
    }
    for (int[] edge : edges) {
        int u = edge[0], v = edge[1], w = edge[2];
        graph.get(u).add(new Edge(v, w));
        graph.get(v).add(new Edge(u, w));
    }
    return graph;
}

5.1.2 邻接矩阵(Dense图·稠密图专属)

适用场景:节点数≤1000、边极多的稠密图,判断两点是否连通O(1),缺点是空间复杂度O(n²),大数据量直接超时爆内存。

java 复制代码
public static int[][] buildMatrixGraph(int n, int[][] edges) {
    int INF = Integer.MAX_VALUE / 2; // 防止加法溢出
    int[][] graph = new int[n + 1][n + 1];
    // 初始化:所有边置为无穷大,自身为0
    for (int i = 0; i <= n; i++) {
        Arrays.fill(graph[i], INF);
        graph[i][i] = 0;
    }
    // 填充边权重
    for (int[] edge : edges) {
        int u = edge[0], v = edge[1], w = edge[2];
        graph[u][v] = w;
        graph[v][u] = w;
    }
    return graph;
}

5.1.3 存储方式选型口诀(刷题秒选)

  • 节点多、边少、常规刷题 → 邻接表

  • 节点少、边极密、需要快速判连通 → 邻接矩阵

  • 带权图、最短路、生成树算法 → 必用带权邻接表

5.2 图的遍历算法(DFS/BFS·基础核心)

核心作用:遍历所有节点与边,解决连通性、路径查找、岛屿问题、图染色、环路检测,是所有图论算法的基础。

5.2.1 DFS 深度优先遍历(递归+迭代双模板)

核心思想:一路走到底,走不通再回溯,适合路径枚举、连通块统计、拓扑排序前置、环检测。

java 复制代码
import java.util.*;

public class GraphDFS {
    static boolean[] visited; // 访问标记数组

    // 递归DFS(代码简洁·刷题常用)
    public static void dfs(int cur, List<List<Integer>> graph) {
        visited[cur] = true;
        // 遍历当前节点所有邻接点
        for (int next : graph.get(cur)) {
            if (!visited[next]) {
                dfs(next, graph);
            }
        }
    }

    // 迭代DFS(防栈溢出·大数据量专用)
    public static void dfsIter(int start, List<List<Integer>> graph) {
        Stack<Integer> stack = new Stack<>();
        stack.push(start);
        visited[start] = true;
        while (!stack.isEmpty()) {
            int cur = stack.pop();
            for (int next : graph.get(cur)) {
                if (!visited[next]) {
                    visited[next] = true;
                    stack.push(next);
                }
            }
        }
    }
}

5.2.2 BFS 广度优先遍历(层序遍历·最短路基础)

核心思想:逐层向外扩散,无权图单源最短路唯一最优解,适合最短路径、最小步数、多源扩散问题,无递归栈溢出风险。

java 复制代码
// BFS层序遍历 + 无权图最短路
public static int bfs(int start, int end, List<List<Integer>> graph, int n) {
    boolean[] visited = new boolean[n + 1];
    Queue<int[]> queue = new LinkedList<>();
    queue.offer(new int[]{start, 0}); // [节点, 步数]
    visited[start] = true;

    while (!queue.isEmpty()) {
        int[] cur = queue.poll();
        int node = cur[0];
        int step = cur[1];

        // 到达终点,返回最短步数
        if (node == end) return step;

        for (int next : graph.get(node)) {
            if (!visited[next]) {
                visited[next] = true;
                queue.offer(new int[]{next, step + 1});
            }
        }
    }
    return -1; // 不可达
}

5.2.3 多源BFS(大厂高频·最小距离扩散)

核心场景:矩阵多起点扩散、所有节点最近源头距离、洪水填充问题,核心优化:所有源点同时入队,避免多次单源BFS超时。

5.2.4 二分图染色(BFS/DFS·判二分图)

二分图定义:图中所有边的两个端点分属两个集合,无同集合相连,无奇数环。

核心用途:二分图匹配前置判断、图分区问题。

java 复制代码
// 二分图染色判断:0未染色,1红色,2蓝色
static int[] color;
public static boolean isBipartite(int cur, List<List<Integer>> graph) {
    for (int next : graph.get(cur)) {
        if (color[next] == 0) {
            // 未染色,染相反颜色
            color[next] = 3 - color[cur];
            if (!isBipartite(next, graph)) return false;
        } else if (color[next] == color[cur]) {
            // 同色相邻,不是二分图
            return false;
        }
    }
    return true;
}

5.3 图论最短路算法(三大核心·全覆盖)

5.3.1 Dijkstra 算法(单源非负权·工业最常用)

核心约束 :边权重非负 ,负权直接失效;最优实现:优先队列堆优化,时间复杂度O(m log n)。

核心思想:每次选取当前最短距离节点,松弛更新邻边距离,贪心思想求解单源最短路。

java 复制代码
// Dijkstra堆优化模板:单源非负权最短路
public static int[] dijkstra(int start, int n, List<List<Edge>> graph) {
    int INF = Integer.MAX_VALUE / 2;
    int[] dist = new int[n + 1];
    Arrays.fill(dist, INF);
    dist[start] = 0;

    // 小顶堆:(距离, 节点)
    PriorityQueue<int[]> heap = new PriorityQueue<>(Comparator.comparingInt(a -> a[0]));
    heap.offer(new int[]{0, start});

    while (!heap.isEmpty()) {
        int[] cur = heap.poll();
        int curDist = cur[0];
        int u = cur[1];

        // 过期节点,直接跳过
        if (curDist > dist[u]) continue;

        // 松弛操作
        for (Edge e : graph.get(u)) {
            int v = e.to;
            int w = e.weight;
            if (dist[v] > dist[u] + w) {
                dist[v] = dist[u] + w;
                heap.offer(new int[]{dist[v], v});
            }
        }
    }
    return dist;
}

5.3.2 SPFA 算法(负权适配·判负环)

定位 :Bellman-Ford队列优化版,支持负权边,可判断负环,效率远高于暴力BF。

核心禁忌:无法处理负权回路,存在负环则无最短路。

java 复制代码
// SPFA 单源最短路 + 判负环
public static int[] spfa(int start, int n, List<List<Edge>> graph) {
    int INF = Integer.MAX_VALUE / 2;
    int[] dist = new int[n + 1];
    int[] cnt = new int[n + 1]; // 记录松弛次数,判负环
    boolean[] inQueue = new boolean[n + 1];
    Arrays.fill(dist, INF);
    dist[start] = 0;

    Queue<Integer> queue = new LinkedList<>();
    queue.offer(start);
    inQueue[start] = true;

    while (!queue.isEmpty()) {
        int u = queue.poll();
        inQueue[u] = false;

        for (Edge e : graph.get(u)) {
            int v = e.to;
            int w = e.weight;
            if (dist[v] > dist[u] + w) {
                dist[v] = dist[u] + w;
                if (!inQueue[v]) {
                    queue.offer(v);
                    inQueue[v] = true;
                    cnt[v]++;
                    // 松弛次数超过节点数,存在负环
                    if (cnt[v] > n) return null;
                }
            }
        }
    }
    return dist;
}

5.3.3 Floyd 算法(多源最短路·暴力万能)

核心优势 :无需建复杂结构,直接邻接矩阵求解任意两点最短路,支持负权、不支持负环,代码极简。

复杂度:O(n³),仅适配节点数n≤300的小规模图。

java 复制代码
// Floyd多源最短路模板
public static void floyd(int[][] graph, int n) {
    // 中间节点k中转
    for (int k = 1; k <= n; k++) {
        // 起点i
        for (int i = 1; i <= n; i++) {
            // 终点j
            for (int j = 1; j <= n; j++) {
                graph[i][j] = Math.min(graph[i][j], graph[i][k] + graph[k][j]);
            }
        }
    }
}

5.3.4 最短路算法终极选型(必背)

  • 单源、非负权、大数据量 → Dijkstra堆优化

  • 单源、含负权、判负环 → SPFA

  • 多源最短路、小规模图 → Floyd

5.4 最小生成树(MST·两大核心算法)

核心定义:无向带权连通图中,选取n-1条边,连接所有节点且总权重最小,无环、连通、权重最小。

5.4.1 Kruskal 算法(边优先·并查集加持)

核心思想:边排序+贪心选边+并查集判环,优先选权重最小的边,不构成环则加入生成树。

适用场景:稀疏图、边少节点多,代码极简,刷题首选。

java 复制代码
// Kruskal最小生成树 + 并查集
static class EdgeSort implements Comparable<EdgeSort> {
    int u, v, w;
    public EdgeSort(int u, int v, int w) {
        this.u = u;
        this.v = v;
        this.w = w;
    }
    @Override
    public int compareTo(EdgeSort o) {
        return this.w - o.w;
    }
}

// 复用前文并查集模板
static class UnionFind{
    int[] fa;
    public UnionFind(int n){
        fa = new int[n];
        for(int i = 0; i < n; i++) fa[i] = i;
    }
    public int find(int x){
        if(fa[x] != x) fa[x] = find(fa[x]);
        return fa[x];
    }
    public void union(int x, int y){
        int fx = find(x), fy = find(y);
        if(fx != fy) fa[fy] = fx;
    }
}

// 返回最小生成树总权重
public static int kruskal(int n, List<EdgeSort> edges) {
    Collections.sort(edges);
    UnionFind uf = new UnionFind(n + 1);
    int res = 0;
    int count = 0; // 已选边数

    for (EdgeSort e : edges) {
        int fx = uf.find(e.u);
        int fy = uf.find(e.v);
        if (fx != fy) {
            uf.union(fx, fy);
            res += e.w;
            count++;
            if (count == n - 1) break; // 选够n-1条边,提前终止
        }
    }
    return count == n - 1 ? res : -1;
}

5.4.2 Prim 算法(点优先·稠密图专属)

核心思想:从起点出发,每次选取连接生成树内外的最小权边,拓展节点,适配稠密图。

5.4.3 生成树算法选型

  • 稀疏图、刷题常规场景 →Kruskal+并查集

  • 稠密图、节点少边多 →Prim

5.5 有向图进阶(拓扑排序+全场景判环·面试/刷题满分版)

核心总述(必背) :拓扑排序是**有向无环图(DAG)**的专属核心算法,核心价值是对存在依赖关系的节点进行线性排序,同时可高效实现有向图判环。区别于无向图判环,有向图环检测无法通过简单并查集实现,必须依托拓扑排序或DFS回溯,是Java笔试高频题型(课程表、任务调度、依赖解析)。

核心前置定义

  • 拓扑序列:将DAG所有节点排序,使得任意有向边 u→v,u 一定排在 v 之前

  • 核心特性:有向图存在合法拓扑序列 ⇔ 该图无环

  • 入度:指向当前节点的边总数,代表节点的前置依赖数量

5.5.1 BFS拓扑排序(入度表法·主流首选)

算法原理:统计所有节点入度,将入度为0(无前置依赖)的节点优先入队,逐层遍历节点、删除当前节点的出边、更新邻节点入度,最终通过遍历节点总数判断是否存在环。

核心优势:代码直观、可直接输出拓扑序列、时间复杂度O(n+m)(n节点m边)、适配所有刷题场景。

java 复制代码
// 拓扑排序 + 有向图判环(BFS入度版·完整版)
// 返回值:合法拓扑序列(无环)/ 空集合(有环)
public static List<Integer> topologicalSortBFS(int n, List<List<Integer>> graph) {
    // 1. 初始化入度数组
    int[] inDegree = new int[n + 1];
    for (int i = 1; i <= n; i++) {
        for (int next : graph.get(i)) {
            inDegree[next]++;
        }
    }

    // 2. 入度为0的节点入队
    Queue<Integer> queue = new LinkedList<>();
    List<Integer> topoRes = new ArrayList<>();
    for (int i = 1; i <= n; i++) {
        if (inDegree[i] == 0) {
            queue.offer(i);
        }
    }

    // 3. 逐层遍历更新入度
    while (!queue.isEmpty()) {
        int cur = queue.poll();
        topoRes.add(cur);
        // 遍历当前节点所有出边
        for (int next : graph.get(cur)) {
            inDegree[next]--;
            // 前置依赖全部完成,入队
            if (inDegree[next] == 0) {
                queue.offer(next);
            }
        }
    }

    // 4. 判环:遍历节点数不等于总节点数,存在环路
    return topoRes.size() == n ? topoRes : new ArrayList<>();
}

5.5.2 DFS拓扑排序(回溯版·进阶面试)

算法原理 :依托DFS回溯,记录节点遍历状态(未访问、访问中、已完成),遍历节点所有邻边,回溯完成后记录节点,最终反转序列得到拓扑排序,可精准检测环的位置。

状态定义

  • 0:未访问

  • 1:访问中(当前递归栈中,未回溯完成)

  • 2:访问完成(所有子节点遍历完毕)

判环核心 :遍历中遇到**访问中(1)**的节点,说明存在有向环

java 复制代码
// DFS回溯版拓扑排序 + 有向图判环
static int[] status; // 节点状态数组
static List<Integer> topoList;
static boolean hasCycle;

public static List<Integer> topologicalSortDFS(int n, List<List<Integer>> graph) {
    status = new int[n + 1];
    topoList = new ArrayList<>();
    hasCycle = false;

    // 遍历所有未访问节点(适配非连通图)
    for (int i = 1; i <= n; i++) {
        if (status[i] == 0) {
            dfsTopo(i, graph);
            if (hasCycle) return new ArrayList<>();
        }
    }

    // DFS结果逆序为合法拓扑序列
    Collections.reverse(topoList);
    return topoList;
}

// 拓扑DFS核心递归
private static void dfsTopo(int cur, List<List<Integer>> graph) {
    // 出现环,直接终止
    if (hasCycle) return;
    // 遇到正在遍历的节点,存在环
    if (status[cur] == 1) {
        hasCycle = true;
        return;
    }
    // 已遍历完成,直接返回
    if (status[cur] == 2) return;

    // 标记为正在遍历
    status[cur] = 1;
    // 遍历所有邻接节点
    for (int next : graph.get(cur)) {
        dfsTopo(next, graph);
    }
    // 遍历完成,标记状态并加入集合
    status[cur] = 2;
    topoList.add(cur);
}

5.5.3 有向图 VS 无向图 判环核心区别(高频易错)

刷题90%的判环错误源于混淆两种图的判环逻辑,核心区别必背:

  • 无向图判环 :可使用并查集、普通DFS,遍历遇到已访问且非父节点即存在环,无方向限制

  • 有向图判环禁止使用并查集 ,必须用拓扑排序/状态标记DFS,仅当遍历到当前递归栈内节点才判定为环(单向依赖闭环)

5.5.4 经典刷题场景(LeetCode真题对应)

  • 课程表问题(LeetCode207):核心有向图判环,判断能否完成所有课程(无环则可行)

  • 课程表II(LeetCode210):输出合法拓扑排序序列,BFS/DFS双模板通用

  • 项目依赖排序:业务场景拓扑排序,解决任务先后依赖问题

  • 循环依赖检测:Spring Bean循环依赖底层判环逻辑

5.5.5 高频易错陷阱(面试扣分点)

  • 误区1:用并查集判断有向图环,完全失效(并查集无方向感知)

  • 误区2:仅遍历单连通分量,忽略非连通有向图,导致漏判环

  • 误区3:BFS拓扑不校验最终节点总数,误将有环图判定为无环

5.6 大厂进阶图论(压轴考点)

5.6.1 Tarjan 算法(强连通分量/割点/割边)

核心作用:求解有向图强连通分量、无向图割点割边、缩点建图,是大厂笔试压轴题核心。

核心概念

  • 强连通分量:有向图中任意两点互相可达的子图

  • 割点:删除后图连通分量增加的节点

  • 割边(桥):删除后图连通分量增加的边

5.6.2 匈牙利算法(二分图最大匹配)

核心场景:二分图配对、任务分配、男女匹配、最大匹配数求解,面试小众、笔试压轴高频。

5.6.3 欧拉回路/通路

判定规则(必背)

  • 无向欧拉通路:恰好0个或2个奇度节点

  • 无向欧拉回路:所有节点度数为偶数

  • 有向欧拉回路:所有节点入度=出度

5.7 图论面试满分总结(直接背诵)

图论核心分为存储、遍历、最短路、生成树、进阶拓扑五大模块。存储优先使用邻接表适配稀疏图,邻接矩阵适配稠密图;DFS适合路径回溯与连通统计,BFS适配无权最短路与层序扩散。

最短路场景严格区分:非负权用Dijkstra、负权判环用SPFA、多源最短路用Floyd。最小生成树主流使用Kruskal+并查集解决稀疏图问题。拓扑排序基于入度表实现DAG排序与判环,Tarjan算法、匈牙利算法、欧拉路径为大厂进阶压轴考点,覆盖所有Java图论笔试面试场景。

第六章 字符串算法(Java 高频·面试/刷题全覆盖)

核心总述(面试必背) :字符串是Java笔试面试最高频模块之一,区别于普通模拟题,高效字符串算法核心解决「海量文本匹配、最长回文、子串查找、前缀后缀匹配」四大刚需。本章全覆盖基础暴力匹配、三大高效匹配算法、回文专属算法、字符串哈希、高级后缀结构,全部配套Java默写模板、复杂度分析、面试考点,适配LeetCode刷题、大厂手撕、工程文本处理场景。

字符串算法核心分类:基础匹配算法、高效模式匹配、回文专项算法、字符串哈希、高级后缀结构

6.1 基础暴力匹配算法(BF算法)

6.1.1 算法原理

BF(Brute Force)暴力枚举算法,是最基础的字符串匹配方式。核心逻辑:以主串每个字符为起点,逐位与模式串比对,匹配失败则主串回溯、模式串重置,继续下一轮匹配。无需预处理,逻辑简单,但存在大量无效重复比对。

6.1.2 复杂度分析

  • 时间复杂度:最坏O(n*m)(n主串长度、m模式串长度)

  • 空间复杂度:O(1),无额外预处理空间

6.1.3 Java可直接默写模板

java 复制代码
/**
 * BF暴力字符串匹配算法
 * @param text 主串
 * @param pattern 模式串
 * @return 首次匹配起始下标,无匹配返回-1
 */
public int bfMatch(String text, String pattern) {
    int n = text.length();
    int m = pattern.length();
    if (m == 0) return 0;
    if (n < m) return -1;

    // i:主串指针,j:模式串指针
    int i = 0, j = 0;
    while (i < n && j < m) {
        if (text.charAt(i) == pattern.charAt(j)) {
            // 字符匹配,双指针后移
            i++;
            j++;
        } else {
            // 匹配失败,主串回溯,模式串重置
            i = i - j + 1;
            j = 0;
        }
    }
    // 匹配完成:模式串遍历完毕
    return j == m ? i - j : -1;
}

6.1.4 适用场景与优缺点

  • 优点:逻辑简单、无预处理、无空间开销、小数据量稳定

  • 缺点:存在大量重复匹配,海量文本场景极易超时

  • 适用:短字符串匹配、入门场景、数据量极小的刷题场景

6.2 KMP算法(必考·线性字符串匹配)

面试核心考点 :KMP是Java算法面试必手撕算法,核心解决BF算法的重复匹配问题,通过预处理next前缀数组,记录模式串前缀后缀最长公共匹配长度,匹配失败时无需回溯主串,仅滑动模式串,实现线性匹配。

6.2.1 核心原理

匹配失败时,利用模式串自身的前缀对称性 ,跳过已知匹配的前缀部分,避免重复比对。核心载体为next前缀数组,nexti表示模式串0~i子串的最长相等前缀、后缀长度。

6.2.2 复杂度分析

  • 预处理next数组:O(m)

  • 字符串匹配:O(n)

  • 整体复杂度:O(n+m) 线性级,海量文本最优基础匹配算法

  • 空间复杂度:O(m)(存储next数组)

6.2.3 完整Java模板(next数组+匹配逻辑·可直接默写)

java 复制代码
/**
 * 构建KMP前缀next数组
 * @param pattern 模式串
 * @return next数组
 */
public int[] getNext(String pattern) {
    int m = pattern.length();
    int[] next = new int[m];
    // j:前缀匹配长度,初始0
    int j = 0;
    // i从1开始遍历(next[0]默认0)
    for (int i = 1; i < m; i++) {
        // 匹配失败,回溯前缀长度
        while (j > 0 && pattern.charAt(i) != pattern.charAt(j)) {
            j = next[j - 1];
        }
        // 匹配成功,前缀长度+1
        if (pattern.charAt(i) == pattern.charAt(j)) {
            j++;
        }
        next[i] = j;
    }
    return next;
}

/**
 * KMP主匹配算法
 * @param text 主串
 * @param pattern 模式串
 * @return 首次匹配下标,无匹配返回-1
 */
public int kmpMatch(String text, String pattern) {
    int n = text.length();
    int m = pattern.length();
    if (m == 0) return 0;
    if (n < m) return -1;

    int[] next = getNext(pattern);
    int j = 0; // 模式串指针

    for (int i = 0; i < n; i++) {
        // 匹配失败,通过next数组回溯
        while (j > 0 && text.charAt(i) != pattern.charAt(j)) {
            j = next[j - 1];
        }
        // 匹配成功,指针后移
        if (text.charAt(i) == pattern.charAt(j)) {
            j++;
        }
        // 完全匹配,返回起始下标
        if (j == m) {
            return i - m + 1;
        }
    }
    return -1;
}

6.2.4 面试高频问答(必背)

  • Q:KMP为什么比BF快? A:BF匹配失败会回溯主串,重复比对已匹配字符;KMP通过next数组利用模式串前缀后缀特性,主串不回溯,仅滑动模式串,消除重复比对。

  • Q:next数组的核心作用? A:存储模式串每个位置的最长相等前后缀长度,为匹配失败提供最优滑动距离。

  • Q:KMP工程使用场景? A:日志文本匹配、关键词过滤、字符串检索、代码编辑器字符匹配。

6.3 Sunday算法(超快匹配·刷题优选)

刷题神器:Sunday算法是效率高于KMP的短文本匹配算法,预处理简单、滑动步长更大,平均匹配速度远超KMP、BM,是LeetCode字符串匹配最优解之一。

6.3.1 核心原理

匹配失败时,观察主串中模式串末尾的下一个字符:若该字符存在于模式串中,将模式串对齐该字符最后出现的位置;若不存在,模式串直接整体后移m位(m为模式串长度),滑动步长最大化。

6.3.2 Java完整模板

java 复制代码
import java.util.HashMap;
import java.util.Map;

/**
 * Sunday字符串匹配算法
 * @param text 主串
 * @param pattern 模式串
 * @return 首次匹配下标
 */
public int sundayMatch(String text, String pattern) {
    int n = text.length();
    int m = pattern.length();
    if (m == 0) return 0;
    if (n < m) return -1;

    // 预处理:记录模式串字符最后出现的下标
    Map<Character, Integer> lastIndex = new HashMap<>();
    for (int i = 0; i < m; i++) {
        lastIndex.put(pattern.charAt(i), i);
    }

    int i = 0; // 主串起始匹配位置
    while (i <= n - m) {
        int j = 0;
        // 逐位匹配
        while (j < m && text.charAt(i + j) == pattern.charAt(j)) {
            j++;
        }
        // 匹配成功
        if (j == m) return i;

        // 匹配失败,获取主串下一个字符
        char nextChar = text.charAt(i + m);
        // 计算滑动步长
        i += lastIndex.containsKey(nextChar) ? m - lastIndex.get(nextChar) : m + 1;
    }
    return -1;
}

6.3.3 优缺点与场景

  • 优点:平均速度最快、预处理极简、滑动步长大

  • 缺点:最坏复杂度仍为O(n*m),极端场景弱于KMP

  • 适用:刷题场景、普通文本匹配、短模式串快速检索

6.4 BM算法(工程级高效匹配)

BM(Boyer-Moore)算法是工业级标准字符串匹配算法 ,Java、Python底层文本检索均采用优化版BM,核心特性:从后往前匹配+坏字符规则+好后缀规则,跳过大量无效字符,海量文本匹配效率远超KMP。

6.4.1 两大核心规则

  • 坏字符规则:匹配失败的字符为坏字符,根据坏字符在模式串中的位置,向后滑动对应距离

  • 好后缀规则:已匹配成功的后缀为好后缀,根据好后缀在模式串中的前置匹配位置滑动

  • 实际滑动步长:取两大规则的最大值,保证最优效率

6.4.2 适用场景

海量日志检索、文本编辑器查找、大数据量字符串匹配(工程首选),面试了解原理即可,刷题优先KMP/Sunday。

6.5 Manacher算法(最长回文子串·O(n)最优解)

笔试压轴考点 :Manacher算法是唯一能在线性时间内求解最长回文子串的算法,彻底解决中心扩散O(n²)的低效问题,是字符串回文问题的终极模板。

6.5.1 核心优化思路

  1. 字符串预处理:所有字符间插入特殊占位符(如#),统一奇偶回文场景;2. 利用已求解的回文区间信息,避免重复中心扩散;3. 维护回文中心、回文右边界,实现线性遍历求解。

6.5.2 Java完整默写模板

java 复制代码
/**
 * Manacher算法 最长回文子串 O(n)
 * @param s 原始字符串
 * @return 最长回文子串
 */
public String manacher(String s) {
    if (s.length() == 0) return "";
    // 1. 预处理:插入#,统一奇偶回文
    StringBuilder sb = new StringBuilder();
    sb.append("#");
    for (char c : s.toCharArray()) {
        sb.append(c).append("#");
    }
    char[] str = sb.toString().toCharArray();
    int n = str.length;

    // p[i]:i位置为中心的最大回文半径
    int[] p = new int[n];
    int maxRight = 0; // 当前回文最右边界
    int center = 0;   // 当前最右边界对应的回文中心
    int maxLen = 0;   // 最长回文半径
    int maxCenter = 0;// 最长回文中心

    for (int i = 0; i < n; i++) {
        // 2. 利用对称特性,初始化半径
        p[i] = maxRight > i ? Math.min(p[2 * center - i], maxRight - i) : 1;

        // 3. 中心扩散
        while (i + p[i] < n && i - p[i] >= 0 && str[i + p[i]] == str[i - p[i]]) {
            p[i]++;
        }

        // 4. 更新右边界与中心
        if (i + p[i] - 1 > maxRight) {
            maxRight = i + p[i] - 1;
            center = i;
        }

        // 5. 记录最大值
        if (p[i] > maxLen) {
            maxLen = p[i];
            maxCenter = i;
        }
    }

    // 截取原始最长回文子串
    int start = (maxCenter - maxLen) / 2;
    return s.substring(start, start + maxLen - 1);
}

6.5.3 复杂度与面试考点

  • 时间复杂度:O(n),每个字符仅遍历一次,无重复扩散

  • 空间复杂度:O(n),预处理字符串+半径数组

  • 面试必问:对比中心扩散法,Manacher如何优化重复计算?

6.6 字符串前缀哈希(滚动哈希·刷题大杀器)

万能模板 :前缀哈希将字符串转化为数值哈希值,实现O(1)任意子串哈希比对,秒杀子串重复、回文校验、字符串匹配问题,替代复杂KMP,刷题效率极高。

6.6.1 核心原理

采用进制哈希(常用131/13331),预处理前缀哈希数组、幂次数组,通过哈希公式快速计算任意区间子串哈希值,通过哈希值相等判定字符串相等(极低碰撞概率)。

6.6.2 Java标准模板(无冲突优化)

java 复制代码
/**
 * 字符串前缀滚动哈希模板
 * 进制:131,模数:大质数,极低碰撞概率
 */
public class StringHash {
    private static final long BASE = 131L;
    private static final long MOD = (long) 1e9 + 7;
    private long[] hash;
    private long[] pow;

    // 初始化哈希数组与幂次数组
    public StringHash(String s) {
        int n = s.length();
        hash = new long[n + 1];
        pow = new long[n + 1];
        pow[0] = 1;

        for (int i = 1; i <= n; i++) {
            hash[i] = (hash[i - 1] * BASE + s.charAt(i - 1)) % MOD;
            pow[i] = (pow[i - 1] * BASE) % MOD;
        }
    }

    // 获取[l,r]子串哈希(原字符串0下标,闭区间)
    public long getHash(int l, int r) {
        return (hash[r + 1] - hash[l] * pow[r - l + 1] % MOD + MOD) % MOD;
    }
}

6.6.3 核心场景

  • 快速判断两个子串是否相等

  • 最长重复子串、最长公共子串

  • 替代KMP实现快速字符串匹配

  • 回文字符串快速校验

6.7 高级后缀结构(大厂压轴)

6.7.1 后缀数组

将字符串所有后缀按字典序排序,存储后缀起始下标,可求解最长公共前缀、最长重复子串、子串计数等高阶问题。Java刷题较少手写,大厂笔试压轴题高频考察原理与应用。

6.7.2 SAM后缀自动机

字符串最强数据结构,线性空间、线性时间构建,可解决所有字符串子串问题:子串数量、不同子串统计、最长公共子串、多次模式匹配,是算法竞赛、大厂高端笔试压轴考点,工程中用于文本检索、字符串压缩。

6.8 字符串算法选型口诀(刷题秒选)

  • 短文本简单匹配 → BF暴力

  • 常规字符串匹配、面试手撕 → KMP

  • 刷题快速匹配、追求效率 → Sunday

  • 海量工程文本检索 → BM算法

  • 最长回文子串、线性最优解 → Manacher

  • 子串快速比对、批量匹配 → 前缀滚动哈希

  • 高阶子串统计、复杂字符串问题 → SAM后缀自动机

6.9 面试满分总结(直接背诵)

Java字符串算法核心分为基础匹配、高效匹配、回文专项、哈希优化、高级后缀结构五大类。基础BF算法逻辑简单但效率低,适用于小数据场景;KMP通过前缀next数组消除重复匹配,实现O(n+m)线性匹配,是面试必考手撕算法;Sunday算法平均效率最优,为刷题首选;BM算法为工业级匹配算法,适配海量文本场景。Manacher算法线性求解最长回文子串,突破传统中心扩散的O(n²)瓶颈。字符串前缀哈希通过数值映射实现O(1)子串比对,大幅简化刷题逻辑。后缀数组与SAM后缀自动机为高阶结构,覆盖所有复杂字符串算法场景,适配大厂压轴笔试。

第七章 排序算法(Java 全部全覆盖·面试手撕+源码底层+刷题适配)

本章核心总述(面试必背) :排序算法是Java算法基础核心,所有刷题、面试、集合底层均依赖排序逻辑。本章全覆盖十大经典排序,包含基础简单排序、高效分治排序、线性特殊排序,配套可直接默写的Java完整代码、逐行原理、稳定性分析、场景选型、JDK底层源码适配、高频面试问答,彻底解决排序手撕、原理背诵、场景选型三大难题。

排序核心分类

  • 比较类排序:冒泡、插入、选择、希尔、归并、快排、堆排(依赖元素比较,通用所有数据类型)

  • 非比较类排序:计数、基数、桶排序(基于数值特性,仅适配整数,线性时间)

7.1 十大排序算法终极总表(必背·面试默写版)

|----------|-----------|-----------|-----------|---------|---------------|
| 排序算法 | 平均复杂度 | 最坏复杂度 | 空间复杂度 | 稳定性 | 核心场景 |
| 冒泡排序 | O(n²) | O(n²) | O(1) | 稳定 | 教学演示、极小数据量 |
| 插入排序 | O(n²) | O(n²) | O(1) | 稳定 | 有序数据、JDK小数组优化 |
| 选择排序 | O(n²) | O(n²) | O(1) | 不稳定 | 极少使用,仅教学 |
| 希尔排序 | O(n^1.3) | O(n²) | O(1) | 不稳定 | 中等数据量、原地优化排序 |
| 快速排序 | O(nlogn) | O(n²) | O(logn) | 不稳定 | 基础类型数组、工业主流 |
| 归并排序 | O(nlogn) | O(nlogn) | O(n) | 稳定 | 对象排序、需保序场景 |
| 堆排序 | O(nlogn) | O(nlogn) | O(1) | 不稳定 | TopK问题、海量数据排序 |
| 计数排序 | O(n+k) | O(n+k) | O(n+k) | 稳定 | 数值范围小的整数排序 |
| 基数排序 | O(d(n+k)) | O(d(n+k)) | O(n+k) | 稳定 | 大数值整数、手机号排序 |
| 桶排序 | O(n+k) | O(n²) | O(n+k) | 稳定 | 均匀分布浮点数据 |

7.2 基础简单排序(O(n²)·面试入门手撕)

7.2.1 冒泡排序

核心原理:相邻元素两两比对,逆序则交换,每轮冒泡将当前最大值冒泡至数组末尾,多轮遍历完成整体有序。可通过标记优化有序数组无效遍历。

稳定性:稳定(相等元素不交换位置)

优缺点:代码极简、原地排序;效率极低,仅适用于极小数据量

java 复制代码
// 冒泡排序(优化版·有序提前终止)
public static void bubbleSort(int[] nums) {
    int n = nums.length;
    // 标记本轮是否发生交换,优化已有序数组
    boolean swapped;
    for (int i = 0; i < n - 1; i++) {
        swapped = false;
        for (int j = 0; j < n - 1 - i; j++) {
            // 逆序交换,相等不交换保证稳定性
            if (nums[j] > nums[j + 1]) {
                int temp = nums[j];
                nums[j] = nums[j + 1];
                nums[j + 1] = temp;
                swapped = true;
            }
        }
        // 无交换说明数组已有序,直接终止
        if (!swapped) break;
    }
}

7.2.2 插入排序

核心原理 :将数组分为有序前缀、无序后缀,逐个取出无序元素,向前遍历找到合法插入位置,后移元素完成插入。JDK底层核心优化依赖,小数组排序效率极高。

稳定性:稳定

核心特性:数据越有序,效率越高,最优复杂度O(n)

java 复制代码
// 插入排序(标准原地版)
public static void insertSort(int[] nums) {
    int n = nums.length;
    for (int i = 1; i < n; i++) {
        // 待插入元素
        int cur = nums[i];
        int j = i - 1;
        // 向前遍历,大于cur的元素后移
        while (j >= 0 && nums[j] > cur) {
            nums[j + 1] = nums[j];
            j--;
        }
        // 插入合法位置
        nums[j + 1] = cur;
    }
}

7.2.3 选择排序

核心原理:每轮遍历无序区间,找到最小值下标,与无序区间首位元素交换,逐步构建有序前缀。

稳定性:不稳定(跨位置交换会打乱相等元素顺序)

优缺点:交换次数少,效率略优于冒泡;无序数据效率差,无工程使用价值

java 复制代码
// 选择排序(标准版)
public static void selectSort(int[] nums) {
    int n = nums.length;
    for (int i = 0; i < n - 1; i++) {
        int minIndex = i;
        // 查找无序区间最小值下标
        for (int j = i + 1; j < n; j++) {
            if (nums[j] < nums[minIndex]) {
                minIndex = j;
            }
        }
        // 交换最小值到有序区间末尾
        int temp = nums[i];
        nums[i] = nums[minIndex];
        nums[minIndex] = temp;
    }
}

7.2.4 希尔排序(插入排序优化版)

核心原理:也称缩小增量排序,基于插入排序优化。先设置增量间隔,将数组分为若干子数组分组插入排序,逐步缩小增量至1,完成全局有序。解决插入排序大数据量低效问题。

稳定性:不稳定

java 复制代码
// 希尔排序(经典增量序列)
public static void shellSort(int[] nums) {
    int n = nums.length;
    // 初始增量为数组一半,逐步缩小
    for (int gap = n / 2; gap > 0; gap /= 2) {
        // 分组插入排序
        for (int i = gap; i < n; i++) {
            int cur = nums[i];
            int j;
            for (j = i; j >= gap && nums[j - gap] > cur; j -= gap) {
                nums[j] = nums[j - gap];
            }
            nums[j] = cur;
        }
    }
}

7.3 高效分治排序(O(nlogn)·面试核心手撕)

7.3.1 快速排序(Java工业最主流)

核心原理:分治思想,选取基准值,将数组分区为「小于基准、大于基准」两部分,递归分区排序。JDK基础类型排序核心实现。

稳定性:不稳定

JDK核心优化:三数取中选基准、随机基准规避最坏情况、小数组退化插入排序、尾递归优化

java 复制代码
// 快速排序(优化版·可直接面试手撕)
public static void quickSort(int[] nums, int left, int right) {
    if (left >= right) return;
    // 三数取中优化基准值,规避有序数组最坏O(n²)
    int mid = left + (right - left) / 2;
    if (nums[left] > nums[mid]) swap(nums, left, mid);
    if (nums[left] > nums[right]) swap(nums, left, right);
    if (nums[mid] > nums[right]) swap(nums, mid, right);
    // 基准值放置右端
    swap(nums, mid, right);
    int pivot = nums[right];

    // 分区操作
    int i = left;
    for (int j = left; j < right; j++) {
        if (nums[j] < pivot) {
            swap(nums, i, j);
            i++;
        }
    }
    // 基准值归位
    swap(nums, i, right);

    // 递归左右分区
    quickSort(nums, left, i - 1);
    quickSort(nums, i + 1, right);
}

// 数组交换工具方法
private static void swap(int[] nums, int a, int b) {
    int temp = nums[a];
    nums[a] = nums[b];
    nums[b] = temp;
}

7.3.2 归并排序(稳定排序首选)

核心原理 :标准分治,先递归拆分数组为最小子区间,再有序合并子区间,逐层回溯完成全局排序。是唯一稳定的O(nlogn)通用排序

稳定性:稳定(合并时优先取左区间元素,保留相等元素顺序)

JDK适配:对象类型排序默认使用归并排序,保证有序稳定性

java 复制代码
// 归并排序(稳定版·可直接默写)
public static void mergeSort(int[] nums, int left, int right) {
    if (left >= right) return;
    int mid = left + (right - left) / 2;
    // 分:递归拆分左右区间
    mergeSort(nums, left, mid);
    mergeSort(nums, mid + 1, right);
    // 合:有序合并两个有序区间
    merge(nums, left, mid, right);
}

// 合并两个有序数组
private static void merge(int[] nums, int left, int mid, int right) {
    // 临时数组存储合并结果
    int[] temp = new int[right - left + 1];
    int i = left, j = mid + 1, k = 0;
    // 双指针有序合并
    while (i <= mid && j <= right) {
        if (nums[i] <= nums[j]) {
            temp[k++] = nums[i++];
        } else {
            temp[k++] = nums[j++];
        }
    }
    // 补全剩余元素
    while (i <= mid) temp[k++] = nums[i++];
    while (j <= right) temp[k++] = nums[j++];
    // 覆盖原数组
    System.arraycopy(temp, 0, nums, left, temp.length);
}

7.3.3 堆排序(TopK专属)

核心原理:基于大顶堆结构,先将数组构建为大顶堆,逐次取出堆顶最大值,交换至数组末尾,重新调整堆结构,最终实现升序排序。原地O(nlogn)排序。

稳定性:不稳定

核心场景:海量数据TopK、无需稳定的大数据排序

java 复制代码
// 堆排序(标准版·升序)
public static void heapSort(int[] nums) {
    int n = nums.length;
    // 1. 构建大顶堆(从最后一个非叶子节点向上调整)
    for (int i = n / 2 - 1; i >= 0; i--) {
        heapify(nums, n, i);
    }
    // 2. 逐次取出堆顶,调整堆
    for (int i = n - 1; i > 0; i--) {
        // 堆顶最大值交换至末尾
        swap(nums, 0, i);
        // 剩余元素重新堆化
        heapify(nums, i, 0);
    }
}

// 堆调整函数:维护大顶堆特性
private static void heapify(int[] nums, int len, int idx) {
    int maxIdx = idx;
    int left = 2 * idx + 1;
    int right = 2 * idx + 2;
    // 左右子节点找最大值
    if (left < len && nums[left] > nums[maxIdx]) maxIdx = left;
    if (right < len && nums[right] > nums[maxIdx]) maxIdx = right;
    // 最大值非根节点,交换并递归调整
    if (maxIdx != idx) {
        swap(nums, idx, maxIdx);
        heapify(nums, len, maxIdx);
    }
}

private static void swap(int[] nums, int a, int b) {
    int temp = nums[a];
    nums[a] = nums[b];
    nums[b] = temp;
}

7.4 线性非比较排序(O(n)·特殊场景秒杀)

7.4.1 计数排序

核心原理 :统计每个数值出现频次,根据频次直接回填有序数组,无元素比较操作。仅适配数值范围有限的整数

稳定性:稳定

适用场景:分数排序、年龄排序、小范围整数数组

java 复制代码
// 计数排序(稳定标准版)
public static void countSort(int[] nums) {
    if (nums.length == 0) return;
    // 找到数值最大最小值,压缩统计空间
    int min = nums[0], max = nums[0];
    for (int num : nums) {
        min = Math.min(min, num);
        max = Math.max(max, num);
    }
    // 统计频次
    int[] count = new int[max - min + 1];
    for (int num : nums) {
        count[num - min]++;
    }
    // 回填有序数组
    int idx = 0;
    for (int i = 0; i < count.length; i++) {
        while (count[i] > 0) {
            nums[idx++] = i + min;
            count[i]--;
        }
    }
}

7.4.2 基数排序

核心原理:基于计数排序,从低位到高位逐位排序,逐位收敛全局有序,适配大范围整数排序。

稳定性:稳定

适用场景:手机号、身份证、大数值整数排序

java 复制代码
// 基数排序(低位优先·正整数排序)
public static void radixSort(int[] nums) {
    if (nums.length == 0) return;
    // 找到最大值,确定最大位数
    int max = nums[0];
    for (int num : nums) max = Math.max(max, num);
    // 按每一位排序
    for (int digit = 1; max / digit > 0; digit *= 10) {
        // 0-9十个桶
        int[][] bucket = new int[10][nums.length];
        int[] bucketCnt = new int[10];
        // 入桶
        for (int num : nums) {
            int d = (num / digit) % 10;
            bucket[d][bucketCnt[d]++] = num;
        }
        // 出桶回填
        int idx = 0;
        for (int i = 0; i < 10; i++) {
            for (int j = 0; j < bucketCnt[i]; j++) {
                nums[idx++] = bucket[i][j];
            }
        }
    }
}

7.4.3 桶排序

核心原理 :将数值区间划分为若干有序桶,元素归入对应桶,桶内单独排序,最后合并所有桶数据。适合均匀分布数据

稳定性:稳定

7.5 JDK 排序底层源码深度解析(面试必考)

7.5.1 Arrays.sort() 双分支逻辑

(1)基本数据类型(int/long/double) :使用 DualPivotQuicksort 双轴快排

优化点:双基准分区、三数取中、有序数组预判、小数组插入排序

优势:比传统单轴快排更快,规避大部分最坏场景

(2)对象类型(String/自定义对象) :使用 TimSort 归并优化排序

核心特性:稳定排序、适配已有序数据、减少归并次数

核心优势:保证相等元素相对位置不变,适配业务有序场景

7.5.2 JDK排序核心优化细节(面试高频)

  • 数组长度小于47:直接使用插入排序,规避递归开销

  • 数组高度有序:TimSort 自适应优化,时间复杂度趋近O(n)

  • 双轴快排:划分三个区间,减少无效元素交换

7.6 排序稳定性详解(面试必问)

稳定性定义 :排序前后,相等元素相对位置保持不变即为稳定排序,反之不稳定。

稳定排序列表:冒泡、插入、归并、计数、基数、桶排序

不稳定排序列表:选择、希尔、快排、堆排

面试核心问答:为什么对象排序需要稳定排序? 答:业务场景中对象存在多个排序维度,稳定排序可保留上一轮排序的有序性,实现多维度叠加排序(如先按分数排序、再按学号排序)。

7.7 排序算法终极选型口诀(刷题/工程秒选)

  • 小数组、接近有序 → 插入排序

  • 基础类型大数据量、追求极致速度 → 快速排序

  • 对象排序、需要保序 →归并/TimSort

  • 海量数据TopK、原地排序 → 堆排序

  • 小范围整数、分数数据 → 计数排序

  • 大数值整数、固定格式数据 → 基数排序

  • 均匀分布浮点数据 → 桶排序

7.8 面试高频易错陷阱(满分避坑·超全补全版)

  • 误区1:快排复杂度认知错误:认为快排所有场景都是O(nlogn),有序/逆序数组、重复值极多的数组,未做基准优化时会退化至最坏O(n²),递归栈深度拉满,极易栈溢出。工程JDK双轴快排已规避该问题,但手撕快排必须手动做三数取中/随机基准优化。

  • 误区2:归并排序空间认知偏差:误以为存在原地、稳定、O(nlogn)的归并排序,标准归并排序必须依赖O(n)额外辅助数组,无额外空间开销的原地归并排序算法复杂、效率极低,无工程使用价值,面试默认归并排序空间复杂度为O(n)。

  • 误区3:堆排序稳定性误判 :认为堆排序可以实现稳定排序,堆排序通过堆顶元素与末尾元素交换实现排序,会直接打乱相等元素的原始相对位置,绝对不稳定,任何优化版本都无法原生保证稳定性。

  • 误区4:非比较排序通用化误区:盲目使用计数/基数/桶排序,三类非比较排序仅适配整数类数值数据,无法直接排序字符串、对象、浮点型数据,且计数排序仅适用于数值范围小、无极端正负值的场景,数值跨度极大时会造成空间爆炸。

  • 误区5:插入排序效率认知错误 :认为插入排序效率远低于快排,忽略核心特性:数据越有序,插入排序效率越高,数组长度小于50、接近有序时,插入排序效率优于所有O(nlogn)排序,也是JDK底层小数组排序的核心原因。

  • 误区6:排序稳定性场景误用:刷题/面试中忽略稳定排序的业务价值,多维度排序场景(先按年龄排序、再按分数排序)使用快排/堆排等不稳定排序,会导致前序排序结果被打乱,业务数据错乱。

  • 误区7:希尔排序复杂度混淆:记错希尔排序复杂度,常规希尔排序平均复杂度为O(n^1.3),最坏为O(n²),并非O(nlogn),不属于高效分治排序,仅作为基础排序优化版使用。

  • 误区8:基数排序位数遍历错误:基数排序仅支持正整数,直接排序负数会出现下标越界、排序错乱问题,排序负数需手动预处理偏移转正;且必须从低位到高位逐位排序,高位优先会导致整体排序失效。

  • 误区9:桶排序场景滥用 :桶排序仅适用于数值均匀分布的数据,数据极端集中/离散时,会出现单桶数据量爆炸、多桶空置的情况,复杂度退化至O(n²),效率远低于常规排序。

  • 误区10:JDK排序调用误区:混淆JDK两种排序底层,对基本类型数组使用TimSort、对象数组使用双轴快排,会导致性能浪费、稳定性出错;且自定义对象排序未实现Comparable/Comparator接口,会直接抛出类型转换异常。

  • 误区11:原地排序定义误解 :认为O(1)空间排序就是无任何空间开销,原地排序仅代表无随数据量n增长的额外空间,方法内临时变量、交换缓存变量等常数空间不计入,不影响原地排序判定。

  • 误区12:快排递归栈空间忽略:计算快排空间复杂度时,只看辅助变量、忽略递归栈空间,优化后平均栈空间O(logn),最坏有序场景栈空间O(n),极易触发StackOverflowError,大数据量递归快排需手动改迭代实现。

  • 误区13:计数排序空间优化遗漏:直接以数组最大值作为统计数组长度,忽略负数场景、极大数值跨度场景,未做最值偏移压缩空间,导致内存溢出、空数组浪费。

  • 误区14:归并排序合并逻辑错误:合并两个有序区间时,优先取右区间元素,破坏排序稳定性;相等元素必须优先保留左区间元素,才能保证原始相对位置不变。

  • 误区15:堆排序堆化逻辑颠倒:构建升序数组误用小顶堆,堆排序升序必须用大顶堆、降序用小顶堆,逻辑颠倒会导致排序结果完全错乱。

7.9 排序面试满分背诵模板

排序算法分为比较类与非比较类,比较类包含基础O(n²)排序与高效O(nlogn)排序。冒泡、插入为稳定简单排序,适配有序小数组;快排为工业主流基础类型排序,通过分治双轴优化实现高效排序,不稳定;归并排序为稳定高效排序,是JDK对象排序底层实现;堆排序适合TopK海量数据处理。非比较类排序包含计数、基数、桶排序,基于数值特性实现线性排序,仅适配特定数据场景。工程中优先使用JDK原生排序,根据数据类型、有序性、稳定性需求选型,刷题场景按需使用对应模板。

第八章 工程级高级数据结构(Java 大厂必备·全覆盖·可手撕·工程落地)

本章核心定位(大厂面试核心分水岭) :普通算法侧重刷题解题,本章高级数据结构对标互联网大厂工程实战、中间件底层、海量数据处理、高并发缓存架构 。所有结构均为Redis、JDK、微服务、大数据框架底层核心,100%覆盖大厂面试压轴考点,配套可直接默写的Java源码、工程场景、优缺点、面试标准答案,零基础可直接背诵手撕。

本章全覆盖考点清单:LRU缓存、LFU缓存、布隆过滤器、BitMap位图、一致性哈希、跳表、基数树、树状数组、线段树,全部补齐底层原理+手写代码+实战场景。


8.1 LRU 缓存(最近最少使用·面试必手撕·Redis核心)

核心原理:LRU(Least Recently Used)最近最少淘汰策略。

核心思想:缓存容量满时,淘汰最久未被访问的数据,保留近期高频访问数据,完美适配热点数据缓存场景。

工程应用:Redis内存淘汰策略、JDK缓存、本地缓存、浏览器页面缓存、操作系统页面置换。

8.1.1 实现方案对比

  • HashMap+双向链表:纯手写底层,面试满分标准答案,自主控制读写、淘汰逻辑

  • LinkedHashMap:JDK封装实现,极简工程写法,实际项目首选

  • 线程安全版本:ConcurrentHashMap+双向链表,适配高并发场景

8.1.2 面试满分手撕(纯底层手写·无封装依赖)

java 复制代码
/**
 * LRU缓存 纯手写底层(面试手撕满分版)
 * 结构:哈希表+双向链表,查询O(1)、插入O(1)、淘汰O(1)
 */
public class LRUCache {
    // 双向链表节点
    static class Node {
        int key;
        int value;
        Node prev;
        Node next;
        public Node(int key, int value) {
            this.key = key;
            this.value = value;
        }
    }

    private final int capacity;
    private final Map<Integer, Node> map;
    // 虚拟头尾节点,规避空指针判断
    private final Node head;
    private final Node tail;

    public LRUCache(int capacity) {
        this.capacity = capacity;
        this.map = new HashMap<>();
        head = new Node(-1, -1);
        tail = new Node(-1, -1);
        head.next = tail;
        tail.prev = head;
    }

    // 查询数据:存在则移至头部(最新访问)
    public int get(int key) {
        if (!map.containsKey(key)) {
            return -1;
        }
        Node node = map.get(key);
        // 移除原位置,插入头部
        removeNode(node);
        addHeadNode(node);
        return node.value;
    }

    // 写入数据:存在则更新+置顶,不存在则新增,满容量淘汰尾部
    public void put(int key, int value) {
        // 已存在:更新值并置顶
        if (map.containsKey(key)) {
            Node node = map.get(key);
            node.value = value;
            removeNode(node);
            addHeadNode(node);
            return;
        }
        // 容量已满:淘汰最久未使用的尾部节点
        if (map.size() >= capacity) {
            Node delNode = tail.prev;
            removeNode(delNode);
            map.remove(delNode.key);
        }
        // 新增节点放入头部
        Node newNode = new Node(key, value);
        map.put(key, newNode);
        addHeadNode(newNode);
    }

    // 将节点插入头部(最新访问位置)
    private void addHeadNode(Node node) {
        node.next = head.next;
        head.next.prev = node;
        head.next = node;
        node.prev = head;
    }

    // 移除任意节点
    private void removeNode(Node node) {
        node.prev.next = node.next;
        node.next.prev = node.prev;
    }
}

8.1.3 JDK极简工程实现(项目实战首选)

java 复制代码
/**
 * 工程级LRU缓存(基于LinkedHashMap)
 * accessOrder=true:开启访问排序,实现LRU特性
 */
class LRUCacheSimple extends LinkedHashMap<Integer, Integer> {
    private final int capacity;

    public LRUCacheSimple(int capacity) {
        super(capacity, 0.75f, true);
        this.capacity = capacity;
    }

    // 重写淘汰规则:容量超过阈值自动删除最久未访问数据
    @Override
    protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
        return size() > capacity;
    }

    public int get(int key) {
        return super.getOrDefault(key, -1);
    }
}

8.1.4 面试高频问答

  • 为什么用双向链表?:单向链表无法O(1)删除中间节点,双向链表可通过哈希表直接定位节点,实现快速删除、置顶

  • 为什么存储key?:淘汰尾部节点时,需要通过key删除HashMap中的映射关系,避免内存泄漏

  • 时间复杂度?:读写、删除、淘汰均为O(1)

  • 缺点?:无法区分访问频次,高频冷门数据、低频热点数据会被错误淘汰


8.2 LFU 缓存(最少使用频次·进阶缓存)

核心原理:LFU(Least Frequently Used)最少频次淘汰策略。

核心:缓存满时,淘汰访问次数最少的数据;次数相同时,淘汰最久未访问数据,解决LRU低频热点数据淘汰问题,缓存命中率更高。

工程应用:Redis进阶缓存策略、分布式缓存、热点数据常驻缓存。

8.2.1 核心数据结构设计

  • HashMap<K, Node>:存储key与节点映射,O(1)查询

  • HashMap<Integer, 双向链表>:存储「频次-节点链表」映射,相同频次节点归为一组

  • 最小频次变量:全局记录当前最小访问频次,快速定位淘汰节点

8.2.2 Java 完整手撕代码(面试进阶满分)

java 复制代码
import java.util.*;

/**
 * LFU缓存 完整手撕版
 * 核心:按访问频次淘汰,频次最小优先淘汰,同频次淘汰最久未访问
 */
public class LFUCache {
    // 缓存节点:存储键值、访问频次
    static class Node {
        int key, val, freq;
        Node prev, next;
        public Node(int key, int val) {
            this.key = key;
            this.val = val;
            this.freq = 1; // 初始频次为1
        }
    }

    // 频次链表:存储同一频次的所有节点
    static class FreqList {
        Node head, tail;
        public FreqList() {
            head = new Node(-1, -1);
            tail = new Node(-1, -1);
            head.next = tail;
            tail.prev = head;
        }

        // 头部添加节点(最新访问)
        public void addHead(Node node) {
            node.next = head.next;
            head.next.prev = node;
            head.next = node;
            node.prev = head;
        }

        // 删除任意节点
        public void remove(Node node) {
            node.prev.next = node.next;
            node.next.prev = node.prev;
        }

        // 判断链表是否为空
        public boolean isEmpty() {
            return head.next == tail;
        }
    }

    private final int capacity;
    private final Map<Integer, Node> keyMap;
    private final Map<Integer, FreqList> freqMap;
    private int minFreq;

    public LFUCache(int capacity) {
        this.capacity = capacity;
        this.keyMap = new HashMap<>();
        this.freqMap = new HashMap<>();
        this.minFreq = 0;
    }

    public int get(int key) {
        if (!keyMap.containsKey(key)) return -1;
        Node node = keyMap.get(key);
        // 频次更新
        updateFreq(node);
        return node.val;
    }

    public void put(int key, int value) {
        if (capacity == 0) return;
        // 已存在:更新值+更新频次
        if (keyMap.containsKey(key)) {
            Node node = keyMap.get(key);
            node.val = value;
            updateFreq(node);
            return;
        }
        // 容量满:淘汰最小频次最久节点
        if (keyMap.size() >= capacity) {
            FreqList minFreqList = freqMap.get(minFreq);
            Node delNode = minFreqList.tail.prev;
            minFreqList.remove(delNode);
            keyMap.remove(delNode.key);
            // 链表空则删除频次分组
            if (minFreqList.isEmpty()) {
                freqMap.remove(minFreq);
            }
        }
        // 新增节点
        Node newNode = new Node(key, value);
        keyMap.put(key, newNode);
        freqMap.computeIfAbsent(1, k -> new FreqList()).addHead(newNode);
        minFreq = 1;
    }

    // 更新节点频次
    private void updateFreq(Node node) {
        int oldFreq = node.freq;
        // 从旧频次链表移除
        FreqList oldList = freqMap.get(oldFreq);
        oldList.remove(node);
        // 最小频次链表为空,更新最小频次
        if (oldList.isEmpty() && oldFreq == minFreq) {
            minFreq++;
            freqMap.remove(oldFreq);
        }
        // 频次+1,加入新链表
        node.freq++;
        freqMap.computeIfAbsent(node.freq, k -> new FreqList()).addHead(node);
    }
}

8.2.3 LRU与LFU核心对比(面试必背)

  • LRU:侧重访问时间,实现简单,存在缓存污染(低频热点长期未访问被淘汰)

  • LFU:侧重访问频次,缓存命中率更高,可规避缓存污染,实现复杂,存在冷数据预热慢问题


8.3 布隆过滤器(海量去重·大厂必考)

核心定位 :海量数据去重、存在性判断的终极解决方案,极小内存占用,可判断「数据一定不存在」或「数据可能存在」,广泛用于Redis穿透拦截、爬虫去重、海量ID判重。

核心特性不存在一定准确,存在可能误判,不支持删除数据。

8.3.1 底层原理

  1. 基于二进制位图 ,初始化全0;2. 一个数据经过多个不同哈希函数计算出多个下标;3. 将对应下标位置的二进制位设为1;4. 判断存在性:所有下标均为1则可能存在,任意一位为0则一定不存在。

8.3.2 Java 手写极简实现

java 复制代码
import java.util.BitSet;

/**
 * 布隆过滤器 手写实现
 * 核心:多哈希函数+位图,海量数据去重
 */
public class BloomFilter {
    // 位图容量
    private static final int CAPACITY = 1000000;
    // 多个哈希种子(不同哈希函数)
    private static final int[] SEEDS = {3, 7, 11, 13, 17, 19};
    private final BitSet bitSet;
    private final SimpleHash[] hashFuncs;

    public BloomFilter() {
        bitSet = new BitSet(CAPACITY);
        hashFuncs = new SimpleHash[SEEDS.length];
        // 初始化多个哈希函数
        for (int i = 0; i < SEEDS.length; i++) {
            hashFuncs[i] = new SimpleHash(CAPACITY, SEEDS[i]);
        }
    }

    // 添加数据到位图
    public void add(String value) {
        for (SimpleHash func : hashFuncs) {
            int index = func.hash(value);
            bitSet.set(index);
        }
    }

    // 判断数据是否存在
    public boolean contains(String value) {
        for (SimpleHash func : hashFuncs) {
            int index = func.hash(value);
            // 任意位为0,一定不存在
            if (!bitSet.get(index)) {
                return false;
            }
        }
        // 全为1,可能存在(误判)
        return true;
    }

    // 自定义简易哈希函数
    static class SimpleHash {
        private final int cap;
        private final int seed;

        public SimpleHash(int cap, int seed) {
            this.cap = cap;
            this.seed = seed;
        }

        public int hash(String str) {
            int res = 0;
            for (char c : str.toCharArray()) {
                res = res * seed + c;
            }
            return (res & 0x7FFFFFFF) % cap;
        }
    }
}

8.3.3 面试核心考点(必背)

  • 误判原因:不同数据哈希下标重叠,导致空位置全部被占,误判不存在的数据为存在

  • 为什么不能删除?:多个数据共用二进制位,删除一个数据会影响其他数据的判定结果

  • 优化方案:增大位图容量、增加哈希函数数量、使用计数布隆过滤器(支持删除)

  • 工程场景:Redis缓存穿透拦截、爬虫URL去重、用户ID海量去重、黑名单过滤


8.4 BitMap 位图(海量数据压缩存储)

核心原理 :用1个二进制位 存储1个数据状态(0/1),相比数组存储整数,内存压缩32倍,专门解决海量整数数据判重、排序、统计问题。

核心优势:极致内存压缩、查询O(1)、排序O(n),适合数值范围固定的海量整数场景。

8.4.1 Java 手写实现(海量数据去重+排序)

java 复制代码
/**
 * BitMap 位图 完整实现
 * 功能:海量整数去重、状态标记、有序遍历
 */
public class BitMap {
    // 一个long占64位,存储64个数据状态
    private final long[] bits;
    private final int maxNum;

    public BitMap(int maxNum) {
        this.maxNum = maxNum;
        // 计算需要的long数组长度:向上取整
        this.bits = new long[(maxNum + 63) / 64];
    }

    // 将数字num对应的位设为1(标记存在)
    public void add(int num) {
        if (num < 0 || num > maxNum) return;
        int index = num / 64;
        int bit = num % 64;
        bits[index] |= (1L << bit);
    }

    // 判断数字是否存在
    public boolean contains(int num) {
        if (num < 0 || num > maxNum) return false;
        int index = num / 64;
        int bit = num % 64;
        return (bits[index] & (1L << bit)) != 0;
    }

    // 删除数字标记
    public void remove(int num) {
        if (num < 0 || num > maxNum) return;
        int index = num / 64;
        int bit = num % 64;
        bits[index] &= ~(1L << bit);
    }

    // 遍历所有存在的数字(天然有序)
    public void foreach() {
        for (int i = 0; i <= maxNum; i++) {
            if (contains(i)) {
                System.out.print(i + " ");
            }
        }
    }
}

8.4.2 适用场景与优缺点

  • 场景:海量手机号排序、用户ID去重、签到状态统计、黑名单白名单

  • 优点:内存占用极低、查询插入删除均O(1)、天然有序

  • 缺点:仅适配非负整数、数值跨度极大时内存浪费严重


8.5 一致性哈希(分布式核心·面试高频)

核心痛点解决 :传统哈希取模扩容时,几乎所有数据映射关系失效,大量缓存雪崩 ;一致性哈希实现扩容/缩容仅少量数据迁移,是分布式缓存、负载均衡底层核心。

8.5.1 核心原理

  1. 构建 0~2^32 的哈希环

  2. 将服务节点IP哈希映射到环上;

  3. 数据key哈希映射到环上,顺时针匹配最近的服务节点;

  4. 新增/删除节点时,仅影响相邻区间数据,全局数据无需迁移。

8.5.2 虚拟节点优化(解决数据倾斜)

真实节点过少会导致数据分布不均,引入虚拟节点,一个真实节点对应多个虚拟节点,打散数据分布,保证负载均衡。

8.5.3 Java 手写一致性哈希(含虚拟节点)

java 复制代码
import java.util.SortedMap;
import java.util.TreeMap;

/**
 * 一致性哈希算法(含虚拟节点·解决数据倾斜)
 * 分布式缓存负载均衡核心
 */
public class ConsistentHash {
    // 虚拟节点数量
    private static final int VIRTUAL_NODE_NUM = 160;
    // 哈希环:key-哈希值,value-真实节点
    private final SortedMap<Long, String> hashRing;

    public ConsistentHash() {
        hashRing = new TreeMap<>();
    }

    // 添加真实节点及虚拟节点
    public void addNode(String node) {
        for (int i = 0; i < VIRTUAL_NODE_NUM; i++) {
            long hash = hash(node + "#" + i);
            hashRing.put(hash, node);
        }
    }

    // 删除真实节点及所有虚拟节点
    public void removeNode(String node) {
        for (int i = 0; i < VIRTUAL_NODE_NUM; i++) {
            long hash = hash(node + "#" + i);
            hashRing.remove(hash);
        }
    }

    // 根据key匹配目标节点
    public String getNode(String key) {
        if (hashRing.isEmpty()) return null;
        long hash = hash(key);
        // 顺时针找第一个大于等于当前哈希值的节点
        SortedMap<Long, String> subMap = hashRing.tailMap(hash);
        long targetHash = subMap.isEmpty() ? hashRing.firstKey() : subMap.firstKey();
        return hashRing.get(targetHash);
    }

    // 自定义哈希函数
    private long hash(String str) {
        long res = 0;
        for (char c : str.toCharArray()) {
            res = res * 31 + c;
        }
        return res &amp; 0xFFFFFFFFL;
    }
}

8.5.4 面试必背总结

一致性哈希通过哈希环映射实现分布式数据分片,相比传统取模哈希,扩容缩容仅迁移少量数据,有效避免缓存雪崩;通过虚拟节点机制解决节点少导致的数据倾斜问题,广泛应用于Redis集群、Nginx负载均衡、分布式存储系统。


8.6 跳表(SkipList·Redis底层核心)

核心定位 :有序链表的优化结构,平衡树的替代品,通过多层索引实现链表二分查找,查询/插入/删除平均O(logn),空间换时间,是Redis ZSet底层核心。

8.6.1 核心原理

  1. 底层是有序单链表;

  2. 基于随机概率向上建立多层索引;

  3. 高层索引快速缩小查找范围,底层链表精准定位;

  4. 无需像红黑树频繁平衡,实现简单、读写性能稳定。

8.6.2 跳表 vs 红黑树(面试高频对比)

  • 跳表:实现简单、无旋转平衡开销、范围查询更优、随机概率稳定,适合海量有序数据

  • 红黑树:严格平衡、最坏性能稳定、单点查询更优,适合JDK有序集合


8.7 基数树(RadixTree·前缀树进阶)

核心定位:压缩版前缀树,合并相同前缀的连续节点,减少前缀树冗余节点,内存占用更低,是IP路由匹配、字符串前缀检索、URL路由的底层核心。

核心优势:相比普通Trie树,极致压缩内存,超长前缀字符串检索效率极高。


8.8 树状数组 & 线段树(区间操作终极结构)

8.8.1 树状数组(BIT)

核心定位 :轻量化区间统计结构,代码极简、常数极小,支持单点更新、区间查询,时间复杂度O(logn),适配简单区间求和、最值问题。

适用场景:前缀和查询、单点修改、逆序对统计、频次统计。

java 复制代码
/**
 * 树状数组 标准版(单点更新+区间查询)
 */
public class BIT {
    private final int[] tree;
    private final int n;

    public BIT(int size) {
        this.n = size;
        tree = new int[n + 1];
    }

    // 单点更新:下标idx增加val
    public void update(int idx, int val) {
        for (; idx <= n; idx += idx & -idx) {
            tree[idx] += val;
        }
    }

    // 查询[1,idx]前缀和
    public int query(int idx) {
        int res = 0;
        for (; idx > 0; idx -= idx & -idx) {
            res += tree[idx];
        }
        return res;
    }

    // 查询[l,r]区间和
    public int rangeQuery(int l, int r) {
        return query(r) - query(l - 1);
    }
}

8.8.2 线段树(SegmentTree)

核心定位 :全能区间操作结构,支持区间更新、区间查询、懒标记延迟更新,解决树状数组无法处理的复杂区间修改问题,覆盖所有区间算法场景。

适用场景:区间批量加减、区间最值、区间求和、区间染色、离线区间问题。

java 复制代码
/**
 * 线段树 懒标记版(区间更新+区间查询)
 */
public class SegmentTree {
    private final int[] tree;
    private final int[] lazy;
    private final int n;

    public SegmentTree(int[] nums) {
        this.n = nums.length;
        tree = new int[4 * n];
        lazy = new int[4 * n];
        build(0, 0, n - 1, nums);
    }

    // 构建线段树
    private void build(int node, int l, int r, int[] nums) {
        if (l == r) {
            tree[node] = nums[l];
            return;
        }
        int mid = l + (r - l) / 2;
        build(2*node+1, l, mid, nums);
        build(2*node+2, mid+1, r, nums);
        tree[node] = tree[2*node+1] + tree[2*node+2];
    }

    // 懒标记下传
    private void pushDown(int node, int l, int r) {
        if (lazy[node] == 0) return;
        int mid = l + (r - l) / 2;
        // 更新左子树
        tree[2*node+1] += lazy[node] * (mid - l + 1);
        lazy[2*node+1] += lazy[node];
        // 更新右子树
        tree[2*node+2] += lazy[node] * (r - mid);
        lazy[2*node+2] += lazy[node];
        // 清空当前懒标记
        lazy[node] = 0;
    }

    // 区间更新:[ul,ur]全部加val
    public void rangeUpdate(int ul, int ur, int val) {
        rangeUpdate(0, 0, n-1, ul, ur, val);
    }

    private void rangeUpdate(int node, int l, int r, int ul, int ur, int val) {
        if (ur < l || ul > r) return;
        if (ul <= l && r <= ur) {
            tree[node] += val * (r - l + 1);
            lazy[node] += val;
            return;
        }
        pushDown(node, l, r);
        int mid = l + (r - l) / 2;
        rangeUpdate(2*node+1, l, mid, ul, ur, val);
        rangeUpdate(2*node+2, mid+1, r, ul, ur, val);
        tree[node] = tree[2*node+1] + tree[2*node+2];
    }

    // 区间查询和
    public int rangeQuery(int ql, int qr) {
        return rangeQuery(0, 0, n-1, ql, qr);
    }

    private int rangeQuery(int node, int l, int r, int ql, int qr) {
        if (qr < l || ql > r) return 0;
        if (ql <= l && r <= qr) return tree[node];
        pushDown(node, l, r);
        int mid = l + (r - l) / 2;
        return rangeQuery(2*node+1, l, mid, ql, qr) + rangeQuery(2*node+2, mid+1, r, ql, qr);
    }
}

8.8.3 二者选型口诀(必背)

  • 仅单点更新、区间查询 → 树状数组(代码短、速度快)

  • 区间批量更新、复杂区间操作 → 线段树(功能全覆盖)


8.9 高级工程结构终极选型与面试总结

本章所有高级数据结构均为Java后端、中间件、分布式系统核心底层:缓存场景优先LRU/LFU实现热点数据缓存;海量数据去重、防穿透使用布隆过滤器;海量整数压缩存储选用BitMap;分布式分片负载依赖一致性哈希;有序海量数据检索采用跳表;前缀路由、字符串压缩使用基数树;区间统计、批量修改场景选用树状数组与线段树。所有结构均规避了基础数据结构的性能瓶颈,是大厂工程开发与面试压轴的核心考点。

第九章 全套 Java 企业实战可直接默写模板(无遗漏·面试+工程双适配)

模板核心说明 :所有代码均为企业项目落地版+面试满分手撕版,无语法糖、无冗余依赖、边界全覆盖、可直接编译运行,适配LeetCode刷题、大厂手撕面试、后端工程开发,零基础直接背诵复用。

统一编码规范:空指针判空、边界校验、变量语义化、注释极简、杜绝超时/越界,符合阿里Java开发手册。


9.1 链表全套模板(企业高频·增删改查+进阶操作)

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; }
}

/**
 * 链表全套企业实战模板
 * 包含:反转、判环、找环入口、找中点、倒数第k节点、合并两个有序链表、去重
 */
public class ListNodeTemplate {

    // 1. 迭代反转链表(工程最优·O(1)空间)
    public ListNode reverseList(ListNode head) {
        ListNode pre = null;
        ListNode cur = head;
        while (cur != null) {
            ListNode temp = cur.next;
            cur.next = pre;
            pre = cur;
            cur = temp;
        }
        return pre;
    }

    // 2. 递归反转链表(面试手撕)
    public ListNode reverseListRecur(ListNode head) {
        if (head == null || head.next == null) {
            return head;
        }
        ListNode newHead = reverseListRecur(head.next);
        head.next.next = head;
        head.next = null;
        return newHead;
    }

    // 3. 链表判环(快慢指针·企业必考)
    public boolean hasCycle(ListNode head) {
        ListNode slow = head, fast = head;
        while (fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;
            if (slow == fast) return true;
        }
        return false;
    }

    // 4. 寻找环入口节点
    public ListNode detectCycle(ListNode head) {
        ListNode slow = head, fast = head;
        while (fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;
            if (slow == fast) {
                // 相遇后慢指针从头同速遍历
                slow = head;
                while (slow != fast) {
                    slow = slow.next;
                    fast = fast.next;
                }
                return slow;
            }
        }
        return null;
    }

    // 5. 寻找链表中点(偶数取中左)
    public ListNode findMiddle(ListNode head) {
        ListNode slow = head, fast = head;
        while (fast.next != null && fast.next.next != null) {
            slow = slow.next;
            fast = fast.next.next;
        }
        return slow;
    }

    // 6. 删除倒数第k个节点(一趟遍历·工程最优)
    public ListNode removeNthFromEnd(ListNode head, int n) {
        // 虚拟头节点,规避头节点删除特殊场景
        ListNode dummy = new ListNode(0, head);
        ListNode fast = dummy, slow = dummy;
        // 快指针先行n步
        for (int i = 0; i <= n; i++) {
            fast = fast.next;
        }
        // 同速移动
        while (fast != null) {
            fast = fast.next;
            slow = slow.next;
        }
        // 删除目标节点
        slow.next = slow.next.next;
        return dummy.next;
    }

    // 7. 合并两个有序链表(稳定·企业通用)
    public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
        ListNode dummy = new ListNode();
        ListNode cur = dummy;
        while (l1 != null && l2 != null) {
            if (l1.val <= l2.val) {
                cur.next = l1;
                l1 = l1.next;
            } else {
                cur.next = l2;
                l2 = l2.next;
            }
            cur = cur.next;
        }
        // 拼接剩余节点
        cur.next = l1 == null ? l2 : l1;
        return dummy.next;
    }

    // 8. 有序链表去重
    public ListNode deleteDuplicates(ListNode head) {
        ListNode cur = head;
        while (cur != null && cur.next != null) {
            if (cur.val == cur.next.val) {
                cur.next = cur.next.next;
            } else {
                cur = cur.next;
            }
        }
        return head;
    }
}

9.2 二叉树全套模板(工程遍历+递归/迭代全覆盖)

java 复制代码
// 标准二叉树节点定义
class TreeNode {
    int val;
    TreeNode left;
    TreeNode right;
    TreeNode() {}
    TreeNode(int val) { this.val = val; }
    TreeNode(int val, TreeNode left, TreeNode right) {
        this.val = val;
        this.left = left;
        this.right = right;
    }
}

import java.util.*;
/**
 * 二叉树企业实战全套模板
 * 前/中/后序(递归+迭代)、层序遍历、深度、对称、翻转、路径求和
 */
public class TreeNodeTemplate {

    // ====================== 递归遍历(简洁刷题版)======================
    // 前序遍历 根-左-右
    public List<Integer> preOrderRecur(TreeNode root) {
        List<Integer> res = new ArrayList<>();
        preDfs(root, res);
        return res;
    }
    private void preDfs(TreeNode node, List<Integer> res) {
        if (node == null) return;
        res.add(node.val);
        preDfs(node.left, res);
        preDfs(node.right, res);
    }

    // 中序遍历 左-根-右
    public List<Integer> inOrderRecur(TreeNode root) {
        List<Integer> res = new ArrayList<>();
        inDfs(root, res);
        return res;
    }
    private void inDfs(TreeNode node, List<Integer> res) {
        if (node == null) return;
        inDfs(node.left, res);
        res.add(node.val);
        inDfs(node.right, res);
    }

    // 后序遍历 左-右-根
    public List<Integer> postOrderRecur(TreeNode root) {
        List<Integer> res = new ArrayList<>();
        postDfs(root, res);
        return res;
    }
    private void postDfs(TreeNode node, List<Integer> res) {
        if (node == null) return;
        postDfs(node.left, res);
        postDfs(node.right, res);
        res.add(node.val);
    }

    // ====================== 迭代遍历(工程高效版·无递归栈溢出)======================
    // 前序迭代
    public List<Integer> preOrderIter(TreeNode root) {
        List<Integer> res = new ArrayList<>();
        if (root == null) return res;
        Deque<TreeNode> stack = new LinkedList<>();
        stack.push(root);
        while (!stack.isEmpty()) {
            TreeNode node = stack.pop();
            res.add(node.val);
            if (node.right != null) stack.push(node.right);
            if (node.left != null) stack.push(node.left);
        }
        return res;
    }

    // 中序迭代
    public List<Integer> inOrderIter(TreeNode root) {
        List<Integer> res = new ArrayList<>();
        if (root == null) return res;
        Deque<TreeNode> stack = new LinkedList<>();
        TreeNode cur = root;
        while (cur != null || !stack.isEmpty()) {
            while (cur != null) {
                stack.push(cur);
                cur = cur.left;
            }
            cur = stack.pop();
            res.add(cur.val);
            cur = cur.right;
        }
        return res;
    }

    // ====================== 层序遍历(BFS·工程最常用)======================
    public List<List<Integer>> levelOrder(TreeNode root) {
        List<List<Integer>> res = new ArrayList<>();
        if (root == null) return res;
        Queue<TreeNode> queue = new LinkedList<>();
        queue.offer(root);
        while (!queue.isEmpty()) {
            int size = queue.size();
            List<Integer> level = new ArrayList<>();
            for (int i = 0; i < size; i++) {
                TreeNode cur = queue.poll();
                level.add(cur.val);
                if (cur.left != null) queue.offer(cur.left);
                if (cur.right != null) queue.offer(cur.right);
            }
            res.add(level);
        }
        return res;
    }

    // ====================== 高频工程操作======================
    // 翻转二叉树
    public TreeNode invertTree(TreeNode root) {
        if (root == null) return null;
        TreeNode temp = root.left;
        root.left = invertTree(root.right);
        root.right = invertTree(temp);
        return root;
    }

    // 二叉树最大深度
    public int maxDepth(TreeNode root) {
        return root == null ? 0 : Math.max(maxDepth(root.left), maxDepth(root.right)) + 1;
    }

    // 判断对称二叉树
    public boolean isSymmetric(TreeNode root) {
        return check(root, root);
    }
    private boolean check(TreeNode l, TreeNode r) {
        if (l == null && r == null) return true;
        if (l == null || r == null || l.val != r.val) return false;
        return check(l.left, r.right) && check(l.right, r.left);
    }
}

9.3 优先队列 TopK 企业实战模板(大数据量筛选)

java 复制代码
import java.util.*;
/**
 * TopK 全套企业模板
 * 前K大、前K小、重复TopK、海量数据筛选
 */
public class TopKTemplate {

    // 1. 求数组前 K 大元素(小顶堆·O(nlogk)·工程最优)
    public List<Integer> topKLarge(int[] nums, int k) {
        PriorityQueue<Integer> minHeap = new PriorityQueue<>();
        for (int num : nums) {
            minHeap.offer(num);
            // 堆大小超过k,弹出最小值,保留最大k个
            if (minHeap.size() > k) {
                minHeap.poll();
            }
        }
        return new ArrayList<>(minHeap);
    }

    // 2. 求数组前 K 小元素(大顶堆)
    public List<Integer> topKSmall(int[] nums, int k) {
        PriorityQueue<Integer> maxHeap = new PriorityQueue<>((a, b) -> b - a);
        for (int num : nums) {
            maxHeap.offer(num);
            if (maxHeap.size() > k) {
                maxHeap.poll();
            }
        }
        return new ArrayList<>(maxHeap);
    }

    // 3. 数组第 K 大元素(面试手撕高频)
    public int findKthLargest(int[] nums, int k) {
        PriorityQueue<Integer> minHeap = new PriorityQueue<>();
        for (int num : nums) {
            minHeap.offer(num);
            if (minHeap.size() > k) minHeap.poll();
        }
        return minHeap.peek();
    }
}

9.4 并查集完整版(工程连通性判断·无向图必备)

java 复制代码
/**
 * 并查集 企业完整版(路径压缩+按秩合并·时间复杂度近似O(1))
 * 适用:连通分量、朋友圈、岛屿数量、网络连通性判断
 */
public class UnionFind {
    // 父节点数组
    private int[] parent;
    // 秩:记录树高度,用于按秩合并
    private int[] rank;
    // 连通分量数量
    private int count;

    // 初始化
    public UnionFind(int n) {
        count = n;
        parent = new int[n];
        rank = new int[n];
        for (int i = 0; i < n; i++) {
            parent[i] = i;
            rank[i] = 1;
        }
    }

    // 查找根节点+路径压缩
    public int find(int x) {
        if (parent[x] != x) {
            parent[x] = find(parent[x]);
        }
        return parent[x];
    }

    // 合并两个集合+按秩合并
    public void union(int x, int y) {
        int fx = find(x);
        int fy = find(y);
        if (fx == fy) return;

        // 小树合并到大树,平衡树高度
        if (rank[fx] < rank[fy]) {
            parent[fx] = fy;
        } else {
            parent[fy] = fx;
            if (rank[fx] == rank[fy]) {
                rank[fx]++;
            }
        }
        count--;
    }

    // 判断两个节点是否连通
    public boolean isConnected(int x, int y) {
        return find(x) == find(y);
    }

    // 获取连通分量总数
    public int getCount() {
        return count;
    }
}

9.5 KMP算法完整版(字符串匹配·工程高效去重)

java 复制代码
/**
 * KMP 企业完整版
 * 核心:无回溯匹配,时间O(n+m),解决暴力匹配超时问题
 * 适用:字符串匹配、敏感词过滤、文本检索
 */
public class KMPTemplate {

    // 构建next前缀数组
    public int[] getNext(String pattern) {
        int m = pattern.length();
        int[] next = new int[m];
        int j = 0;
        for (int i = 1; i < m; i++) {
            // 不匹配则回退
            while (j > 0 && pattern.charAt(i) != pattern.charAt(j)) {
                j = next[j - 1];
            }
            if (pattern.charAt(i) == pattern.charAt(j)) {
                j++;
            }
            next[i] = j;
        }
        return next;
    }

    // KMP匹配主方法
    public int kmpSearch(String text, String pattern) {
        if (pattern.length() == 0) return 0;
        int[] next = getNext(pattern);
        int n = text.length();
        int m = pattern.length();
        int j = 0;
        for (int i = 0; i < n; i++) {
            while (j > 0 && text.charAt(i) != pattern.charAt(j)) {
                j = next[j - 1];
            }
            if (text.charAt(i) == pattern.charAt(j)) {
                j++;
            }
            // 匹配完成,返回起始下标
            if (j == m) {
                return i - m + 1;
            }
        }
        return -1;
    }

    // 全局匹配所有出现位置(工程检索用)
    public List<Integer> kmpSearchAll(String text, String pattern) {
        List<Integer> res = new ArrayList<>();
        if (pattern.length() == 0) return res;
        int[] next = getNext(pattern);
        int n = text.length(), m = pattern.length();
        int j = 0;
        for (int i = 0; i < n; i++) {
            while (j > 0 && text.charAt(i) != pattern.charAt(j)) {
                j = next[j - 1];
            }
            if (text.charAt(i) == pattern.charAt(j)) j++;
            if (j == m) {
                res.add(i - m + 1);
                j = next[j - 1];
            }
        }
        return res;
    }
}

9.6 LRU/LFU 工程极简模板(项目直接复用)

java 复制代码
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * LRU缓存 工程极简版(SpringCache、本地缓存通用)
 * 线程不安全,高并发需加锁/使用ConcurrentLinkedHashMap
 */
class LRUCache extends LinkedHashMap<Integer, Integer> {
    private final int capacity;

    public LRUCache(int capacity) {
        super(capacity, 0.75f, true);
        this.capacity = capacity;
    }

    // 重写淘汰策略
    @Override
    protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
        return size() > capacity;
    }

    // 安全查询
    public int get(int key) {
        return super.getOrDefault(key, -1);
    }

    // 写入缓存
    public void put(int key, int value) {
        super.put(key, value);
    }
}

9.7 四大高频算法工程模板(单调栈、滑动窗口、DFS、BFS、Dijkstra)

java 复制代码
import java.util.*;
/**
 * 企业核心算法模板全覆盖(可直接手撕落地)
 */
public class CoreAlgorithmTemplate {

    // ====================== 1. 单调栈(NextGreater元素·温度、柱状图)======================
    public int[] nextGreaterElement(int[] nums) {
        int n = nums.length;
        int[] res = new int[n];
        Deque<Integer> stack = new LinkedList<>();
        for (int i = n - 1; i >= 0; i--) {
            // 弹出栈中比当前元素小的值
            while (!stack.isEmpty() && stack.peek() <= nums[i]) {
                stack.pop();
            }
            res[i] = stack.isEmpty() ? -1 : stack.peek();
            stack.push(nums[i]);
        }
        return res;
    }

    // ====================== 2. 不定长滑动窗口(最长无重复子串通用)======================
    public int longestSubstringWithoutDuplicate(String s) {
        Set<Character> set = new HashSet<>();
        int left = 0, maxLen = 0;
        for (int right = 0; right < s.length(); right++) {
            // 收缩左窗口
            while (set.contains(s.charAt(right))) {
                set.remove(s.charAt(left++));
            }
            set.add(s.charAt(right));
            maxLen = Math.max(maxLen, right - left + 1);
        }
        return maxLen;
    }

    // ====================== 3. DFS深度优先搜索(通用回溯模板)======================
    List<List<Integer>> res = new ArrayList<>();
    public List<List<Integer>> subsets(int[] nums) {
        backtrack(nums, 0, new ArrayList<>());
        return res;
    }
    private void backtrack(int[] nums, int start, List<Integer> path) {
        res.add(new ArrayList<>(path));
        for (int i = start; i < nums.length; i++) {
            path.add(nums[i]);
            backtrack(nums, i + 1, path);
            path.remove(path.size() - 1);
        }
    }

    // ====================== 4. BFS广度优先搜索(最短路径、层序遍历通用)======================
    public int bfsShortestPath(int[][] grid) {
        int m = grid.length, n = grid[0].length;
        boolean[][] visited = new boolean[m][n];
        Queue<int[]> queue = new LinkedList<>();
        // 起始节点入队
        queue.offer(new int[]{0, 0});
        visited[0][0] = true;
        int step = 0;
        // 上下左右四个方向
        int[][] dirs = {{-1,0},{1,0},{0,-1},{0,1}};

        while (!queue.isEmpty()) {
            int size = queue.size();
            for (int i = 0; i < size; i++) {
                int[] cur = queue.poll();
                int x = cur[0], y = cur[1];
                // 到达终点
                if (x == m - 1 && y == n - 1) return step;
                // 遍历四个方向
                for (int[] dir : dirs) {
                    int nx = x + dir[0], ny = y + dir[1];
                    if (nx >= 0 && nx < m && ny >= 0 && ny < n && !visited[nx][ny]) {
                        visited[nx][ny] = true;
                        queue.offer(new int[]{nx, ny});
                    }
                }
            }
            step++;
        }
        return -1;
    }

    // ====================== 5. Dijkstra最短路径(加权图·工程路由最优)======================
    // n:节点数,edges:边集合,start:起始节点
    public int[] dijkstra(int n, List<int[]> edges, int start) {
        // 构建邻接表
        List<List<int[]>> graph = new ArrayList<>();
        for (int i = 0; i < n; i++) graph.add(new ArrayList<>());
        for (int[] edge : edges) {
            int u = edge[0], v = edge[1], w = edge[2];
            graph.get(u).add(new int[]{v, w});
        }

        // 初始化距离数组
        int[] dist = new int[n];
        Arrays.fill(dist, Integer.MAX_VALUE);
        dist[start] = 0;
        // 小顶堆:{节点, 距离}
        PriorityQueue<int[]> heap = new PriorityQueue<>(Comparator.comparingInt(a -> a[1]));
        heap.offer(new int[]{start, 0});
        boolean[] visited = new boolean[n];

        while (!heap.isEmpty()) {
            int[] cur = heap.poll();
            int u = cur[0], d = cur[1];
            if (visited[u]) continue;
            visited[u] = true;
            // 遍历邻接节点
            for (int[] next : graph.get(u)) {
                int v = next[0], w = next[1];
                if (dist[v] > d + w) {
                    dist[v] = d + w;
                    heap.offer(new int[]{v, dist[v]});
                }
            }
        }
        return dist;
    }
}

9.8 工程高频工具模板(面试冷门但项目必备)

java 复制代码
import java.util.*;
/**
 * 企业开发高频工具模板
 * 快速排序、二分答案、前缀和、数组去重、字符串反转、幂运算
 */
public class EngineeringUtilTemplate {

    // 1. 三数取中快排(工程优化版·杜绝有序数组退化)
    public void quickSort(int[] nums, int left, int right) {
        if (left >= right) return;
        // 三数取中选基准
        int mid = left + (right - left) / 2;
        int pivot = getPivot(nums, left, mid, right);
        // 分区
        int l = left, r = right;
        while (l < r) {
            while (l < r && nums[r] >= pivot) r--;
            nums[l] = nums[r];
            while (l < r && nums[l] <= pivot) l++;
            nums[r] = nums[l];
        }
        nums[l] = pivot;
        quickSort(nums, left, l - 1);
        quickSort(nums, l + 1, right);
    }
    private int getPivot(int[] nums, int a, int b, int c) {
        int x = nums[a], y = nums[b], z = nums[c];
        if ((x > y) ^ (x > z)) return x;
        if ((y > x) ^ (y > z)) return y;
        return z;
    }

    // 2. 二分答案通用模板
    public int binaryAnswer(int left, int right) {
        int ans = 0;
        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (isValid(mid)) {
                ans = mid;
                right = mid - 1;
            } else {
                left = mid + 1;
            }
        }
        return ans;
    }
    private boolean isValid(int val) {
        // 自定义业务校验逻辑
        return true;
    }

    // 3. 一维前缀和工具
    public int[] buildPreSum(int[] nums) {
        int n = nums.length;
        int[] pre = new int[n + 1];
        for (int i = 1; i <= n; i++) {
            pre[i] = pre[i - 1] + nums[i - 1];
        }
        return pre;
    }
    public int getRangeSum(int[] pre, int l, int r) {
        return pre[r + 1] - pre[l];
    }

    // 4. 数组去重(有序/无序通用)
    public List<Integer> arrayDistinct(int[] nums) {
        return new ArrayList<>(new LinkedHashSet<>(Arrays.stream(nums).boxed().toList()));
    }

    // 5. 快速幂(防止溢出·取模运算)
    public long quickPow(long a, long b, long mod) {
        long res = 1;
        a %= mod;
        while (b > 0) {
            if ((b & 1) == 1) {
                res = (res * a) % mod;
            }
            a = (a * a) % mod;
            b >>= 1;
        }
        return res;
    }
}

第十章 学习路线(Java 专属)

第一阶段:基础必考(入门)

数组、链表、栈队列、二分、基础排序、二叉树、哈希表

第二阶段:面试高频(进阶)

堆 TopK、单调栈、BFS/DFS、并查集、KMP、快排、HashMap 底层

第三阶段:大厂进阶(压轴)

树状数组、线段树、Tarjan、最短路、AC自动机、Manacher、LRU、布隆过滤器、可持久化结构

最终校验:本文档无任何知识点遗漏

覆盖:全部线性结构、全部树形结构、全部哈希、全部图论、全部字符串算法、全部排序、全部工程高级结构、全部Java源码底层

相关推荐
JAVA面经实录9172 小时前
操作系统面试题
java·服务器·数据库·计算机网络·面试
一杯奶茶¥2 小时前
基于springboot的失物招领管理系统带万字文档 校园失物招领管理系统 失物认领管理系统java springboot vue
java·vue.js·spring boot·java项目
不能只会打代码2 小时前
边缘视频分析平台的架构设计与性能优化——从750ms到190ms的调优之路
java·spring boot·redis·性能优化·边缘计算·物联网竞赛
小刘|2 小时前
Spring AI Alibaba 集成和风天气 API 实战
java·服务器·前端
KANGBboy3 小时前
java知识五(继承)
java·开发语言
AI人工智能+电脑小能手3 小时前
【大白话说Java面试题 第117题】【并发篇】第17题:线程有几种状态,之间如何转换?
java·开发语言·面试
DIY源码阁3 小时前
JavaSwing饮品管理系统 - MySQL版
java·数据库·mysql·eclipse
开源Z3 小时前
LeetCode 42 · 接雨水:从暴力到双指针的三步优化
算法·leetcode
旖-旎3 小时前
《LeetCode 695 岛屿的最大面积 FloodFill DFS 解法》
c++·算法·力扣·深度优先遍历·floodfill