概述
一种非常典型的编程思想,滑动窗口。
专门针对滑动窗口的问题,框架更是一看就懂。
while (right < s.length()) {
//移动窗口右边时机,向窗口添加元素
w.add(s.charAt(right++));
// 判断是否需要移动窗口坐标,移除元素
while (w.check()) {
w.remove(s.charAt(left++));
}
// debug时机,查看当前窗口,或打印窗口内容
// 检查合法条件时机,以下为举例
size = Math.max(size, w.size);
}
这个框架的好处是非常清晰,主要逻辑——添加元素,删除元素,检查合法性,用什么来帮助快速检查合法性都交给窗口。窗口对象需要实现这些方法。
窗口类型
固定窗口
特点是left和right一同移动。
567. Permutation in String
- Permutation in String
- s2是否包含s1的全排列
- https://leetcode.com/problems/permutation-in-string/
以这道题是我做的第一道题,将进行详细展开。如果不以滑动窗口的方式去考虑问题,那这种题会变得很麻烦。优化策略很多,也不好实现。
我一开始考虑到的解法:
for c in s2:
if 当前子串是否等于s1的全排列之一
记录下标
复杂度主要来源于if的判断。
解法一:暴力
全排列,只需要所有字母一致即可,暴力遍历即可求解。
- 时间复杂度:O(N M ^ 2), O(M ^ 2)为两个for循环,在当前子串中搜索s1的所有字母
- 空间复杂度,O(1)
解法二:排序
上面O(M^2)有待优化,既然求是否全排列之一,那么就可以对其排序,那就可以直接在O(M)比出结果
- 时间复杂度,O(N M log M),主要是排序开销
- 空间复杂度,O(1)
解法三:hash
能否在O(1)求解是否为全排列之一?s1不需要每次都遍历一遍。可以将s1的全排列的所有可能都放入hash table中,然后遍历,每次判断当前子串的时候只需要判断它在不在hash table中即可。
- 时间复杂度,O(N M),很遗憾即使有hash table,每次判断在不在hash table,这个操作也是O(M)而不是hash查找的O(1),这也是常见的误区之一,核心原因是因为hash函数的求取直接和s1长度M挂钩。
- 空间复杂度,O(M!),指数级复杂度,等于s1的全排列。
看到了吧,以上种种方式都似乎指出该问题不简单,不管是复杂度还是实现上。但该问题其实有时间上O(1)空间上O(M)的高效解法。这就是滑动窗口的强大。
解法四:滑动窗口
滑动窗口的优化核心是窗口滑动新增一个元素(删除一个元素),这意味着不需要完全遍历,有O(1)解决问题的潜力,只要能很好利用旧窗口的知识。
所以,实现窗口也就是如何利用已有信息,更新新的信息。
对于本题,窗口利用hash桶,记录出现过的字符。新增元素则加一,移除元素则减一。若所有桶都为0,则表示当前字符串和s1的字符完全一致。
/*
全新滑动窗口框架
Runtime: 9 ms, faster than 52.34% of Java online submissions for Permutation in String.
Memory Usage: 40.2 MB, less than 8.87% of Java online submissions for Permutation in String.
* */
public class SolutionV2 {
public boolean checkInclusion(String s1, String s2) {
int l,r;
l = r = 0;
Window w = new Window(s1);
while (r < s2.length()) {
w.add(s2.charAt(r++));
if (r > w.size) {
w.remove(s2.charAt(l++));
}
// System.out.println(l+ " " + r);
// w.disp();
if (w.check()) return true;
}
return false;
}
// public static void main(String[] args) {
// System.out.println(new SolutionV2().checkInclusion(
// "a",
// "a"));
// }
class Window {
int[] map;
int size, count;
public Window(String target) {
map = new int[130];
this.size = target.length();
for (int i = 0; i < size; ++i) {
if (map[target.charAt(i)]++ == 0) ++count;
}
}
public void add(char c) {
if (--map[c] == 0) --count;
}
public void remove(char c) {
if (map[c]++ == 0) ++count;
}
public boolean check() {
if (count == 0) return true;
return false;
}
public void disp() {
for (int i = 'A'; i <= 'z'; i++) {
System.out.print(map[i] + " ");
}
System.out.println();
}
}
}
438. Find All Anagrams in a String
- Find All Anagrams in a String
- 寻找所有异构字串起始下标
- https://leetcode.com/problems/find-all-anagrams-in-a-string/
和上一道题基本完全一致。
package com.leetcode.datastructure.array.window.q438;
import java.util.ArrayList;
import java.util.List;
/*
* 438. Find All Anagrams in a String
* 寻找所有异构字串起始下标
* https://leetcode.com/problems/find-all-anagrams-in-a-string/
* */
/*
滑动窗口
时间复杂度,O(N)
滑动遍历N个字符,由于hash,异构字符串判断为O(1)
空间复杂度,O(K),K为不同字符数
Runtime: 7 ms, faster than 76.50% of Java online submissions for Find All Anagrams in a String.
Memory Usage: 39.5 MB, less than 98.83% of Java online submissions for Find All Anagrams in a String.
*/
class Solution {
public List<Integer> findAnagrams(String s, String p) {
int left, right;
Window w = new Window(p);
List<Integer> res = new ArrayList<>();
left = right = 0;
while (right < s.length()) {
w.add(s.charAt(right++));
if (right > w.size) {
w.remove(s.charAt(left++));
}
// w.disp();
if (w.check()) res.add(left);
// System.out.println(left + " " + right );
}
return res;
}
// public static void main(String[] args) {
// System.out.println(new Solution().findAnagrams("cbaebabacd",
// "abc"));
// }
class Window {
int[] map;
int size, count;
public Window(String target) {
map = new int[130];
this.size = target.length();
for (int i = 0; i < size; ++i) {
if (map[target.charAt(i)]++ == 0) ++count;
}
}
public void add(char c) {
if (--map[c] == 0) --count;
}
public void remove(char c) {
if (map[c]++ == 0) ++count;
}
public boolean check() {
if (count == 0) return true;
return false;
}
public void disp() {
for (int i = 'A'; i <= 'z'; i++) {
System.out.print(map[i] + " ");
}
System.out.println();
}
}
}
不定长窗口
76. Minimum Window Substring
- Minimum Window Substring
- 寻找包含目标字所有字母的最小子串
- https://leetcode.com/problems/minimum-window-substring/
和567非常像。
版本一,想当然地遍历了所有可能的窗口大小,因此算法复杂度为O(N^2)。但实际上,该类问题属于不定长窗口,不需要遍历所有窗口。它的基本思想是:移动右边,直到符合要求,才开始移动左边。采用双指针的方式,可以在O(N)的时间内解决问题,非常经典。
/*
* 76. Minimum Window Substring
* 寻找包含目标字所有字母的最小子串
* https://leetcode.com/problems/minimum-window-substring/
*
* */
/*
算法复杂度,O(N N) + O(N M)
其中N,M分别为s,t的长度。
里层如q567,O(N) + O(M),重复N次。
空间复杂度,O(M + K)
K为桶固定常数开销。
Runtime: 871 ms, faster than 5.02% of Java online submissions for Minimum Window Substring.
Memory Usage: 39.5 MB, less than 54.77% of Java online submissions for Minimum Window Substring.
*/
class Solution {
public String minWindow(String s, String t) {
for (int n = t.length(); n <= s.length(); ++n) {
Window w = new Window(t, n);
for (int i = 0; i < s.length(); ++i) {
if (i < w.size) {
w.add(s.charAt(i));
} else {
w.remove(s.charAt(i - w.size));
w.add(s.charAt(i));
}
System.out.println(s.charAt(i));
w.disp();
if (w.check()) return s.substring(i-w.size+1,i+1);
}
System.out.println("=============");
}
return "";
}
public static void main(String[] args) {
System.out.println(new Solution().minWindow("a"
, "a"));
}
}
class Window {
int[] map;
int size, count;
public Window(String target, int size) {
map = new int[130];
this.size = size;
for (int i = 0; i < target.length(); ++i) {
if (map[target.charAt(i)]++ == 0) ++count;
}
}
public void add(char c) {
if (--map[c] == 0) --count;
}
public void remove(char c) {
if (map[c]++ == 0) ++count;
}
public boolean check() {
if (count == 0) return true;
return false;
}
public void disp() {
for (int i = 'A'; i <= 'z'; i++) {
System.out.print(map[i]+" ");
}
System.out.println();
}
}
版本二:
/*
全新框架,单边收缩版本
Runtime: 4 ms, faster than 83.81% of Java online submissions for Minimum Window Substring.
Memory Usage: 40.9 MB, less than 13.46% of Java online submissions for Minimum Window Substring.
*/
public class SolutionV2 {
public String minWindow(String s, String t) {
int left, right, rl, rr;
Window w = new Window(t);
left = right = 0;
rl = 0;
rr = Integer.MAX_VALUE;
// 移动右边
while (right < s.length()) {
w.add(s.charAt(right++));
// 收缩左边
if (w.check()) {
while (w.check()) {
w.remove(s.charAt(left++));
// System.out.println(left + " " + right);
}
if (rr - rl > right - left + 1) {
rr = right;
rl = left - 1;
}
}
}
return rr == Integer.MAX_VALUE ? "" : s.substring(rl, rr);
}
// public static void main(String[] args) {
// System.out.println(new SolutionV2().minWindow("ADOBECODEBANC"
// , "ABC"));
// }
class Window {
int[] map;
int size, count;
public Window(String target) {
map = new int[130];
this.size = target.length();
for (int i = 0; i < size; ++i) {
if (map[target.charAt(i)]++ == 0) ++count;
}
}
public void add(char c) {
if (--map[c] == 0) --count;
}
public void remove(char c) {
if (map[c]++ == 0) ++count;
}
public boolean check() {
if (count == 0) return true;
return false;
}
public void disp() {
for (int i = 'A'; i <= 'z'; i++) {
System.out.print(map[i] + " ");
}
System.out.println();
}
}
}
3. Longest Substring Without Repeating Characters
- Longest Substring Without Repeating Characters
- 不重复的最长字串
- https://leetcode.com/problems/longest-substring-without-repeating-characters/
这个问题和问题76不太一样,窗口需要重写,但同样属于不定长窗口。
/*
* 3. Longest Substring Without Repeating Characters
* 不重复的最长字串
* https://leetcode.com/problems/longest-substring-without-repeating-characters/
*
* */
/*
滑动窗口的思想下,非常简单
移动右边,直到出现重复,则开始移动左边,直到不再重复。
时间复杂度 o(n),hash加持下下,重复判断只需要O(1)
空间复杂度 o(k),k为不同字符个数,常数
Runtime: 5 ms, faster than 81.47% of Java online submissions for Longest Substring Without Repeating Characters.
Memory Usage: 40.4 MB, less than 21.48% of Java online submissions for Longest Substring Without Repeating Characters.
*/
class Solution {
public int lengthOfLongestSubstring(String s) {
int left, right, size;
Window w = new Window();
left = right = size = 0;
while (right < s.length()) {
w.add(s.charAt(right++));
while (w.check()) {
w.remove(s.charAt(left++));
}
size = Math.max(size, w.size);
}
return size;
}
class Window {
int[] map;
int size, idx;
public Window() {
map = new int[130];
}
public void add(char c) {
if (++map[c] == 2) idx = c;
++size;
}
public void remove(char c) {
--map[c];
--size;
}
public boolean check() {
if (map[idx] == 2) return true;
return false;
}
public void disp() {
for (int i = 'A'; i <= 'z'; i++) {
System.out.print(map[i] + " ");
}
System.out.println();
}
}
}