当前位置: 首页 > news >正文

LeetCode Hot 100 Python (11~20)

滑动窗口最大值:困难

给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。

返回 滑动窗口中的最大值 

示例

1

2

输入

nums = [1,3,-1,-3,5,3,6,7], k = 3

nums = [1], k = 1

输出

[3,3,5,5,6,7]

[1]

解释

滑动窗口的位置

最大值

[ 1

3

-1]

-3

5

3

6

7

3

1

[ 3

-1

-3]

5

3

6

7

3

1

3

[-1

-3

5 ]

3

6

7

5

1

3

-1

[-3

5

3 ]

6

7

5

1

3

-1

-3

[ 5

3

6 ]

7

6

1

3

-1

-3

5

[ 3

6

7 ]

7

单调队列套路

  1. 入(元素进入队尾,同时维护队列单调性
  2. 出(元素离开队首
  3. 记录/维护答案(根据队首

这个解法使用双端队列维护一个单调递减的队列,确保队首始终是当前窗口的最大值。具体步骤如下:

  1. 入队操作:遍历数组时,将当前元素与队尾元素比较,若当前元素更大,则弹出队尾元素,直到队列为空或队尾元素更大。然后将当前元素索引加入队尾,保持队列的单调递减性。
  2. 出队操作:检查队首元素是否超出当前窗口范围(即索引差≥k),若超出则弹出队首元素,确保队列中的元素都在窗口内。
  3. 记录结果:当窗口形成(i≥k-1)时,队首元素即为当前窗口的最大值,将其加入结果列表。
class Solution:def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:ans = []q = deque()  # 双端队列for i, x in enumerate(nums):# 1. 入while q and nums[q[-1]] <= x:q.pop()  # 维护 q 的单调性q.append(i)  # 入队# 2. 出if i - q[0] >= k:  # 队首已经离开窗口了q.popleft()# 3. 记录答案if i >= k - 1:# 由于队首到队尾单调递减,所以窗口最大值就是队首ans.append(nums[q[0]])return ans

时间复杂度

空间复杂度

O(n)

O(min(k,U))

其中 n 为 nums 的长度。由于每个下标至多入队出队各一次,所以二重循环的循环次数是 O(n) 的

其中 U 是 nums 中的不同元素个数(本题至多为 20001)。双端队列至多有 k 个元素,同时又没有重复元素,所以也至多有 U 个元素,所以空间复杂度为 O(min(k,U))。返回值的空间不计入

最小覆盖子串:困难

给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 "" 。

注意:对于 t 中重复字符,我们寻找的子字符串中该字符数量必须不少于 t 中该字符数量;如果 s 中存在这样的子串,我们保证它是唯一的答案。

示例

1

2

3

输入

s = "ADOBECODEBANC"

t = "ABC"

s = "a", t = "a"

s = "a", t = "aa"

输出

"BANC"

"a"

""

解释

最小覆盖子串 "BANC" 包含来自字符串 t 的 'A'、'B' 和 'C'。

整个字符串 s 是最小覆盖子串。

t 中两个字符 'a' 均应包含在 s 的子串中,因此没有符合条件的子字符串,返回空字符串。

「涵盖」看示例 1,s 的子串 BANC 中每个字母的出现次数,都大于等于 t=ABC 中每个字母的出现次数,这就叫涵盖

滑动窗口怎么滑

我们枚举 s 子串的右端点 right(子串最后一个字母的下标),如果子串涵盖 t,就不断移动左端点 left 直到不涵盖为止。在移动过程中更新最短子串的左右端点。具体来说:

  1. 初始化 ansLeft=−1, ansRight=m,用来记录最短子串的左右端点,其中 m 是 s 的长度。
  2. 用一个哈希表(或者数组)cntT 统计 t 中每个字母的出现次数。
  3. 初始化 left=0,以及一个空哈希表(或者数组)cntS,用来统计 s 子串中每个字母的出现次数。
  4. 遍历 s,设当前枚举的子串右端点为 right,把 s[right] 的出现次数加一。
  5. 遍历 cntS 中的每个字母及其出现次数,如果出现次数都大于等于 cntT 中的字母出现次数:
    1. 如果 right−left<ansRight−ansLeft,说明我们找到了更短的子串,更新 ansLeft=left, ansRight=right。
    2. 把 s[left] 的出现次数减一。
    3. 左端点右移,即 left 加一。
    4. 重复上述三步,直到 cntS 有字母的出现次数小于 cntT 中该字母的出现次数为止。
  6. 最后,如果 ansLeft<0,说明没有找到符合要求的子串,返回空字符串,否则返回下标 ansLeft 到下标 ansRight 之间的子串。

由于本题大写字母和小写字母都有,为了方便,代码实现时可以直接创建大小为 128 的数组,保证所有 ASCII 字符都可以统计。

法一:

# 请选择 Python3 提交代码,而不是 Python
class Solution:def minWindow(self, s: str, t: str) -> str:ans_left, ans_right = -1, len(s)cnt_s = Counter()  # s 子串字母的出现次数cnt_t = Counter(t)  # t 中字母的出现次数left = 0for right, c in enumerate(s):  # 移动子串右端点cnt_s[c] += 1  # 右端点字母移入子串while cnt_s >= cnt_t:  # 涵盖if right - left < ans_right - ans_left:  # 找到更短的子串ans_left, ans_right = left, right  # 记录此时的左右端点cnt_s[s[left]] -= 1  # 左端点字母移出子串left += 1return "" if ans_left < 0 else s[ans_left: ans_right + 1]

时间复杂度

空间复杂度

O(∣Σ∣m+n)

O(∣Σ∣)

其中 m 为 s 的长度,n 为 t 的长度,∣Σ∣ 为字符集合的大小,本题字符均为英文字母,所以 ∣Σ∣=52。注意 left 只会增加不会减少,left 每增加一次,我们就花费 O(∣Σ∣) 的时间。因为 left 至多增加 m 次,所以二重循环的时间复杂度为 O(∣Σ∣m),再算上统计 t 字母出现次数的时间 O(n),总的时间复杂度为 O(∣Σ∣m+n)

如果创建了大小为 128 的数组,则 ∣Σ∣=128

法二:优化

上面的代码每次都要花费 O(∣Σ∣) 的时间去判断是否涵盖,能不能优化到 O(1) 呢?可以用一个变量 less 维护目前子串中有 less 种字母的出现次数小于 t 中字母的出现次数。具体来说(注意下面算法中的 less 变量):

  1. 初始化 ansLeft=−1, ansRight=m,用来记录最短子串的左右端点,其中 m 是 s 的长度。
  2. 用一个哈希表(或者数组)cntT 统计 t 中每个字母的出现次数。
  3. 初始化 left=0,以及一个空哈希表(或者数组)cntS,用来统计 s 子串中每个字母的出现次数。
  4. 初始化 less 为 t 中的不同字母个数。
  5. 遍历 s,设当前枚举的子串右端点为 right,把字母 c=s[right] 的出现次数加一。加一后,如果 cntS[c]=cntT[c],说明 c 的出现次数满足要求,把 less 减一。
  6. 如果 less=0,说明 cntS 中的每个字母及其出现次数都大于等于 cntT 中的字母出现次数,那么:
    1. 如果 right−left<ansRight−ansLeft,说明我们找到了更短的子串,更新 ansLeft=left, ansRight=right。
    2. 把字母 x=s[left] 的出现次数减一。减一前,如果 cntS[x]=cntT[x],说明 x 的出现次数不满足要求,把 less 加一。
    3. 左端点右移,即 left 加一。
    4. 重复上述三步,直到 less>0,即 cntS 有字母的出现次数小于 cntT 中该字母的出现次数为止。
  7. 最后,如果 ansLeft<0,说明没有找到符合要求的子串,返回空字符串,否则返回下标 ansLeft 到下标 ansRight 之间的子串。

代码实现时,可以把 cntS 和 cntT 合并成一个 cnt,定义:cnt[x]=cntT[x]−cntS[x]

如果 cnt[x]=0,就意味着窗口内字母 x 的出现次数和 t 的一样多。

class Solution:def minWindow(self, s: str, t: str) -> str:ans_left, ans_right = -1, len(s)cnt = defaultdict(int)  # 比 Counter 更快for c in t:cnt[c] += 1less = len(cnt)  # 有 less 种字母的出现次数 < t 中的字母出现次数left = 0for right, c in enumerate(s):  # 移动子串右端点cnt[c] -= 1  # 右端点字母移入子串if cnt[c] == 0:# 原来窗口内 c 的出现次数比 t 的少,现在一样多less -= 1while less == 0:  # 涵盖:所有字母的出现次数都是 >=if right - left < ans_right - ans_left:  # 找到更短的子串ans_left, ans_right = left, right  # 记录此时的左右端点x = s[left]  # 左端点字母if cnt[x] == 0:# x 移出窗口之前,检查出现次数,# 如果窗口内 x 的出现次数和 t 一样,# 那么 x 移出窗口后,窗口内 x 的出现次数比 t 的少less += 1cnt[x] += 1  # 左端点字母移出子串left += 1return "" if ans_left < 0 else s[ans_left: ans_right + 1]

时间复杂度

空间复杂度

O(m+n) 或 O(m+n+∣Σ∣)

O(∣Σ∣)

其中 m 为 s 的长度,n 为 t 的长度,∣Σ∣=128。注意 left 只会增加不会减少,二重循环的时间复杂度为 O(m)。使用哈希表写法的时间复杂度为 O(m+n),数组写法的时间复杂度为 O(m+n+∣Σ∣)。

无论 m 和 n 有多大,额外空间都不会超过 O(∣Σ∣)。

最大子数组和:中等

给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

子数组是数组中的一个连续部分。

示例

1

2

3

输入

nums = [-2,1,-3,4,-1,2,1,-5,4]

nums = [1]

nums = [5,4,-1,7,8]

输出

"6

1

23

解释

连续子数组 [4,-1,2,1] 的和最大,为 6

法一:前缀和 + 贪心

前置知识:前缀和

由于子数组的元素和等于两个前缀和的差,所以求出 nums 的前缀和,问题就变成 121. 买卖股票的最佳时机 了。本题子数组不能为空,相当于一定要交易一次。

我们可以一边遍历数组计算前缀和,一边维护前缀和的最小值(相当于股票最低价格),用当前的前缀和(卖出价格)减去前缀和的最小值(买入价格),就得到了以当前元素结尾的子数组和的最大值(利润),用它来更新答案的最大值(最大利润)。

请注意,由于题目要求子数组不能为空,应当先计算前缀和-最小前缀和,再更新最小前缀和。相当于不能在同一天买入股票又卖出股票。

如果先更新最小前缀和,再计算前缀和-最小前缀和,就会把空数组的元素和 0 算入答案。

示例 1 [−2,1,−3,4,−1,2,1,−5,4] 的计算流程如下,可以对照代码理解。注意计算顺序。

元素值

前缀和

最小前缀和

前缀和-最小前缀和

−2

−2

0

−2

1

−1

−2

1

−3

−4

−2

−2

4

0

−4

4

−1

−1

−4

3

2

1

−4

5

1

2

−4

6

−5

−3

−4

1

4

1

−4

5

前缀和-最小前缀和的最大值等于 6,即为答案。

class Solution:def maxSubArray(self, nums: List[int]) -> int:ans = -infmin_pre_sum = pre_sum = 0for x in nums:pre_sum += x  # 当前的前缀和ans = max(ans, pre_sum - min_pre_sum)  # 减去前缀和的最小值min_pre_sum = min(min_pre_sum, pre_sum)  # 维护前缀和的最小值return ans

时间复杂度

空间复杂度

O(n)

O(1)

其中 n 为 nums 的长度

仅用到若干额外变量

法二:动态规划

定义 f[i] 表示以 nums[i] 结尾的最大子数组和。

分类讨论:

  • nums[i] 单独组成一个子数组,那么 f[i]=nums[i]。
  • nums[i] 和前面的子数组拼起来,也就是在以 nums[i−1] 结尾的最大子数组和之后添加 nums[i],那么 f[i]=f[i−1]+nums[i]。

两种情况取最大值,得

简单地说,如果 nums[i] 左边的子数组元素和是负的,就不用和左边的子数组拼在一起了。

答案为 max(f)。

注意:答案不是 f[n−1],这仅仅表示以 nums[n−1] 结尾的最大子数组和。或者说 f[n−1] 意味着 nums[n−1] 一定要选,但这不一定正确。

class Solution:def maxSubArray(self, nums: List[int]) -> int:f = [0] * len(nums)f[0] = nums[0]for i in range(1, len(nums)):f[i] = max(f[i - 1], 0) + nums[i]return max(f)

空间优化

由于计算 f[i] 只会用到 f[i−1],不会用到更早的状态,所以可以用一个变量滚动计算。具体请看视频讲解 动态规划入门:从记忆化搜索到递推。

状态转移方程简化为:f=max(f,0)+nums[i](f 可以初始化成 0 或者任意负数)。

class Solution:def maxSubArray(self, nums: List[int]) -> int:ans = -inf  # 注意答案可以是负数,不能初始化成 0f = 0for x in nums:f = max(f, 0) + xans = max(ans, f)return ans

时间复杂度

空间复杂度

O(n)

O(1)

其中 n 为 nums 的长度

仅用到若干额外变量

合并区间:中等

以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间 。

示例

1

2

输入

intervals = [[1,3],[2,6],[8,10],[15,18]]

intervals = [[1,4],[4,5]]

输出

[[1,6],[8,10],[15,18]]

[[1,5]]

解释

区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6]

区间 [1,4] 和 [4,5] 可被视为重叠区间

以示例 1 为例,我们有 [1,3],[2,6],[8,10],[15,18] 这四个区间。

为方便合并,把区间按照左端点从小到大排序(示例 1 已经按照左端点排序了)。

排序后,我们就知道了第一个合并区间的左端点,即 intervals[0][0]=1。

第一个合并区间的右端点是多少?目前只知道其 ≥intervals[0][1]=3,但具体是多少现在还不确定,得向右遍历。

具体算法如下:

把 intervals[0] 加入答案。注意,答案的最后一个区间表示当前正在合并的区间。

遍历到 intervals[1]=[2,6],由于左端点 2 不超过当前合并区间的右端点 3,可以合并。由于右端点 6>3,那么更新当前合并区间的右端点为 6。注意,由于我们已经按照左端点排序,所以 intervals[1] 的左端点 2 必然大于等于合并区间的左端点,所以无需更新当前合并区间的左端点。

遍历到 intervals[2]=[8,10],由于左端点 8 大于当前合并区间的右端点 6,无法合并(两个区间不相交)。再次利用区间按照左端点排序的性质,更后面的区间的左端点也大于 6,无法与当前合并区间相交,所以当前合并区间 [1,6] 就固定下来了,把新的合并区间 [8,10] 加入答案。

遍历到 intervals[3]=[15,18],由于左端点 15 大于当前合并区间的右端点 10,无法合并(两个区间不相交),我们找到了一个新的合并区间 [15,18] 加入答案。

上述算法同时说明,按照左端点排序后,合并的区间一定是 intervals 中的连续子数组。

能不能按照右端点排序?可以,但是需要倒着遍历 intervals 数组。如果正着遍历,比如 [1,2],[4,5],[1,6] 这三个区间,正确答案是合并成 [1,6],但正着遍历到 [4,5] 这个区间时,无法知道 [4,5] 能否和 [1,2] 彻底断开。但按左端点排序的话,我们就知道这是不会断开的,会和之前的区间合并在一起。

class Solution:def merge(self, intervals: List[List[int]]) -> List[List[int]]:intervals.sort(key=lambda p: p[0])  # 按照左端点从小到大排序ans = []for p in intervals:if ans and p[0] <= ans[-1][1]:  # 可以合并ans[-1][1] = max(ans[-1][1], p[1])  # 更新右端点最大值else:  # 不相交,无法合并ans.append(p)  # 新的合并区间return ans

时间复杂度

空间复杂度

O(nlogn)

O(1)

其中 n 是 intervals 的长度,瓶颈在排序上

排序的栈开销和返回值不计入

轮转数组:中等

给定一个整数数组 nums,将数组中的元素向右轮转 k 个位置,其中 k 是非负数。

示例

1

2

输入

nums = [1,2,3,4,5,6,7], k = 3

nums = [-1,-100,3,99], k = 2

输出

[5,6,7,1,2,3,4]

[3,99,-1,-100]

解释

向右轮转 1 步: [7,1,2,3,4,5,6]

向右轮转 2 步: [6,7,1,2,3,4,5]

向右轮转 3 步: [5,6,7,1,2,3,4]

向右轮转 1 步: [99,-1,-100,3]

向右轮转 2 步: [3,99,-1,-100]

例子

把 [1,2,3,4,5,6,7] 变成 [5,6,7,1,2,3,4],首先要保证 5,6,7 在 1,2,3,4 前面,这可以通过反转整个数组做到。

反转后数组变成 [7,6,5,4,3,2,1],对比最终目标可以发现,前三个数需要反转,后四个数需要反转,这样就得到了 [5,6,7,1,2,3,4]。

一般化

这里假设 0≤k<n,对于 k≥n 的情况,可以转换成 0≤k<n 的情况(证明见后文)。

设 nums=A+B,其中 A 是 nums 的前 n−k 个数,B 是后 k 个数。在上例中,A=[1,2,3,4],B=[5,6,7]。

题目要求把 A+B 变成 B+A,这可以用三次反转实现:

  1. 把 nums 反转,我们得到了 rev(B)+rev(A),其中 rev(A) 表示数组 A 反转后的结果。在上例中,rev(B)+rev(A)=[7,6,5]+[4,3,2,1]。
  2. 单独反转 rev(B),因为一个数组反转两次是不变的,所以 rev(rev(B))=B,我们得到了 B。
  3. 单独反转 rev(A),得到 A。
  4. 现在数组变成 B+A。在上例中,B+A=[5,6,7]+[1,2,3,4],这正是我们想要的结果。

正确性证明

向右轮转 k 个位置后,下标 i 的元素移动到下标 (i+k)modn 上,其中 n 是 nums 的长度。

下面证明上述方法(三次反转)的正确性。

假设 0≤k<n,也就是 0≤k≤n−1,分类讨论:

  1. 如果 n−k≤i≤n−1,那么第一次反转(整个数组的反转)会把下标 i 的元素交换到下标 n−1−i 上(注意 0≤n−1−i≤k−1,在第二次反转的范围中),第二次反转会把下标 n−1−i 的元素交换到下标 k−1−(n−1−i)=i+k−n 上。根据 n−k≤i≤n−1 可得 0≤i+k−n≤k−1,所以 i+k−n=(i+k)modn,符合题目要求。
  2. 如果 0≤i≤n−k−1,那么第一次反转(整个数组的反转)会把下标 i 的元素交换到下标 n−1−i 上(注意 k≤n−1−i≤n−1,在第三次反转的范围中),第三次反转会把下标 n−1−i 的元素交换到下标 k+n−1−(n−1−i)=i+k 上(注*)。根据 0≤i≤n−k−1 可得 k≤i+k≤n−1,所以 i+k=(i+k)modn,符合题目要求。

对于 k≥n 的情况,由于轮转 n 次等同于没有轮转,轮转 n+1 等同于轮转 1 次……依此类推,轮转 k 次等同于轮转 kmodn 次。由于 0≤kmodn≤n−1,所以 k≥n 的情况也是正确的。

综上所述,对于任意非负整数 k,按照图中三次反转的方法,可以把下标 i 的元素移动到下标 (i+k)modn 上。

注:对于下标区间 [L,R] 的反转,由于 i=L 会反转到 R,i=L+1 会反转到 R−1,i=L+2 会反转到 R−2,依此类推,下标 i 和反转后的位置 j 满足 i+j=L+R,所以 i 会反转到 L+R−i。

# 注:请勿使用切片,会产生额外空间
class Solution:def rotate(self, nums: List[int], k: int) -> None:def reverse(i: int, j: int) -> None:while i < j:nums[i], nums[j] = nums[j], nums[i]i += 1j -= 1n = len(nums)k %= n  # 轮转 k 次等于轮转 k % n 次reverse(0, n - 1)reverse(0, k - 1)reverse(k, n - 1)

时间复杂度

空间复杂度

O(n)

O(1)

其中 n 是 nums 的长度

除自身以外数组的乘积:中等

给你一个整数数组 nums,返回 数组 answer ,其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积 。

题目数据 保证 数组 nums之中任意元素的全部前缀元素和后缀的乘积都在  32  整数范围内。

请 不要使用除法,且在 O(n) 时间复杂度内完成此题。

示例

1

2

输入

nums = [1,2,3,4]

nums = [-1,1,0,-3,3]

输出

[24,12,8,6]

[0,0,9,0,0]

answer[i] 等于 nums 中除了 nums[i] 之外其余各元素的乘积。换句话说,如果知道了 i 左边所有数的乘积,以及 i 右边所有数的乘积,就可以算出 answer[i]。

于是:

  1. 定义 pre[i] 表示从 nums[0] 到 nums[i−1] 的乘积。
  2. 定义 suf[i] 表示从 nums[i+1] 到 nums[n−1] 的乘积。

我们可以先计算出从 nums[0] 到 nums[i−2] 的乘积 pre[i−1],再乘上 nums[i−1],就得到了 pre[i],即:pre[i]=pre[i−1]⋅nums[i−1]。

同理有:suf[i]=suf[i+1]⋅nums[i+1]。

初始值:pre[0]=suf[n−1]=1。按照上文的定义,pre[0] 和 suf[n−1] 都是空子数组的元素乘积,我们规定这是 1,因为 1 乘以任何数 x 都等于 x,这样可以方便递推计算 pre[1],suf[n−2] 等。

算出 pre 数组和 suf 数组后,有:answer[i]=pre[i]suf[i]

class Solution:def productExceptSelf(self, nums: List[int]) -> List[int]:n = len(nums)pre = [1] * nfor i in range(1, n):pre[i] = pre[i - 1] * nums[i - 1]suf = [1] * nfor i in range(n - 2, -1, -1):suf[i] = suf[i + 1] * nums[i + 1]return [p * s for p, s in zip(pre, suf)]

时间复杂度

空间复杂度

O(n)

O(n)

其中 n 是 nums 的长度

优化:不使用额外空间

先计算 suf,然后一边计算 pre,一边把 pre 直接乘到 suf[i] 中。最后返回 suf。

题目说「输出数组不被视为额外空间」,所以该做法的空间复杂度为 O(1)。此外,这种做法比上面少遍历了一次。

class Solution:def productExceptSelf(self, nums: List[int]) -> List[int]:n = len(nums)suf = [1] * nfor i in range(n - 2, -1, -1):suf[i] = suf[i + 1] * nums[i + 1]pre = 1for i, x in enumerate(nums):# 此时 pre 为 nums[0] 到 nums[i-1] 的乘积,直接乘到 suf[i] 中suf[i] *= prepre *= xreturn suf

时间复杂度

空间复杂度

O(n)

O(1)

其中 n 是 nums 的长度

返回值不计入

缺失的第一个正数:困难

给你一个未排序的整数数组 nums ,请你找出其中没有出现的最小的正整数。

请你实现时间复杂度为 O(n) 并且只使用常数级别额外空间的解决方案。

示例

1

2

3

输入

nums = [1,2,0]

nums = [3,4,-1,1]

nums = [7,8,9,11,12]

输出

3

2

1

解释

范围 [1,2] 中的数字都在数组中

1 在数组中,但 2 没有

最小的正数 1 没有出现

思路

想象有一个教室,座位从左到右编号为 1 到 n。

有 n 个学生坐在教室的座位上,把 nums[i] 当作坐在第 i 个座位上的学生的学号。我们要做的事情,就是让学号在 1 到 n 中的学生,都坐到编号与自己学号相同的座位上。学号不在 [1,n] 中的学生可以忽略。

学生们交换座位后,从左到右看,第一个学号与座位编号不匹配的学生,其座位编号就是答案。

特别地,如果所有学生都坐在正确的座位上,那么答案是 n+1。

第一个例子

为方便描述思路,假设数组的下标是从 1 开始的。假设 nums=[2,3,1]。

  1. 从 nums[1] 开始。这个座位上的学生,学号是 2,他应当坐在 nums[2] 上,所以他和 nums[2] 交换。交换后 nums=[3,2,1]。
  2. 仍然看 nums[1],这个座位上的学生,学号是 3,他应当坐在 nums[3] 上,所以他和 nums[3] 交换。交换后 nums=[1,2,3]。
  3. 仍然看 nums[1],这个座位上的学生,学号是 1,他坐在正确的座位上。
  4. 向后遍历,nums[2]=2,他坐在正确的座位上。
  5. 向后遍历,nums[3]=3,他坐在正确的座位上。
  6. 换座位过程结束。
  7. 再次遍历 nums,发现 nums[i]=i 都满足,说明数组中 1,2,3 都有,所以缺失的第一个正数是 4。

第二个例子

假设 nums=[3,4,−1,1],这是题目中的示例 2。

  1. 从 nums[1] 开始。这个座位上的学生,学号是 3,他应当坐在 nums[3] 上,所以他和 nums[3] 交换。交换后 nums=[−1,4,3,1]。
  2. 仍然看 nums[1],这个座位上的学生,学号是 −1,忽略。
  3. 向后遍历,nums[2]=4,他应当坐在 nums[4] 上,所以他和 nums[4] 交换。交换后 nums=[−1,1,3,4]。
  4. 仍然看 nums[2]=1,他应当坐在 nums[1] 上,所以他和 nums[1] 交换。交换后 nums=[1,−1,3,4]。
  5. 仍然看 nums[2],这个座位上的学生,学号是 −1,忽略。
  6. 向后遍历,nums[3]=3,他坐在正确的座位上。
  7. 向后遍历,nums[4]=4,他坐在正确的座位上。
  8. 换座位过程结束。
  9. 再次遍历 nums,发现 nums[2]=−12,说明教室中没有学号为 2 的学生(否则他会坐在 nums[2] 上),所以答案是 2。

第三个例子

注意 nums 中可能有重复元素。在这种情况下,算法仍然是正确的吗?

假设 nums=[1,1,2]。

  1. 从 nums[1] 开始。这个座位上的学生坐在正确的座位上。
  2. 继续遍历,nums[2]=1,这是 1 号学生的影分身。由于 1 号学生的真身已经坐在正确的座位上,我们可以在第二次遍历中知道「数组中有 1」这个信息,所以可以忽略 nums[2],向后遍历。
  3. nums[3]=2,他应当坐在 nums[2] 上,所以他和 nums[2] 交换。交换后 nums=[1,2,1]。
  4. 仍然看 nums[3]=1,同样地,由于 1 号学生已经坐在正确的座位上,所以可以忽略 nums[3]。
  5. 换座位过程结束。
  6. 再次遍历 nums,发现 nums[3]=13,说明教室中没有学号为 3 的学生,所以答案是 3。

细节

判断「学生是否坐在正确的座位上」,能用 nums[i]=i 判断吗?注意有影分身(重复元素)。

在第三个例子中,虽然 nums[2]=12,但由于 nums[nums[2]]=nums[1]=1,所以 nums[2] 是个影分身,并且其真身坐在了正确的座位上,所以可以忽略 nums[2],向后遍历。注意这种情况是不能交换的,因为 nums[2]=nums[1],交换后 nums=[1,1,2] 是不变的,这会导致死循环。

为避免死循环,可以改成判断 nums[2] 和 nums[nums[2]] 是不是一样的。如果一样,就不执行交换,继续向后遍历。

一般地,为了兼容「当前学生是真身,坐在正确的座位上」和「当前学生是影分身,且其真身坐在正确的座位上」两种情况,我们可以把 i=nums[i] 套一层 nums,用 nums[i]=nums[nums[i]] 判断。

  1. 无论「当前学生是真身,坐在正确的座位上」还是「当前学生是影分身,且其真身坐在正确的座位上」,上式都是成立的。
  2. 如果「当前学生是真身,不坐在正确的座位上」,那么上式左边是当前学生的学号,右边是要交换的学生的学号。
  3. 如果「当前学生是影分身,且其真身不坐在正确的座位上」,那么上式左边是当前学生的学号,右边是要交换的学生的学号。虽然是用影分身交换的,但交换后,可以认为真身已经坐在了正确的座位上。

代码实现时,由于 nums 的下标是从 0 开始的,通过学号访问下标,要把学号减一。

class Solution:def firstMissingPositive(self, nums: list[int]) -> int:n = len(nums)for i in range(n):# 如果当前学生的学号在 [1,n] 中,但(真身)没有坐在正确的座位上while 1 <= nums[i] <= n and nums[i] != nums[nums[i] - 1]:# 那么就交换 nums[i] 和 nums[j],其中 j 是 i 的学号j = nums[i] - 1  # 减一是因为数组下标从 0 开始nums[i], nums[j] = nums[j], nums[i]# 找第一个学号与座位编号不匹配的学生for i in range(n):if nums[i] != i + 1:return i + 1# 所有学生都坐在正确的座位上return n + 1

时间复杂度

空间复杂度

O(n)

O(1)

其中 n 是 nums 的长度。虽然我们写了个二重循环,但每次交换都会把一个学生换到正确的座位上,所以总交换次数至多为 n,所以内层循环的总循环次数是 O(n) 的,所以时间复杂度是 O(n)

矩阵置零:中等

给定一个 m x n 的矩阵,如果一个元素为 ,则将其所在行和列的所有元素都设为 0 。请使用 原地 算法

示例

1

2

输入

matrix = [[1,1,1],[1,0,1],[1,1,1]]

matrix = [[0,1,2,0],[3,4,5,2],[1,3,1,5]]

输出

[[1,0,1],[0,0,0],[1,0,1]]

[[0,0,0,0],[0,4,5,0],[0,3,1,0]]

法一: 用 O(m+n)额外空间

这个解法通过记录需要置零的行和列,然后遍历矩阵进行置零操作。具体步骤如下:

  1. 记录需要置零的行和列:遍历矩阵,当遇到元素为0时,记录其行号和列号到集合 row_zero 和 col_zero 中。
  2. 置零操作:再次遍历矩阵,如果当前行或列在 row_zero 或 col_zero 中,则将当前元素置零。
class Solution:def setZeroes(self, matrix: List[List[int]]) -> None:"""Do not return anything, modify matrix in-place instead."""row = len(matrix)col = len(matrix[0])row_zero = set()col_zero = set()for i in range(row):for j in range(col):if matrix[i][j] == 0:row_zero.add(i)col_zero.add(j)for i in range(row):for j in range(col):if i in row_zero or j in col_zero:matrix[i][j] = 0

时间复杂度

空间复杂度

O(mn)

O(m+n)

其中 m 和 n 分别是矩阵的行数和列数

法二: O(1)空间

这个解法通过使用矩阵的第一行和第一列来记录需要置零的行和列,从而优化了空间复杂度。具体步骤如下:

  1. 检查第一行和第一列是否有零:分别遍历第一行和第一列,记录是否存在零,以便后续处理。
  2. 使用第一行和第一列作为标志位:遍历矩阵的其余部分,如果某个元素为零,则将该元素所在行的第一个元素和所在列的第一个元素置零。
  3. 根据标志位置零:再次遍历矩阵的其余部分,如果某行的第一个元素或某列的第一个元素为零,则将该元素置零。
  4. 处理第一行和第一列:根据之前的记录,如果第一行或第一列原本有零,则将整行或整列置零。
class Solution:def setZeroes(self, matrix: List[List[int]]) -> None:"""Do not return anything, modify matrix in-place instead."""row = len(matrix)col = len(matrix[0])row0_flag = Falsecol0_flag = False# 找第一行是否有0for j in range(col):if matrix[0][j] == 0:row0_flag = Truebreak# 第一列是否有0for i in range(row):if matrix[i][0] == 0:col0_flag = Truebreak# 把第一行或者第一列作为 标志位for i in range(1, row):for j in range(1, col):if matrix[i][j] == 0:matrix[i][0] = matrix[0][j] = 0#print(matrix)# 置0for i in range(1, row):for j in range(1, col):if matrix[i][0] == 0 or matrix[0][j] == 0:matrix[i][j] = 0if row0_flag:for j in range(col):matrix[0][j] = 0if col0_flag:for i in range(row):matrix[i][0] = 0

简化版

class Solution:def setZeroes(self, matrix: List[List[int]]) -> None:"""Do not return anything, modify matrix in-place instead."""flag_col = Falserow = len(matrix)col = len(matrix[0])for i in range(row):if matrix[i][0] == 0: flag_col = Truefor j in range(1,col):if matrix[i][j] == 0:matrix[i][0] = matrix[0][j] = 0for i in range(row - 1, -1, -1):for j in range(col - 1, 0, -1):if matrix[i][0] == 0 or matrix[0][j] == 0:matrix[i][j] = 0if flag_col == True: matrix[i][0] = 0

时间复杂度

空间复杂度

O(mn)

O(1)

其中 m 和 n 分别是矩阵的行数和列数

螺旋矩阵:中等

给你一个 m 行 n 列的矩阵 matrix ,请按照 顺时针螺旋顺序 ,返回矩阵中的所有元素。

示例

1

2

输入

matrix = [[1,2,3],[4,5,6],[7,8,9]]

matrix = [[1,2,3,4],[5,6,7,8],[9,10,11,12]]

输出

[1,2,3,6,9,8,7,4,5]

[1,2,3,4,8,12,11,10,9,5,6,7]

总体思路

根据题意,我们从左上角 (0,0) 出发,按照「右下左上」的顺序前进:

  1. 首先向右走,如果到达矩阵边界,则向右转 90o,前进方向变为向下。
  2. 然后向下走,如果到达矩阵边界,则向右转 90o,前进方向变为向左。
  3. 然后向左走,如果到达矩阵边界,则向右转 90o,前进方向变为向上。
  4. 然后向上走,先从 7 走到 4,然后从 4 准备向上走,但上面的 1 是一个已经访问过的数字,那么向右转 90o,前进方向变为向右。
  5. 重复上述过程,直到答案的长度为 mn。

法一:标记

对于已经访问过的数字,可将其标记为 ∞ 或者空,从而避免重复访问。

用一个长为 4 的方向数组 DIRS=[(0,1),(1,0),(0,−1),(−1,0)] 分别表示右下左上 4 个方向。同时用一个下标 di 表示当前方向,初始值为 0。

每次移动,相当于把行号增加 DIRS[di][0],把列号增加 DIRS[di][1]。

向右转90o,相当于把 di 增加 1,但在 di=3 时要回到 di=0。两种情况合二为一,把 di 更新为 (di+1)mod4

DIRS = (0, 1), (1, 0), (0, -1), (-1, 0)  # 右下左上class Solution:def spiralOrder(self, matrix: List[List[int]]) -> List[int]:m, n = len(matrix), len(matrix[0])ans = []i = j = di = 0for _ in range(m * n):  # 一共走 mn 步ans.append(matrix[i][j])matrix[i][j] = None  # 标记,表示已经访问过(已经加入答案)x, y = i + DIRS[di][0], j + DIRS[di][1]  # 下一步的位置# 如果 (x, y) 出界或者已经访问过if x < 0 or x >= m or y < 0 or y >= n or matrix[x][y] is None:di = (di + 1) % 4  # 右转 90°i += DIRS[di][0]j += DIRS[di][1]  # 走一步return ans

时间复杂度

空间复杂度

O(mn)

O(1)

其中 m 和 n 分别是矩阵的行数和列数

法二:不标记

上面的做法需要修改 matrix,能否不修改呢?

示例 2 这 12 个数字,可以分为以下 5 组:

  1. 1→2→3→4
  2. 8→12
  3. 11→10→9
  4. 5
  5. 6→7

其中第 1,3,5 组都是向右或者向左走的,长度依次为 4,3,2,这是一个从 n=4 开始的逐渐递减的序列。

其中第 2,4 组都是向下或者向上走的,长度依次为 2,1,这是一个从 m−1=2 开始的逐渐递减的序列。

由于走的步数是有规律的,我们可以精确地控制在每个方向上要走多少步,无需判断是否出界、是否重复访问:

  1. 从 (0,−1) 开始。
  2. 一开始,向右走 n 步,每次先走一步,再把数字加入答案。走 n 步即 1→2→3→4,矩阵第一排的数都加入了答案。
  3. 然后向下走 m−1 步,即 8→12。
  4. 然后向左走 n−1 步,即 11→10→9。
  5. 然后向上走 m−2 步,即 5。
  6. 然后向右走 n−2 步,即 6→7。
  7. 重复上述过程,直到答案的长度等于 mn。

代码实现时,可以这样简化代码:

  1. 一开始走 n 步。
  2. 把 n,m 分别更新为 m−1,n,这样下一轮循环又可以走 n 步(相当于走了 m−1 步),无需修改其他逻辑。
  3. 把 n,m 分别更新为 m−1,n,这样下一轮循环又可以走 n 步(相当于走了 n−1 步)。
  4. 把 n,m 分别更新为 m−1,n,这样下一轮循环又可以走 n 步(相当于走了 m−2 步)。
  5. 依此类推,每次只需把 n,m 分别更新为 m−1,n 即可。

如何说明这个规律的正确性?在上文的例子中,我们把示例 2 分成了 5 组。一般地,每 4 组,我们会把矩阵最外面一圈去掉(就像剥洋葱),矩阵的行数会减少 2,列数会减少 2。所以行列的减少是有规律的。

DIRS = (0, 1), (1, 0), (0, -1), (-1, 0)  # 右下左上class Solution:def spiralOrder(self, matrix: List[List[int]]) -> List[int]:m, n = len(matrix), len(matrix[0])size = m * nans = []i, j, di = 0, -1, 0  # 从 (0, -1) 开始while len(ans) < size:dx, dy = DIRS[di]for _ in range(n):  # 走 n 步(注意 n 会减少)i += dxj += dy  # 先走一步ans.append(matrix[i][j])  # 再加入答案di = (di + 1) % 4  # 右转 90°n, m = m - 1, n  # 减少后面的循环次数(步数)return ans

时间复杂度

空间复杂度

O(mn)

O(1)

其中 m 和 n 分别是矩阵的行数和列数

旋转图像:中等

给定一个 × n 的二维矩阵 matrix 表示一个图像。请你将图像顺时针旋转 90 度。

你必须在 原地 旋转图像,这意味着你需要直接修改输入的二维矩阵。请不要 使用另一个矩阵来旋转图像。

示例

1

2

输入

matrix = [[1,2,3],[4,5,6],[7,8,9]]

matrix = [[5,1,9,11],[2,4,8,10],[13,3,6,7],[15,14,12,16]]

输出

[[7,4,1],[8,5,2],[9,6,3]]

[[15,13,2,5],[14,3,4,1],[12,6,8,9],[16,7,10,11]]

题意

把一个方阵(n×n 的矩阵)顺时针旋转 90o。

要求:不能创建另一个矩阵,空间复杂度必须是 O(1)。

分析

顺时针旋转90o后,位于 (i,j) 的元素去哪了?

竖着看:

  1. 第一列的元素去到第一行。
  2. 第二列的元素去到第二行。
  3. ……
  4. 第 j 列的元素去到第 j 行。

横着看:

  1. 第一行的元素去到最后一列。
  2. 第二行的元素去到倒数第二列。
  3. ……
  4. 第 i 行的元素去到第 n−1−i 列。

所以位于 i 行 j 列的元素,去到 j 行 n−1−i 列,即 (i,j)→(j,n−1−i)。

两次翻转等于一次旋转

(i,j)→(j,n−1−i) 可以通过两次翻转操作得到:

  1. 转置:把矩阵按照主对角线翻转,位于 (i,j) 的元素去到 (j,i)。
  2. 行翻转:把每一行的内部元素翻转,位于 (j,i) 的元素去到 (j,n−1−i)。

示例 1 的操作过程如下:

注:一般地,把一个点绕 O 旋转任意 θ 角度,可以通过两个翻转操作实现。要求这两条翻转的对称轴,交点为 O 且夹角为θ2。对于本题,每个元素需要绕矩阵中心顺时针旋转90o,这可以通过关于主对角线翻转,关于垂直中轴翻转实现。这两条对称轴的交点为矩阵中心,且夹角为45o。

实现

  1. 转置:把主对角线下面的元素 matrix[i][j] 和(关于主对角线)对称位置的元素 matrix[j][i] 交换。
  2. 行翻转:遍历每一行 row=matrix[i],把左半边的元素 row[j] 和(关于垂直中轴)对称位置的元素 row[n−1−j] 交换。或者,使用库函数翻转 row。

这两步操作都可以原地实现。

class Solution:def rotate(self, matrix: List[List[int]]) -> None:n = len(matrix)# 第一步:转置for i in range(n):for j in range(i):  # 遍历对角线下方元素matrix[i][j], matrix[j][i] = matrix[j][i], matrix[i][j]# 第二步:行翻转for row in matrix:row.reverse()

时间复杂度

空间复杂度

O(n2)

O(1)

其中n 是矩阵的行数和列数

http://www.dtcms.com/a/356989.html

相关文章:

  • 微服务入门指南(一):从单体架构到服务注册发现
  • 将自己的jar包发布到maven中央仓库(2025-08-29)
  • 【Web安全】文件上传下载安全测试的全面剖析与实践指南
  • 如何在实际应用中选择Blaze或Apache Gluten?
  • 深入解析PCIe 6.0拓扑架构:从根复合体到端点的完整连接体系
  • 【国内电子数据取证厂商龙信科技】ES 数据库重建
  • shell命令扩展
  • Qt类-扩充_xiaozuo
  • ArcGIS Pro中 Nodata和nan 黑边的处理
  • 沃尔玛AI系统Wally深度拆解:零售业库存周转提速18%,动态定价争议与员工转型成热议点
  • 【lua】Lua 入门教程:从环境搭建到基础编程
  • Java深拷贝与浅拷贝核心解析
  • C primer plus (第六版)第十一章 编程练习第1,2,3,4题
  • typescript postgres@3.x jsonb数据插入绕过类型检测错误问题
  • Linux驱动学习-spi接口
  • 手写一个Spring框架
  • 【树论】树上启发式合并
  • ansible的playbook练习题
  • 短剧小程序系统开发:助力影视行业数字化转型
  • 算法---字符串
  • Speculation Rules API
  • PDF转图片工具实现
  • 天气查询系统
  • 2025_WSL2_Ubuntu20.04_C++20_concept 环境配置
  • el-select多选下拉框出现了e611
  • MySQL 中ORDER BY排序规则
  • 物联网平台中的Swagger(二)安全认证与生产实践
  • Socket编程核心API与结构解析
  • 【C++】掌握类模板:多参数实战技巧
  • 构筑沉浸式3D世界:渲染、资源与体验的协同之道