1. LeetCode HOT 100与Java刷题指南
刷算法题是每个程序员成长的必经之路,而LeetCode HOT 100则是这条路上的黄金标准。作为过来人,我深知初学者面对这些题目时的困惑——不是看不懂题目,就是写不出代码,好不容易写出来了又超时。这套题解就是为解决这些问题而生。
为什么选择Java?因为它在企业级开发中应用广泛,语法严谨又不像C++那么复杂,特别适合用来表达算法思想。我当年就是用Java刷了3遍HOT 100,最终拿下了多个大厂offer。下面这些核心解题方法,都是我在实战中验证过的:
- 双指针法:像玩夹娃娃机一样,用两个指针一前一后解决问题。比如"盛最多水的容器"这道题,用暴力法需要O(n²)时间,而双指针只需O(n)
- 动态规划:把大问题拆成小问题,像搭积木一样逐步构建解决方案。"最长递增子序列"就是典型例子
- 回溯算法:像走迷宫时留下面包屑,遇到死路就回退。"全排列"问题用回溯法写出来特别优雅
提示:刷题时建议准备错题本,记录每道题的思考过程和易错点。我当初就是用这个方法,第二次刷题时间缩短了60%
2. 高频算法题型精讲
2.1 双指针的妙用
双指针就像两个人的默契配合。在"三数之和"这道题中,我们先用排序把数组整理好(时间复杂度O(nlogn)),然后用一个固定指针+两个活动指针来寻找解:
Arrays.sort(nums); // 先排序是关键 for(int i=0; i<nums.length-2; i++){ if(i>0 && nums[i]==nums[i-1]) continue; // 去重 int left=i+1, right=nums.length-1; while(left<right){ int sum = nums[i]+nums[left]+nums[right]; if(sum==0){ res.add(Arrays.asList(nums[i],nums[left],nums[right])); while(left<right && nums[left]==nums[left+1]) left++; // 跳过重复 while(left<right && nums[right]==nums[right-1]) right--; left++; right--; } else if(sum<0) left++; else right--; } }这个解法把O(n³)的时间复杂度降到了O(n²),体现了算法优化的精髓。我建议初学者先用暴力法写出解法,再逐步优化,这样能更好理解算法演进的过程。
2.2 动态规划套路
动态规划(DP)是面试中的常客,也是很多人觉得最难的部分。其实掌握套路后,DP题反而最容易拿分。以"最长回文子串"为例:
- 定义状态:dp[i][j]表示s[i...j]是否为回文
- 状态转移:
- 当j-i<=1时,单个字符肯定是回文
- 当s[i]==s[j]时,看dp[i+1][j-1]是否为true
- 边界条件:对角线上的单个字符都是回文
boolean[][] dp = new boolean[n][n]; int maxLen = 1, start=0; for(int j=0; j<n; j++){ for(int i=0; i<=j; i++){ if(s.charAt(i)==s.charAt(j)){ if(j-i<=1) dp[i][j]=true; else dp[i][j]=dp[i+1][j-1]; } if(dp[i][j] && j-i+1>maxLen){ maxLen = j-i+1; start = i; } } } return s.substring(start, start+maxLen);这个解法时间复杂度O(n²),空间复杂度O(n²)。我在面试中被问过多次这道题,每次都能稳稳拿下。
3. 数据结构实战应用
3.1 哈希表的灵活运用
哈希表是算法题中的瑞士军刀。"两数之和"这道经典题目展示了它的威力:
public int[] twoSum(int[] nums, int target) { Map<Integer, Integer> map = new HashMap<>(); for(int i=0; i<nums.length; i++){ int complement = target - nums[i]; if(map.containsKey(complement)){ return new int[]{map.get(complement), i}; } map.put(nums[i], i); } throw new IllegalArgumentException("No solution"); }这个解法只需遍历一次数组,时间复杂度O(n)。我建议初学者要熟练掌握HashMap的常用方法:containsKey、get、put等。在"字母异位词分组"中,我们还可以用排序后的字符串作为key:
Map<String, List<String>> map = new HashMap<>(); for(String s : strs){ char[] chars = s.toCharArray(); Arrays.sort(chars); String key = new String(chars); if(!map.containsKey(key)) map.put(key, new ArrayList<>()); map.get(key).add(s); } return new ArrayList<>(map.values());3.2 链表操作技巧
链表问题考验指针操作的功底。"反转链表"是基础中的基础:
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; }更复杂的如"合并K个升序链表",可以用优先队列(最小堆)优化:
PriorityQueue<ListNode> heap = new PriorityQueue<>((a,b)->a.val-b.val); for(ListNode node : lists){ if(node!=null) heap.offer(node); } ListNode dummy = new ListNode(0); ListNode tail = dummy; while(!heap.isEmpty()){ ListNode min = heap.poll(); tail.next = min; tail = min; if(min.next != null) heap.offer(min.next); } return dummy.next;这个解法时间复杂度O(Nlogk),其中N是总节点数,k是链表个数。我在面试中遇到过这道题的变种,当时就是用这个方法解决的。
4. 二叉树与高级算法
4.1 二叉树遍历大全
二叉树是面试中的常客,必须掌握各种遍历方式。以"二叉树的层序遍历"为例:
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 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; }"二叉树的最近公共祖先"则展示了递归的魅力:
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) { if(root==null || root==p || root==q) return root; TreeNode left = lowestCommonAncestor(root.left, p, q); TreeNode right = lowestCommonAncestor(root.right, p, q); if(left!=null && right!=null) return root; return left!=null ? left : right; }4.2 滑动窗口与单调栈
"最小覆盖子串"展示了滑动窗口的典型应用:
public String minWindow(String s, String t) { Map<Character, Integer> need = new HashMap<>(); for(char c : t.toCharArray()) need.put(c, need.getOrDefault(c,0)+1); int left=0, valid=0, start=0, len=Integer.MAX_VALUE; Map<Character, Integer> window = new HashMap<>(); for(int right=0; right<s.length(); right++){ char c = s.charAt(right); if(need.containsKey(c)){ window.put(c, window.getOrDefault(c,0)+1); if(window.get(c).equals(need.get(c))) valid++; } while(valid == need.size()){ if(right-left+1 < len){ start = left; len = right-left+1; } char d = s.charAt(left++); if(need.containsKey(d)){ if(window.get(d).equals(need.get(d))) valid--; window.put(d, window.get(d)-1); } } } return len==Integer.MAX_VALUE ? "" : s.substring(start, start+len); }而"柱状图中最大的矩形"则需要单调栈:
public int largestRectangleArea(int[] heights) { Stack<Integer> stack = new Stack<>(); int maxArea = 0; for(int i=0; i<=heights.length; i++){ int h = (i==heights.length) ? 0 : heights[i]; while(!stack.isEmpty() && h < heights[stack.peek()]){ int height = heights[stack.pop()]; int width = stack.isEmpty() ? i : i - stack.peek() - 1; maxArea = Math.max(maxArea, height * width); } stack.push(i); } return maxArea; }这些高级算法看起来复杂,但只要掌握核心思想,就能举一反三。我建议每做完一道题,都要思考能否应用到其他类似场景。