LeetCode Hot 100|普通数组专题学习笔记(零基础版)
本笔记整理三道题:
- LeetCode 53:最大子数组和------动态规划
- LeetCode 56:合并区间------排序 + 一次遍历
- LeetCode 189:轮转数组------双指针 + 三次反转
学习建议:先理解题意,再看暴力思路,最后学习最优解。不要只背代码,要能解释每一行的作用。
第一题:最大子数组和
题号:LeetCode 53
题目链接:https://leetcode.cn/problems/maximum-subarray/
1. 题目理解
给定整数数组,找到一个非空、连续的子数组,使元素和最大,返回最大和。
python
nums = [-2, 1, -3, 4, -1, 2, 1, -5, 4]
最大和来自 [4, -1, 2, 1]:
text
4 + (-1) + 2 + 1 = 6
答案是 6。子数组必须连续,而且不能为空。
2. 暴力解法
思路: 1. 外层循环选择子数组左端点 left。 2. 内层循环选择右端点
right。 3. 用 total 累加当前子数组的和。 4. 每次累加后更新最大和。
python
class Solution:
def maxSubArray(self, nums: list[int]) -> int:
n = len(nums)
max_sum = nums[0]
for left in range(n):
total = 0
for right in range(left, n):
total += nums[right]
max_sum = max(max_sum, total)
return max_sum
逐行理解
max_sum = nums[0]:不要初始化为0。数组可能全是负数,例如
[-4, -2, -7],正确答案是-2。for left in range(n):枚举每一个起点。total = 0:每换一个起点,都要重新累计。for right in range(left, n):让右端点逐渐向右扩展。total += nums[right]:只加上新进入区间的数字,不必每次重新计算整个区间。max_sum = max(max_sum, total):保留历史最大和。
复杂度: - 时间:O(n²),需要枚举所有连续子数组。 -
额外空间:O(1),只使用固定数量的变量。
3. 动态规划最优解
定义状态
dp[i] 表示:必须以 nums[i] 结尾的最大连续子数组和。
当前位置只有两种选择:
- 从当前数字重新开始:
nums[i] - 接在前一个位置的最优子数组后面:
dp[i - 1] + nums[i]
所以状态转移公式是:
python
dp[i] = max(nums[i], dp[i - 1] + nums[i])
例如 nums = [2, -1, 3],对应 dp = [2, 1, 4]。最后答案是
max(dp) = 4。
DP 数组版
python
class Solution:
def maxSubArray(self, nums: list[int]) -> int:
n = len(nums)
dp = [0] * n
dp[0] = nums[0]
for i in range(1, n):
dp[i] = max(nums[i], dp[i - 1] + nums[i])
return max(dp)
空间优化版(推荐掌握)
python
class Solution:
def maxSubArray(self, nums: list[int]) -> int:
current_sum = nums[0]
max_sum = nums[0]
for i in range(1, len(nums)):
current_sum = max(nums[i], current_sum + nums[i])
max_sum = max(max_sum, current_sum)
return max_sum
两个变量的区别:
current_sum:必须以当前位置结尾的最大连续子数组和,可能变大或变小。max_sum:从开始到当前位置见过的最大和,只会保持不变或增大。
例如 [5, -10, 4]: - 看到 5:current_sum = 5,max_sum = 5 - 看到
-10:current_sum = -5,max_sum = 5 - 看到
4:current_sum = 4,max_sum = 5
复杂度: - 暴力:时间 O(n²),额外空间 O(1) - DP 数组:时间
O(n),额外空间 O(n) - 空间优化 DP:时间 O(n),额外空间 O(1)
易错点: 1. max_sum 不要初始化为 0,全负数数组会出错。 2. 更新
current_sum 时不能漏掉 + nums[i]。 3. 更新全局最大值要写
max_sum = max(max_sum, current_sum)。 4. 最后返回
max_sum,不能直接返回 current_sum。
自测:
输入 答案
[-2,1,-3,4,-1,2,1,-5,4] 6
[5,4,-1,7,8] 23
[-4,-2,-7] -2
[5,-10,4] 5
第二题:合并区间
题号:LeetCode 56
题目链接:https://leetcode.cn/problems/merge-intervals/
1. 题目理解
每个区间是 [起点, 终点]。如果区间重叠,就合并。
python
intervals = [[1, 3], [2, 6], [8, 10], [15, 18]]
[1, 3] 和 [2, 6] 重叠,合并成 [1, 6]。答案:
python
[[1, 6], [8, 10], [15, 18]]
端点相接也算重叠,例如 [1, 4] 和 [4, 5] 应合并为 [1, 5]。
2. 为什么先排序?
python
intervals.sort(key=lambda x: x[0])
x 代表一个区间,x[0]
是区间起点。按起点从小到大排序后,就能从左到右扫描。
3. 判断与合并
假设 result[-1] 是已经合并的最后一个区间,current 是当前区间。
没有重叠:
python
current[0] > result[-1][1]
当前区间起点大于上一个区间终点,所以可以直接添加。
有重叠:
python
result[-1][1] = max(result[-1][1], current[1])
只更新终点,并保留两个终点中的较大值。这样即使当前区间完全被已有区间包含,也不会把终点错误地改小。
4. 完整代码
python
class Solution:
def merge(self, intervals: list[list[int]]) -> list[list[int]]:
intervals.sort(key=lambda x: x[0])
result = []
for current in intervals:
if not result or current[0] > result[-1][1]:
result.append(current)
else:
result[-1][1] = max(result[-1][1], current[1])
return result
手算示例
输入:
python
[[1, 3], [2, 6], [8, 10]]
result为空,添加[1, 3]。[2, 6]的起点2不大于已有终点3,发生重叠,终点更新为
max(3, 6) = 6。[8, 10]的起点8大于已有终点6,没有重叠,添加新区间。
结果:
python
[[1, 6], [8, 10]]
复杂度: - 时间:O(n log n),主要由排序决定;排序后遍历是 O(n)。 -
空间:通常记为 O(n),结果列表最多保存 n
个区间,排序实现也可能使用额外空间。
易错点: 1. 比较 current[0] 和
result[-1][1],即当前起点与上一个终点。 2. 没有重叠的条件是
current[0] > result[-1][1]。 3. 合并时不能把整个 current 赋给终点。
- 终点更新要用
max()。 5. 使用result[-1]前要先检查result
是否为空。
自测:
输入 答案
[[1,3],[2,6],[8,10],[15,18]] [[1,6],[8,10],[15,18]]
[[1,4],[2,3]] [[1,4]]
[[1,8],[2,5],[6,9]] [[1,9]]
[[1,4],[4,5]] [[1,5]]
第三题:轮转数组
题号:LeetCode 189
题目链接:https://leetcode.cn/problems/rotate-array/
1. 题目理解
把数组中的元素向右轮转 k 个位置。
python
nums = [1, 2, 3, 4, 5, 6, 7]
k = 3
结果:
python
[5, 6, 7, 1, 2, 3, 4]
题目要求原地修改数组,所以函数不需要返回新数组。
2. 暴力解法:每次移动一格
python
class Solution:
def rotate(self, nums: list[int], k: int) -> None:
n = len(nums)
k = k % n
for _ in range(k):
last = nums[-1]
for i in range(n - 1, 0, -1):
nums[i] = nums[i - 1]
nums[0] = last
思路: 1. 暂存最后一个元素。 2. 从右往左,将元素向右移动一格。 3.
把暂存的元素放到下标 0。 4. 重复 k 次。
为什么从右往左?这样不会提前覆盖还没有移动的旧值。
为什么 k = k % n?数组长度为 n,轮转 n 次会回到原样。例如长度为
7,轮转 10 次等价于轮转 3 次。
复杂度: - 时间:O(nk)。 - 额外空间:O(1)。
3. 最优解:三次反转法
例子:
python
nums = [1, 2, 3, 4, 5, 6, 7]
k = 3
第一步,反转整个数组:
text
[1, 2, 3, 4, 5, 6, 7]
→ [7, 6, 5, 4, 3, 2, 1]
第二步,反转前 k 个元素:
text
[7, 6, 5, 4, 3, 2, 1]
→ [5, 6, 7, 4, 3, 2, 1]
第三步,反转剩余部分:
text
[5, 6, 7, 4, 3, 2, 1]
→ [5, 6, 7, 1, 2, 3, 4]
记忆口诀:整体反转 → 反转前 k 个 → 反转剩余部分。
4. 双指针反转函数
python
def reverse(nums, left, right):
while left < right:
nums[left], nums[right] = nums[right], nums[left]
left += 1
right -= 1
left 指向左端,right
指向右端。交换后两个指针向中间移动,直到相遇或交错。
5. 最优解完整代码
python
class Solution:
def rotate(self, nums: list[int], k: int) -> None:
n = len(nums)
k = k % n
def reverse(left, right):
while left < right:
nums[left], nums[right] = nums[right], nums[left]
left += 1
right -= 1
reverse(0, n - 1) # 整体反转
reverse(0, k - 1) # 反转前 k 个元素
reverse(k, n - 1) # 反转剩余部分
下标解释: - 整个数组:0 到 n - 1。 - 前 k 个元素:0 到
k - 1。 - 剩余部分:k 到 n - 1。
复杂度: - 时间:O(n)。 - 额外空间:O(1)。
易错点: 1. 先执行 k = k % n。 2. 前 k 个元素的右边界是
k - 1,不是 k。 3. 反转函数每次交换后要更新 left 和 right。 4.
题目要求原地修改,所以不需要 return nums。 5. LeetCode
输入数组非空;通用代码如果允许空列表,需要先处理空数组再执行取余。
自测:
输入 k 结果
[1,2,3,4,5,6,7] 3 [5,6,7,1,2,3,4]
[-1,-100,3,99] 2 [3,99,-1,-100]
[1,2,3] 4 [3,1,2]
[1,2] 0 [1,2]
三道题对比总结
题目 核心问题 核心技巧 最优时间复杂度
最大子数组和 连续子数组的最大和 动态规划 / Kadane 算法 O(n)
合并区间 合并重叠区间 排序 + 一次遍历 O(n log n)
轮转数组 向右移动元素 双指针 + 三次反转 O(n)
建议复习方法
- 遮住代码,先口头讲清楚思路。
- 自己写出暴力解法。
- 再独立写出最优解。
- 用边界用例测试,特别关注全负数、区间包含、端点相接、
k
大于数组长度等情况。 - 能不看答案写出来,并解释关键行,才算真正掌握。
学算法不是记住一段代码,而是遇到类似问题时,能判断该使用什么思路,以及为什么。