1. 高频Java算法题解析背景
在技术面试中,算法能力往往是区分候选人的重要标尺。根据对近三年国内一线互联网公司面试题的统计分析,约85%的Java开发岗位都会考察算法实现能力,而其中70%的题目都集中在约100个高频考点上。这个系列文章将系统梳理这些必考算法题,本文是该系列的第三部分,重点解析链表、树结构和动态规划等高频题型。
2. 链表类高频题目
2.1 反转链表(LeetCode 206)
反转单链表是面试中最基础的链表操作题,要求在不使用额外空间的情况下完成。核心思路是通过三个指针(prev、current、next)逐步调整节点指向:
java复制public ListNode reverseList(ListNode head) {
ListNode prev = null;
ListNode curr = head;
while (curr != null) {
ListNode next = curr.next;
curr.next = prev;
prev = curr;
curr = next;
}
return prev;
}
注意:在修改next指针前必须保存原next节点,否则会导致链表断裂。实际面试中,约30%的候选人会在这个细节上出错。
2.2 环形链表检测(LeetCode 141)
使用快慢指针(Floyd判圈算法)可以在O(1)空间复杂度内完成检测:
java复制public boolean hasCycle(ListNode head) {
if (head == null) return false;
ListNode slow = head;
ListNode fast = head.next;
while (slow != fast) {
if (fast == null || fast.next == null) return false;
slow = slow.next;
fast = fast.next.next;
}
return true;
}
常见误区包括:
- 初始条件处理不当(空链表或单节点链表)
- 快指针移动速度设置错误(必须每次移动两步)
- 终止条件判断顺序错误(应先判断fast是否为null)
3. 二叉树相关算法
3.1 二叉树层序遍历(LeetCode 102)
使用队列实现BFS是标准解法,需要注意每层节点的分离:
java复制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> level = new ArrayList<>();
for (int i = 0; i < levelSize; i++) {
TreeNode node = queue.poll();
level.add(node.val);
if (node.left != null) queue.offer(node.left);
if (node.right != null) queue.offer(node.right);
}
res.add(level);
}
return res;
}
关键点:
- 在开始处理每层前记录当前队列大小
- 子节点入队前必须进行非空判断
- 时间复杂度O(n),空间复杂度O(n)
3.2 二叉树最大深度(LeetCode 104)
递归解法最简洁,但面试官可能要求解释调用栈空间:
java复制public int maxDepth(TreeNode root) {
if (root == null) return 0;
return Math.max(maxDepth(root.left), maxDepth(root.right)) + 1;
}
迭代解法使用层序遍历计数:
java复制public int maxDepth(TreeNode root) {
if (root == null) return 0;
int depth = 0;
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
int size = queue.size();
depth++;
for (int i = 0; i < size; i++) {
TreeNode node = queue.poll();
if (node.left != null) queue.offer(node.left);
if (node.right != null) queue.offer(node.right);
}
}
return depth;
}
4. 动态规划专题
4.1 爬楼梯问题(LeetCode 70)
经典入门DP问题,展示状态转移方程的建立过程:
java复制public int climbStairs(int n) {
if (n <= 2) return n;
int[] dp = new int[n+1];
dp[1] = 1;
dp[2] = 2;
for (int i = 3; i <= n; i++) {
dp[i] = dp[i-1] + dp[i-2];
}
return dp[n];
}
空间优化版(O(1)空间):
java复制public int climbStairs(int n) {
if (n <= 2) return n;
int a = 1, b = 2;
for (int i = 3; i <= n; i++) {
int c = a + b;
a = b;
b = c;
}
return b;
}
4.2 最长递增子序列(LeetCode 300)
展示二维DP问题的解法优化:
基础DP解法(O(n^2)):
java复制public int lengthOfLIS(int[] nums) {
int[] dp = new int[nums.length];
Arrays.fill(dp, 1);
int max = 1;
for (int i = 1; i < nums.length; i++) {
for (int j = 0; j < i; j++) {
if (nums[i] > nums[j]) {
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
max = Math.max(max, dp[i]);
}
return max;
}
优化解法(O(nlogn))使用二分查找维护递增序列:
java复制public int lengthOfLIS(int[] nums) {
int[] tails = new int[nums.length];
int size = 0;
for (int num : nums) {
int i = 0, j = size;
while (i != j) {
int m = (i + j) / 2;
if (tails[m] < num) {
i = m + 1;
} else {
j = m;
}
}
tails[i] = num;
if (i == size) size++;
}
return size;
}
5. 高频问题实战技巧
5.1 白板编码注意事项
- 先明确问题边界条件和特殊输入
- 用具体例子演示算法流程
- 变量命名要有实际意义
- 先写注释说明思路再写代码
- 完成后必须进行测试用例验证
5.2 时间复杂度分析模板
以二分查找为例:
text复制初始化:O(1)
循环次数:每次范围减半,最多log2(n)次
单次操作:O(1)
总复杂度:O(logn)
空间复杂度:O(1)
5.3 常见优化策略
- 空间换时间:使用哈希表替代线性查找
- 双指针技巧:处理有序数组或链表
- 滑动窗口:子串/子数组问题
- 前缀和:快速计算区间和
- 单调栈:解决Next Greater Element类问题
6. 面试实战案例解析
6.1 LRU缓存实现(LeetCode 146)
结合哈希表和双向链表的经典设计题:
java复制class LRUCache {
class DLinkedNode {
int key;
int value;
DLinkedNode prev;
DLinkedNode next;
}
private void addNode(DLinkedNode node) {
node.prev = head;
node.next = head.next;
head.next.prev = node;
head.next = node;
}
private void removeNode(DLinkedNode node) {
DLinkedNode prev = node.prev;
DLinkedNode next = node.next;
prev.next = next;
next.prev = prev;
}
private void moveToHead(DLinkedNode node) {
removeNode(node);
addNode(node);
}
private DLinkedNode popTail() {
DLinkedNode res = tail.prev;
removeNode(res);
return res;
}
private Map<Integer, DLinkedNode> cache = new HashMap<>();
private int size;
private int 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();
newNode.key = key;
newNode.value = value;
cache.put(key, newNode);
addNode(newNode);
++size;
if (size > capacity) {
DLinkedNode tail = popTail();
cache.remove(tail.key);
--size;
}
} else {
node.value = value;
moveToHead(node);
}
}
}
6.2 合并K个有序链表(LeetCode 23)
使用优先队列的优化解法:
java复制public ListNode mergeKLists(ListNode[] lists) {
if (lists == null || lists.length == 0) return null;
PriorityQueue<ListNode> queue = new PriorityQueue<>(lists.length, (a, b) -> a.val - b.val);
ListNode dummy = new ListNode(0);
ListNode tail = dummy;
for (ListNode node : lists) {
if (node != null) queue.add(node);
}
while (!queue.isEmpty()) {
tail.next = queue.poll();
tail = tail.next;
if (tail.next != null) {
queue.add(tail.next);
}
}
return dummy.next;
}
时间复杂度分析:
- 建堆:O(k)
- 每次取出/插入:O(logk)
- 总节点数:N
- 总复杂度:O(Nlogk)
7. 算法学习路线建议
-
基础阶段(2周):
- 掌握数组、链表、栈、队列等基础数据结构
- 理解递归和迭代的实现转换
- 熟悉常见排序算法
-
提高阶段(3周):
- 深入二叉树和图算法
- 掌握回溯和DFS/BFS应用
- 理解动态规划基本思想
-
冲刺阶段(2周):
- 专项突破高频难题
- 模拟面试环境练习
- 总结个人易错点
推荐练习节奏:
- 每日3道新题+2道复习题
- 周末进行专题强化
- 建立错题本记录解题思路