在没有思路之前,可以先想到使用暴力枚举来解决。创建一个指针 left 来遍历原数组,之后再创建一个指针 right 每次从遍历时 left 的下标位置开始向后移动,并创建一个变量统计 left 到 right 这段区间内的元素值之和。每次统计之后再判断 sum 是否大于等于 target,是的话就停止这一个 left 位置的子区间统计,并记入这时子数组的长度,将 left 向后移动一位进行下一次的统计。最后当 left 移动到超出数组的范围之后就结束了所有满足条件的子数组的统计,最后将满足条件的长度最小的子数组长度返回即可。
通过以上使用暴力枚举时的流程图可以看出,其实在过程当中有很多的步骤是在做无用功的。当 right 移动到的位置 l 之后,left 到 right 区间之间的数组元素和正好大于给定的值 target 时,在暴力枚举当中我们是在将 left 向后移动一位时再将 right 也移动到 left 的位置再进行新一轮的统计。但其实在这个过程就没有必要将 right 回退到 left,这是因为在之前 left 到 right 的区间内的数组之和才恰好大于或者等于 target,这时我们将 left 右移一位之后区间内的和一定是小于或者等于或者大于 target 的,那么就说明不需要再将 right 回退到 left 的位置,这时就直接判断此时 left 到 right 之间的所有元素之和即可;这样就避免了很多的重复计算。
以上的算法优化简单来说就是每次移动 right 指针之前都判断当前 left 到 right 区间内的元素之和是否大于或者等于 target,若不是就将 right++,否则就将 left++,并且每次满足条件的区间都统计长度。这时就会发现这个过程当中 left 都是一直朝着一个方向移动,属于同向双指针,这就符合滑动窗口的条件。
并且通过以上的分析也就证明了滑动窗口算法的合理性。
并且我们还知道了为什么滑动窗口算法时间复杂度更低
窗口寻找的是:以当前窗口最左侧元素(记为 left)为基准,符合条件的情况。也就是在这道题中,从 left 开始,满足区间和 sum >= target 时的最右侧(记为 right)能到哪里。
既然已经找到从 left 开始的最优的区间,那么就可以大胆舍去 left。但是如果继续像方法一一样,重新开始统计第二个元素(left)往后的和,势必会有大量重复的计算(因为我们在求第一段区间的时候,已经算出很多元素的和了,这些和是可以再计算下次区间和的时候用上的)。
classSolution {
public:
intminSubArrayLen(int target, vector<int>& nums){
// 定义 len 为满足条件的子数组的最小值,一开始定义为 int 范围内的最大值// sum 为 left 到 right 区间内的元素值之和int n = nums.size(), len = INT_MAX, sum = 0;
for (int left = 0, right = 0; right < n; right++) {
// 进窗口
sum += nums[right];
// 判断当前区间是否满足条件while (sum >= target) {
// 更新最小子数组的长度,如果当前满足条件的区间长度比 len 小就更新
len = min(right - left + 1, len);
// 出窗口
sum -= nums[left++];
}
}
// 最后返回 len 时先判断 len 是否为 INT_MAX;// 是就说明数组所有元素值之和都无法大于或者等于 target,此时就返回 0return len == INT_MAX ? 0 : len;
}
};
在看到这道算法题时,一般是先想到使用暴力枚举的方式来解决,那就是创建两个指针变量 left 和 right;使用 left 来遍历数组之后使用 right 来从每个 left 指针位置开始寻找满足条件的子字符,在此过程中每次子字符串满足条件时就更新最长子串长度 len。最后就可以得到满足条件的最长的子串。
这时就要想如何基于暴力枚举下进行算法的优化,将暴力枚举内的重复计算给消除。在暴力枚举当中就可以看出当固定 left 之后从后找满足条件的区间,这时在暴力枚举当中是在 right 移动之后出现子串中出现重复字符就将 left++,并且将 right 移动到 left 的位置。但其实这个将 right 回退的过程是完全没必要的,因为将 left 向后移动一位之后当前到原来 right 的区间满足条件的子串不可能比原来的子串还长,所以这时就不要将 right 回退了,毕竟回退了之后还会移动到当前的位置就没必要做这些无用功了。
所以优化之后的算法简单来说就是当 left 到 right 之间的子串满足条件时就先更新子串的长度,之后直到子串出现重复字符就将 left++;此时 right 不要动,之后进行完 left 的移动之后再进行判断不满足就继续移动 left,直到 left 到 right 的区间内的子串满足条件就继续移动 right。
此时该优化之后的算法 left 和 right 为同向双指针,就属于滑动窗口算法,流程图如下所示:
在此就需要使用一个数组来模拟哈希表,这样就能判断当前 left 到 right 的区间内是否无重复字符。
注:在此 s 由英文字母、数字、符号和空格组成,所以我们开一个大小为 128 的数组就完全足够了。
代码实现
classSolution {
public:
intlengthOfLongestSubstring(string s){
// 创建数组 hash 来模拟哈希表,一开始将表内的数据都初始化为 0int hash[128] = {0};
// 创建变量 len 来统计满足条件的最长字串长度int n = s.size(), len = 0;
for (int left = 0, right = 0; right < n; right++) {
// 进窗口,将对应的哈希表内的值个数加一char in = s[right];
hash[in]++;
// 判断当前 hash 表下标 in 位置是否值大于 1while (hash[in] > 1) {
// 进行出窗口操作,将 hash 表内下标 out 位置的值减一char out = s[left];
hash[out]--;
// 将 left 右移一位
left++;
}
// 更新最大子串 len 的长度
len = max(len, right - left + 1);
}
return len;
}
};
其实在该算法题当中不要去想怎么翻转,不要把问题想的很复杂,这道题的结果无非就是一段连续的 1 中间塞了 k 个 0 嘛。因此,我们可以把问题转化成:求数组中一段最长的连续区间,要求这段区间内 0 的个数不超过 k 个。
因此要求最长的一段连续区间就可以使用滑动窗口来解决了。
算法原理讲解
在该算法题中要求最长的一段连续区间使用滑动窗口来解决就需要一开始定义两个指针 left 和 right 一开始都指向数组的首元素,之后当数组内的 0 元素个数没有超过 k 时就进行进窗口操作也就是继续将 right 右移一位,如果出现了数组内的 0 元素个数超过 k 就进行出窗口操作也就是将 left 右移一位,直到数组内的 0 元素个数不超过 k 就更新最长子数组的长度,最后当 left 移动到数组的末尾时就完成了整个的操作。
算法的流程图如下所示:
在此我们创建一个变量 count 来统计 left 到 right 区间内的元素 0 的个数。
代码实现
classSolution {
public:
intlongestOnes(vector<int>& nums, int k){
// 变量 count 统计窗口内值为 0 的元素个数,len 为满足条件最长的子数组长度int n = nums.size(), count = 0, len = 0;
for (int left = 0, right = 0; right < n; right++) {
// 进窗口,若 right 指向的数组下标位置的元素值为 0,count 就++if (nums[right] == 0) count++;
// 判断 count 是否大于 kwhile (count > k) {
// 出窗口,若 left 指向的数组下标位置的元素值为 0,count 就--if (nums[left++] == 0) count--;
}
// 更新最长子数组的长度
len = max(right - left + 1, len);
}
return len;
}
};
在此在这道题当中我们要从原数组两边当中选取出能使得 x 减到 0 的最小元素个数,其实不就是和求数组当中除被选取的元素之外数组内剩下的元素个数最多情况;这时数组当中除被选取的元素为最多且个数就为原数组的长度减去未被选择的元素个数。在此如果无法实现 x 恰好减到 0 这时未被选取到的元素个数就等于数组的长度。
这里我们就创建一个变量 sum 存储数组当中所有元素值减去给定值 x 的结果。
所以综上分析就可以得出该算法题被我们转化为求数组当中和为 sum 的子数组当中最长的子数组长度,如果找不到一个子数组内的元素值为 sum 就返回 -1。
代码实现
classSolution {
public:
intminOperations(vector<int>& nums, int x){
// s 变量统计数组内所有元素值得和,len 为最终窗口和内元素值为 sum 的最小值int sum = 0, s = 0, len = -1, n = nums.size();
// 遍历原数组统计数组元素之和for (auto i : nums) s += i;
// sum 变量为 s 变量减去 x 变量的差
sum = s - x;
// 处理特殊情况当 sum 小于 0 时就说明数组内不可能实现元素之和为 xif (sum < 0) return-1;
// 进行滑动窗口操作,count 统计窗口内的所有元素值之和for (int count = 0, left = 0, right = 0; right < n; right++) {
// 进窗口,将 right 指向的下标位置的数组元素加到 count 变量内
count += nums[right];
// 判断当前窗口内的和是否大于 sumwhile (count > sum) {
// 出窗口,将 left 指向的下标位置的数组元素值从 count 中减去
count -= nums[left++];
}
// 更新 len 变量的值if (count == sum) len = max(right - left + 1, len);
}
// 返回时判断是否有窗口和为 sumreturn len == -1 ? -1 : n - len;
}
};
在这道题当中如果我们使用暴力枚举来从字符串 s 当中找出字符串 p 的所有异位词就需要一开始创建指针变量 left 一开始指向字符串 s 当中的首元素,创建一个哈希表 1 来存储字符串 p 内的元素;再创建一个哈希表 2 来存储 left 之后的三个元素。之后使用 left 遍历字符串 s 直到倒数第三个元素,每次遍历时都将 left 之后的三个元素存放到哈希表当中,比较此时的哈希表 1 和哈希表 2 是否内的元素是否相等,是的话就说明这三个字符是字符串 p 的异位词。接下来将 left++ 之后将哈希表内的元素值重新赋值为 0,再重复以上的操作直到 left 移动到字符串 s 的末尾。这时就可以将所有异位词的首元素坐标找出。
在以上暴力枚举算法当中我们需要遍历一次字符串 s 并且每次遍历到了 left 之后要将 left 位置开始的三个字符存放到哈希表当中,这样算法的效率是很低的,那么接下来我们就要想着该如何来优化暴力解法。
在此由于暴力解法当中的特性我们就可以试着将暴力解法优化为滑动窗口,在此就需要再创建一个变量 right 来表示窗口的右边界,当哈希表 2 内的元素种类和和个数大于哈希表 1 的时就将进行出窗口操作,直到当哈希表 2 内的元素种类和和个数和哈希表内相等就进行更新对应的索引位置。
使用滑动窗口算法的流程图如下所示:
在以上当中 len 表示的是字符串 p 的长度,len2 表示的 left 到 right 窗口的长度,count1 表示的字符串 p 当中的元素种类数,count2 表示的是 left 到 right 窗口内的字符种类数。