leetcode刷题笔记(七)—双指针双索引的用法遍历数组二

前言

接上一篇的双索引问题

1、给定一个含有 n 个正整数的数组和一个正整数 s ,找出该数组中满足其和 ≥ s 的长度最小的连续子数组。如果不存在符合条件的连续子数组,返回 0。

示例:

输入: s = 7, nums = [2,3,1,2,4,3]
输出: 2
解释: 子数组 [4,3] 是该条件下的长度最小的连续子数组。
进阶:

如果你已经完成了O(n) 时间复杂度的解法, 请尝试 O(n log n) 时间复杂度的解法。
思路1:
碰到这种子序列子数组子字符串的问题,可以想想滑动窗口的用法,我们从左向右扫描,时间复杂度为O(n),缩小窗口的循环几乎考虑,所以并没有n^2的复杂度
情况1:当区间的和小于s的时候,就继续向右移动,求和,直到和大于等于s的时候,我们再来一层循环
情况2:在内存循环中,我们的目的是尝试去缩小区间,先记录下区间长度,取已有长度和现在长度更小的,然后缩小区间,当这一层出来时,又可以继续向后遍历了,最终输出最小的区间长度
根据思路写代码就很容易了

def minSubArrayLen(self, s: int, nums: List[int]) -> int:

        if len(nums) == 0:
            return 0
        l = 0 #用来尝试缩小区间的
        cur_sum = 0
        res = len(nums) + 1

        for i in range(len(nums)):#右指针就是用来遍历的指针
            cur_sum += nums[i]

            while cur_sum >= s:#遍历找到了和大于s时候

                res = min(res,i - l + 1) #获取长度

                #减去左边的值,尝试缩小长度
                cur_sum -= nums[l] 
                l += 1

        
        return 0 if res == len(nums) + 1 else res

还有一种二分法的思路,没有这种解法好,感兴趣的可以了解一下,我就不废脑细胞了
思路2:二分法,利用“数组是正整数”这个条件,构造前缀和数组,这个前缀和数组一定是严格增加的。 任意区间和可以通过前缀和数组得到,这是我们常见的一种做法。 起点固定的时候,区间越长,区间和越大。

public class Solution {

    public int minSubArrayLen(int s, int[] nums) {
        int len = nums.length;
        if (len == 0) {
            return 0;
        }
        // 构造前缀和数组
        // 因为 nums 全都是正整数,因此 preSum 严格单调增加
        int[] preSum = new int[len];
        preSum[0] = nums[0];
        for (int i = 1; i < len; i++) {
            preSum[i] = preSum[i - 1] + nums[i];
        }
        // 因为前缀和数组严格单调增加,因此我们可以使用二分查找算法
        // 最后一位没有下一位了,所以外层遍历到最后一位的前一位就可以了
        int ret = len + 1;
        for (int i = 0; i < len - 1; i++) {
            // 计算区间和
            int l = i;
            int r = len - 1;
            // 设置成一个比较大的数,但是这个数有下界
            // i 的最大值是 len - 2,
            // ans - i + 1 >= len + 1
            // ans >= i + len = 2 * len -2
            int ans = 2 * len - 2;
            // int ans = 2 * len - 1; 能通过
            // int ans = 2 * len - 3; 不能通过
            // 退出循环的条件是 l > r
            while (l <= r) {
                int mid = l + (r - l) / 2;
                // 计算一下区间和,找一个位置,使得这个位置到索引 i 的区间和为 s
                // 13 14 15 17 19 20
                int segmentSum = preSum[mid] - (i == 0 ? 0 : preSum[i - 1]);
                if (segmentSum >= s) {
                    ans = mid;
                    r = mid - 1;
                } else {
                    l = mid + 1;
                }
            }
            ret = Integer.min(ans - i + 1, ret);
        }
        if (ret == len + 1) {
            return 0;
        }
        return ret;
    }
}

2、给定一个字符串 s 和一个非空字符串 p,找到 s 中所有是 p 的字母异位词的子串,返回这些子串的起始索引。

字符串只包含小写英文字母,并且字符串 s 和 p 的长度都不超过 20100。

说明:

字母异位词指字母相同,但排列不同的字符串。
不考虑答案输出的顺序。
示例 1:

输入:
s: “cbaebabacd” p: “abc”

输出:
[0, 6]

解释:
起始索引等于 0 的子串是 “cba”, 它是 “abc” 的字母异位词。
起始索引等于 6 的子串是 “bac”, 它是 “abc” 的字母异位词。
示例 2:

输入:
s: “abab” p: “ab”

输出:
[0, 1, 2]

解释:
起始索引等于 0 的子串是 “ab”, 它是 “ab” 的字母异位词。
起始索引等于 1 的子串是 “ba”, 它是 “ab” 的字母异位词。
起始索引等于 2 的子串是 “ab”, 它是 “ab” 的字母异位词。

思路:
用一个map去保存p中出现的字符计数,然后用滑窗遍历s,当右边界滑过时,就让对应的计数减1,而左边滑过就需要让计数加1,设置一个distance表示子串的长度,初始和p相等,然后,当滑窗滑动时,右边的字符如果在就让distance-1,如果左边的游标移动,就让distance+1,当长度为0时,就表示子串长度刚好为p的,而且,字符也满足正好是p中字符的条件。

这题的思想还是很难理解的:
1、用distance记录长度,限定了子串的长度,distance只有在子串的字符还在hash计数中有的时候才会减1,这保证了子串的长度刚好为p的长度时,子串的字符计数和p完全一致;
2、hash计数在什么时候变化呢?当右边滑窗边界增加之前,先判断是否这个字符是出现在p中的去改变distance,然后让hash计数减1证明,已经用distance计数了
3、判断distance是否满足,满足就记录下左边游标,注意前半段会把滑窗给撕开,长度总是会在plen和plen+1之间变化,一旦为plen+1,就去调整左边,左边界的那个字符如果出现在p中,这时,它的计数肯定是大于等于0的,因为被右滑窗减过,所以要移出它,也得记得把distance恢复一,然后在移出这个字符,让hash中计数加1。

总结这个滑窗思想就是利用了两个游标此消彼长的特点,然后处理好细节就行了,有点难理解

    def findAnagrams(self, s: str, p: str) -> List[int]:

        from collections import defaultdict
        dict_hash = defaultdict(int)
        size = len(s)
        l = 0
        r = 0
        res = []

        #用hash记录p中字符的出现个数
        for c in p:
            dict_hash[c] += 1

        plen = len(p)
        distance = plen #用来统计长度
        while r < size:
            #右边界增加
            if dict_hash[s[r]] > 0:
                #当前字符出现在hash中,让距离减小1
                distance -= 1
            #让对应的s[r]减去1,本来不在就为负数了,本来在的就减1,减去该字符的一个计数
            dict_hash[s[r]] -= 1
            r += 1

            #证明找到了一个子串
            if distance == 0:
                res.append(l)
            if r - l == plen: #当左右边之间差了一个plen的时候,需要调整左边了,子串长度已经超过了p
                if dict_hash[s[l]] >= 0:#如果左边对应的值在p中,因为这个时候,hash值已经减过一次了
                    distance += 1 #让ditance加回1
                #恢复该字符的一个计数
                dict_hash[s[l]] += 1
                l += 1
                
          
        return res

思路二:
这种思路比较清晰,就是先给p中的字符做一个频数统计,然后遍历s的时候,统计子串的字符频数,一致的话就满足条件

  def findAnagrams(self, s: str, p: str) -> List[int]:

        res = []

        slen = len(s)
        plen = len(p)

        scnt = [0]*26
        pcnt = [0]*26

        for c in p:
            pcnt[ord(c) - ord('a')] += 1   

        for end in range(slen):

            if end >= plen:
                scnt[ord(s[end - plen]) - ord('a')] -= 1
            scnt[ord(s[end]) - ord('a')] += 1   
                 
            if scnt == pcnt:
                res.append(end - plen + 1)
        return res

3、给你一个字符串 S、一个字符串 T,请在字符串 S 里面找出:包含 T 所有字母的最小子串。

示例:

输入: S = “ADOBECODEBANC”, T = “ABC”
输出: “BANC”
说明:

如果 S 中不存这样的子串,则返回空字符串 “”。
如果 S 中存在这样的子串,我们保证它是唯一的答案。

思路:这题是比较难的
1、先用一个map去统计t中的字符个数,
2、然后使用两个游标,l,r,先去找s中包含t中所有元素的子串,用另一个map,每出现一个字符在t中就让这个新map中的值加1,然后当两个map中对应的字符计数相等时,代表这个字符满足了,用一个计数变量加以记录,当计数变量和需要的相等时,就证明,当前滑窗之中已经包含了t中所有的字符。
3、然后进行内部循环,尝试去缩短左边界,先更新结果字符串,选取较短的那个更新,然后如果左边界的字符是t中的话,就把我们新map对应的值减1,并且满足的计数变量也需要减1,否则就缩小左游标
4、重复第二步直到遍历完整个字符串


def minWindow(s, t) -> str:

    from collections import defaultdict
    dict_need = defaultdict(int)  # 模板的map
    dict_window = defaultdict(int)  # 统计子串的map
    l = r = 0  # 左右游标

    size = len(s)

    match_len = 0  # 匹配的len
    for c in t:
        dict_need[c] += 1
    need_len = len(dict_need)  # 需要的len
    res = ''
    flag = True  # 判断是否是第一次满足匹配
    while r < size:

        if dict_need[s[r]] > 0:
            dict_window[s[r]] += 1
            if dict_need[s[r]] == dict_window[s[r]]:
                # 当一个字符完全满足时就让满足的加1
                match_len += 1
        r += 1

        while match_len == need_len:

            if flag:
                res = s[l:r]
            else:
                res = res if len(res) < len(s[l:r]) else s[l:r]  # 取更小长度的子串
            # 开始左移
            # 要是左边界字符是t中的,需要将window中的减1,此时match也要减1
            if dict_need[s[l]] > 0:
                dict_window[s[l]] -= 1
                if dict_window[s[l]] < dict_need[s[l]]: #只有window中的值比需要的小的时候才把满足的减1
                    match_len -= 1
            l += 1

    return res

总结:

滑窗思想在解决字符串子串问题中还是很好用的,虽然加上了一些东西以后,比较困难,多练习,多思考就可以解决的。
愿每一个程序员都能被温柔相待

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值