Day2,Hot100(双指针+子串)

双指针

283. 移动零

要求:必须在原数组上操作,且保持非零元素的顺序

思路:把0移到后面 <—> 把非0元逐个移动到前面,剩余后面的就都是0

class Solution:
    def moveZeroes(self, nums: List[int]) -> None:
        """
        Do not return anything, modify nums in-place instead.
        """        
        idx = 0
        # 扫描非0元,并把它移动到前面
        for i in range(len(nums)):
            if nums[i] != 0:
                nums[idx] = nums[i]
                idx += 1
		# 剩余后面的位置就是0,直接赋值
        for i in range(idx, len(nums)):
            nums[i] = 0

15. 三数之和(X)

要求满足,i != j、i != k 且 j != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0,最终三元组不重复(顺序不一样也算重复)

根据<167. 两数之和 II - 输入有序数组>的思路
我们将数组转换为有序序列,然后逐个遍历i,找到剩余元素中符合num[i]+num[j]+num[k]=0j,k

class Solution:
    def threeSum(self, nums: List[int]) -> List[List[int]]:
        # 遍历所有的 i, 找到满足 nums[j] + nums[k] = nums[-i] 
        # 答案中不可以包含重复的三元组(当 nums[i] 相同时才会出现)
        # 所以跳过相同的 nums[i] 即可, 同时还要跳过相同的 nums[j] nums[k]

        nums.sort()
        ans = []
        n = len(nums)

        for i in range(n - 2):
            if i > 0 and nums[i] == nums[i-1]:
                continue
            j,k = i+1, n-1

            while j < k:
                s = nums[i] + nums[j] + nums[k]
                if s > 0:
                    k -= 1
                elif s < 0:
                    j += 1
                else:
                    ans.append([nums[i],nums[j],nums[k]])
                    # 跳过 j k 相同的数
                    j += 1 
                    while j < k and nums[j] == nums[j-1]:
                        j += 1
                    k -= 1
                    while j < k and nums[k] == nums[k+1]:
                        k -= 1
        return ans

前置题目
在这里插入图片描述
有序序列,所以从两头开始遍历

  • 如果 numbers[l] + numbers[r] > target,则需要移动较大的一端,找一个更小的r
  • 如果 numbers[l] + numbers[r] < target,则需要移动较小的一端,找一个更大的l
class Solution:
    def twoSum(self, numbers: List[int], target: int) -> List[int]:
        lens = len(numbers)
        l, r = 0,lens-1

        while l < r:
            if numbers[l] + numbers[r] > target:
                r -= 1
            elif numbers[l] + numbers[r] < target:
                l += 1
            else:
                return [l+1,r+1]

11. 盛最多水的容器

左右指针分别从两端开始逐步向中间移动(因为两端是宽度最大的)
但是,移动左指针还是右指针需要思考:
(1)怎么样才能提高面积?——增加高度,因为宽度一开始就是最大的
于是,我们在每次移动对应的高度较小的指针,希望找到更高的高度,增加面积

class Solution:
    def maxArea(self, height: List[int]) -> int:
        r = len(height) - 1
        l = 0
        ans = 0

        while l < r:
            h = min(height[l], height[r])
            area = (r-l) * h
            ans = max(ans, area)
            # 移动矮边,寻找更高边
            # 才有可能在宽度减小的情况下,提高面积
            if h == height[l]:
                l += 1 
            else:
                r -= 1
        return ans

42. 接雨水

方法一:前缀max + 后缀max
pre_max[i]=[0, i]中高度最高的,suf_max[i]=[i,len-1]中高度最高的
==>对每个位置求能接水的体积 = 左右max中短板的高度 - 位置i的高度)
==> v = min(pre_max[i],suf_max[i]) - height[i]

class Solution:
# 时间复杂度O(n)--3n
# 空间复杂度O(n)
    def trap(self, height: List[int]) -> int:
        n = len(height)
        pre_max = [height[0]] * n
        suf_max = [height[-1]] * n
        ans = 0
        for i in range(1,n):
            pre_max[i] = max(pre_max[i-1], height[i])
        for j in range(n-2, -1, -1):
            suf_max[j] = max(suf_max[j+1], height[j])

        for i in range(n):
            ans += min(pre_max[i], suf_max[i]) - height[i]
        return ans

方法二:双指针(优化空间,并把时间从3n优化到n)

(1)左右指针l,r,同时维护位置lpre_max和位置rsuf_max
(2)如果 pre_max < suf_max,说明位置lmin(pre_max, suf_max)=pre_max。因为位置rsuf_max更大了,right往中心移动只可能会加大suf_max。此时位置l的体积计算完了,于是left+=1
(3)同理,如果 pre_max > suf_max,说明位置rmin(pre_max, suf_max)=suf_max

class Solution:
    def trap(self, height: List[int]) -> int:
        n = len(height)
        pre_max, suf_max = 0, 0
        left, right = 0, n-1
        ans = 0

        while left < right:
            pre_max = max(pre_max, height[left])
            suf_max = max(suf_max, height[right])

            if pre_max < suf_max:
                ans += (pre_max -height[left])
                left += 1
            else:
                ans += (suf_max - height[right])
                right -= 1
        return ans

子串

560. 和为 K 的子数组(X)

求连续序列的和为k的个数

方法:哈希+前缀和

  • 前缀和 s,求出所有的前缀和
  • 类似<两数之和>,当前位置的前缀和为s,要求得和为k的区间,则前面要有满足和为s-k的前缀和
  • 于是用字典来统计前缀和为x的个数
from collections import defaultdict
class Solution:
    def subarraySum(self, nums: List[int], k: int) -> int:
        d = defaultdict(int)
        d[0] = 1
        s = 0
        ans = 0
        
        for i,v in enumerate(nums):
            s += v
            t = s-k
            if t in d:
                ans += d[t] 
            d[s] += 1

        return ans

其他题解

  • 假设k = 2,前缀和数组pre,我们知道 pre[r] = 5。 那么我们的目标就是求出满足r‘ < rpre[r] - k = pre[r’] 的 r’ 的个数,即 pre[r’] = 3
  • 如下图所示,pre(r’) = 3的前缀和存在两个,为了减少查询 r’ 的时间,我们可以使用哈希表。 hashmap(v) = k,表示前缀和为v出现的次数。
    • 注意!我们要计算的是当前位置 r 之前,有多少次 pre[r’] = pre[r] - k
    • 为实现该目的,我们应该从左往右边计算前缀和,边统计个数。
    • 这样在对每一个位置求 pre[r’] 的个数,就只包含 r’ < r的情况

  • 特别的,在初始时要默认前缀和为0的个数=1
  • 这样在 k = 5时,我们求 pre[r] - k = 0的个数才是正确的
from collections import defaultdict
class Solution:
    def subarraySum(self, nums: List[int], k: int) -> int:
        pre = 0
        times = {0:1}
        ans = 0

        for v in nums:
            pre += v
            ans += times.get(pre-k, 0)
            times[pre] = times.get(pre, 0) + 1
        return ans

239. 滑动窗口最大值

单调队列
(1)队列中只维护可能为最大值的元素
(2)当遇到4后,4左边的所有元素都不可能是最大值了,可以删除左边比4小的值
(3)即维护的队列,是从左到右递减的,于是最大值就是最左边的数

from collections import deque
class Solution:
    def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
        ans = []
        q = deque()  # 保存nums的下标

        for i, x in enumerate(nums):
            # 1、入队尾(右)
            while q and nums[q[-1]] <= x:
                q.pop()  # 维护单调性(保证入队后,左边没有<=x的元素)
            q.append(i)

            # 2、出队首(左)
            if i - q[0]  + 1 > k:
                q.popleft()
            
            # 3、记录窗口最大值
            if i >= k-1:
                ans.append(nums[q[0]])  # 由于单调性,队首就是最大元素

        return ans

76. 最小覆盖子串

前置同类型题目:209. 长度最小的子数组(双指针+前缀和)

左右指针l,r。s记录当前窗口的和
(1)如果 s >= target,此时通过移动左指针,缩小窗口,找到满足 >= target的更小区间
(2)当区间内的和小于 target 时,移动右指针,直到满足后,再重复(1)

class Solution:
    def minSubArrayLen(self, target: int, nums: List[int]) -> int:
        n = len(nums)
        s = 0
        ans = inf
        left = 0
        
        for right, x in enumerate(nums):
            s += x
            while s >= target:
                ans = min(ans, right - left + 1)
                s -= nums[left]
                left += 1

        return ans if ans <= n else 0

字符串的覆盖 <==> 子串中每个字符出现的次数都 >= 目标串中每个字符出现的次数
(该操作在python3.10及以上可以使用Counter来实现,直接对比两个Counter对象就是比较相同键的值的大小)

方法一:类比最小子数组的方式

from collections import Counter
class Solution:
    def minWindow(self, s: str, t: str) -> str:
        ans_left, ans_right = -1, len(s)  # 记录最小子串的idx
        left = 0
        cnt_s = Counter()
        cnt_t = Counter(t)

        for 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 += 1
                
        return s[ ans_left: ans_right+1] if ans_left != -1 else ""

优化方法一
存在的问题:方法一中,右指针每次移动都会判断子串是否是 t 的覆盖,十分耗时
优化方式:修改覆盖的判断方式,使用kinds,记录窗口内的元素数是否满足覆盖

from collections import Counter
class Solution:
    def minWindow(self, s: str, t: str) -> str:
        ans_left, ans_right = -1, len(s)  # 记录最小子串的idx
        left = 0
        cnt_s = Counter()
        cnt_t = Counter(t)
        n = len(cnt_t)
        kinds = 0  # 子串中val >= cnt_t中key的val的个数

        for right, c in enumerate(s):
            cnt_s[c] += 1
            if cnt_s[c] == cnt_t[c]:
                kinds += 1

            while kinds == n:  # 满足覆盖
                if right - left < ans_right - ans_left:
                    ans_left, ans_right = left, right

                x = s[left]
            # 当前val相等,说明移除left后,子串的val就不满足目标了,于是 kinds -= 1
                if cnt_s[x] == cnt_t[x]:  
                    kinds -= 1

                cnt_s[ x ] -= 1
                left += 1
        return s[ ans_left: ans_right+1] if ans_left != -1 else ""
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值