给你一个字符串,请找出该字符串不包含重复字符的最长子串。
1. 暴力法 (超时)
检查所有的子串,看它是否不含重复字符
为了迭代给定字符串的所有子串,我们需要找到这些子串的开始和结束下标。分别用i和j表示开始和结束下标,则得到
0≤i<j≤n ([i,j)),然后使用两个迭代的循环,来遍历所有的子串。
为了检测一个子串是否不含重复字符,我们可以使用set,在遍历给定字符串的每一个字符时,只要set中不包含,就把它放进去,否则返回false,表示已包含该字符。循环结束后,返回true。
public class Solution {
public int lengthOfLongestSubstring(String s) {
int n = s.length();
int ans = 0;
for (int i = 0; i < n; i++)
for (int j = i + 1; j <= n; j++)
if (allUnique(s, i, j)) ans = Math.max(ans, j - i);
return ans;
}
public boolean allUnique(String s, int start, int end) {
Set<Character> set = new HashSet<>();
for (int i = start; i < end; i++) {
Character ch = s.charAt(i);
if (set.contains(ch)) return false;
set.add(ch);
}
return true;
}
}
时间复杂度: O(n^3).
空间复杂度: O(min(n, m))
2. 滑动窗口
使用暴力法,我们不断的检查一个子串是否含有重复字符,这样效率很低。通常而言,当一个子串s[i,j)已经检查过不含
重复字符,我们只需要检查s[j]是否包含在s[i,j)中。为了检查一个字符是否包含在子串中,可以扫描子串,复杂度为O(n2)
不过,使用一个HashSet作为滑动窗口,来检查某个字符时,只需要O(1)的时间。
我们使用HashSet来存储当前窗口[i,j)中的字符,然后向右移动下标j,如果它不在Hashset中,再进一步移动j,直到s[j]出现
在滑动窗口中。此时,不含重复字符的子串的起始于i,结束于j,当对所有的i执行该操作,就可以解决该问题。
public class Solution {
public int lengthOfLongestSubstring(String s) {
int n = s.length();
Set<Character> set = new HashSet<>();
int ans = 0, i = 0, j = 0;
while (i < n && j < n) {
// try to extend the range [i, j]
if (!set.contains(s.charAt(j))){
set.add(s.charAt(j++));
ans = Math.max(ans, j - i);
}
else {
set.remove(s.charAt(i++));
}
}
return ans;
}
}
时间复杂度:O(2n)=O(n).
空间复杂度: O(min(m, n))
3. 优化后的滑动窗口
我们不是通过使用一个set来判断某个字符是否出现过,而是定义一个map将一个字符映射到其下标,然后当遇到一个重复字符时,就可以迅速的跳过。
如果s[j]在窗口[i,j)范围内,必然存在j',使得s[j] == s[j']。我们不必一点点的增加i,而是直接跳过[i,j']之间的元素,使当前窗口
变为[j'+1,j)。
public class Solution {
public int lengthOfLongestSubstring(String s) {
int n = s.length(), ans = 0;
Map<Character, Integer> map = new HashMap<>(); // current index of character
// try to extend the range [i, j]
for (int j = 0, i = 0; j < n; j++) {
if (map.containsKey(s.charAt(j))) {
i = Math.max(map.get(s.charAt(j)), i);
}
ans = Math.max(ans, j - i + 1);
map.put(s.charAt(j), j + 1);
}
return ans;
}
}
(以上内容摘自LeetCode官网)
)