1. LeetCode热题100深度解析(三)
作为程序员面试的"金标准",LeetCode题目往往能真实反映一个开发者的算法思维和编码能力。今天我将分享自己在刷LeetCode热题100过程中的解题思路和优化心得,这些题目覆盖了二叉树、二分查找、动态规划等高频考点。不同于简单的题解罗列,我会重点剖析每道题目的思考过程、常见误区以及性能优化技巧。
2. 二叉树与递归问题实战
2.1 有序数组转二叉搜索树(#108)
这道题要求我们将升序数组转换为高度平衡的二叉搜索树。初次看到题目时,我陷入了如何保证"高度平衡"的困惑中。经过分析发现:
二叉搜索树的中序遍历就是升序序列,因此数组的中间元素自然就是树的根节点
这个性质是解题的关键。通过递归地将数组分为左右两部分,可以构建出平衡的BST:
java复制public TreeNode sortedArrayToBST(int[] nums) {
return buildNode(nums, 0, nums.length - 1);
}
private TreeNode buildNode(int[] nums, int left, int right) {
if (left > right) return null;
int mid = left + (right - left) / 2; // 防止整数溢出
TreeNode node = new TreeNode(nums[mid]);
node.left = buildNode(nums, left, mid - 1);
node.right = buildNode(nums, mid + 1, right);
return node;
}
注意事项:
- 计算mid时使用
left + (right - left)/2而非(left + right)/2,避免整数溢出 - 递归终止条件是left > right而非left == right,因为当区间长度为1时仍需处理
- 时间复杂度O(n),每个节点只被访问一次
3. 二分查找的巧妙应用
3.1 搜索插入位置(#35)
这道基础二分查找题要求我们在有序数组中找到目标值的插入位置。我的第一版实现虽然能通过测试,但存在几个问题:
java复制// 初始冗长实现
public int searchInsert(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while (left < right) {
int mid = (left + right) / 2;
if (nums[mid] < target) {
left = mid;
} else if (nums[mid] > target) {
right = mid;
} else {
return mid;
}
if (left + 1 == right) return right;
}
return left;
}
优化后的版本更加简洁高效:
java复制public int searchInsert(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) return mid;
else if (nums[mid] < target) left = mid + 1;
else right = mid - 1;
}
return left; // 关键点:最终left就是插入位置
}
经验总结:
- 二分查找的循环条件应该是
left <= right而非left < right - 每次移动指针时应该跳过mid位置(mid±1)
- 当循环结束时,left指针自然指向应该插入的位置
- 时间复杂度O(log n),空间复杂度O(1)
4. 栈的经典应用场景
4.1 有效的括号(#20)
判断括号匹配是栈结构的经典应用。我的初始实现忽略了栈可能提前为空的情况:
java复制public boolean isValid(String s) {
Stack<Character> stack = new Stack<>();
for (char c : s.toCharArray()) {
if (c == '(' || c == '[' || c == '{') {
stack.push(c);
} else {
if (stack.isEmpty()) return false; // 关键检查
char top = stack.pop();
if ((c == ')' && top != '(') ||
(c == ']' && top != '[') ||
(c == '}' && top != '{')) {
return false;
}
}
}
return stack.isEmpty();
}
避坑指南:
- 遇到右括号时必须检查栈是否为空
- 最后需要检查栈是否完全清空
- 可以使用HashMap存储括号对,使代码更简洁
- 时间复杂度O(n),空间复杂度O(n)
5. 动态规划实战技巧
5.1 买卖股票的最佳时机(#121)
这道题要求计算股票的最大利润。暴力解法O(n²)会超时,优化思路是:
如果在历史最低点买入,那么今天的利润就是当前价格减去历史最低价
java复制public int maxProfit(int[] prices) {
int minPrice = Integer.MAX_VALUE;
int maxProfit = 0;
for (int price : prices) {
if (price < minPrice) {
minPrice = price;
} else if (price - minPrice > maxProfit) {
maxProfit = price - minPrice;
}
}
return maxProfit;
}
优化要点:
- 维护一个历史最低价变量
- 每次遇到更低价格时更新最低价
- 否则计算当前利润并更新最大值
- 时间复杂度O(n),空间复杂度O(1)
5.2 爬楼梯问题(#70)
这道题看似简单,实则考察对动态规划的理解。通过分析发现:
到达第n阶的方法数 = 到达第n-1阶的方法数 + 到达第n-2阶的方法数
这实际上就是斐波那契数列:
java复制public int climbStairs(int n) {
if (n <= 2) return n;
int a = 1, b = 2;
for (int i = 3; i <= n; i++) {
int sum = a + b;
a = b;
b = sum;
}
return b;
}
进阶思考:
- 可以扩展到每次能爬1、2或3阶的情况
- 使用矩阵快速幂可以将时间复杂度优化到O(log n)
- 注意避免递归实现的重复计算问题
6. 数组与数学技巧
6.1 杨辉三角(#118)
生成杨辉三角的关键是发现其数学规律:
每一行的首尾元素为1,其余元素等于上一行相邻两元素之和
java复制public List<List<Integer>> generate(int numRows) {
List<List<Integer>> triangle = new ArrayList<>();
for (int i = 0; i < numRows; i++) {
List<Integer> row = new ArrayList<>();
for (int j = 0; j <= i; j++) {
if (j == 0 || j == i) {
row.add(1);
} else {
row.add(triangle.get(i-1).get(j-1) + triangle.get(i-1).get(j));
}
}
triangle.add(row);
}
return triangle;
}
性能优化:
- 预分配ArrayList大小避免频繁扩容
- 可以使用数学公式计算组合数C(n,k)
- 时间复杂度O(n²),空间复杂度O(1)(不考虑输出)
6.2 只出现一次的数字(#136)
这道题要求在线性时间和常数空间内找出唯一出现的数字。常规的哈希表解法不符合空间要求:
java复制// 哈希表解法
public int singleNumber(int[] nums) {
Map<Integer, Integer> map = new HashMap<>();
for (int num : nums) {
map.put(num, map.getOrDefault(num, 0) + 1);
}
for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
if (entry.getValue() == 1) return entry.getKey();
}
return -1;
}
最优解是利用异或运算的性质:
java复制public int singleNumber(int[] nums) {
int result = 0;
for (int num : nums) result ^= num;
return result;
}
位运算技巧:
- 任何数和0异或都是它本身
- 任何数和自身异或都是0
- 异或满足交换律和结合律
- 时间复杂度O(n),空间复杂度O(1)
7. 高级算法思想应用
7.1 多数元素(#169)
找出出现次数超过⌊n/2⌋的元素,可以使用哈希表统计:
java复制public int majorityElement(int[] nums) {
Map<Integer, Integer> counts = new HashMap<>();
for (int num : nums) {
int count = counts.getOrDefault(num, 0) + 1;
if (count > nums.length / 2) return num;
counts.put(num, count);
}
return -1;
}
更巧妙的Boyer-Moore投票算法:
java复制public int majorityElement(int[] nums) {
int count = 0;
Integer candidate = null;
for (int num : nums) {
if (count == 0) candidate = num;
count += (num == candidate) ? 1 : -1;
}
return candidate;
}
算法分析:
- 维护一个候选人和计数器
- 遇到相同元素计数器+1,不同则-1
- 计数器归零时更换候选人
- 时间复杂度O(n),空间复杂度O(1)
7.2 最大子数组和(#53)
经典的动态规划问题,Kadane算法提供了优雅的解决方案:
java复制public int maxSubArray(int[] nums) {
int maxEndingHere = nums[0];
int maxSoFar = nums[0];
for (int i = 1; i < nums.length; i++) {
maxEndingHere = Math.max(nums[i], maxEndingHere + nums[i]);
maxSoFar = Math.max(maxSoFar, maxEndingHere);
}
return maxSoFar;
}
DP思想解析:
- maxEndingHere表示以当前元素结尾的最大子数组和
- maxSoFar记录全局最大值
- 每个元素有两个选择:加入前一个子数组或重新开始
- 时间复杂度O(n),空间复杂度O(1)
7.3 合并区间(#56)
合并重叠区间的关键在于先排序再合并:
java复制public int[][] merge(int[][] intervals) {
Arrays.sort(intervals, (a, b) -> Integer.compare(a[0], b[0]));
LinkedList<int[]> merged = new LinkedList<>();
for (int[] interval : intervals) {
if (merged.isEmpty() || merged.getLast()[1] < interval[0]) {
merged.add(interval);
} else {
merged.getLast()[1] = Math.max(merged.getLast()[1], interval[1]);
}
}
return merged.toArray(new int[merged.size()][]);
}
实现细节:
- 按区间起点排序
- 使用链表便于获取最后一个区间
- 比较当前区间与最后一个合并区间的终点
- 时间复杂度O(n log n)(排序耗时),空间复杂度O(n)
8. 刷题经验与面试建议
在实际面试中,仅仅给出正确答案是不够的。面试官更看重:
- 沟通能力:明确需求,确认边界条件
- 解题思路:从暴力解法开始,逐步优化
- 代码质量:变量命名、边界处理、异常情况
- 测试意识:主动设计测试用例验证代码
对于准备面试的开发者,我的建议是:
- 按题型分类练习(如数组、字符串、树、图等)
- 总结常见算法模式(双指针、滑动窗口、DFS/BFS等)
- 定期复习错题和难题
- 参加在线编程竞赛锻炼实战能力
刷题不是目的,而是培养算法思维的手段。理解每个问题背后的设计思想,比单纯记住解法更重要。