Java面试85题图解版 · 特别篇:2026后端高频面试题复盘(算法底层逻辑+高并发架构设计全解析,附Java实战代码)
📝 摘要:基于2026春招/实习面经,精选10+道后端高频核心题,涵盖双指针、动态规划、回溯剪枝、滑动窗口、环形链表、LRU缓存、环形打家劫舍、SQL原地操作、B+树索引、KMP字符串匹配、HTTP演进与TCP调优。每道题附带完整可运行Java代码、面试官追问、进阶思考。末尾补充系统设计REACT推导法,助你打通技术与业务的底层逻辑。
📚 系列导航
- 上一篇 :面试官问:JDK、JRE、JVM有什么区别?
- 下一篇预告 :面试官问:基本类型和引用类型到底有什么不同?
- 全部85题目录 :点击查看
一、算法与大模型前沿篇
1. 链表相交节点检测
- 考察点:双指针技巧、空间复杂度优化
- 解题思路 :使用双指针,分别从两链表头出发,到达末尾后跳转至另一链表头部继续遍历。若相交必在交点相遇;若不相交最终均为
null。时间复杂度O(m+n),空间复杂度O(1)。 - 进阶思考:如果允许修改原链表结构,有没有更快的方法?如果链表存在环,这道题该怎么解?主动提及这些边界条件能极大提升你的工程素养。
java
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode p = headA, q = headB;
while (p != q) {
// 走到末尾则跳转到另一条链表的头部
p = (p == null) ? headB : p.next;
q = (q == null) ? headA : q.next;
}
return p; // 若不相交,最终 p 和 q 均为 null
}
💬 你在写双指针时,有没有遇到过因为空指针判断顺序而导致的bug?欢迎评论区分享。
2. 动态规划状态定义误区(乘积最大子数组)
- 考察点:DP状态定义准确性、负数场景应对
- 解题思路 :以"乘积最大子数组"为例,需同时维护以
i结尾的最大值和最小值,因为负数会让最小值瞬间变为最大值。错误的状态定义 :"前i个元素的最大乘积";正确的状态定义 :"以第i个元素结尾的子数组的最值"。 - 进阶思考:该模型可直接映射到金融风控中的"连续异常交易最大回撤与收益乘积"检测。
java
public int maxProduct(int[] nums) {
if (nums == null || nums.length == 0) return 0;
// 状态定义:以当前位置结尾的最大/最小乘积
int maxEndingHere = nums[0], minEndingHere = nums[0], res = nums[0];
for (int i = 1; i < nums.length; i++) {
int preMax = maxEndingHere, preMin = minEndingHere;
maxEndingHere = Math.max(nums[i], Math.max(preMax * nums[i], preMin * nums[i]));
minEndingHere = Math.min(nums[i], Math.min(preMax * nums[i], preMin * nums[i]));
res = Math.max(res, maxEndingHere);
}
return res;
}
3. 回溯法剪枝优化(组合问题)
- 考察点:搜索空间压缩、剪枝条件推导
- 解题思路 :在组合枚举中,当剩余可选元素不足以填满目标数量时,提前终止搜索。剪枝条件为
i <= n - (k - path.size()) + 1。 - 进阶思考:在大模型的束搜索(Beam Search)和推荐系统的候选集生成中,类似的剪枝策略被广泛应用于减少解码空间。
java
public List<List<Integer>> combine(int n, int k) {
List<List<Integer>> res = new ArrayList<>();
backtrack(1, new ArrayList<>(), n, k, res);
return res;
}
private void backtrack(int start, List<Integer> path, int n, int k, List<List<Integer>> res) {
if (path.size() == k) {
res.add(new ArrayList<>(path));
return;
}
for (int i = start; i <= n - (k - path.size()) + 1; i++) {
path.add(i);
backtrack(i + 1, path, n, k, res);
path.remove(path.size() - 1);
}
}
4. 滑动窗口求最大连续1的个数(含k次翻转)
- 考察点:窗口收缩条件控制
- 解题思路 :维护窗口内0的个数不超过
k,右指针不断扩展窗口,当0的个数超标时收缩左指针。利用前缀和思想统计0的个数,时间复杂度O(n)。 - 进阶思考:滑动窗口思想可无缝迁移到大模型RAG系统中的长文本分块处理,以及API限流的滑动日志窗口统计。
java
public int longestOnes(int[] nums, int k) {
int left = 0, maxLen = 0, zeroCount = 0;
for (int right = 0; right < nums.length; right++) {
if (nums[right] == 0) zeroCount++;
while (zeroCount > k) {
if (nums[left++] == 0) zeroCount--;
}
maxLen = Math.max(maxLen, right - left + 1);
}
return maxLen;
}
5. 环形链表检测与入环点定位
- 考察点:快慢指针、数学推导
- 解题思路:快指针每次走两步,慢指针走一步,相遇则存在环。定位入环点时,将快指针重置为头节点,两指针同步移动,再次相遇点即为入环点。
- 进阶思考:该算法在JVM内存泄漏检测(循环引用)和分布式系统死锁检测中有重要应用。
java
public ListNode detectCycle(ListNode head) {
if (head == null || head.next == null) return null;
ListNode slow = head, fast = head;
boolean hasCycle = false;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
if (slow == fast) { hasCycle = true; break; }
}
if (!hasCycle) return null;
ListNode ptr = head;
while (ptr != slow) {
ptr = ptr.next;
slow = slow.next;
}
return ptr;
}
💬 你在推导入环点公式时,有没有更直观的记忆方法?欢迎评论区交流。
二、系统设计与高并发实战篇
6. LRU缓存机制实现
- 考察点:哈希表与双向链表结合、O(1)操作
- 解题思路 :
HashMap存储key到节点的映射,双向链表维护访问顺序。get操作将节点移至头部,put操作在容量满时淘汰尾部节点。 - 面试官追问 :
- 多线程环境下如何保证线程安全? → 使用
ReentrantLock分段锁,或ConcurrentHashMap+ CAS操作。 - 分布式场景下如何解决缓存穿透、击穿、雪崩? → 布隆过滤器、互斥锁/逻辑过期、TTL加随机值与多级缓存兜底。
- 多线程环境下如何保证线程安全? → 使用
java
class LRUCache {
class DLinkedNode {
int key, value;
DLinkedNode prev, next;
DLinkedNode() {}
DLinkedNode(int key, int value) { this.key = key; this.value = value; }
}
private Map<Integer, DLinkedNode> cache = new HashMap<>();
private int size, capacity;
private DLinkedNode head, tail;
public LRUCache(int capacity) {
this.size = 0;
this.capacity = capacity;
head = new DLinkedNode();
tail = new DLinkedNode();
head.next = tail;
tail.prev = head;
}
public int get(int key) {
DLinkedNode node = cache.get(key);
if (node == null) return -1;
moveToHead(node);
return node.value;
}
public void put(int key, int value) {
DLinkedNode node = cache.get(key);
if (node == null) {
DLinkedNode newNode = new DLinkedNode(key, value);
cache.put(key, newNode);
addToHead(newNode);
size++;
if (size > capacity) {
DLinkedNode removed = removeTail();
cache.remove(removed.key);
size--;
}
} else {
node.value = value;
moveToHead(node);
}
}
private void addToHead(DLinkedNode node) {
node.prev = head;
node.next = head.next;
head.next.prev = node;
head.next = node;
}
private void removeNode(DLinkedNode node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}
private void moveToHead(DLinkedNode node) {
removeNode(node);
addToHead(node);
}
private DLinkedNode removeTail() {
DLinkedNode res = tail.prev;
removeNode(res);
return res;
}
}
7. 打家劫舍II(环形房屋)
- 考察点:动态规划变体、环形问题拆解
- 解题思路:将环形问题拆分为两个线性问题:不偷第一间或不偷最后一间,分别求解后取最大值,巧妙避免首尾冲突。
java
public int rob(int[] nums) {
int n = nums.length;
if (n == 1) return nums[0];
// 比较 [0, n-2] 与 [1, n-1] 两个区间
return Math.max(robRange(nums, 0, n - 2), robRange(nums, 1, n - 1));
}
private int robRange(int[] nums, int start, int end) {
int prev = 0, curr = 0;
for (int i = start; i <= end; i++) {
int temp = Math.max(curr, prev + nums[i]);
prev = curr;
curr = temp;
}
return curr;
}
三、数据库与存储篇
8. 原地操作优化(SQL技巧)
- 考察点:减少临时表、利用CASE WHEN与变量
- 解题思路 :使用
CASE WHEN实现值交换、使用用户变量实现行号及累加,避免显式创建临时表,降低I/O开销。 - 进阶思考:在数据迁移和宽表构建中,原地更新比临时表+重命名更节省空间和锁等待时间。
示例:交换性别字段值
UPDATE students SET sex = CASE WHEN sex = 'm' THEN 'f' ELSE 'm' END;
9. B+树索引底层与优化
- 考察点:B+树结构、聚集索引与覆盖索引
- 解题思路:B+树所有数据存于叶子节点,叶子通过双向链表连接。聚集索引的叶子存储整行数据,非聚集索引叶子存储主键值。覆盖索引可直接从索引返回数据,避免回表。
- 面试官追问:为什么B+树比B树更适合做数据库索引? → B+树范围查询效率更高,且非叶子节点不存数据使得树更矮、IO更少。
四、网络协议深水区
10. KMP字符串匹配算法
- 考察点:模式串预处理、主串指针不回退
- 解题思路 :预处理模式串生成
next数组(最长公共前后缀表),发生不匹配时根据next滑动模式串,时间复杂度O(m+n)。 - 进阶思考:KMP被广泛应用于海量日志关键字检索、敏感词过滤系统以及编辑器内的"查找"功能。
java
public int strStr(String haystack, String needle) {
if (needle.isEmpty()) return 0;
int[] next = getNext(needle);
int j = 0;
for (int i = 0; i < haystack.length(); i++) {
while (j > 0 && haystack.charAt(i) != needle.charAt(j)) {
j = next[j - 1];
}
if (haystack.charAt(i) == needle.charAt(j)) j++;
if (j == needle.length()) return i - j + 1;
}
return -1;
}
private int[] getNext(String s) {
int[] next = new int[s.length()];
int j = 0;
for (int i = 1; i < s.length(); i++) {
while (j > 0 && s.charAt(i) != s.charAt(j)) j = next[j - 1];
if (s.charAt(i) == s.charAt(j)) j++;
next[i] = j;
}
return next;
}
11. HTTP全版本演进与TCP调优
- 考察点:协议底层原理、内核级调优
- 面试官追问 :
- HTTP/3基于QUIC解决了HTTP/2的哪些痛点? → 解决了TCP层的队头阻塞问题,并在握手上实现了0-RTT。
- 线上服务器出现大量TIME_WAIT该如何排查与调优? → 使用
ss -tan state time-wait | wc -l查看数量;若为短连接过多导致,可调整内核参数net.ipv4.tcp_tw_reuse=1,并考虑连接池化。
五、高频后端系统设计概要(REACT推导)
为了让"高并发架构设计"名副其实,这里用 REACT 推导流程 拆解一道常见设计题:
题目:设计一个短链接系统
| 步骤 | 内容 |
|---|---|
| R (Requirement) | 支持长链转短链、短链跳转长链;短链永不过期;日均1亿+PV。 |
| E (Estimate) | 每月需存储约30亿条映射;单表过大需分库分表。 |
| A (Architecture) | 采用分布式ID生成(雪花算法)得到唯一ID,转为62进制作为短链码。 |
| C (Component) | Web服务层 → 短链服务 → 分布式缓存(Redis) → 分库分表(MySQL)。 |
| T (Tradeoff) | 使用302临时重定向以保留原始请求数据;热点短链做本地缓存。 |
通过这种结构化的推理过程,能向面试官展示扎实的系统设计功底。
总结与备考建议
| 领域 | 高频考点 | 推荐掌握程度 |
|---|---|---|
| 算法 | 双指针、动态规划、回溯剪枝 | 熟练手写代码,主动分析时空复杂度 |
| 系统设计 | LRU缓存、短链接/热搜架构 | 理解REACT推导流程,掌握高并发组件 |
| 数据库 | 原地操作、B+树索引原理 | 掌握核心SQL优化与底层存储机制 |
| 网络 | KMP、HTTP演进、TCP调优 | 深入理解协议细节与线上排障方案 |
核心建议 :面对技术问题,请养成使用 "是什么 → 为什么 → 怎么实现 → 有什么问题 → 怎么优化" 的逻辑闭环进行阐述。在刷题策略上,优先保障中等难度题目的正确率,重点突破动态规划状态定义、双指针技巧及LRU缓存的独立手写能力。
📌 Java实战可运行完整代码
java
import java.util.*;
public class InterviewMaster2026 {
// ---------- 链表节点定义 ----------
static class ListNode {
int val;
ListNode next;
ListNode(int x) { val = x; }
}
// ---------- 1. 链表相交节点检测 ----------
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode p = headA, q = headB;
while (p != q) {
p = (p == null) ? headB : p.next;
q = (q == null) ? headA : q.next;
}
return p;
}
// ---------- 2. 乘积最大子数组 ----------
public int maxProduct(int[] nums) {
if (nums == null || nums.length == 0) return 0;
int maxEndingHere = nums[0], minEndingHere = nums[0], res = nums[0];
for (int i = 1; i < nums.length; i++) {
int preMax = maxEndingHere, preMin = minEndingHere;
maxEndingHere = Math.max(nums[i], Math.max(preMax * nums[i], preMin * nums[i]));
minEndingHere = Math.min(nums[i], Math.min(preMax * nums[i], preMin * nums[i]));
res = Math.max(res, maxEndingHere);
}
return res;
}
// ---------- 3. 组合(回溯+剪枝) ----------
public List<List<Integer>> combine(int n, int k) {
List<List<Integer>> res = new ArrayList<>();
backtrack(1, new ArrayList<>(), n, k, res);
return res;
}
private void backtrack(int start, List<Integer> path, int n, int k, List<List<Integer>> res) {
if (path.size() == k) {
res.add(new ArrayList<>(path));
return;
}
for (int i = start; i <= n - (k - path.size()) + 1; i++) {
path.add(i);
backtrack(i + 1, path, n, k, res);
path.remove(path.size() - 1);
}
}
// ---------- 4. 滑动窗口:最大连续1(含k次翻转) ----------
public int longestOnes(int[] nums, int k) {
int left = 0, maxLen = 0, zeroCount = 0;
for (int right = 0; right < nums.length; right++) {
if (nums[right] == 0) zeroCount++;
while (zeroCount > k) {
if (nums[left++] == 0) zeroCount--;
}
maxLen = Math.max(maxLen, right - left + 1);
}
return maxLen;
}
// ---------- 5. 环形链表检测与入环点定位 ----------
public ListNode detectCycle(ListNode head) {
if (head == null || head.next == null) return null;
ListNode slow = head, fast = head;
boolean hasCycle = false;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
if (slow == fast) { hasCycle = true; break; }
}
if (!hasCycle) return null;
ListNode ptr = head;
while (ptr != slow) {
ptr = ptr.next;
slow = slow.next;
}
return ptr;
}
// ---------- 6. LRU 缓存实现 ----------
static class LRUCache {
class DLinkedNode {
int key, value;
DLinkedNode prev, next;
DLinkedNode() {}
DLinkedNode(int k, int v) { key = k; value = v; }
}
private Map<Integer, DLinkedNode> cache = new HashMap<>();
private int size, capacity;
private DLinkedNode head, tail;
public LRUCache(int capacity) {
this.size = 0;
this.capacity = capacity;
head = new DLinkedNode();
tail = new DLinkedNode();
head.next = tail;
tail.prev = head;
}
public int get(int key) {
DLinkedNode node = cache.get(key);
if (node == null) return -1;
moveToHead(node);
return node.value;
}
public void put(int key, int value) {
DLinkedNode node = cache.get(key);
if (node == null) {
DLinkedNode newNode = new DLinkedNode(key, value);
cache.put(key, newNode);
addToHead(newNode);
size++;
if (size > capacity) {
DLinkedNode removed = removeTail();
cache.remove(removed.key);
size--;
}
} else {
node.value = value;
moveToHead(node);
}
}
private void addToHead(DLinkedNode node) {
node.prev = head;
node.next = head.next;
head.next.prev = node;
head.next = node;
}
private void removeNode(DLinkedNode node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}
private void moveToHead(DLinkedNode node) {
removeNode(node);
addToHead(node);
}
private DLinkedNode removeTail() {
DLinkedNode res = tail.prev;
removeNode(res);
return res;
}
}
// ---------- 7. 打家劫舍II(环形房屋) ----------
public int rob(int[] nums) {
int n = nums.length;
if (n == 1) return nums[0];
return Math.max(robRange(nums, 0, n - 2), robRange(nums, 1, n - 1));
}
private int robRange(int[] nums, int start, int end) {
int prev = 0, curr = 0;
for (int i = start; i <= end; i++) {
int temp = Math.max(curr, prev + nums[i]);
prev = curr;
curr = temp;
}
return curr;
}
// ---------- 8. KMP 字符串匹配 ----------
public int strStr(String haystack, String needle) {
if (needle.isEmpty()) return 0;
int[] next = getNext(needle);
int j = 0;
for (int i = 0; i < haystack.length(); i++) {
while (j > 0 && haystack.charAt(i) != needle.charAt(j)) {
j = next[j - 1];
}
if (haystack.charAt(i) == needle.charAt(j)) j++;
if (j == needle.length()) return i - j + 1;
}
return -1;
}
private int[] getNext(String s) {
int[] next = new int[s.length()];
int j = 0;
for (int i = 1; i < s.length(); i++) {
while (j > 0 && s.charAt(i) != s.charAt(j)) j = next[j - 1];
if (s.charAt(i) == s.charAt(j)) j++;
next[i] = j;
}
return next;
}
// ---------- 测试主函数 ----------
public static void main(String[] args) {
InterviewMaster2026 app = new InterviewMaster2026();
// 1. 相交链表测试
ListNode a1 = new ListNode(1); ListNode a2 = new ListNode(2);
ListNode b1 = new ListNode(3); ListNode b2 = new ListNode(4);
ListNode c1 = new ListNode(5); ListNode c2 = new ListNode(6);
a1.next = a2; a2.next = c1; c1.next = c2;
b1.next = b2; b2.next = c1; // 在 c1 相交
ListNode intersect = app.getIntersectionNode(a1, b1);
System.out.println("相交节点值: " + (intersect != null ? intersect.val : "null"));
// 2. 乘积最大子数组
int[] nums = {2, 3, -2, 4};
System.out.println("最大乘积: " + app.maxProduct(nums)); // 6
// 3. 组合测试
List<List<Integer>> comb = app.combine(4, 2);
System.out.println("组合 C(4,2): " + comb);
// 4. 滑动窗口测试
int[] arr = {1,1,0,1,1,1};
System.out.println("最长连续1 (k=0): " + app.longestOnes(arr, 0)); // 3
int[] arr2 = {0,0,1,1,0,0,1,1,1,0,1,1,0,0,0,1,1,1,1};
System.out.println("最长连续1 (k=3): " + app.longestOnes(arr2, 3)); // 10
// 5. 环形链表测试
ListNode head = new ListNode(1);
ListNode node2 = new ListNode(2);
ListNode node3 = new ListNode(3);
head.next = node2; node2.next = node3; node3.next = node2; // 环入口为 node2
ListNode cycleNode = app.detectCycle(head);
System.out.println("入环点值: " + (cycleNode != null ? cycleNode.val : "null"));
// 6. LRU 缓存测试
LRUCache lru = new LRUCache(2);
lru.put(1, 1);
lru.put(2, 2);
System.out.println("LRU get(1): " + lru.get(1)); // 1
lru.put(3, 3); // 淘汰 key=2
System.out.println("LRU get(2): " + lru.get(2)); // -1
lru.put(4, 4); // 淘汰 key=1
System.out.println("LRU get(1): " + lru.get(1)); // -1
System.out.println("LRU get(3): " + lru.get(3)); // 3
System.out.println("LRU get(4): " + lru.get(4)); // 4
// 7. 打家劫舍II
int[] houses = {2, 3, 2};
System.out.println("打家劫舍II max: " + app.rob(houses)); // 3
int[] houses2 = {1, 2, 3, 1};
System.out.println("打家劫舍II max: " + app.rob(houses2)); // 4
// 8. KMP 测试
String str = "hello", pattern = "ll";
System.out.println("KMP 匹配位置: " + app.strStr(str, pattern)); // 2
String str2 = "aaaaa", pattern2 = "bba";
System.out.println("KMP 匹配位置: " + app.strStr(str2, pattern2)); // -1
}
}
运行说明:
- 将以上代码保存为
InterviewMaster2026.java - 编译:
javac InterviewMaster2026.java - 运行:
java InterviewMaster2026
输出预览:
相交节点值: 5
最大乘积: 6
组合 C(4,2): [[1, 2], [1, 3], [1, 4], [2, 3], [2, 4], [3, 4]]
最长连续1 (k=0): 3
最长连续1 (k=3): 10
入环点值: 2
LRU get(1): 1
LRU get(2): -1
LRU get(1): -1
LRU get(3): 3
LRU get(4): 4
打家劫舍II max: 3
打家劫舍II max: 4
KMP 匹配位置: 2
KMP 匹配位置: -1
📚 系列导航
- 上一篇 :面试官问:JDK、JRE、JVM有什么区别?
- 下一篇预告 :面试官问:基本类型和引用类型到底有什么不同?
- 全部85题目录 :点击查看