导语java
滑动窗口类问题是面试当中的高频题,问题自己其实并不复杂,可是实现起来细节思考很是的多,想着想着可能由于变量变化,指针移动等等问题,致使程序反复删来改去,有思路,可是程序写不出是这类问题最大的障碍。本文会将 LeetCode 里面的大部分滑动窗口问题分析、总结、分类,并提供一个能够参考的模版,相信能够有效减小面试当中的算法实现部分的不肯定性。面试
滑动窗口这类问题通常须要用到双指针来进行求解,另一类比较特殊则是须要用到特定的数据结构,像是 sorted_map。后者有特定的题型,后面会列出来,可是,对于前者,题形变化很是的大,通常都是基于字符串和数组的,因此咱们重点总结这种基于双指针的滑动窗口问题。算法
题目问法大体有这几种:数组
除此以外,还有一些其余的问法,可是不变的是,这类题目脱离不开主串(主数组)和子串(子数组)的关系,要求的时间复杂度每每是 ,空间复杂度每每是常数级的。之因此是滑动窗口,是由于,遍历的时候,两个指针一前一后夹着的子串(子数组)相似一个窗口,这个窗口大小和范围会随着先后指针的移动发生变化。bash
根据前面的描述,滑动窗口就是这类题目的重点,换句话说,窗口的移动就是重点。咱们要控制先后指针的移动来控制窗口,这样的移动是有条件的,也就是要想清楚在什么状况下移动,在什么状况下保持不变,我在这里讲讲个人想法,个人思路是保证右指针每次往前移动一格,每次移动都会有新的一个元素进入窗口,这时条件可能就会发生改变,而后根据当前条件来决定左指针是否移动,以及移动多少格。我写来一个模版在这里,能够参考:数据结构
public int slidingWindowTemplate(String[] a, ...) {
// 输入参数有效性判断
if (...) {
...
}
// 申请一个散列,用于记录窗口中具体元素的个数状况
// 这里用数组的形式呈现,也能够考虑其余数据结构
int[] hash = new int[...];
// 预处理(可省), 通常状况是改变 hash
...
// l 表示左指针
// count 记录当前的条件,具体根据题目要求来定义
// result 用来存放结果
int l = 0, count = ..., result = ...;
for (int r = 0; r < A.length; ++r) {
// 更新新元素在散列中的数量
hash[A[r]]--;
// 根据窗口的变动结果来改变条件值
if (hash[A[r]] == ...) {
count++;
}
// 若是当前条件不知足,移动左指针直至条件知足为止
while (count > K || ...) {
...
if (...) {
count--;
}
hash[A[l]]++;
l++;
}
// 更新结果
results = ...
}
return results;
}
复制代码
这里面的 “移动左指针直至条件知足” 部分,须要具体题目具体分析,其余部分的变化不大,我如今来看看具体的题目。框架
438. Find All Anagrams in a Stringpost
别看这是一道 easy 难度的题目,若是限定你在 时间复杂度内实现呢?按照模版会很简单,首先窗口是固定的,窗口长度就是输入参数中第二个字符串的长度,也就是说,右指针移动到某个位置后,左指针必须跟着一同移动,且每次移动都是一格,模版中 count 用来记录窗口内知足条件的元素,直到 count 和窗口长度相等便可更新答案spa
public List<Integer> findAnagrams(String s, String p) {
if (s.length() < p.length()) {
return new ArrayList<Integer>();
}
char[] sArr = s.toCharArray();
char[] pArr = p.toCharArray();
int[] hash = new int[26];
for (int i = 0; i < pArr.length; ++i) {
hash[pArr[i] - 'a']++;
}
List<Integer> results = new ArrayList<>();
int l = 0, count = 0, pLength = p.length();
for (int r = 0; r < sArr.length; ++r) {
hash[sArr[r] - 'a']--;
if (hash[sArr[r] - 'a'] >= 0) {
count++;
}
if (r > pLength - 1) {
hash[sArr[l] - 'a']++;
if (hash[sArr[l] - 'a'] > 0) {
count--;
}
l++;
}
if (count == pLength) {
results.add(l);
}
}
return results;
}
复制代码
76. Minimum Window Substring指针
一样是两个字符串之间的关系问题,由于题目求的最小子串,也就是窗口的最小长度,说明这里的窗口大小是可变的,这里移动左指针的条件变成,只要左指针指向不须要的字符,就进行移动
public String minWindow(String s, String t) {
if (s.length() < t.length()) {
return "";
}
char[] sArr = s.toCharArray();
char[] tArr = t.toCharArray();
int[] hash = new int[256];
for (int i = 0; i < tArr.length; ++i) {
hash[tArr[i]]++;
}
int l = 0, count = tArr.length, max = s.length() + 1;
String result = "";
for (int r = 0; r < sArr.length; ++r) {
hash[sArr[r]]--;
if (hash[sArr[r]] >= 0) {
count--;
}
while (l < r && hash[sArr[l]] < 0) {
hash[sArr[l]]++;
l++;
}
if (count == 0 && max > r - l + 1) {
max = r - l + 1;
result = s.substring(l, r + 1);
}
}
return result;
}
复制代码
159. Longest Substring with At Most Two Distinct Characters
这题窗口大小仍是变化的,重点仍是在什么时候更新左指针。这里的 count 能够用来记录当前有多少种不一样的字符,只要当前进入窗口的字符使 count 不知足题目条件,咱们就移动左指针让 count 知足条件。这里 result 初始化为 1,由于只要输入的字串不是空的,窗口大小不可能小于 1。
public int lengthOfLongestSubstringTwoDistinct(String s) {
if (s == null || s.length() == 0) {
return 0;
}
char[] sArr = s.toCharArray();
int[] hash = new int[256];
int l = 0, count = 0, result = 1;
for (int r = 0; r < sArr.length; ++r) {
hash[sArr[r]]++;
if (hash[sArr[r]] == 1) {
count++;
}
while (count > 2) {
hash[sArr[l]]--;
if (hash[sArr[l]] == 0) {
count--;
}
l++;
}
result = Math.max(result, r - l + 1);
}
return result;
}
复制代码
340. Longest Substring with At Most K Distinct Characters
和上题同样,把 2 变成 k 便可
public int lengthOfLongestSubstringKDistinct(String s, int k) {
if (s == null || s.length() == 0 || k == 0) {
return 0;
}
char[] sArr = s.toCharArray();
int[] hash = new int[256];
int l = 0, count = 0, result = 1;
for (int r = 0; r < sArr.length; ++r) {
hash[sArr[r]]++;
if (hash[sArr[r]] == 1) {
count++;
}
while (count > k) {
hash[sArr[l]]--;
if (hash[sArr[l]] == 0) {
count--;
}
l++;
}
result = Math.max(result, r - l + 1);
}
return result;
}
复制代码
3. Longest Substring Without Repeating Characters
输入只有一个字符串,要求子串里面不可以有重复的元素,这里 count 都不须要定义,直接判断哈希散列里面的元素是否是在窗口内便可,是的话得移动左指针去重。
public int lengthOfLongestSubstring(String s) {
if (s == null || s.length() == 0) {
return 0;
}
char[] sArr = s.toCharArray();
int[] hash = new int[256];
int l = 0, result = 1;
for (int r = 0; r < sArr.length; ++r) {
hash[sArr[r]]++;
while (hash[sArr[r]] != 1) {
hash[sArr[l]]--;
l++;
}
result = Math.max(result, r - l + 1);
}
return result;
}
复制代码
和 438 那题很相似,可是这里不须要记录答案了,有就直接返回 true。
public boolean checkInclusion(String s1, String s2) {
if (s1.length() > s2.length()) {
return false;
}
char[] s1Arr = s1.toCharArray();
char[] s2Arr = s2.toCharArray();
int[] hash = new int[26];
for (int i = 0; i < s1Arr.length; ++i) {
hash[s1Arr[i] - 'a']++;
}
int l = 0, count = 0;
for (int r = 0; r < s2Arr.length; ++r) {
hash[s2Arr[r] - 'a']--;
if (hash[s2Arr[r] - 'a'] >= 0) {
count++;
}
if (r >= s1Arr.length) {
hash[s2Arr[l] - 'a']++;
if (hash[s2Arr[l] - 'a'] >= 1) {
count--;
}
l++;
}
if (count == s1Arr.length) {
return true;
}
}
return false;
}
复制代码
992. Subarrays with K Different Integers
看完了字符串类型的题目,此次来看看数组类型的,题目中的 subarray 已经明确了这个题能够考虑用滑动窗口,这题比较 trick 的一个地方在于,这里不是求最小值最大值,而是要你计数,可是若是每次仅仅加 1 的话又不太对,例如 A = [1,2,1,2,3], K = 2
这个例子,假如右指针移到 index 为 3 的位置,若是按以前的思路左指针根据 count 来移动,当前窗口是 [1,2,1,2]
,可是怎么把 [2,1]
给考虑进去呢?能够从数组和子数组的关系来思考,假如 [1,2,1,2]
是符合条件的数组,若是要计数的话,[1,2,1,2]
要求的结果是否和 [1,2,1]
的结果存在联系?这两个数组的区别在于多了一个新进来的元素,以前子数组计数没考虑到这个元素,假如把这个元素放到以前符合条件的子数组中组成的新数组也是符合条件的,咱们看看这个例子中全部知足条件的窗口以及对应的知足条件的子数组状况:
[1,2,1,2,3] // 窗口知足条件
l r // 知足条件的子数组 [1,2]
[1,2,1,2,3] // 窗口知足条件
l r // 知足条件的子数组 [1,2],[2,1],[1,2,1]
[1,2,1,2,3] // 窗口知足条件
l r // 知足条件的子数组 [1,2],[2,1],[1,2,1],[1,2],[2,1,2],[1,2,1,2]
[1,2,1,2,3] // 窗口不知足条件,移动左指针至知足条件
l r
[1,2,1,2,3] // 窗口知足条件
l r // 知足条件的子数组 [1,2],[2,1],[1,2,1],[1,2],[2,1,2],[1,2,1,2],[2,3]
复制代码
你能够看到对于一段连续的数组,新的元素进来,窗口增长 1,每次的增量都会在前一次增量的基础上加 1。当新的元素进来打破当前条件会使这个增量重新回到 1,这样咱们左指针移动条件就是只要是移动不会改变条件,就移动,否则就中止
public int subarraysWithKDistinct(int[] A, int K) {
if (A == null || A.length < K) {
return 0;
}
int[] hash = new int[A.length + 1];
int l = 0, results = 0, count = 0, result = 1;
for (int r = 0; r < A.length; ++r) {
hash[A[r]]++;
if (hash[A[r]] == 1) {
count++;
}
while (hash[A[l]] > 1 || count > K) {
if (count > K) {
result = 1;
count--;
} else {
result++;
}
hash[A[l]]--;
l++;
}
if (count == K) {
results += result;
}
}
return results;
}
复制代码
424. Longest Repeating Character Replacement
这道题想 accept 的话不难,可是问题在于怎么知道当前窗口中数量最多的字符的数量,由于须要替换的字符就是当前窗口的大小减去窗口中数量最多的字符的数量。最简单的方法就是把哈希散列遍历一边找到最大的字符数量,可是仔细想一想若是咱们每次新进元素都更新这个最大数量,且只更新一次,咱们保存的是当前遍历过的全局的最大值,它确定是比实际的最大值大的,咱们左指针移动的条件是 r - l + 1 - maxCount > k
,保存的结果是 result = Math.max(r - l + 1, result);
这里 maxCount 比实际偏大的话,虽然致使左指针不能移动,可是不会记录当前的结果,因此最后的答案并不会受影响
public int characterReplacement(String s, int k) {
if (s == null || s.length() == 0) {
return 0;
}
char[] sArr = s.toCharArray();
int[] hash = new int[26];
int l = 0, maxCount = 0, result = 0;
for (int r = 0; r < sArr.length; ++r) {
hash[sArr[r] - 'A']++;
maxCount = Math.max(maxCount, hash[sArr[r] - 'A']);
while (r - l + 1 - maxCount > k) {
hash[sArr[l] - 'A']--;
l++;
}
result = Math.max(r - l + 1, result);
}
return result;
}
复制代码
其余题型
能够参考我以前写的 Arts 的算法部分 -> Arts 第三周
求中值的问题比较特殊,若是是乱序的状况下最好就是借助堆,利用两个堆,一个大顶,一个小顶,而且保证两个堆内存放的数目相差不超过 1,这样若是当前元素数目是偶数,两个堆顶元素的均值就是结果,若是是奇数,存放元素多的那个堆的堆顶元素就是结果。
public double[] medianSlidingWindow(int[] nums, int k) {
if (nums == null || nums.length < k ) {
return new double[0];
}
double[] results = new double[nums.length - k + 1];
PriorityQueue<Integer> minHeap = new PriorityQueue<>();
PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Collections.reverseOrder());
for (int i = 0; i < nums.length; ++i) {
// add current element into queue
maxHeap.offer(nums[i]);
minHeap.offer(maxHeap.poll());
if (minHeap.size() > maxHeap.size()) {
maxHeap.offer(minHeap.poll());
}
// record answer
if (i >= k - 1) {
results[i - k + 1] = minHeap.size() < maxHeap.size()
? maxHeap.peek() : ((long)maxHeap.peek() + minHeap.peek()) * 0.5;
if (maxHeap.contains(nums[i - k + 1])) {
maxHeap.remove(nums[i - k + 1]);
} else {
minHeap.remove(nums[i - k + 1]);
}
}
}
return results;
}
复制代码
双指针类的滑动窗口问题思惟复杂度并不高,可是出错点每每在细节。记忆经常使用的解题模版仍是颇有必要的,特别是对于这种变量名多,容易混淆的题型。有了这个框架,思考的点就转化为 “什么条件下移动左指针”,无关信息少了,思考加实现天然不是问题。