前言
本文为全网最完整 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 -
区间不匹配:循环条件必须和区间定义对应,闭区间用≤、开区间用<
-
边界收缩错误:已排除的区间必须±1,否则死循环
-
无序数组二分 :二分仅适用于单调有序结构,无序数组直接失效
-
二分答案漏存结果:必须用变量记录合法答案,不能直接返回left/right
七、面试标准答题话术(直接背诵)
二分算法基于单调有序特性,通过不断收缩有效查找区间,将O(n)线性查找优化为O(logn)对数级查找。主流分为三种区间模板,适配精准查找与边界查找场景;二分答案模板通过猜答案+合法性校验的思路,解决传统遍历无法高效求解的最值类问题。Java实现需规避整数溢出问题,严格匹配区间定义与循环条件,避免死循环与边界错误。
1.3.3 前缀和与差分(数组区间运算核心·Java刷题必通)
核心定位 :前缀和、差分是数组区间运算的一对万能互补工具 。专门解决两类高频痛点:大量区间查询求和 、大量区间批量修改,将暴力O(n)单次操作优化为O(1),是数组、矩阵刷题的基础核心,所有区间类题目优先想到该模型。
核心口诀 :查和用前缀,改区间用差分。
一、一维前缀和(区间求和神器)
1. 原理定义
前缀和数组 preSum,preSum[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 底层核心结构
-
高频有序数据场景:海量有序数据查询、范围遍历、排名统计,替代平衡树简化实现
-
刷题场景:有序数据高效查找、区间最值、排名问题
一、跳表核心原理(逐层查找逻辑)
-
跳表由多层单向有序链表组成,最底层为存储全量数据的原始链表,上层均为索引链表;
-
查询时从最高层索引开始遍历,找到当前层最后一个小于目标值的节点,下沉至下一层继续查找;
-
逐层缩小查询范围,直至下沉到最底层原始链表,精准定位目标节点;
-
插入/删除节点时,通过随机算法生成节点层高,同步更新对应层级索引,维持跳表结构完整性。
二、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. 红黑树五大铁律(必背满分)
-
- 每个节点只能是红色 或黑色
-
- 根节点必须是黑色
-
- 所有**叶子节点(NIL空节点)**均为黑色
-
- 红色节点的两个子节点必须是黑色(红节点不能相邻)
-
- 任意节点到其所有叶子节点的路径,黑色节点数量相同(黑高一致)
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. 构建规则(贪心)
-
初始化:将所有权重节点作为独立叶子节点;
-
每次选取权重最小的两个节点,构建新父节点,父节点权重为两节点权重和;
-
将新节点加入节点集合,重复操作直至仅剩一个节点,即为哈夫曼树根。
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. 极简核心原理
-
对原数组数值离散化(压缩值域,适配大范围数值);
-
构建权值线段树,统计每个数值出现次数;
-
每次更新新建节点,保留历史版本树根;
-
通过两个版本树根差值,计算区间数值分布,二分查找第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() 重写规则(必考)
核心契约:
-
两个对象equals()相等,hashCode()必须相等
-
两个对象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²) % cap、newIdx = (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 面试满分答题模板(实战版)
哈希冲突主流有四种解决方案:
-
链地址法,通过链表/红黑树挂载冲突节点,无哈希堆积、增删高效,是Java HashMap采用的工业方案,适配高并发、大数据量场景;
-
线性探测法,冲突后顺序向后探测空位,实现简单但易产生哈希堆积、查询效率衰减;
-
二次探测法,通过二次函数双向偏移探测,优化线性探测的堆积问题,但无法完全杜绝聚集;
-
双重哈希法,依托双哈希函数动态计算下标,冲突率最低、散列效果最优,缺点是实现复杂度较高。笔试重点考察四种方案的原理对比,面试重点考察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 核心优化思路
- 字符串预处理:所有字符间插入特殊占位符(如#),统一奇偶回文场景;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 底层原理
- 基于二进制位图 ,初始化全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 核心原理
-
构建 0~2^32 的哈希环;
-
将服务节点IP哈希映射到环上;
-
数据key哈希映射到环上,顺时针匹配最近的服务节点;
-
新增/删除节点时,仅影响相邻区间数据,全局数据无需迁移。
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 & 0xFFFFFFFFL;
}
}
8.5.4 面试必背总结
一致性哈希通过哈希环映射实现分布式数据分片,相比传统取模哈希,扩容缩容仅迁移少量数据,有效避免缓存雪崩;通过虚拟节点机制解决节点少导致的数据倾斜问题,广泛应用于Redis集群、Nginx负载均衡、分布式存储系统。
8.6 跳表(SkipList·Redis底层核心)
核心定位 :有序链表的优化结构,平衡树的替代品,通过多层索引实现链表二分查找,查询/插入/删除平均O(logn),空间换时间,是Redis ZSet底层核心。
8.6.1 核心原理
-
底层是有序单链表;
-
基于随机概率向上建立多层索引;
-
高层索引快速缩小查找范围,底层链表精准定位;
-
无需像红黑树频繁平衡,实现简单、读写性能稳定。
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源码底层