首页 重启DAY2 滑动窗口
文章
取消

重启DAY2 滑动窗口

#

跟着灵茶山艾府大佬学习算法思路的day2,目标是在一个月内掌握hot100

b站链接如下:

滑动窗口【基础算法精讲 03】

长度最小的子数组 题目描述:

给定一个含有 n 个正整数的数组和一个正整数 target

找出该数组中满足其总和大于等于 target 的长度最小的 子数组 [numsl, numsl+1, ..., numsr-1, numsr] ,并返回其长度如果不存在符合条件的子数组,返回 0

示例 1:

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

示例 2:

1
2
输入:target = 4, nums = [1,4,4]
输出:1

示例 3:

1
2
输入:target = 11, nums = [1,1,1,1,1,1,1,1]
输出:0

思路:

暴力做法是比较好想的,只要从数组第一个元素开始,逐步往后包入新元素,包入全部元素后就停止,然后第二个元素也同理处理,最后的时间复杂度是o(n^2)(众所周知,这样总会超时)。

最优解可以被称为“蠕动法”,一开始与暴力做法类似,从第一个元素开始,窗口逐步往后包入新元素,但在包入新元素,使得数组大于target时,此时我们就不继续包入新元素了,而是将头部指针往后移动一位,使得总和变小。如果此时总和依旧大于等于target,就更新答案最小值;如总和小于target,就包入新元素。如此循环即可。

值得注意的是虽然这道题有两个循环,但left和right指针在全局内的移动次数都仅是o(n),所以最后的时间复杂度就是o(n)。

代码:

​ 思路1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var minSubArrayLen = function(target, nums) {
    const n = nums.length;
    let sum = 0;
    let len = n + 1;
    let left = 0;
    
    for (let right = 0; right < n; right++) {
        sum += nums[right];
        while (sum - nums[left] >= target) {
            // 先尝试减去左指针的值
            sum -= nums[left];
            left++;
        }
        if (sum >= target) {
        // 无论是否进入while循环,if都会记录当前更优值
            len = Math.min(len, right - left +1);
        }

    }
    return (len <= n ? len : 0);
};

​ 错误思路1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var minSubArrayLen = function(target, nums) {
    const n = nums.length;
    let sum = 0;
    let len = n + 1;
    let left = 0;
    
    for (let right = 0; right < n; right++) {
        sum += nums[right];
        while (sum - nums[left] >= target) {
            sum -= nums[left];
            left++;
            if (sum >= target) {
                len = Math.min(len, right - left +1);
            }
        }

    }
    return (len <= n ? len : 0);
};

​ 思路2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var minSubArrayLen = function(target, nums) {
    const n = nums.length;
    let sum = 0;
    let len = n + 1;
    let left = 0;
    
    for (let right = 0; right < n; right++) {
        sum += nums[right];
        while (sum >= target) {
        // 最大的区别:当sum >= target时,就尝试记录和缩小窗口
            len = Math.min(len, right - left + 1);
            // 记录当前窗口长度
            sum -= nums[left]; // 缩小窗口
            left++;
        }
    }
    
    return len <= n ? len : 0;
};

总结:

两种思路最大的区别就是在扩展窗口后, 是先选择减去left值,还是先选择更新窗口长度。

思路1面对最优答案即为数组长度本身的特殊测试用例时,是不会进入while循环的,只能由if来更新(即错误思路1)。而对于其他长度小于数组长度本身的测试用例,即使将if判断放入while循环中,也可以通过用例。可以见得思路1面对不同的测试用例可能会带来一些边界判断的问题。

而思路2通过先更新窗口长度就避免了这一问题,在我看来思路2会更显简洁一些。

乘积小于 K 的子数组 题目描述:

给你一个整数数组 nums 和一个整数 k ,请你返回子数组内所有元素的乘积严格小于 k 的连续子数组的数目。

示例 1:

1
2
3
4
输入:nums = [10,5,2,6], k = 100
输出:8
解释:8 个乘积小于 100 的子数组分别为:[10]、[5]、[2]、[6]、[10,5]、[5,2]、[2,6]、[5,2,6]。
需要注意的是 [10,5,2] 并不是乘积小于 100 的子数组。

示例 2:

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

思路:

这道题和上道题不一样的地方主要有两处:一者是要求返回的是满足条件的连续子数组的数目;一者是要求乘积需要严格小于 k。

对于每个子数组的检索,依然可以采用与上一题一样的思路,先移动右端点扩大子数组乘积,直到不满足条件,就移动左端点缩小数组。

那么如何计算子数组的总数目呢?对于一个左端点为 l ,右端点为 r 的子数组而言,采用固定 r ,移动 l 的方式,所能形成的子数组个数,就是l到r的所有元素个数,即 r - l + 1。固定 r ,移动 l 的子数组计算方式便于在每次更新 r 的值时,同步计算一批新增的子数组。

如果在子数组计算时,采用固定 l , 移动 r 的方式,同样也可行,只是边界条件会复杂一些。例如常规的遍历子数组的思路是先移动右端点,在右端点持续移动,左端点还未移动过时,新的子数组其实已经源源不断地产生了。如果要等到左端点开始移动才去计算新的子数组,就会错过开始的一批子数组,这一批子数组就需要另花功夫单独进行计算。

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var numSubarrayProductLessThanK = function(nums, k) {
    if (k <= 1)
        return 0;
    const len = nums.length;
    let sums = 0;
    let left = 0;
    let multi = 1;
    let right = 0;
    while (right < len) {
        multi *= nums[right];
        while (multi >= k) {
            multi /= nums[left];
            left++;
        } // 保持multi结果严格小于k
        sums += (right - left + 1);
        right++;
    }
    return sums;

};

总结:

是长度最小的子数组的略微升级版,时空复杂度与上一题相同,边界条件判断和子数组总数的计算要注意。

无重复字符的最长子串 题目描述:

给定一个字符串 s ,请你找出其中不含有重复字符的 最长 子串 的长度。

示例 1:

1
2
3
输入: s = "abcabcbb"
输出: 3 
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。注意 "bca" 和 "cab" 也是正确答案。

示例 2:

1
2
3
输入: s = "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。

示例 3:

1
2
3
4
输入: s = "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
     请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。

思路:

这一题相较于之前的题目,更难一些的地方在于不同语言对于字符串的处理。而就题目本身而言,重复字符一定来自新接入的字符,则可以采用哈希表或集合记录字符的出现次数,哈希表会存储键值对,而集合则只有值,两种集合类型都可以解决这道题,因为我们只需知道目前的字符串中已有了哪些字符。

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var lengthOfLongestSubstring = function(s) {
    const n = s.length;
    if (n <= 1) return n;
    
    let maxLen = 0;
    let left = 0;
    const charSet = new Set();
    
    for (let right = 0; right < n; right++) {
        const char = s[right];
         
        while (charSet.has(char)) {
            charSet.delete(s[left]);
            left++;
        }// 如果字符已存在,移动左指针直到移除重复字符
        
        charSet.add(char);
        // 添加当前字符到集合
        maxLen = Math.max(maxLen, right - left + 1);
    }
    
    return maxLen;
};

总结:

对比一下这两种数据结构

1. 创建和初始化

1
2
3
4
5
6
7
// Set - 存储唯一值
const mySet = new Set();
const setFromArray = new Set([1, 2, 3, 3, 2]); // {1, 2, 3}

// Map - 存储键值对
const myMap = new Map();
const mapFromArray = new Map([['a', 1], ['b', 2]]);

2. 基本操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Set操作
const set = new Set();
set.add('apple');      // 添加值
set.has('apple');      // 检查是否存在: true
set.delete('apple');   // 删除值
set.size;              // 获取大小
set.clear();           // 清空

// Map操作
const map = new Map();
map.set('name', '张三'); // 设置键值对
map.get('name');        // 获取值: '张三'
map.has('name');        // 检查键是否存在: true
map.delete('name');     // 删除键值对
map.size;               // 获取大小
map.clear();            // 清空
本文由作者按照 CC BY 4.0 进行授权

Chirpy 暗色模式切换又失效了?根因是 ModeToggle.clearMode()

重启DAY3 二分查找