LeetCode热题100子串题解析

183 阅读4分钟

难度标识:⭐:简单,⭐⭐:中等,⭐⭐⭐:困难

tips:这里的难度不是根据LeetCode难度定义的,而是根据我解题之后体验到题目的复杂度定义的。

1.和为 K 的子数组 ⭐⭐

思路

这题可以使用前缀和的思路。

  1. 前缀和:对于数组中的每一个位置 i,都可以求一个从位置 0 到位置 i 的累加和,称之为“前缀和”。假如位置 j 到位置 ij < i)的元素和为 k,那么前缀和就满足 preSum[i] - preSum[j-1] = k

  2. 初始化:

  • preSum 用来计算从位置 0 到当前位置 i 的前缀和。

  • res 用来记录满足条件的子数组的数量。

  • map 用来存放前缀和的值及其出现的次数。初始化为 {0: 1} 是因为如果某个前缀和正好为 k,那么从位置 0 到当前位置的子数组满足条件,这时应该计算在结果内。

  1. 遍历数组:
  • 对数组 nums 进行遍历,不断更新 preSum 的值。

  • 检查 preSum - k 的值是否在 map 中,如果存在,说明从某个位置到当前位置的子数组和为 k。这个位置的个数就是 map.get(preSum - k),将这个数值加到结果 res 上。

  • 更新 map 的值,将当前的 preSum 的出现次数加1。

代码

var subarraySum = function (nums, k) {
    let preSum = 0, res = 0;
    const map = new Map()
    map.set(0, 1)
    for (let i = 0; i < nums.length; i++) {
        preSum += nums[i]
        if (map.has(preSum - k)) {
            res += map.get(preSum - k)
        }
        map.set(preSum, (map.get(preSum) || 0) + 1)
    }
    return res
};

思考:上面的代码为什么不将 map.set(preSum, (map.get(preSum) || 0) + 1) 这行代码放到 if (map.has(preSum - k)) { res += map.get(preSum - k) }之前呢?

当我们输入的数组为 [1] ,k值为 0 的时候,如果按照上面将代码放到前面,计算出来的结果就不对,结果会变成1,其实结果是0。

2.滑动窗口最大值 ⭐⭐

思路

tips:如果看不懂可以看这个视频的讲解,单调队列的思路讲解的很清楚。

这题可以使用单调队列或者叫双端队列的解法。维护一个双端队列,队列中存储的是数组的索引,队列的头部始终为当前窗口的最大值的索引。

  1. 初始化双端队列:
  • 使用一个双端队列(Deque)来维护窗口中的元素。关键点在于:这个队列中存储的是元素的索引,而不是元素的值。
  1. 处理队列的第一部分:
  • 当队列非空,首先考察队列头部的索引是否还在窗口范围内,如果不在,则将头部索引弹出。

  • 接着,当队列非空,且当前考察的元素值大于队尾所对应的元素值时,将队尾的索引弹出,直到队尾元素值大于当前考察元素值或队列为空。

  • 将当前考察元素的索引放入队列的尾部。

  1. 滑动窗口:
  • 当窗口开始滑动时(当窗口的大小大于等于k-1时,也就是窗口大小为k时开始取最大值加入结果数组中):

  • 每次滑动,队列的头部索引对应的元素就是当前窗口的最大值。将这个值 push 到结果数组中。

我们维护的这个队列是个双端单调递减(指索引对应的值)的队列,所以队列头部索引对应的元素是当前窗口的最大值。

代码

var maxSlidingWindow = function (nums, k) {
    const q = [], res = [];
    for (let i = 0; i < nums.length; i++) {
        if (q.length && q[0] <= i - k) {
            q.shift()
        }
        while (q.length && nums[i] >= nums[q[q.length - 1]]) {
            q.pop()
        }
        q.push(i)
        if (i >= k - 1) {
            res.push(nums[q[0]])
        }
    }
    return res
};

3.最小覆盖子串 ⭐⭐⭐

思路

这题可以使用滑动窗口的思路解决。

  1. 哈希表记录需求与窗口状态:利用两个对象,一个记录t中字符的需求,另一个记录当前窗口内的字符状态。

  2. 双指针扫描:使用leftright两个指针,动态调整窗口的大小。right指针向右移动扩大窗口,直到窗口中的字符满足t的需求。此时开始移动left指针,缩小窗口,直到窗口中的字符刚好或不满足t的需求。

  3. 持续更新最小子串信息:在滑动窗口的过程中,持续地记录并更新找到的满足条件的最小子串的起始位置和长度。

  4. 结果返回:根据记录的最小子串信息,从s中提取并返回结果。如果未找到满足条件的子串,则返回空字符串。

这题如果你掌握了滑动窗口的技巧,那其实还是很简单的,滑动窗口其实就是套模板。如果不会也可以看看这个参考资料 以及我上篇滑动窗口的题目文章

代码

var minWindow = function (s, t) {
    const obj = {}
    for (let ch of t) {
        obj[ch] = (obj[ch] || 0) + 1
    }
    let left = 0, right = 0, count = 0;
    const window = {}
    const needLen = Object.keys(obj).length
    let start = 0, len = Infinity
    while (right < s.length) {
        const c = s[right++]
        if (obj[c]) {
            window[c] = (window[c] || 0) + 1
            if (obj[c] === window[c]) {
                count++
            }
        }
        while (count === needLen) {
            if (right - left < len) {
                start = left
                len = right - left
            }
            const d = s[left++]
            if (obj[d]) {
                if (obj[d] === window[d]) {
                    count--
                }
                window[d]--
            }
        }
    }
    return len === Infinity ? '' : s.substr(start, len)
};