题目描述:
Given a string, find the length of the longest substring without repeating characters.
Examples:
Given abcabcbb
, the answer is abc
, which the length is 3.
Given bbbbb
, the answer is b
, with the length of 1.
Given pwwkew
, the answer is wke
, with the length of 3. Note that the answer must be a substring, pwke
is a subsequence and not a substring.
给定一个字符串,找出不含有重复字符的最长子串的长度。
示例:
给定 abcabcbb
,没有重复字符的最长子串是 abc
,那么长度就是3。
给定 bbbbb
,最长的子串就是 b
,长度是1。
给定 pwwkew
,最长子串是 wke
,长度是3。请注意答案必须是一个子串,pwke
是 子序列 而不是子串。
解法一(Brute Force):
暴力解法,枚举所有的子串,使用两层循环,外层 i 为 [0…n-1],内层 j 为 [i+1…n]。接下来就是这么判断子串中是否有重复的元素,我们可以使用 Set 集合数据结构,一旦发现遍历的字符,集合中已经存在,则返回 false 即可。
public int lengthOfLongestSubstring(String s) {
int n = s.length();
int max = 0;
for ( int i = 0 ; i < n - 1; i ++ ) {
for ( int j = i + 1 ; j < n ; j ++ ) {
if (allUnique(s, i, j))
max = Math.max(max, j - i + 1);
}
}
return max;
}
public boolean allUnique(String s, int start, int end) {
// [start...end]
HashSet<Character> set = new HashSet<>();
for (int i = start ; i <= end ; i ++) {
char c = s.charAt(i);
if (set.contains(c))
return false;
set.add(c);
}
return true;
}
时间复杂度:
O(n3)
O
(
n
3
)
,为了检查字符是否在
[i,j)
[
i
,
j
)
,需要扫描它们,时间复杂度
O(j−i)
O
(
j
−
i
)
。
空间复杂度:
O(min(n,m))
O
(
m
i
n
(
n
,
m
)
)
,我们需要
O(k)
O
(
k
)
的空间来检查子串中是否含有重复元素,k由Set的大小决定。n为Set集合大小的上界,m为字符集大小/字母数量。
解法二(Sliding Window):
解法一对子串做了许多重复的检查,有一大部分是不需要的。比如子串 sij s i j 从 i i 到 已经确认没有重复的字符,那么我们只需要判断 s[j] s [ j ] 在子串 sij s i j 中存在。为了检查字符是否已经包含在子串中,我们扫描了子串,导致时间复杂度 O(n2) O ( n 2 ) 。
如果使用HashSet
作为滑动窗口,就可以使时间复杂度降为
O(1)
O
(
1
)
。所以我们可以使用HashSet
储存当前窗口的字符
[i,j)(j=i
[
i
,
j
)
(
j
=
i
初始时),j向右滑动窗口,如果不在HashSet
中,就继续滑动j,直到
s[j]
s
[
j
]
已经在HashSet
中了,这样我们就找到了以
i
i
开始最长的没有重复字符的子串,一直遍历所有的,就可以得到答案。
public int lengthOfLongestSubstring(String s) {
// [i,j)
int n = s.length();
Set<Character> set = new HashSet<>();
int max = 0;
int i = 0;
int j = 0;
while (i < n && j < n) {
if (!set.contains(s.charAt(j))) {
set.add(s.charAt(j++));
max = Math.max(max, j - i );
}
else {
set.remove(s.charAt(i++));
}
}
return max;
}
时间复杂度:
O(2n)=O(n)
O
(
2
n
)
=
O
(
n
)
,最坏的情况是每个字符要遍历两次。
空间复杂度:
O(min(n,m))
O
(
m
i
n
(
n
,
m
)
)
,我们需要
O(k)
O
(
k
)
的空间来检查子串中是否含有重复元素,k由Set的大小决定。n为Set集合大小的上界,m为字符集大小/字母数量。
解法三(Sliding Window Optimized):
解法二的优化,解法二中最多需要 2n 步,实际上并不需要那么多,当在确认右边界处的字符在集合中已经存在,那么不是一步一步地向前推进左边界到重复元素处,而是立即跳过包括重复元素在内之前的元素。
public int lengthOfLongestSubstring(String s) {
// 优化:map 的value保存的是字符在s中的索引位置
// 假如s[j] 与 s[i...j)中有重复元素j'
// 更新 l 可以跳过 [l...j'] 范围内的所有元素,并直接将 l 变为 j' + 1
Map<Character, Integer> map = new HashMap<>();
int l = 0;
int r = 0;
int max = 0;
while ( l < s.length() && r < s.length()) {
if (!map.containsKey(s.charAt(r))) {
map.put(s.charAt(r++), r); // without repeat, move r everytime
max = Math.max( max, r-l );
}
else {
Integer remove = map.remove(s.charAt(l)); // remove repeat character
l = remove + 1;
}
}
return max;
}
如果事先已经知道字符串中的字符种类很少,那么就可以将 Map 替换为整形数组直接访问,常见的数组为:
int[26]
for Letters ‘a’ - ‘z’ or ‘A’ - ‘Z’int[128]
for ASCIIint[256]
for Extended ASCII
public int lengthOfLongestSubstring(String s) {
int[] freq = new int[256];
int l = 0;
int r = 0;
int max = 0;
while ( l < s.length() && r < s.length()) {
if (freq[s.charAt(r)] == 0) {
freq[s.charAt(r++)] ++;
max = Math.max( max, r-l );
}
else {
freq[s.charAt(l++)] --;
}
}
return max;
}
时间复杂度:
O(n)
O
(
n
)
,索引 j 将会迭代 n 次。
空间复杂度(HashMap):
O(min(m,n))
O
(
m
i
n
(
m
,
n
)
)
。
空间复杂度(Table):
O(m)
O
(
m
)
,m是字符集的大小。
解法四:
还有一种思路是设置一个counter计数变量,计算当前窗口中重复的元素:如果没有重复的元素,右指针就依次向右移动,并记录最大不重复子串的长度;如果有重复的元素(counter > 0),那么就向右移动左指针,并判断当前元素是否是重复的元素,如果是那么counter将相应的减一,如果不是则直接移动左指针,直到counter等于0。
public int lengthOfLongestSubstring(String s) {
int[] hash = new int[256];
int l = 0;
int r = 0;
int max = 0;
int counter = 0; // 重复元素数量
while ( r < s.length() ) {
if ( hash[s.charAt(r++)] ++ > 0 )
counter++;
while (counter > 0) // 找到重复元素,右移左边界
if (hash[s.charAt(l++)] -- > 1)
counter --; // 变为可用
max = Math.max( max, r-l );
}
return max;
}
代码都是可以直接运行的,希望能帮到一些人,如果文章里有说得不对的地方请前辈多多指正~