3. 无重复字符的最长子串
给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。
示例 1:
输入: s = “abcabcbb”
输出: 3
解释: 因为无重复字符的最长子串是 “abc”,所以其长度为 3。
示例 2:
输入: s = “bbbbb”
输出: 1
解释: 因为无重复字符的最长子串是 “b”,所以其长度为 1。
示例 3:
输入: s = “pwwkew”
输出: 3
解释: 因为无重复字符的最长子串是 “wke”,所以其长度为 3。
请注意,你的答案必须是 子串 的长度,“pwke” 是一个子序列,不是子串。
示例 4:
输入: s = “”
输出: 0
提示:
0 <= s.length <= 5 * 104
s 由英文字母、数字、符号和空格组成
使用双指针滑动窗口解题
hash 数组设置为 128 看一下 ascii 码表就懂了
class Solution {
public int lengthOfLongestSubstring(String s) {
// 索引是字符的ascii码,值是字符出现的次数
int[] hash=new int[128];
int left=0;
int res=0;
for(int right=0; right<s.length(); right++){
// 一旦出现重复字符,广快移动左边界
// 直到把重复的字符移出窗口(s.charAt(right)就是重复的字符)
// 用来从hash表中取值的索引,要用字符的ascii码
while(hash[s.charAt(right)]==1){
hash[s.charAt(left)]--;
left++;
}
hash[s.charAt(right)]++;
res=Math.max(res, right-left+1);
}
return res;
}
}
还可以优化
为什么要用 while 呢?
因为如果是 [b, c, a, e, d, a] 这种情况,当 right 指针走到第二个 a 时就会发现出现重复的字符了,因此要移动 left 指针到 下标为 3 的位置( e 的位置),从而窗口中不包含两个 a (窗口为 [left, right])
仔细想想,这不就是移动到上一个 a 的位置 +1 的位置吗
,那我们直接移动 left 指针到这个位置不就好了吗?
因此我们只需要记录上一次该重复字符出现的位置就行了
用 hash 表来记录重复字符上一次出现的位置。
我们写出了第一版的代码
此方法暂时有 bug
class Solution {
public int lengthOfLongestSubstring(String s) {
// 记录字符上一次出现的位置
int[] hash=new int[128];
int left=0;
int res=0;
for(int right=0; right<s.length(); right++){
// 不等于0了,说明出现了重复的字符
if(hash[s.charAt(right)]!=0){
left=hash[s.charAt(right)]+1;
}
hash[s.charAt(right)]=right;
res=Math.max(res, right-left+1);
}
return res;
}
}
bug 是什么呢?
我们这个 hash 表中值为 0 代表未出现,但是数组是从 0 开始的,因此下标为 0 的元素(也就是第一个元素),我们会误认为它没有出现过,比如 [a, b, c, a],这样就会多统计一个数。
那么让 hash 表初始值为 -1 就行了
第二版代码
此写法也有 bug
class Solution {
public int lengthOfLongestSubstring(String s) {
// 记录字符上一次出现的位置
int[] hash=new int[128];
Arrays.fill(hash, -1);
int left=0;
int res=0;
for(int right=0; right<s.length(); right++){
// 不等于-1了,说明出现了重复的字符
if(hash[s.charAt(right)]!=-1){
left=hash[s.charAt(right)]+1;
}
hash[s.charAt(right)]=right;
res=Math.max(res, right-left+1);
}
return res;
}
}
bug 是什么呢?
我们只是简单粗暴的把 left 指针移动到,上一次该字符出现的位置 +1 的位置,万一是 [a, b, b, a] 这种情况,left 指针本来都走到下标为 2 的位置,结果到最后一个字符时,就回退到 0 的位置。
解决 bug 的关键,就是让 left 指针不发生回退
第三版代码
class Solution {
public int lengthOfLongestSubstring(String s) {
// 记录字符上一次出现的位置
int[] hash=new int[128];
Arrays.fill(hash, -1);
int left=0;
int res=0;
for(int right=0; right<s.length(); right++){
// 不等于-1了,说明出现了重复的字符
if(hash[s.charAt(right)]!=-1){
// 很简单,取个max就可以不让left发生回退
left=Math.max(left, hash[s.charAt(right)]+1);
}
hash[s.charAt(right)]=right;
res=Math.max(res, right-left+1);
}
return res;
}
}
这个 if 判断条件可以去掉,因为数组的每个索引都是大于等于 0 的
于是就出现了和热评第一相同的代码
class Solution {
public int lengthOfLongestSubstring(String s) {
// 记录字符上一次出现的位置
int[] hash=new int[128];
Arrays.fill(hash, -1);
int left=0;
int res=0;
for(int right=0; right<s.length(); right++){
left=Math.max(left, hash[s.charAt(right)]+1);
hash[s.charAt(right)]=right;
res=Math.max(res, right-left+1);
}
return res;
}
}
最后的最后
其实第二种方法就是对滑动窗口 left 指针的移动进行了优化,left 不再一步一步走了,而是跳跃式的移动
有些代码虽然看上去很精简,很牛B,但其实它缺少了很多的细节,导致我们难以理解;编程语言是语言(搁这搁这呢),就像我们说话一样,如果我们一句话说的很精简,那么就可能缺失一些信息,导致别人难以理解。
所以并不是越精简的代码越好,信息全面,容易理解的代码才是最好的