前后指针法是 解决很多算法问题当中经常会出现的一种解决方法,也被称为是滑动窗口解法。
很多人在遇到这种问题的时候 ,可能连答案,题解什么的都懒得推敲和总结就放弃了,其实这个方法学会之后,你可以举一反三的使用在很多的算法题解题当中去。今天我们就来看看这种方法与模式。
滑动窗口解法,顾名思义,是用来解决顺序数组列表问题的,因为在算法运行过程当中,滑动窗口是一个不会断开的整体,所以一定是用于解决顺序的,甚至是排序之和 的那种数组啊,列表啊之类的问题。
举个例子,leetcode中无重复字符的最长字串的长度。
abcabcbb,这个就可以用到滑动窗口的解决办法,因为这就是一个需要顺序的,连续的数组长度作为最后结果的这么一道题。
对于滑动窗口咱们如何去用代码实现,从理解上可以使用两个 指针,一前一后。
var lengthOfLongestSubstring = function (s) {
let n = s.length;
let set = new Set();
let right = -1;
let max = 0;
for (let i=0;i<s.length;i++) {
if (i!=0) {
set.delete(s.charAt(i-1)); // i 只要不等于0就会从set中delete掉一个最前面的值。
}
while (right + 1 < n && !set.has(s.charAt(right + 1))) {
set.add(s.charAt(right+1));
++right;
}
max = Math.max(max,right - i + 1);
}
return max;
}
以上这段代码是官方给出的一个题解,但是仔细探究之后会发现,这个滑动窗口性能太差,为什么?
按道理来讲,外层for循环,进去之后,索引i 的 角色就成了 后 指针,咱们首先不管if(i!=0){} 这个里面的逻辑,先看while()里面的 rsk一开始在运行外部赋值为 -1,为什么??? rsk在这段代码中的含义就是 right,前指针,从for循环开始 i = 0, 一进入while 基本上 如果遇到 输入的测试字符串是abcdef 就出不来了,会一直到 f 才从while循环中出来,因为abcdef中没有相同的字母,
rsk+1<n && !set.has(s.charAt(rsk+1))的判断也就一直成立,rsk角色就是这个滑动窗口前指针,而且它还有一个角色,就是while循环的循环索引,++rsk,while循环就循环着,出了while循环,max就用来记录这个滑动窗口中,字符不一样的组成的那个字符串的长度,而且是最长的长度,因为每次都需要做一个比较 max = Math.max(max,rsk-i+1); 就是将之前的max的值与新构成的滑动窗口的字符串长度做比较,记录最长长度,
这里感觉最别扭的就是if()那段逻辑中的delete方法了。
if (i!=0) set.delete(s.charAt(i-1)); 只要从while循环里面出来,再次走进for循环,就会进入这段删除逻辑,那么这段删除逻辑是为了干啥呢?咱们走一个过程来看就清楚了。
第一步 (a)bcabcbb,进入while,没执行 delete, 记录最大值max为1
第二步 (ab)cabcbb,依然在while,没执行delete, 记录最大值max为2
第三步 (abc)abcbb, while最后一次,没执行delete,记录最大值max为3
第四步 a(bca)bcbb, while第二次进入,执行了delete,前指针 i 后移(因为for循环的 i++ 了),
第五步 a(bca)bcbb, 遇到了新的b,再次出了while循环,记录最大值 max为3
第六步 ab(cab)cbb,执行delete删除了b,i又再次因为for循环++了,while循环却又遇到了新的c。 记录最大值 max为3
第七步 abc(abc)bb,执行delete删除了c,i又再次因为for循环++了,while循环却又遇到了新的b, 记录最大值 max为3
第八步 abc[a(bc)b]b,此时delete的劣势显现了出来,也就是咱们遇到的abcb类型,a先出来,但是这一次执行delete之后,没有进入while循环,而是a(bc)b此时要等b从括号中delete出来,ab(c)b ,这个b才能进入括号中去 ab(cb)。
到最后 (cb)b 结尾。c先出来,c(b)b,然后b再出来,cb(b) 这样才算。
这个滑动窗口虽然让咱们学习到了,可以通过两层循环去控制一个滑动窗口的移动,但是,他的这个滑动窗口,是前面的那个指针需要刻意等待 后面那个指针跟上来,所以这个滑动窗口需要改进。
var lengthOfLongestSubstring = function (s) {
let obj = {};
let max = 0;
let left = 0;
for (let i=0;i<s.length;i++) {
if (obj.hasOwnProperty(s.charAt(i))) {
left = Math.max(left, obj[s.charAt(i)] + 1);
}
obj[s.charAt(i)] = i;
max = Math.max(max,i - left + 1);
}
return max;
}
这个就是改进的滑动窗口,后面的指针,也就是左边的指针,不会一步一步的走,让right指针等待,而是一步就跨越过去
(a)bcabcbb
(ab)cabcbb
(abc)abcbb
a(bca)bcbb
ab(cab)cbb
abc(abc)bb
abcab(cb)b 这里遇到abcb的类型的时候, (abc)b,左指针无需等待,直接跳段到c,ab(cb)。
这就是改进之后的滑动窗口。
是不是跟 教科书 里面 的滑动窗口 和 改进之和的滑动窗口 一模一样
前者用两层循环,for中 嵌套while,一个set数据结构搞定,
后者用一层for循环,一个 对象obj搞定。