3. 无重复字符的最长子串
题目
原题链接:https://leetcode-cn.com/problems/longest-substring-without-repeating-characters/
给定一个字符串,请你找出其中不含有重复字符的最长子串的长度。
输入: "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
输入: "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。
输入: "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。
解题思路
以字符串\(\texttt{abcabcbb}\)为例,其中括号中表示选中的字符以及最长的字符串:
- 以\(\texttt{(a)bcabcbb}\)开始的最长字符串为\(\texttt{(abc)abcbb}\);
- 以\(\texttt{a(b)cabcbb}\)开始的最长字符串为\(\texttt{a(bca)bcbb}\);
- 以\(\texttt{ab(c)abcbb}\)开始的最长字符串为\(\texttt{ab(cab)cbb}\);
- 以\(\texttt{abc(a)bcbb}\)开始的最长字符串为\(\texttt{abc(abc)bb}\);
- 以\(\texttt{abca(b)cbb}\)开始的最长字符串为\(\texttt{abca(bc)bb}\);
- 以\(\texttt{abcab(c)bb}\)开始的最长字符串为\(\texttt{abcab(cb)b}\);
- 以\(\texttt{abcabc(b)b}\)开始的最长字符串为\(\texttt{abcabc(b)b}\);
- 以\(\texttt{abcabcb(b)}\)开始的最长字符串为\(\texttt{abcabcb(b)}\)。
如果依次递增地枚举子串的起始位置,那么子串的结束位置也是递增的!
假设选择字符串中的第\(k\)个字符作为起始位置,并且得到了不包含重复字符的最长子串的结束位置为\(r_k\)。那么当选择第\(k+1\)个字符作为起始位置时(移除第\(k\)个字符hashSet.remove(s.charAt(i - 1));),首先从\(k+1\)到\(r_k\)的字符显然是不重复的,并且由于少了原本的第\(k\)个字符,可以尝试继续增大\(r_k\),直到右侧出现了重复字符为止(不必从第\(k+2\)个字符开始遍历)。
因此,可以使用滑动窗口来解决这个问题:
-
使用两个指针表示字符串中的某个子串(的左右边界)。其中左指针代表着上文中枚举子串的起始位置,而右指针即为上文中的\(r_k\);
-
在每一步的操作中
- 左指针向右移动一格,表示开始枚举下一个字符作为起始位置;
- 不断地向右移动右指针,但需要保证这两个指针对应的子串中没有重复的字符;
- 在移动结束后,这个子串就对应着以左指针开始的,不包含重复字符的最长子串。记录下这个子串的长度;
-
在枚举结束后,找到的最长的子串的长度即为答案。
判断重复字符:
还需要使用一种数据结构来判断是否有重复的字符,常用的数据结构为哈希集合(HashSet)。在左指针向右移动的时候,从哈希集合中移除一个字符,在右指针向右移动的时候,往哈希集合中添加一个字符。
代码实现思路:
- 遍历字符;
- 以当前字符为起点,向其右侧遍历字符,如果在
HashSet中不存在遍历到的字符,则存入HashSet中(避免重复),直到遇到HashSet中存在的字符,记录截至位置rk; - 向右侧移动起点,重复步骤二。
代码实现
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.HashSet;
import java.util.Set;
/**
* 3.无重复字符的最长子串
* @date 2021-3-7
* @author chenzufeng
*/
public class No3_LengthOfLongestSubstring {
public static void main(String[] args) throws IOException {
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
System.out.println(getLengthOfLongestSubstring(bufferedReader.readLine()));
}
public static int getLengthOfLongestSubstring(String string) {
// 使用HashSet来判断是否有重复的字符
Set<Character> hashSet = new HashSet<>();
int lengthOfString = string.length();
// 当前最长字串的截止位置
int endIndexOfLongestSubstring = 0;
int result = 0;
// 以字符串每一个字符为起点,向右侧遍历,找到不重复的长度
for (int i = 0; i < lengthOfString; i++) {
// 当选择下一个字符作为起始位置时,需要去除前一个字符
if (i != 0) {
hashSet.remove(string.charAt(i - 1));
}
while (endIndexOfLongestSubstring < lengthOfString &&
! hashSet.contains(string.charAt(endIndexOfLongestSubstring))) {
// 注意这里是endIndexOfLongestSubstring
hashSet.add(string.charAt(endIndexOfLongestSubstring));
endIndexOfLongestSubstring++;
}
// 跳出while循环后,此时endIndexOfLongestSubstring对应的是重复的字符
result = Math.max(result, endIndexOfLongestSubstring - i);
}
return result;
}
}
复杂度分析
时间复杂度:\(O(N)\),其中 \(N\) 是字符串的长度。左指针和右指针分别会遍历整个字符串一次。
空间复杂度:\(O(|\Sigma|)\),其中\(\Sigma\)表示字符集(即字符串中可以出现的字符),\(|\Sigma|\)表示字符集的大小。在本题中没有明确说明字符集,因此可以默认为所有 ASCII 码在\([0, 128)\)内的字符,即\(|\Sigma| = 128\)。我们需要用到哈希集合来存储出现过的字符,而字符最多有\(|\Sigma|\)个,因此空间复杂度为 \(O(|\Sigma|)\)。

浙公网安备 33010602011771号