引言
在数字化时代背景下,软件工程师的职业发展越来越依赖于扎实的编程技能与深厚的算法知识。而力扣(LeetCode),作为全球范围内备受程序员青睐的学习平台,以其丰富多样的编程题目、详实的解题指南与活跃的社区氛围,为技术学习者提供了绝佳的实践场所与成长舞台。本文将深入探讨力扣在技术学习中的价值、特色功能及其对个人职业发展的推动作用。
一、基础算法与数据结构
题目一:两数之和(Two Sum)
题目描述: 给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那两个整数,并返回它们的数组下标。
解题思路: 使用哈希表(HashMap)存储每个元素与其下标的映射。遍历数组,对于当前元素 num,计算目标差值 target - num,检查哈希表中是否存在这个差值。如果存在,说明找到了两个数之和为目标值的组合,返回这两个数的下标;如果不存在,将当前元素及其下标存入哈希表,继续遍历。遍历结束后仍未找到解,则返回空数组。
Java代码实现:
import java.util.HashMap;
import java.util.Map;
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 two sum solution"); // 仅当无解时抛出,实际题目返回空数组即可
}
题目二:反转链表(Reverse Linked List)
题目描述: 给你单链表的头节点 head ,请你反转链表,并返回反转后的链表头节点。
解题思路: 使用迭代法或递归法实现链表反转。这里采用迭代法,维护三个指针:prev 用于记录当前节点的前一个节点,初始为 null;curr 用于遍历链表,初始为 head;next 用于记录当前节点的下一个节点,用于更新指针关系。在遍历过程中,每次都将 curr 的 next 指向 prev,然后将三个指针向前移动一位,直到 curr 为 null。
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; // 返回新的头节点
}
class ListNode {
int val;
ListNode next;
ListNode(int x) {
val = x;
}
}
题目三:合并两个有序数组(Merge Sorted Array)
题目描述: 给你两个有序整数数组 nums1 和 nums2,请你将 nums2 合并到 nums1 中,使 nums1 成为一个有序数组。
解题思路: 使用双指针法,分别初始化两个指针 i 和 j 分别指向 nums1 和 nums2 的末尾。比较两个指针所指元素的大小,将较小者放入 nums1 末尾,然后移动指向较小元素的指针。重复此过程,直到其中一个数组遍历完,将剩余数组的所有元素依次添加到 nums1 末尾。
Java代码实现:
public void merge(int[] nums1, int m, int[] nums2, int n) {
int i = m - 1, j = n - 1, k = m + n - 1;
while (i >= 0 && j >= 0) {
if (nums1[i] > nums2[j]) {
nums1[k--] = nums1[i--];
} else {
nums1[k--] = nums2[j--];
}
}
while (j >= 0) { // 若nums2未遍历完,将其剩余元素复制到nums1末尾
nums1[k--] = nums2[j--];
}
}
二、字符串处理
题目一:最长回文子串(Longest Palindromic Substring)
题目描述: 给定一个字符串 s,找到其中最长的回文子串。
解题思路: 使用动态规划或中心扩展法求解。这里采用中心扩展法,其基本思想是从字符串的每个字符出发,尝试向两边扩展,寻找以该字符为中心的最长回文子串。具体实现时,遍历字符串,对于每个字符,分别以它为起点和终点(奇数长度回文子串)以及以它及其右侧字符为起点(偶数长度回文子串)进行扩展,记录下最长回文子串及其长度。
Java代码实现:
public String longestPalindrome(String s) {
if (s == null || s.length() < 2) return s; // 空串或单字符直接返回
int start = 0, maxLength = 1;
for (int i = 0; i < s.length(); i++) {
// 以当前字符为起点的奇数长度回文子串
int len1 = expandAroundCenter(s, i, i);
// 以当前字符及其右侧字符为起点的偶数长度回文子串
int len2 = expandAroundCenter(s, i, i + 1);
int len = Math.max(len1, len2);
if (len > maxLength) {
maxLength = len;
start = i - (len - 1) / 2;
}
}
return s.substring(start, start + maxLength);
}
private int expandAroundCenter(String s, int left, int right) {
int L = left, R = right;
while (L >= 0 && R < s.length() && s.charAt(L) == s.charAt(R)) {
L--;
R++;
}
return R - L - 1;
}
题目二:翻转字符串里的单词(Reverse Words in a String)
题目描述: 给定一个字符串 s,将字符串中的所有单词逆序排放(单词间保持原有顺序),并返回结果字符串。
解题思路: 首先使用空格将字符串 s 分割成单词列表,然后使用 Collections.reverse 方法对列表进行整体逆序,最后将逆序后的单词列表用空格拼接成字符串。
Java代码实现:
import java.util.Arrays;
public String reverseWords(String s) {
String[] words = s.trim().split("\\s+");
Collections.reverse(Arrays.asList(words));
return String.join(" ", words);
}
题目三:实现 strStr()(Implement strStr())
题目描述: 实现 strStr() 函数。给定两个字符串 haystack 和 needle ,在 haystack 字符串中找出 needle 字符串出现的第一个位置(从0开始)。如果不存在,则返回 -1。
解题思路: 使用KMP算法或朴素的双指针法实现。这里采用朴素的双指针法,使用两个指针 i 和 j 分别遍历 haystack 和 needle。当两者指向的字符相等时,同时移动指针;否则,仅移动 haystack 的指针 i。当 needle 的指针 j 移动到末尾时,说明找到了子串,返回 i - j 作为子串在 haystack 中的起始位置。若遍历结束仍未找到子串,返回 -1。
Java代码实现:
public int strStr(String haystack, String needle) {
if (needle.isEmpty()) return 0; // 空串在任何位置都匹配
for (int i = 0; ; i++) {
if (i + needle.length() > haystack.length()) return -1; // 无匹配可能,提前返回
boolean matched = true;
for (int j = 0; j < needle.length(); j++) {
if (haystack.charAt(i + j) != needle.charAt(j)) {
matched = false;
break;
}
}
if (matched) return i; // 找到匹配,返回起始位置
}
}
三、数组与矩阵
题目一:寻找旋转排序数组中的最小值(Find Minimum in Rotated Sorted Array)
题目描述: 假设按照升序排序的数组在预先未知的某个点进行了旋转(例如,原数组 0,1,2,4,5,6,7 在旋转后可能变为 4,5,6,7,0,1,2)。请找出旋转数组中的最小元素。
解题思路: 使用二分查找法。考虑数组的性质,如果中间元素小于或等于其左侧元素,说明最小元素在中间元素右侧;反之,最小元素在中间元素左侧。根据比较结果不断缩小查找范围,直至找到最小值。
Java代码实现:
public int findMin(int[] nums) {
int left = 0, right = nums.length - 1;
while (left < right) {
int mid = left + (right - left) / 2;
if (nums[mid] <= nums[right]) {
right = mid; // 最小值在左半部分
} else {
left = mid + 1; // 最小值在右半部分
}
}
return nums[left]; // 左边界即为最小值
}
题目二:买卖股票的最佳时机(Best Time to Buy and Sell Stock)
题目描述: 给定一个数组 prices,表示每天的股票价格。你可以选择任意一天买入这只股票,并在未来的某一天卖出。设计一个算法来计算你所能获取的最大利润。
解题思路: 遍历数组,维护一个变量 minPrice 记录遍历过程中的最低价格,同时用变量 maxProfit 记录当前最大利润。对于每一天,更新 maxProfit 为 Math.max(maxProfit, prices[i] - minPrice),即当前利润与历史最大利润取较大值。遍历完成后,maxProfit 即为最大利润。
Java代码实现:
public int maxProfit(int[] prices) {
int minPrice = Integer.MAX_VALUE;
int maxProfit = 0;
for (int price : prices) {
minPrice = Math.min(minPrice, price); // 更新最低价
maxProfit = Math.max(maxProfit, price - minPrice); // 更新最大利润
}
return maxProfit;
}
题目三:三数之和(3Sum)
题目描述: 给定一个整数数组 nums,找到所有和为零的三元组。
解题思路: 使用双指针法。固定一个数,对数组进行排序,然后遍历数组,对于每个数,使用两个指针分别从其右侧和左侧开始向中间扫描,寻找两个数之和等于 -currentNum 的情况。为了避免重复,对固定数进行剪枝,确保其与之前固定过的数不相等,并且在移动指针时跳过与之前相同数值的元素。
Java代码实现:
public List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
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) {
result.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--;
}
}
}
return result;
}
四、递归与分治
题目一:斐波那契数列(Fibonacci Number)
题目描述: 给定整数 n,返回第 n 个斐波那契数。
解题思路: 斐波那契数列可以通过递归或动态规划方法求解。递归解法直观地遵循定义:F(n) = F(n-1) + F(n-2),但存在大量重复计算,时间复杂度较高。动态规划则利用备忘录或表格避免重复计算,时间复杂度较低。这里展示递归解法。
Java代码实现(递归):
public int fib(int n) {
if (n <= 1) return n; // 基本情况:F(0) = 0, F(1) = 1
return fib(n - 1) + fib(n - 2); // 递归公式:F(n) = F(n-1) + F(n-2)
}
题目二:二叉树的最大深度(Maximum Depth of Binary Tree)
题目描述: 给定一个二叉树,找出其最大深度。
解题思路: 使用递归分治法。二叉树的最大深度等于左子树的最大深度与右子树的最大深度中的较大值加1(即当前节点的高度)。递归终止条件是遇到空节点,其深度为0。
Java代码实现:
class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int val) {
this.val = val;
}
}
public int maxDepth(TreeNode root) {
if (root == null) return 0; // 终止条件:空节点深度为0
int leftDepth = maxDepth(root.left); // 递归计算左子树深度
int rightDepth = maxDepth(root.right); // 递归计算右子树深度
return Math.max(leftDepth, rightDepth) + 1; // 当前节点深度为左右子树最大深度加1
}
题目三:不同的二叉搜索树 II(Unique Binary Search Trees II)
题目描述: 给定一个整数 n,生成所有值为 1 到 n 的二叉搜索树。
解题思路: 使用分治法和递归生成。对于每个 1 <= i <= n,以 i 为根节点,可以将其左边的 1...i-1 生成所有可能的左子树,右边的 i+1...n 生成所有可能的右子树。将这些左子树与右子树与根节点 i 组合成完整的二叉搜索树。最后,将所有这些二叉搜索树收集起来返回。
Java代码实现:
class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int val) {
this.val = val;
}
}
public List<TreeNode> generateTrees(int n) {
return generateTreesHelper(1, n);
}
private List<TreeNode> generateTreesHelper(int start, int end) {
List<TreeNode> allTrees = new ArrayList<>();
if (start > end) { // 终止条件:区间为空,返回空列表
allTrees.add(null);
return allTrees;
}
for (int i = start; i <= end; i++) { // 以i为根节点,递归生成左右子树
List<TreeNode> leftSubtrees = generateTreesHelper(start, i - 1);
List<TreeNode> rightSubtrees = generateTreesHelper(i + 1, end);
for (TreeNode left : leftSubtrees) {
for (TreeNode right : rightSubtrees) {
TreeNode root = new TreeNode(i);
root.left = left;
root.right = right;
allTrees.add(root);
}
}
}
return allTrees;
}
五、动态规划
题目一:爬楼梯(Climbing Stairs)
题目描述: 假设你正在爬楼梯。需要 n 阶你才能到达楼顶。每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
解题思路: 这是一个经典的动态规划问题。定义状态 dp[i] 表示爬到第 i 阶楼梯的方法数。根据题目规则,爬到第 i 阶的方法有两种:从第 i-1 阶直接爬 1 阶上来,或者从第 i-2 阶爬 2 阶上来。因此,状态转移方程为 dp[i] = dp[i-1] + dp[i-2],与斐波那契数列类似。初始状态为 dp[0] = 1(直接站在第 0 阶),dp[1] = 1(爬 1 阶到第 1 阶)。
Java代码实现:
public int climbStairs(int n) {
if (n <= 2) return n; // 基本情况:n=1或n=2时,只有一种爬法
int[] dp = new int[n + 1]; // 初始化动态规划数组
dp[0] = 1; // 站在第0阶
dp[1] = 1; // 爬1阶到第1阶
for (int i = 2; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2]; // 状态转移方程
}
return dp[n];
}
题目二:最长公共前缀(Longest Common Prefix)
题目描述: 编写一个函数来查找字符串数组中的最长公共前缀。如果不存在公共前缀,返回空字符串 ""。
解题思路: 动态规划解法。定义状态 dp[i][j] 表示第 i 个字符串与第 j 个字符串的最长公共前缀长度。状态转移方程为:若 strs[i][dp[i][j-1]] == strs[j][dp[i][j-1]],则 dp[i][j] = dp[i][j-1] + 1;否则,dp[i][j] = 0。最后,遍历所有字符串对,找到最长公共前缀长度,根据长度从第一个字符串中截取对应长度的前缀作为结果。
Java代码实现:
public String longestCommonPrefix(String[] strs) {
if (strs == null || strs.length == 0) return ""; // 基本情况:空数组或数组只有一个元素
int[][] dp = new int[strs.length][strs.length]; // 初始化动态规划数组
int maxLength = 0; // 最长公共前缀长度
for (int i = 0; i < strs.length; i++) {
for (int j = i + 1; j < strs.length; j++) {
int length = 0;
while (length < Math.min(strs[i].length(), strs[j].length()) && strs[i].charAt(length) == strs[j].charAt(length)) {
length++;
}
dp[i][j] = dp[j][i] = length;
maxLength = Math.max(maxLength, length);
}
}
return strs[0].substring(0, maxLength); // 返回最长公共前缀
}
题目三:最长连续序列(Longest Consecutive Sequence)
题目描述: 给定一个未排序的整数数组,找出最长连续序列的长度。
解题思路: 使用哈希集合记录已访问过的数字,并使用动态规划思想。遍历数组,对于每个未访问过的数字 num,尝试向左右扩展连续序列,更新最长连续序列长度。在扩展过程中,将访问过的数字加入哈希集合,避免重复计算。
Java代码实现:
public int longestConsecutive(int[] nums) {
if (nums == null || nums.length == 0) return 0; // 基本情况:空数组或数组只有一个元素
Set<Integer> seen = new HashSet<>(); // 存储已访问过的数字
int maxLength = 0;
for (int num : nums) {
if (!seen.contains(num)) {
int currentLength = 1;
seen.add(num);
int left = num - 1, right = num + 1;
while (seen.contains(left)) {
currentLength++;
seen.add(left);
left--;
}
while (seen.contains(right)) {
currentLength++;
seen.add(right);
right++;
}
maxLength = Math.max(maxLength, currentLength);
}
}
return maxLength;
}
六、二叉树与图论
题目一:二叉树的最大深度(Maximum Depth of Binary Tree)
题目描述: 给定一个二叉树,找出其最大深度。
解题思路: 使用深度优先搜索(DFS)递归遍历二叉树。对于每个节点,递归计算其左子树和右子树的最大深度,取较大值作为当前节点的最大深度,并返回。最终结果为根节点的最大深度。
Java代码实现(使用递归):
class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int val) {
this.val = val;
}
}
public int maxDepth(TreeNode root) {
if (root == null) return 0; // 终止条件:空节点深度为0
int leftDepth = maxDepth(root.left); // 递归计算左子树深度
int rightDepth = maxDepth(root.right); // 递归计算右子树深度
return Math.max(leftDepth, rightDepth) + 1; // 当前节点深度为左右子树最大深度加1
}
题目二:路径总和(Path Sum)
题目描述: 给定一个二叉树和一个目标和,判断该树中是否存在从根节点到叶子节点的路径,使得路径上所有节点值之和等于目标和。
解题思路: 使用深度优先搜索(DFS)递归遍历二叉树。对于每个节点,递归检查其左子树和右子树是否存在满足条件的路径。在递归调用时,将当前节点值累加到路径和中。如果当前节点为叶子节点且路径和等于目标和,返回 true;否则,继续递归搜索。
Java代码实现(使用递归):
class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int val) {
this.val = val;
}
}
public boolean hasPathSum(TreeNode root, int targetSum) {
if (root == null) return false; // 终止条件:空节点不存在路径
int currentSum = root.val;
if (root.left == null && root.right == null && currentSum == targetSum) { // 叶子节点且路径和等于目标和
return true;
}
return hasPathSum(root.left, targetSum - currentSum) || hasPathSum(root.right, targetSum - currentSum); // 递归检查左、右子树
}
题目三:岛屿数量(Number of Islands)
题目描述: 给定一个由 '1'(陆地)和 '0'(水)组成的二维网格,计算网格中岛屿的数量。一个岛被水包围,并且它是通过水平方向或垂直方向上相邻的陆地连接而成的。你可以假设网格的四个边均被水包围。
解题思路: 使用深度优先搜索(DFS)遍历二维网格。对于每个陆地(值为 '1' 的格子),标记其为已访问(值为 '2'),然后递归遍历其上下左右四个相邻格子,如果相邻格子也是陆地且未被访问过,则继续递归。每完成一次岛屿的遍历(即遍历完一个连通的陆地区域),岛屿计数器加1。
Java代码实现(使用DFS):
public int numIslands(char[][] grid) {
int islandsCount = 0;
for (int i = 0; i < grid.length; i++) {
for (int j = 0; j < grid[0].length; j++) {
if (grid[i][j] == '1') { // 发现陆地
dfs(grid, i, j); // 从该陆地开始进行深度优先搜索
islandsCount++; // 完成一次岛屿遍历,计数器加1
}
}
}
return islandsCount;
}
private void dfs(char[][] grid, int row, int col) {
if (row < 0 || row >= grid.length || col < 0 || col >= grid[0].length || grid[row][col] != '1') {
return; // 超出边界或非陆地,返回
}
grid[row][col] = '2'; // 标记为已访问
dfs(grid, row - 1, col); // 上
dfs(grid, row + 1, col); // 下
dfs(grid, row, col - 1); // 左
dfs(grid, row, col + 1); // 右
}
结语
力扣凭借其海量优质的题目资源、完善的学习体系与活跃的社区生态,已成为技术学习者提升编程技能、备战面试、拓展职业发展的首选平台。无论是初入编程殿堂的新手,还是寻求突破的技术老兵,都能在力扣找到适合自己成长的道路,实现技术能力与职业发展的双重跃升。