Top100 子串

1.560. 和为 K 的子数组

给你一个整数数组 nums 和一个整数 k ,请你统计并返回 该数组中和为 k 的子数组的个数 

子数组是数组中元素的连续非空序列。

示例 1:

输入:nums = [1,1,1], k = 2
输出:2

示例 2:

输入:nums = [1,2,3], k = 3
输出:2

提示:

  • 1 <= nums.length <= 2 * 10^4
  • -1000 <= nums[i] <= 1000
  • -10^7 <= k <= 10^7

思路:

数据规模也蛮大;思考是不是用滑动窗口,因为是子串,不允许排序,所以用滑动窗口,不断往后移,暴力枚举遍历。

代码:

暴力没成功,当个反面例子,我先贴一会

class Solution(object):
    def subarraySum(self, nums, k):
        i,k=0,0
        count=0
        for i in range(len(nums)):
            for j in range(i,len(nums)):
                if sum(nums[i:j])==k : 
                    print(i,j)
                    print(sum(nums[i:j]))
                    count+=1
        
        return count

换个思路:

使用前缀和的方法可以解决这个问题,因为我们需要找到和为k的连续子数组的个数。通过计算前缀和,我们可以将问题转化为求解两个前缀和之差等于k的情况。

假设数组的前缀和数组为prefixSum,其中prefixSum[i]表示从数组起始位置到第i个位置的元素之和。那么对于任意的两个下标i和j(i < j),如果prefixSum[j] - prefixSum[i] = k,即从第i个位置到第j个位置的元素之和等于k,那么说明从第i+1个位置到第j个位置的连续子数组的和为k。

通过遍历数组,计算每个位置的前缀和,并使用一个哈希表来存储每个前缀和出现的次数。在遍历的过程中,我们检查是否存在prefixSum[j] - k的前缀和,如果存在,说明从某个位置到当前位置的连续子数组的和为k,我们将对应的次数累加到结果中。

这样,通过遍历一次数组,我们可以统计出和为k的连续子数组的个数,并且时间复杂度为O(n),其中n为数组的长度。

每次的前缀和放入map哈希表中,哈希表的索引是pre,如果未出现过,则创建一个键值对。

class Solution {
public:
    int subarraySum(vector<int>& nums, int k) {
        unordered_map<int, int> mp;
        mp[0] = 1;
        int count = 0, pre = 0;
        for (auto& x:nums) {
            pre += x;
            if (mp.find(pre - k) != mp.end()) {
                count += mp[pre - k];
            }
            mp[pre]++;
        }
        return count;
    }
};

2.239. 滑动窗口最大值

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

返回 滑动窗口中的最大值 

示例 1:

输入:nums = [1,3,-1,-3,5,3,6,7], k = 3
输出:[3,3,5,5,6,7]
解释:
滑动窗口的位置                最大值
---------------               -----
[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

示例 2:

输入:nums = [1], k = 1
输出:[1]

提示:

  • 1 <= nums.length <= 10^5
  • -10^4 <= nums[i] <= 10^4
  • 1 <= k <= nums.length

思路:

1)先是暴力解法,就可以按照遍历的方式,从头到尾,以k为长度,然后寻找最大值放入答案的列表中。

2)稍微优化一点点,首先将前k个元素排序,然后进行遍历。遍历过程中需要判断:
是否大于最大值,如果是,则作为队首元素,队尾出队;如果小于最小值,则直接舍弃;如果处于中间,则队尾元素出队,该元素入队;结束后重新排序!(写到这自己都觉得蠢了,这叫优化??)

3)使用双项队列。这里为了效率和快捷使用了 deque 容器,也可以使用 list 容器。

声明了一个变量 deque<int>window,用于存储下标。这个变量有以下 特点:

①变量的最前端(也就是 window.front())是此次遍历的最大值的下标
②当我们遇到新的数时,将新的数和双项队列的末尾(也就是window.back())比较,如果末尾比新数小,则把末尾扔掉,直到该队列的末尾比新数大或者队列为空的时候才停止,做法有点像使用栈进行括号匹配。
③双项队列中的所有值都要在窗口范围内
特点一只是方便我们获取每次窗口滑动一格之后的最大值,我们可以直接通过 window.front() 获得。通过特点二,可以保证队列里的元素是从头到尾降序的,由于队列里只有窗口内的数,所以他们其实就是窗口内第一大,第二大,第三大... 的数。特点三就是根据题意设置的。但我们实际上只用比较现在的下标和 window.front() 就可以了,为什么呢?
因为只要窗口内第一大元素也就是这个 window.front() 在窗口内,那我们可以不用管第二大第三大元素在不在区间内了。因为答案一定是这个第一大元素。如果 window.front() 不在窗口内,则将其弹出,第二个大元素变成第一大元素,第三大元素变成第二大元素以此类推。

代码编写的过程还要时刻检查队列是否为空防止抛出异常,根据上面这些信息我们就可以编写此题的代码了。

代码:

class Solution {
public:
	vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        if(k==0)return {};
		vector<int>res;
		deque<size_t>window;
		/*Init K integers in the list*/
		for (size_t i = 0; i < k; i++) {
			while (!window.empty()  && nums[i] > nums[window.back()]) {
				window.pop_back();
			}
			window.push_back(i);
		}
		res.push_back(nums[window.front()]);
		/*End of initialization*/
		for (size_t i = k; i < nums.size(); i++) {
			if (!window.empty() && window.front() <= i - k) {
				window.pop_front();
			}
			while (!window.empty() && nums[i] > nums[window.back()]) {
				window.pop_back();
			}
			window.push_back(i);
			res.push_back(nums[window.front()]);
		}
		return res;
	}
};

3.76. 最小覆盖子串

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

注意:

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

示例 1:

输入:s = "ADOBECODEBANC", t = "ABC"
输出:"BANC"
解释:最小覆盖子串 "BANC" 包含来自字符串 t 的 'A'、'B' 和 'C'。

示例 2:

输入:s = "a", t = "a"
输出:"a"
解释:整个字符串 s 是最小覆盖子串。

示例 3:

输入: s = "a", t = "aa"
输出: ""
解释: t 中两个字符 'a' 均应包含在 s 的子串中,
因此没有符合条件的子字符串,返回空字符串。

提示:

  • m == s.length
  • n == t.length
  • 1 <= m, n <= 10^5
  • s 和 t 由英文字母组成

思路:

这个不会!直奔题解!

双指针,我们移动右指针同时统计可以匹配目标字符串的字符个数,当一个右指针可以匹配所有的目标字串时,我们更新左端点找到一个最短的子串。

代码:

class Solution {
public:
    string minWindow(string s, string t) {
        int n = s.size(), m = t.size();
        vector<int> res(2, INT_MAX);
        unordered_map<char, int> h;
        unordered_map<char, int> hs;
        for(auto e : t) hs[e]++;
        
        int cnt = 0;

        for(int i = 0, j = 0; i < n; i++)
        {
            h[s[i]]++;
            if(hs.find(s[i]) != hs.end())
            {
                if(h[s[i]] == hs[s[i]]) cnt++; // 统计当前s中可以匹配t的字符个数
            }
            
            while(cnt == hs.size()) // 可以匹配所有字符时,说明我们找到了一个合法子串
            {
                // 尝试更新左端点找到一个最短的子串
                h[s[j]]--;
                if(hs.find(s[j]) != hs.end())
                {
                    if(h[s[j]] < hs[s[j]]) // 左端点的字符更新后如果无法满足和t匹配更新答案和种类数
                    {
                        cnt--;
                        if(i - j + 1 < res[0])
                        {
                            res[0] = i - j + 1;
                            res[1] = j;
                        }
                    }
                }
                j++;
            }
        }

        if(res[0] == INT_MAX) return ""; // 无法匹配当前的t字符串

        return s.substr(res[1], res[0]);
    }
};