力扣刷这题的时候看到评论区给出一个非常精妙的解答,在此记录一下。
思路
滑动窗口:
- start:指向子串的起始位置索引
- end:指向子串的结束位置索引
- end-start+1:子串长度
窗口有了,接下来需要设计一个缓存结构,用来判断每次end向后移动时,指向的字符是否在子串中出现过。这里使用的是HashMap。key为字符,value为字符在字符串中的索引。
代码
public static int lengthOfLongestSubstring(String s) {
HashMap<Character, Integer> cache = new HashMap<>();
//用于记录无重复字符串的最长字串长度
int max = 0;
//起始指针
int start = 0;
for (int end = 0; end < s.length(); end++) {
Character ch = s.charAt(end);
if (cache.containsKey(ch)) {
start = Math.max(cache.get(ch) + 1,start);
}
max = Math.max(end - start + 1, max);
cache.put(ch, end);
}
return max;
}
疑惑
不理解的地方在于这行代码:
start = Math.max(cache.get(ch) + 1,start)
解析
按照我自己的想法,赋值start的时候有必要和start本身比较么,必定是比他大的吧?为什么不直接跳转呢?
于是普确信的我将这行代码改成:
start = cache.get(ch) + 1
果然出问题了!
若输入的字符串为"abba",当end指针指向索引为2的'b'时,由于缓存中以及存在了key为'b'的entry,所以start会变成2;进行下一轮循环时,end变为3,ch变为a。 此时,if判断条件成立,start变成字符串中第一个a的索引+1,即0+1=1。如此一来,start就回退了。 出现这个问题的原因是,当我们第一次移动start时,并没有将<'a',0>这个entry移除出HashMap。 所以,后面再出现元素a时,start就会回退到a第一次出现的索引位置。
更一般的,我们每一次移动start时,都没有将任何entry移除出HashMap,而是利用HashMap.put()方法的性质,对缓存中重复字符的索引进行了覆盖。因此,也就不能简单地让start进行跳转。而是需要先进行一个判断:当start>cache.get(ch)时,表示字符ch虽然在字符串中出现过,但已经不在当前维护的字串中了。所以就没有必要对start进行变动。相反的,若start<cache.get(ch),说明字符ch不仅在字符串中出现过,而且在当前维护的字串中也存在,所以需要让start跳转。
本质上,cache中存放的并非是子串中出现过的字符,而是原字符串中出现的字符和其最近一次出现的索引位置。
总结一下,自己还是太菜了。好多精巧的设计需要自己debug才能弄明白。