题目:给定一个字符串s,请你找出其中不含有重复字符的最长子串的长度。
示例1:
输入:s = "abcabcbb"
输出:3
解释:因为无重复字符的最长子串是"abc",所以其长度为3。
示例2:
输入:s = "bbbbb"
输出:1
解释:因为无重复字符的最长子串是"b",所以其长度为1。
示例3:
输入:s = "pwwkew"
输出:3
解释:因为无重复字符的最长子串是"wke",所以长度为3。
示例4:
输入:s = ""
输出:0
1. 思考
解决思路很简单,依次遍历所有字符作为起始位置,找到所有可能的不重复字符子串,即可以得知其中最长子串的长度。
比如需要判断字符串"abcabcbb"。
在该思路下,有两种实现方案,一是常规解法,一是优化方案,滑动窗口。
其中常规解法就是每次更新起始索引,从该索引开始依次往后匹配,找到最长无重复字符子串。但是这种解法其实每次都会存在重复工作。
从上图匹配步骤可以发现,随着查找的起始位置递增,结束位置也是递增的,比如上次找到begin ~ end间字符是不重复的,那么begin + 1 ~ end间字符也必定是不重复的,而且begin + 1 ~ end去除了begin对应的字符,所以end可以向后继续尝试匹配,直到找到非重复字符。这就是滑动窗口思想,这边本质其实就是复用了上一次匹配得到的信息,避免重复工作。
2. 常规解法
依次更新起始索引,从该位置开始找当前不重复子串最长长度,代码实现如下:
public int lengthOfLongestSubstring(String s) {
if (s == null || s.length() < 1) {
return 0;
}
int maxLen = 0;
for (int begin = 0; begin < s.length(); begin++) {
// 从begin开始往后找不重复字符最长长度
Set<Character> charSet = new HashSet<>();
int currLen = 0;
for (int end = begin; end < s.length() && !charSet.contains(s.charAt(end)); end++) {
// end字符没有重复,更新长度
currLen++;
// 记录当前字符
charSet.add(s.charAt(end));
}
// 判断是否需要更新子串最长长度
maxLen = Math.max(maxLen, currLen);
}
return maxLen;
}
时间复杂度O(n^2),空间复杂度O(A),其中A是字符集大小,因为需要Hash集合存储所有出现过的字符,所以最多需要字符集大小的空间。
3. 滑动窗口
记录上一次判断的结束索引位置end,每次更新起始位置后,先将上一个起始位置字符从已出现过字符集中去除,然后直接从end继续向后判断。代码实现如下:
public int lengthOfLongestSubstring(String s) {
if (s == null || s.length() < 1) {
return 0;
}
Set<Character> charSet = new HashSet<>();
int maxLen = 0;
// 认为begin - end间是检查过的字符,初始为-1表示尚未开始检查
int end = -1;
for (int begin = 0; begin < s.length(); begin++) {
if (begin > 0) {
// 如果存在上一个起始节点,后移时把上个字符去除
charSet.remove(s.charAt(begin - 1));
}
while ((end + 1 < s.length()) && !charSet.contains(s.charAt(end + 1))) {
// end + 1与前面字符不重复,添加进集合
charSet.add(s.charAt(end + 1));
end++;
}
// 判断是否需要更新子串最长长度
maxLen = Math.max(maxLen, end - begin + 1);
}
return maxLen;
}
注意代码中处理end的方式,定义了end初始化值为-1,每次处理从end后一个索引检查,判断是否是重复字符,这样可以避免begin后移后,添加begin所在字符的逻辑在begin这个分支上,统一让end指示当前需要添加和判断的字符。
可以考虑两个边界场景:
1. begin = 0
此时需要把begin对应的字符添加到字符集,如果end = 0,那么在该场景需要特殊处理,把end对应的字符放入字符集,因为一般场景下,是顺序后移end,不需要考虑end对应索引。但是当初始end = -1时,可以保持处理逻辑与一般场景一致,只需要考虑end + 1索引对应的字符是否重复即可。
2. begin = end
与上述场景一样,判断end + 1索引位置可以统一边界场景。
时间复杂度O(n):因为begin和end都只遍历了一遍字符串。
空间复杂度O(A):其中A为字符集大小,因为需要Hash集合保存非重复字符,字符最大个数为字符集大小。