一、子串
3、最小覆盖子串问题
使用滑动窗口技术来解决这个问题,通过在 s
中维护一个窗口来包含 t
的所有字符,然后尝试收缩这个窗口以找到最小的子串。
-
初始化计数器:首先,我们使用
Counter
来记录t
中字符的出现次数,并初始化窗口左右边界和一个less
变量,表示当前窗口中还缺少多少种字符才能完全包含t
中的所有字符。 -
扩展窗口:使用右指针遍历字符串
s
,将每个字符添加到窗口中,同时更新窗口内的字符计数器。如果该字符在t
中的计数达到了要求,less
减少。 -
收缩窗口:一旦窗口中包含了
t
中的所有字符(即less == 0
),尝试从左边开始收缩窗口,直到窗口不再包含t
的所有字符。每次收缩时,检查当前窗口是否比之前记录的最小窗口更小,如果是,更新最小窗口的边界。 -
返回结果:最后,如果找到了有效的窗口,返回这个窗口的字符串;如果没有找到,返回空字符串。
class Solution:
def minWindow(self, s: str, t: str) -> str:
left=0;n=len(s)
ans_left,ans_right=-1,n
cnt_t=Counter(t)
cnt_s=Counter()
less=len(cnt_t)
for right,ch in enumerate(s):
cnt_s[ch]+=1
if cnt_s[ch]==cnt_t[ch]:
less-=1
while less==0:
if right-left<ans_right-ans_left:
ans_left,ans_right=left,right
x=s[left]
if cnt_s[x]==cnt_t[x]:
less+=1
cnt_s[x]-=1
left+=1
return "" if ans_left<0 else s[ans_left:ans_right+1]
二、普通数组
1、最大子数组和
用动态规划的思想来解决这个问题。通过遍历数组,并在每一步计算以当前元素为结尾的最大子数组和,同时更新全局的最大子数组和。
-
初始化变量:定义两个变量
pre
和 ans
。其中pre
表示以当前元素为结尾的子数组的最大和,ans
表示迄今为止找到的最大子数组和。 -
遍历数组:遍历数组中的每个元素 num,更新
pre
为pre + num
和 num 之间的较大值。这一步是动态规划的核心,表示当前元素是否应该加入前面的子数组还是单独作为新的子数组开始。 -
更新最大值:在每一步中,将
ans
更新为ans
和pre
的较大值,以确保ans
始终存储的是遍历到当前元素时的最大子数组和。 -
返回结果:在遍历完数组之后,
ans
即为最大子数组和,返回这个值。
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
pre=0
ans=nums[0]
for num in nums:
pre=max(num,pre+num)
ans=max(pre,ans)
return ans
2、合并区间
首先对区间进行排序(按照每个区间的起始端点),然后通过遍历的方式来合并重叠的区间,最终得到合并后的区间列表。
-
排序区间:首先根据每个区间的起始值对所有区间进行排序,这样可以确保在遍历时每个区间的开始时间都是递增的,方便后续的合并操作。
-
初始化起始和结束变量:在遍历区间之前,先初始化当前区间的起始点
st
和结束点ed
为第一个区间的起始点和结束点。 -
遍历区间:从第二个区间开始遍历每个区间:
- 如果当前区间的起始点小于或等于当前已合并区间的结束点
ed
,说明两个区间有重叠,此时更新已合并区间的结束点ed
为两个区间中结束点的最大值。 - 如果当前区间的起始点大于
ed
,说明没有重叠,将当前已合并的区间加入到结果列表ans
中,然后更新st
和ed
为当前区间的起始点和结束点。
- 如果当前区间的起始点小于或等于当前已合并区间的结束点
-
处理最后一个区间:在遍历完所有区间后,最后一个区间还未被加入到结果列表中,因此需要将它加入到结果列表。
-
返回结果:最终返回合并后的区间列表
ans
。
class Solution:
def merge(self, intervals: List[List[int]]) -> List[List[int]]:
intervals.sort(key=lambda x:x[0])
st,ed=intervals[0][0],intervals[0][1]
ans=[]
for i in range(1,len(intervals)):
if intervals[i][0]<=ed:
ed=max(ed,intervals[i][1])
else:
ans.append([st,ed])
st=intervals[i][0]
ed=intervals[i][1]
ans.append([st,ed])
return ans
3、轮转数组
将一个数组中的元素向右旋转 k
次。可以通过以下步骤来实现这个目标,这种方法的核心思想是通过三次反转来达到旋转数组的效果:
- 先将整个数组反转。
- 然后反转数组的前
k
个元素。 - 最后反转数组的剩余部分。
k
取余是为了处理旋转次数 k
大于数组长度的情况,防止不必要的多次旋转。
class Solution:
def reverse(self,nums,st,ed):
while st<ed:
nums[st],nums[ed]=nums[ed],nums[st]
st+=1;ed-=1
def rotate(self, nums: List[int], k: int) -> None:
"""
Do not return anything, modify nums in-place instead.
"""
k%=len(nums)
self.reverse(nums,0,len(nums)-1)
self.reverse(nums,0,k-1)
self.reverse(nums,k,len(nums)-1)
4、除自身以外的数组的乘积
通过两次遍历数组(一次从左到右,一次从右到左)来分别计算每个位置的前缀乘积(下三角)和后缀乘积(上三角),最后将两者相乘得到结果。
-
初始化:
- 首先获取数组
nums
的长度n
。 - 创建一个大小为
n
的结果数组ans
,初始时所有元素都设置为1
,因为乘法的单位元是1
。 - 初始化一个临时变量
tmp
为1
,用于计算从右到左的后缀乘积。
- 首先获取数组
-
计算下三角乘积(前缀乘积):
- 从左到右遍历数组
nums
,对于每个位置i
,ans[i]
保存的是nums
数组中从0
到i-1
的所有元素的乘积。即ans[i] = ans[i-1] * nums[i-1]
。
- 从左到右遍历数组
-
计算上三角乘积(后缀乘积)并与前缀乘积相乘:
- 从右到左遍历数组
nums
,tmp
用来保存当前元素右边所有元素的乘积。对于每个位置i
,将tmp
与ans[i]
相乘,更新ans[i]
的值为ans[i] * tmp
。 - 更新
tmp
的值为当前元素的后缀乘积。
- 从右到左遍历数组
-
返回结果:
- 最终的
ans
数组即为所求的结果数组,包含每个位置的元素在不包括自身的情况下的乘积。
- 最终的
class Solution:
def productExceptSelf(self, nums: List[int]) -> List[int]:
n=len(nums)
ans=[1]*n;tmp=1
for i in range(1,n):
ans[i]=ans[i-1]*nums[i-1]
for i in range(n-2,-1,-1):
tmp*=nums[i+1]
ans[i]*=tmp
return ans
5、缺失的第一个整数
核心思路是通过将数组中的每个数字放到它应该在的位置上(即数字 i
放在数组的 i-1
位置),然后扫描数组找到第一个不符合条件的位置。这个位置的索引加 1 就是第一个缺失的正整数。
-
交换数字到正确位置:
- 遍历数组
nums
中的每个元素nums[i]
。 - 如果
nums[i]
在 1 到n
(n
是数组长度)之间,并且nums[i]
还没有在正确的位置上(即nums[i]
应该放在nums[nums[i] - 1]
位置上),那么将nums[i]
和nums[nums[i] - 1]
交换。 - 继续检查交换后的
nums[i]
,直到它不需要再交换。【用while循环实现】
- 遍历数组
-
查找第一个缺失的正整数:
- 再次遍历数组,查找第一个位置
i
,使得nums[i] != i + 1
。 - 返回
i + 1
作为第一个缺失的正整数。
- 再次遍历数组,查找第一个位置
-
边界处理:
- 如果所有数字都在正确的位置上,则数组包含从 1 到
n
的所有正整数,因此返回n + 1
。
- 如果所有数字都在正确的位置上,则数组包含从 1 到
class Solution:
def firstMissingPositive(self, nums: List[int]) -> int:
n=len(nums)
for i in range(n):
while 1 <= nums[i] <= n and nums[i] != nums[nums[i] - 1]:
nums[nums[i] - 1], nums[i] = nums[i], nums[nums[i] - 1]
for i in range(n):
if nums[i]!=i+1:
return (i+1)
return n+1
三、矩阵
1、矩阵置零
具体做法是利用矩阵的第一行和第一列来记录每行和每列是否需要置零,同时使用一个标志位flag_col来记录第一列是否需要置零。
第一列m[i][0]是用来记录i行是否需要置零的,第一行m[0][j]是记录j列是否需要置零的。
思路主要分为两步:
- 标记:遍历每一行,先检查每行第一列的元素m[i][0],如果为0,则标志位flag_col=True,在后面置零环节需要最后将第一列元素置零;再遍历一行中的每个元素,如果有元素m[i][j]为0,则标记它的行、列m[i][0]=m[0][j]=0。
- 置零:遍历每一行,如果元素m[i][j]的行列的标志位有一个为0,则置元素为0,最后根据flag_col判断该行第一列元素是否为0。注意遍历的顺序,是从最后一行开始遍历,因为如果从第一行开始遍历,可能会用0覆盖掉原有的标记。
class Solution:
def setZeroes(self, matrix: List[List[int]]) -> None:
m,n=len(matrix),len(matrix[0])
flag_col0=False
for i in range(m):
if matrix[i][0]==0:
flag_col0=True
for j in range(1,n):
if matrix[i][j]==0:
matrix[i][0]=matrix[0][j]=0
for i in range(m-1,-1,-1):
for j in range(1,n):
if not matrix[i][0] or not matrix[0][j]:
matrix[i][j]=0
if flag_col0:
matrix[i][0]=0
2、螺旋矩阵
从矩阵的左上角开始,依次向右、向下、向左、向上重复遍历,逐层向内,直到遍历完整个矩阵。
-
初始化边界和结果容器:
- 首先,获取矩阵的行数
m
和列数n
,并初始化四个边界变量top
、bottom
、left
、right
,分别表示当前层的上边界、下边界、左边界和右边界。 - 初始化一个空列表
ans
,用于存储遍历结果。
- 首先,获取矩阵的行数
-
螺旋遍历矩阵:
- 使用一个
while
循环,条件是top <= bottom
且left <= right
,表示当前还有未遍历的矩阵层。 - 在每次循环中,依次按照从左到右、从上到下、从右到左、从下到上的顺序遍历当前边界的元素,将它们添加到
ans
列表中。
- 使用一个
-
更新边界:
- 每遍历一条边后,更新对应的边界:
- 从左到右遍历完上边界后,
top
加 1。 - 从上到下遍历完右边界后,
right
减 1。 - 从右到左遍历完下边界后,
bottom
减 1。 - 从下到上遍历完左边界后,
left
加 1。
- 从左到右遍历完上边界后,
- 在更新
top
和bottom
或left
和right
后,需要检查是否还存在未遍历的区域,如果已经没有可遍历的区域,则退出循环。
- 每遍历一条边后,更新对应的边界:
-
返回结果:
- 当所有的层都被遍历完成后,
ans
列表即为所求的螺旋顺序遍历结果,返回该列表。
- 当所有的层都被遍历完成后,
class Solution:
def spiralOrder(self, matrix: List[List[int]]) -> List[int]:
m,n=len(matrix),len(matrix[0])
top,bottom,left,right=0,m-1,0,n-1
ans=[]
while top<=bottom and left<=right:
for i in range(left,right+1):
ans.append(matrix[top][i])
top+=1
for i in range(top,bottom+1):
ans.append(matrix[i][right])
right-=1
if top<=bottom and left<=right:
for i in range(right,left-1,-1):
ans.append(matrix[bottom][i])
bottom-=1
for i in range(bottom,top-1,-1):
ans.append(matrix[i][left])
left+=1
return ans
3、旋转图像
通过两步操作:水平翻转和主对角线翻转,可以高效地实现矩阵的顺时针旋转。这种方法基于矩阵的对称性质,可以在 O(n^2)
的时间复杂度和 O(1)
的空间复杂度内完成任务。
-
水平翻转:
- 将矩阵的每一行上下对调,即第
i
行与第n-i-1
行互换。这一步骤相当于将矩阵沿着水平方向翻转。
- 将矩阵的每一行上下对调,即第
-
主对角线翻转:
- 将矩阵沿主对角线(从左上到右下的对角线)进行翻转。具体来说,交换
matrix[i][j]
和matrix[j][i]
的值,这会将矩阵的行列进行互换。
- 将矩阵沿主对角线(从左上到右下的对角线)进行翻转。具体来说,交换
class Solution:
def rotate(self, matrix: List[List[int]]) -> None:
"""
Do not return anything, modify matrix in-place instead.
"""
n=len(matrix)
for i in range(n//2):
for j in range(n):
matrix[i][j],matrix[n-i-1][j]=matrix[n-i-1][j],matrix[i][j]
for i in range(n):
for j in range(i):
matrix[i][j],matrix[j][i]=matrix[j][i],matrix[i][j]
4、搜索二维矩阵 II
从矩阵的右上角开始遍历,根据当前值与目标值的比较结果,决定向左移动一列(减小当前值)或向下移动一行(增大当前值)。这种方法能在 O(m + n)
的时间复杂度内找到目标值。
-
初始化指针:
- 设定初始位置为矩阵的右上角,即
i = 0, j = n - 1
,其中i
为行索引,j
为列索引。
- 设定初始位置为矩阵的右上角,即
-
比较当前值与目标值:
- 如果
matrix[i][j] == target
,则找到了目标值,返回True
。 - 如果
matrix[i][j] > target
,则当前值大于目标值,应该向左移动一列(j -= 1
),因为左边的值较小。 - 如果
matrix[i][j] < target
,则当前值小于目标值,应该向下移动一行(i += 1
),因为下面的值较大。
- 如果
-
终止条件:
- 如果
i
超过矩阵的行数或j
小于 0,表示已经遍历完可能的路径,未找到目标值,返回False
。
- 如果
class Solution:
def searchMatrix(self, matrix: List[List[int]], target: int) -> bool:
m,n=len(matrix),len(matrix[0])
i,j=0,n-1
while i<m and j>=0:
if matrix[i][j]==target:
return True
elif matrix[i][j]>target:
j-=1
else:
i+=1
return False
四、链表
1、相交链表
利用两个指针 pA
和 pB
分别遍历链表 headA
和 headB
。当一个指针到达链表末尾时,将其重定向到另一个链表的头部。这种方式使得两个指针在第二次遍历时可以同步到达相交点(如果存在)。如果链表不相交,两个指针最终会同时到达 null
。
逻辑:
首先,A+B和B+A序列长度是一样的,两个指针的两次遍历走过的距离是相等的。
假如A是1-2-3-4-5,B是6-4-5。两个指针pa,pb初始化分别指向A和B的链表头结点。
pa第一遍遍历链表A,遍历到结尾的时候重定向到链表B的头结点,也就是pa要遍历的序列变成了1-2-3-4-5-6-4-5。
同理,pb第一遍遍历链表B,遍历到结尾的时候重定向到链表A的头结点,也就是pb要遍历的序列变成了6-4-5-1-2-3-4-5。
其次,如果存在相交节点的话,A链和B链从相交节点开始到序列结尾的长度是相同的(也就是从相交结点4开始,4-5是相交序列。相交部分一定在A和B的尾巴,如下面序列加粗部分所示)。
1-2-3-4-5-6-4-5
6-4-5-1-2-3-4-5
因此在第二次遍历的时候,pa和pb一定能够同时遍历到4。【A+B=B+A,且相交部分到结束的长度也相同】
class Solution:
def getIntersectionNode(self, headA: ListNode, headB: ListNode) -> Optional[ListNode]:
if not headA or not headB:
return None
pa=headA;pb=headB
while pa!=pb:
pa=pa.next if pa else headB
pb=pb.next if pb else headA
return pa
2、反转链表
使用递归的方式实现,逐层深入链表直至末尾节点,然后在递归回溯时逐层反转节点指向,最终返回新的头节点。
-
基准条件判断:
- 处理空链表或已经到达最后一个节点的情况。如果链表为空或只有一个节点,则返回该节点(作为新链表的头节点)。
-
递归深入到链表末尾:
- 如果链表有多个节点,递归调用
reverseList
以反转head.next
及后续部分链表。每次递归调用会将当前链表缩短,直至到达最后一个节点。
- 如果链表有多个节点,递归调用
-
反转当前节点指向:
- 在递归回溯阶段,逐层返回新链表的头节点,并逐步将当前节点的
next
的next
指向当前节点,实现局部的链表反转。然后将当前节点的next
置为None
,断开当前节点与后续节点的连接,避免形成环。
- 在递归回溯阶段,逐层返回新链表的头节点,并逐步将当前节点的
-
返回新头节点:
- 最终返回
new_head
,即递归过程中最早返回的节点(原链表的最后一个节点),作为反转后链表的头节点。
- 最终返回
class Solution:
def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:
if not head or not head.next:
return head
new_head=self.reverseList(head.next)
head.next.next=head
head.next=None
return new_head
3、回文链表
主要分三步走:
第一步,使用快慢指针法找到链表的中间位置,将链表分为前后两部分。
第二步,反转链表后半部分。
第三步,比较链表前半部分和后半部分。
class Solution:
def reverseList(self,head):
if not head or not head.next:
return head
new_head=self.reverseList(head.next)
head.next.next=head
head.next=None
return new_head
def isPalindrome(self, head: Optional[ListNode]) -> bool:
if not head:
return True
fast,slow=head,head
while fast and fast.next:
fast=fast.next.next
slow=slow.next
rev_second=self.reverseList(slow)
ph=head;ps=rev_second
while ph!=slow:
if ph.val!=ps.val:
return False
ph=ph.next
ps=ps.next
return True
4、环形链表
快慢指针法来判断链表中是否存在环。通过两个指针(slow
和 fast
),它们以不同的速度遍历链表。如果链表中存在环,那么这两个指针最终会相遇;如果链表无环,则 fast
指针会遍历到链表末尾,终止循环。
class Solution:
def hasCycle(self, head: Optional[ListNode]) -> bool:
slow=fast=head
while fast and fast.next:
slow=slow.next
fast=fast.next.next
if fast is slow:
return True
return False