刷题记录-HOT 100(二)

一、子串

3、最小覆盖子串问题

使用滑动窗口技术来解决这个问题,通过在 s 中维护一个窗口来包含 t 的所有字符,然后尝试收缩这个窗口以找到最小的子串。

  1. 初始化计数器:首先,我们使用 Counter 来记录 t 中字符的出现次数,并初始化窗口左右边界和一个 less 变量,表示当前窗口中还缺少多少种字符才能完全包含 t 中的所有字符。

  2. 扩展窗口:使用右指针遍历字符串 s,将每个字符添加到窗口中,同时更新窗口内的字符计数器。如果该字符在 t 中的计数达到了要求,less 减少。

  3. 收缩窗口:一旦窗口中包含了 t 中的所有字符(即 less == 0),尝试从左边开始收缩窗口,直到窗口不再包含 t 的所有字符。每次收缩时,检查当前窗口是否比之前记录的最小窗口更小,如果是,更新最小窗口的边界。

  4. 返回结果:最后,如果找到了有效的窗口,返回这个窗口的字符串;如果没有找到,返回空字符串。

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,更新 prepre + numnum 之间的较大值。这一步是动态规划的核心,表示当前元素是否应该加入前面的子数组还是单独作为新的子数组开始。

  • 更新最大值:在每一步中,将 ans 更新为 anspre 的较大值,以确保 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 中,然后更新 sted 为当前区间的起始点和结束点。
  • 处理最后一个区间:在遍历完所有区间后,最后一个区间还未被加入到结果列表中,因此需要将它加入到结果列表。

  • 返回结果:最终返回合并后的区间列表 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 次。可以通过以下步骤来实现这个目标,这种方法的核心思想是通过三次反转来达到旋转数组的效果:

  1. 先将整个数组反转。
  2. 然后反转数组的前 k 个元素。
  3. 最后反转数组的剩余部分。

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
    • 初始化一个临时变量 tmp1,用于计算从右到左的后缀乘积。
  • 计算下三角乘积(前缀乘积)

    • 从左到右遍历数组 nums,对于每个位置 ians[i] 保存的是 nums 数组中从 0i-1 的所有元素的乘积。即 ans[i] = ans[i-1] * nums[i-1]
  • 计算上三角乘积(后缀乘积)并与前缀乘积相乘

    • 从右到左遍历数组 numstmp 用来保存当前元素右边所有元素的乘积。对于每个位置 i,将 tmpans[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 到 nn 是数组长度)之间,并且 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
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,并初始化四个边界变量 topbottomleftright,分别表示当前层的上边界、下边界、左边界和右边界。
    • 初始化一个空列表 ans,用于存储遍历结果。
  • 螺旋遍历矩阵

    • 使用一个 while 循环,条件是 top <= bottomleft <= right,表示当前还有未遍历的矩阵层。
    • 在每次循环中,依次按照从左到右、从上到下、从右到左、从下到上的顺序遍历当前边界的元素,将它们添加到 ans 列表中。
  • 更新边界

    • 每遍历一条边后,更新对应的边界:
      • 从左到右遍历完上边界后,top 加 1。
      • 从上到下遍历完右边界后,right 减 1。
      • 从右到左遍历完下边界后,bottom 减 1。
      • 从下到上遍历完左边界后,left 加 1。
    • 在更新 topbottomleftright 后,需要检查是否还存在未遍历的区域,如果已经没有可遍历的区域,则退出循环。
  • 返回结果

    • 当所有的层都被遍历完成后,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、相交链表

利用两个指针 pApB 分别遍历链表 headAheadB。当一个指针到达链表末尾时,将其重定向到另一个链表的头部。这种方式使得两个指针在第二次遍历时可以同步到达相交点(如果存在)。如果链表不相交,两个指针最终会同时到达 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 及后续部分链表。每次递归调用会将当前链表缩短,直至到达最后一个节点。
  • 反转当前节点指向

    • 在递归回溯阶段,逐层返回新链表的头节点,并逐步将当前节点的 nextnext 指向当前节点,实现局部的链表反转。然后将当前节点的 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、环形链表

快慢指针法来判断链表中是否存在环。通过两个指针(slowfast),它们以不同的速度遍历链表。如果链表中存在环,那么这两个指针最终会相遇;如果链表无环,则 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

  • 17
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值