1. 高频Java算法题的价值与学习方法
作为一名有多年Java开发经验的工程师,我深知算法能力在实际工作中的重要性。特别是在面试场景中,算法题往往是考察候选人基本功的核心环节。HOT 100高频Java算法题系列整理了LeetCode等平台中最常出现的题目,这些题目覆盖了数据结构、算法思维和编码实现等多个维度。
为什么这些题目会被反复考察?因为它们代表了计算机科学中最基础、最经典的问题解决模式。掌握这些题目不仅能帮助你在面试中游刃有余,更能提升日常开发中的问题分析和解决能力。我建议的学习方法是:先独立思考和实现,再对比优秀解法,最后总结规律和模板。
2. 高频题目解析与实现思路
2.1 二叉树相关高频题
二叉树是算法题中的常客,下面我们来看几个典型题目:
题目:二叉树的最大深度(LeetCode 104)
这道题要求计算二叉树的最大深度,也就是从根节点到最远叶子节点的最长路径上的节点数。递归解法非常直观:
java复制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;
}
注意:递归解法虽然简洁,但在极端情况下(如树退化为链表)可能导致栈溢出。在实际工程中,对于深度很大的树,建议使用迭代的BFS方法。
题目:二叉树的层序遍历(LeetCode 102)
层序遍历是二叉树的基础算法,也是很多复杂问题的基础:
java复制public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> result = new ArrayList<>();
if (root == null) return result;
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
int levelSize = queue.size();
List<Integer> currentLevel = new ArrayList<>();
for (int i = 0; i < levelSize; i++) {
TreeNode node = queue.poll();
currentLevel.add(node.val);
if (node.left != null) queue.offer(node.left);
if (node.right != null) queue.offer(node.right);
}
result.add(currentLevel);
}
return result;
}
2.2 动态规划经典问题
动态规划是算法中的难点,但掌握后能解决很多复杂问题。
题目:爬楼梯(LeetCode 70)
这是最经典的DP入门题,问的是有n阶楼梯,每次可以爬1或2阶,有多少种不同的方法可以爬到楼顶。
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(n)降到O(1)。
题目:最长递增子序列(LeetCode 300)
这道题要求找出数组中最长的严格递增子序列的长度。
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;
}
3. 链表操作高频题
链表操作是算法面试中的另一个重点考察领域。
3.1 反转链表(LeetCode 206)
反转链表是最基础的链表操作题,但能考察对指针操作的掌握程度。
java复制public ListNode reverseList(ListNode head) {
ListNode prev = null;
ListNode curr = head;
while (curr != null) {
ListNode nextTemp = curr.next;
curr.next = prev;
prev = curr;
curr = nextTemp;
}
return prev;
}
3.2 合并两个有序链表(LeetCode 21)
这道题考察对链表遍历和指针操作的综合能力。
java复制public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
ListNode dummy = new ListNode(-1);
ListNode current = dummy;
while (l1 != null && l2 != null) {
if (l1.val <= l2.val) {
current.next = l1;
l1 = l1.next;
} else {
current.next = l2;
l2 = l2.next;
}
current = current.next;
}
current.next = l1 == null ? l2 : l1;
return dummy.next;
}
4. 字符串处理高频题
字符串处理在实际开发中非常常见,也是算法面试的重点。
4.1 最长无重复字符子串(LeetCode 3)
这道题要求找到字符串中不含有重复字符的最长子串的长度。
java复制public int lengthOfLongestSubstring(String s) {
Map<Character, Integer> map = new HashMap<>();
int max = 0;
int left = 0;
for (int right = 0; right < s.length(); right++) {
char c = s.charAt(right);
if (map.containsKey(c)) {
left = Math.max(left, map.get(c) + 1);
}
map.put(c, right);
max = Math.max(max, right - left + 1);
}
return max;
}
4.2 字符串的排列(LeetCode 567)
判断第二个字符串是否包含第一个字符串的排列。
java复制public boolean checkInclusion(String s1, String s2) {
int[] count = new int[26];
for (char c : s1.toCharArray()) {
count[c - 'a']++;
}
int left = 0;
int right = 0;
int remaining = s1.length();
while (right < s2.length()) {
if (count[s2.charAt(right) - 'a']-- > 0) {
remaining--;
}
right++;
if (remaining == 0) {
return true;
}
if (right - left == s1.length()) {
if (count[s2.charAt(left) - 'a']++ >= 0) {
remaining++;
}
left++;
}
}
return false;
}
5. 排序与搜索高频题
排序和搜索是算法的基础,也是面试中的常见考点。
5.1 快速排序实现
虽然Java有内置的排序方法,但理解快速排序的原理很重要。
java复制public void quickSort(int[] nums, int low, int high) {
if (low < high) {
int pivot = partition(nums, low, high);
quickSort(nums, low, pivot - 1);
quickSort(nums, pivot + 1, high);
}
}
private int partition(int[] nums, int low, int high) {
int pivot = nums[high];
int i = low;
for (int j = low; j < high; j++) {
if (nums[j] < pivot) {
swap(nums, i, j);
i++;
}
}
swap(nums, i, high);
return i;
}
private void swap(int[] nums, int i, int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
5.2 二分查找(LeetCode 704)
二分查找是高效的搜索算法,但实现时需要注意边界条件。
java复制public int search(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) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return -1;
}
6. 回溯算法高频题
回溯算法常用于解决组合、排列、子集等问题。
6.1 全排列(LeetCode 46)
这道题要求给定一个不含重复数字的数组,返回其所有可能的全排列。
java复制public List<List<Integer>> permute(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
backtrack(result, new ArrayList<>(), nums);
return result;
}
private void backtrack(List<List<Integer>> result, List<Integer> temp, int[] nums) {
if (temp.size() == nums.length) {
result.add(new ArrayList<>(temp));
} else {
for (int i = 0; i < nums.length; i++) {
if (temp.contains(nums[i])) continue;
temp.add(nums[i]);
backtrack(result, temp, nums);
temp.remove(temp.size() - 1);
}
}
}
6.2 子集(LeetCode 78)
这道题要求给定一组不含重复元素的整数数组,返回所有可能的子集。
java复制public List<List<Integer>> subsets(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
backtrack(result, new ArrayList<>(), nums, 0);
return result;
}
private void backtrack(List<List<Integer>> result, List<Integer> temp, int[] nums, int start) {
result.add(new ArrayList<>(temp));
for (int i = start; i < nums.length; i++) {
temp.add(nums[i]);
backtrack(result, temp, nums, i + 1);
temp.remove(temp.size() - 1);
}
}
7. 贪心算法高频题
贪心算法通常用于解决最优化问题,通过局部最优选择达到全局最优。
7.1 买卖股票的最佳时机II(LeetCode 122)
这道题允许无限次买卖,计算最大利润。
java复制public int maxProfit(int[] prices) {
int profit = 0;
for (int i = 1; i < prices.length; i++) {
if (prices[i] > prices[i - 1]) {
profit += prices[i] - prices[i - 1];
}
}
return profit;
}
7.2 跳跃游戏(LeetCode 55)
判断是否能够从数组的第一个位置跳到最后一个位置。
java复制public boolean canJump(int[] nums) {
int lastPos = nums.length - 1;
for (int i = nums.length - 2; i >= 0; i--) {
if (i + nums[i] >= lastPos) {
lastPos = i;
}
}
return lastPos == 0;
}
8. 位运算高频题
位运算题目通常考察对二进制操作的理解。
8.1 只出现一次的数字(LeetCode 136)
给定一个非空整数数组,除了某个元素只出现一次外,其余每个元素均出现两次。
java复制public int singleNumber(int[] nums) {
int result = 0;
for (int num : nums) {
result ^= num;
}
return result;
}
8.2 位1的个数(LeetCode 191)
编写一个函数,输入是一个无符号整数,返回其二进制表达式中数字位数为 '1' 的个数。
java复制public int hammingWeight(int n) {
int count = 0;
while (n != 0) {
count += n & 1;
n >>>= 1;
}
return count;
}
9. 数学相关高频题
数学类题目考察对数学概念和公式的理解与应用。
9.1 回文数(LeetCode 9)
判断一个整数是否是回文数。
java复制public boolean isPalindrome(int x) {
if (x < 0 || (x % 10 == 0 && x != 0)) {
return false;
}
int revertedNumber = 0;
while (x > revertedNumber) {
revertedNumber = revertedNumber * 10 + x % 10;
x /= 10;
}
return x == revertedNumber || x == revertedNumber / 10;
}
9.2 快乐数(LeetCode 202)
编写一个算法来判断一个数是不是"快乐数"。
java复制public boolean isHappy(int n) {
Set<Integer> seen = new HashSet<>();
while (n != 1 && !seen.contains(n)) {
seen.add(n);
n = getNext(n);
}
return n == 1;
}
private int getNext(int n) {
int sum = 0;
while (n > 0) {
int d = n % 10;
n = n / 10;
sum += d * d;
}
return sum;
}
10. 设计类高频题
设计类题目考察对系统设计的理解和实现能力。
10.1 LRU缓存机制(LeetCode 146)
设计和实现一个 LRU (最近最少使用) 缓存机制。
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);
}
}
}
10.2 最小栈(LeetCode 155)
设计一个支持 push,pop,top 操作,并能在常数时间内检索到最小元素的栈。
java复制class MinStack {
private Stack<Integer> stack;
private Stack<Integer> minStack;
public MinStack() {
stack = new Stack<>();
minStack = new Stack<>();
}
public void push(int val) {
stack.push(val);
if (minStack.isEmpty() || val <= minStack.peek()) {
minStack.push(val);
}
}
public void pop() {
if (stack.pop().equals(minStack.peek())) {
minStack.pop();
}
}
public int top() {
return stack.peek();
}
public int getMin() {
return minStack.peek();
}
}
11. 图论相关高频题
图论题目虽然相对较少,但也是面试中的难点。
11.1 岛屿数量(LeetCode 200)
给你一个由 '1'(陆地)和 '0'(水)组成的的二维网格,请你计算网格中岛屿的数量。
java复制public int numIslands(char[][] grid) {
if (grid == null || grid.length == 0) {
return 0;
}
int numIslands = 0;
for (int i = 0; i < grid.length; i++) {
for (int j = 0; j < grid[i].length; j++) {
if (grid[i][j] == '1') {
numIslands++;
dfs(grid, i, j);
}
}
}
return numIslands;
}
private void dfs(char[][] grid, int i, int j) {
if (i < 0 || i >= grid.length || j < 0 || j >= grid[i].length || grid[i][j] == '0') {
return;
}
grid[i][j] = '0';
dfs(grid, i + 1, j);
dfs(grid, i - 1, j);
dfs(grid, i, j + 1);
dfs(grid, i, j - 1);
}
11.2 课程表(LeetCode 207)
判断是否可能完成所有课程的学习,即判断有向图是否有环。
java复制public boolean canFinish(int numCourses, int[][] prerequisites) {
List<List<Integer>> adj = new ArrayList<>();
for (int i = 0; i < numCourses; i++) {
adj.add(new ArrayList<>());
}
int[] indegree = new int[numCourses];
for (int[] edge : prerequisites) {
adj.get(edge[1]).add(edge[0]);
indegree[edge[0]]++;
}
Queue<Integer> queue = new LinkedList<>();
for (int i = 0; i < numCourses; i++) {
if (indegree[i] == 0) {
queue.offer(i);
}
}
int count = 0;
while (!queue.isEmpty()) {
int u = queue.poll();
count++;
for (int v : adj.get(u)) {
if (--indegree[v] == 0) {
queue.offer(v);
}
}
}
return count == numCourses;
}
12. 堆/优先队列高频题
堆数据结构常用于解决Top K问题或需要高效获取最大/最小元素的场景。
12.1 数组中的第K个最大元素(LeetCode 215)
在未排序的数组中找到第 k 个最大的元素。
java复制public int findKthLargest(int[] nums, int k) {
PriorityQueue<Integer> heap = new PriorityQueue<>();
for (int num : nums) {
heap.add(num);
if (heap.size() > k) {
heap.poll();
}
}
return heap.peek();
}
12.2 合并K个升序链表(LeetCode 23)
合并 k 个排序链表,返回合并后的排序链表。
java复制public ListNode mergeKLists(ListNode[] lists) {
PriorityQueue<ListNode> queue = new PriorityQueue<>((a, b) -> a.val - b.val);
for (ListNode node : lists) {
if (node != null) {
queue.add(node);
}
}
ListNode dummy = new ListNode(0);
ListNode tail = dummy;
while (!queue.isEmpty()) {
tail.next = queue.poll();
tail = tail.next;
if (tail.next != null) {
queue.add(tail.next);
}
}
return dummy.next;
}
13. 滑动窗口高频题
滑动窗口技术常用于解决数组/字符串的子区间问题。
13.1 最小覆盖子串(LeetCode 76)
给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。
java复制public String minWindow(String s, String t) {
Map<Character, Integer> map = new HashMap<>();
for (char c : t.toCharArray()) {
map.put(c, map.getOrDefault(c, 0) + 1);
}
int left = 0;
int minLeft = 0;
int minLen = Integer.MAX_VALUE;
int count = t.length();
for (int right = 0; right < s.length(); right++) {
char c = s.charAt(right);
if (map.containsKey(c)) {
map.put(c, map.get(c) - 1);
if (map.get(c) >= 0) {
count--;
}
}
while (count == 0) {
if (right - left + 1 < minLen) {
minLen = right - left + 1;
minLeft = left;
}
char leftChar = s.charAt(left);
if (map.containsKey(leftChar)) {
map.put(leftChar, map.get(leftChar) + 1);
if (map.get(leftChar) > 0) {
count++;
}
}
left++;
}
}
return minLen == Integer.MAX_VALUE ? "" : s.substring(minLeft, minLeft + minLen);
}
13.2 找到字符串中所有字母异位词(LeetCode 438)
给定两个字符串 s 和 p,找到 s 中所有 p 的字母异位词的子串。
java复制public List<Integer> findAnagrams(String s, String p) {
List<Integer> result = new ArrayList<>();
if (s.length() < p.length()) return result;
int[] pCount = new int[26];
int[] sCount = new int[26];
for (char c : p.toCharArray()) {
pCount[c - 'a']++;
}
for (int i = 0; i < p.length(); i++) {
sCount[s.charAt(i) - 'a']++;
}
if (Arrays.equals(pCount, sCount)) {
result.add(0);
}
for (int i = p.length(); i < s.length(); i++) {
sCount[s.charAt(i - p.length()) - 'a']--;
sCount[s.charAt(i) - 'a']++;
if (Arrays.equals(pCount, sCount)) {
result.add(i - p.length() + 1);
}
}
return result;
}
14. 前缀和与哈希表高频题
前缀和技术常用于优化区间和的计算。
14.1 和为K的子数组(LeetCode 560)
给定一个整数数组和一个整数 k,你需要找到该数组中和为 k 的连续子数组的个数。
java复制public int subarraySum(int[] nums, int k) {
Map<Integer, Integer> map = new HashMap<>();
map.put(0, 1);
int sum = 0;
int count = 0;
for (int num : nums) {
sum += num;
if (map.containsKey(sum - k)) {
count += map.get(sum - k);
}
map.put(sum, map.getOrDefault(sum, 0) + 1);
}
return count;
}
14.2 连续的子数组和(LeetCode 523)
给定一个包含非负数的数组和一个目标整数 k,编写一个函数来判断该数组是否含有连续的子数组,其大小至少为 2,总和为 k 的倍数。
java复制public boolean checkSubarraySum(int[] nums, int k) {
Map<Integer, Integer> map = new HashMap<>();
map.put(0, -1);
int sum = 0;
for (int i = 0; i < nums.length; i++) {
sum += nums[i];
if (k != 0) {
sum %= k;
}
if (map.containsKey(sum)) {
if (i - map.get(sum) > 1) {
return true;
}
} else {
map.put(sum, i);
}
}
return false;
}
15. 其他高频算法题
15.1 盛最多水的容器(LeetCode 11)
给定 n 个非负整数 a1, a2, ..., an,每个数代表坐标中的一个点 (i, ai)。找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。
java复制public int maxArea(int[] height) {
int max = 0;
int left = 0;
int right = height.length - 1;
while (left < right) {
int currentArea = Math.min(height[left], height[right]) * (right - left);
max = Math.max(max, currentArea);
if (height[left] < height[right]) {
left++;
} else {
right--;
}
}
return max;
}
15.2 接雨水(LeetCode 42)
给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
java复制public int trap(int[] height) {
if (height == null || height.length == 0) return 0;
int left = 0;
int right = height.length - 1;
int leftMax = height[left];
int rightMax = height[right];
int result = 0;
while (left < right) {
if (leftMax < rightMax) {
left++;
leftMax = Math.max(leftMax, height[left]);
result += leftMax - height[left];
} else {
right--;
rightMax = Math.max(rightMax, height[right]);
result += rightMax - height[right];
}
}
return result;
}
16. 算法题解题技巧总结
在解决这些高频算法题的过程中,我总结了一些实用的技巧:
-
理解题目本质:很多题目看似不同,但本质上是同一类问题。例如"滑动窗口最大值"和"每日温度"都可以用单调栈解决。
-
画图辅助:对于复杂的指针操作或递归问题,画图能帮助理清思路。我在解决链表反转和二叉树遍历问题时,总是先画出示意图。
-
边界条件检查:算法题的大部分错误都来自边界条件处理不当。例如空输入、单个元素、全部相同元素等特殊情况。
-
测试用例设计:在实现前先设计几个测试用例,包括常规情况和边界情况,这能帮助发现潜在问题。
-
复杂度分析:养成分析时间复杂度和空间复杂度的习惯,这有助于选择最优解法。
-
模板化思维:很多算法有固定模板,如回溯、DFS、BFS等。掌握这些模板能快速解决类似问题。
-
优化意识:先实现一个可行解,再思考如何优化。例如从暴力解法到动态规划,从O(n^2)到O(nlogn)。
-
代码简洁性:在保证可读性的前提下,尽量使代码简洁。但不要为了简洁牺牲可读性。
17. 算法学习资源推荐
根据我的学习经验,以下资源对提升算法能力非常有帮助:
-
LeetCode:最全面的算法题库,建议按标签分类练习,从简单题开始循序渐进。
-
《算法导论》:经典教材,适合系统学习算法理论,但数学要求较高。
-
《剑指Offer》:针对面试准备的算法书,题目经典且贴近实际面试。
-
《算法图解》:入门级算法书,用通俗易懂的方式讲解复杂算法。
-
VisuAlgo:算法可视化网站,帮助理解算法的执行过程。
-
GeeksforGeeks:包含大量算法教程和实现,适合查阅特定算法。
-
牛客网:国内技术面试平台,有大量公司真题和模拟面试。
-
YouTube频道:如"Back To Back SWE"、"NeetCode"等,有很多优质算法讲解视频。
18. 算法面试准备策略
根据我参与面试和被面试的经验,以下策略能提高算法面试成功率:
-
分类突破:将算法题分为若干类别(如动态规划、二叉树、链表等),逐个击破。
-
高频优先:优先练习高频题目,如本文列举的这些题目。
-
模拟面试:找朋友进行模拟面试,或使用在线平台计时练习。
-
白板编程:习惯在白板或纸上写代码,这是很多公司的面试方式。
-
解释思路:练习在写代码前先解释解题思路,这是面试官考察的重点。
-
错误分析:对做错的题目进行复盘,找出知识盲点。
-
时间管理:在面试中合理分配时间,不要在一道题上卡太久。
-
沟通技巧:保持与面试官的沟通,及时反馈思路和遇到的问题。
19. 算法在实际工程中的应用
虽然面试中算法题很重要,但我们也需要了解算法在实际工程中的应用场景:
-
数据处理:排序、搜索算法在数据处理中广泛应用,如数据库索引、大数据分析等。
-
系统设计:LRU缓存、哈希表等算法是构建高性能系统的基础。
-
网络路由:图算法用于网络路由优化、最短路径计算等。
-
推荐系统:协同过滤、聚类等算法支撑个性化推荐。
-
安全领域:加密算法、哈希算法保障系统安全。
-
游戏开发:A*等寻路算法在游戏AI中广泛应用。
-
金融领域:动态规划用于投资组合优化,随机算法用于风险评估。
-
生物信息:字符串匹配算法用于基因序列分析。
理解这些实际应用场景能帮助我们更好地掌握算法,也能在面试中展示更全面的技术视野。
20. 持续提升算法能力的建议
算法能力的提升是一个长期过程,以下是我个人的一些建议:
-
每日一题:保持每天解决至少一道算法题的习惯,量变引起质变。
-
写解题报告:对每道解决的题目写简要报告,记录思路和收获。
-
参与竞赛:参加LeetCode周赛等编程比赛,锻炼在压力下解题的能力。
-
开源贡献:参与开源项目,阅读优秀算法实现代码。
-
教学相长:尝试向他人讲解算法,这能加深自己的理解。
-
项目实践:在个人项目中尝试应用所学算法,如实现一个小型搜索引擎。
-
关注前沿:了解算法领域的新发展,如机器学习算法的演进。
-
保持好奇:对遇到的每个算法问题保持好奇心,深入探究其原理。
算法能力是程序员的核心竞争力之一,但也不要过分焦虑。通过系统学习和持续练习,每个人都能掌握这些高频算法题的解法。最重要的是理解算法背后的思想,而不仅仅是记住代码实现。