1. 字母异位词分组
题目要求将互为字母异位词的字符串放入同一组。
例如:
["eat", "tea", "tan", "ate", "nat", "bat"]
可以分为:
[["eat", "tea", "ate"], ["tan", "nat"], ["bat"]]
核心思路
字母异位词中,每种字符出现的次数相同。将字符串排序后,互为异位词的字符串一定会得到同一个结果:
"eat" -> "aet"
"tea" -> "aet"
"ate" -> "aet"
使用 HashMap:
key:排序后的字符串
value:具有相同 key 的原字符串列表
复杂度
设字符串平均长度为 k,字符串数量为 n:
时间复杂度:O(n × k log k)
空间复杂度:O(n × k)
2. 环形链表
判断一个链表中是否存在环。
3 -> 2 -> 0 -> -4
^ |
|_________|
核心思路:快慢指针
定义两个指针:
slow:每次走 1 步
fast:每次走 2 步
-
无环:
fast最终会到达null; -
有环:
fast会在环中追上slow,两个指针会指向同一个节点。while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;if (slow == fast) { return true; }}
复杂度
时间复杂度:O(n)
空间复杂度:O(1)
3. 最长回文子串
在字符串中找到最长的回文子串。
输入:"babad"
输出:"bab" 或 "aba"
输入:"cbbd"
输出:"bb"
核心思路:中心扩展
回文串有两种中心:
奇数长度:"aba"
中心是 b
偶数长度:"abba"
中心在两个 b 之间
因此每个位置都需要尝试两次扩展:
expandAroundCenter(s, i, i); // 奇数长度
expandAroundCenter(s, i, i + 1); // 偶数长度
扩展条件:
left >= 0
right < s.length()
s.charAt(left) == s.charAt(right)
起始下标计算
当回文长度为 len,当前左中心为 i 时:
start = i - (len - 1) / 2;
这条公式能统一处理奇数长度与偶数长度回文。
复杂度
时间复杂度:O(n²)
空间复杂度:O(1)
4. 零钱兑换
给定硬币面额 coins 和金额 amount,求凑出目标金额所需的最少硬币数。无法凑出时返回 -1。
coins = [1, 2, 5]
amount = 11
答案:3
解释:5 + 5 + 1
为什么不能贪心
coins = [1, 3, 4]
amount = 6
贪心:4 + 1 + 1,需要 3 枚
最优:3 + 3,只需要 2 枚
因此不能只优先选择面额最大的硬币。
核心思路:一维动态规划
定义:
dp[x]:凑出金额 x 所需的最少硬币数量
初始状态:
dp[0] = 0;
其他位置先初始化为不可能达到的大值:
Arrays.fill(dp, amount + 1);
状态转移:
dp[currentAmount] = Math.min(
dp[currentAmount],
dp[currentAmount - coin] + 1
);
含义是:最后选择一枚 coin 后,前面先凑出 currentAmount - coin。
复杂度
时间复杂度:O(amount × coins.length)
空间复杂度:O(amount)
5. 买卖股票的最佳时机
只能买入一次、卖出一次,求最大利润。
prices = [7, 1, 5, 3, 6, 4]
答案:5
第 2 天以 1 买入,第 5 天以 6 卖出。
核心思路:维护历史最低价格
遍历每一天的股价时,维护:
minPrice:截至当前日期的最低买入价格
maxProfit:截至当前日期的最大利润
for (int price : prices) {
minPrice = Math.min(minPrice, price);
maxProfit = Math.max(maxProfit, price - minPrice);
}
当出现一个更低价格时,它将成为后续更好的买入机会;当前价格则可以尝试作为卖出价格。
复杂度
时间复杂度:O(n)
空间复杂度:O(1)
6. 除自身以外数组的乘积
返回数组 answer,其中:
answer[i] = 除 nums[i] 外其余元素的乘积
输入:[1, 2, 3, 4]
输出:[24, 12, 8, 6]
核心思路:前缀积 + 后缀积
定义:
left[i]:i 左边所有元素的乘积
right[i]:i 右边所有元素的乘积
因此:
answer[i] = left[i] × right[i]
例如:
nums = [1, 2, 3, 4]
left = [1, 1, 2, 6]
right = [24, 12, 4, 1]
answer = [24, 12, 8, 6]
边界位置:
left[0] = 1;
right[n - 1] = 1;
因为没有元素参与相乘时,乘积定义为 1。
复杂度
时间复杂度:O(n)
空间复杂度:O(n)
还可以进一步优化为:将前缀积直接存入 answer,再用一个变量从右向左累乘后缀积,从而将额外空间优化到 O(1)。
7. 最大子数组和
找到一个和最大的连续子数组。
输入:[-2, 1, -3, 4, -1, 2, 1, -5, 4]
输出:6
最大连续子数组:[4, -1, 2, 1]
核心思路:Kadane 算法
不断累加当前连续子数组的和:
-
当前累计和大于等于
0:继续保留; -
当前累计和小于
0:它会拖累后续结果,直接丢弃,从后续元素重新开始。int currentSum = 0;
int maxSum = nums[0];for (int num : nums) {
currentSum += num;
maxSum = Math.max(maxSum, currentSum);if (currentSum < 0) { currentSum = 0; }}
为什么不能把 maxSum 初始化为 0
对于全负数数组:
[-3, -2, -5]
最大连续子数组和应该是:
-2
若初始化为 0,会错误返回 0。
复杂度
时间复杂度:O(n)
空间复杂度:O(1)
8. 合并区间
将所有重叠区间合并。
输入:[[1,3], [2,6], [8,10], [15,18]]
输出:[[1,6], [8,10], [15,18]]
核心思路:排序 + 贪心扫描
先按区间左端点排序:
Arrays.sort(intervals, (a, b) -> a[0] - b[0]);
然后依次判断当前区间是否和结果中最后一个区间重叠。
判断条件:
lastEnd < currentStart
-
若成立:不重叠,直接加入结果;
-
否则:发生重叠,更新右端点。
lastEnd = Math.max(lastEnd, currentEnd);
端点相接也需要合并
[1, 4] 和 [4, 5]
两者在端点 4 相接,结果应为:
[1, 5]
因此"不重叠"的条件必须是:
lastEnd < currentStart
而不能写成:
lastEnd <= currentStart
复杂度
排序:O(n log n)
扫描:O(n)
总时间复杂度:O(n log n)
空间复杂度:O(n)
