LeetCode 第三题 求不包含重复字符的最长子字符串的长度
例如 字符串 abbca 最长的不包含重复字符的子字符串 是 bca ,长度为3
例如 字符串 qqq 最长的不包含重复字符的子字符串 是 qqq ,长度为1
例如 字符串 abcabdb 最长的不包含重复字符的子字符串 是 cabd ,长度为4
解法一:暴力求解
找所有子字符串,判断子字符串是否有重复字符,返回最大不重复字符串的长度。
这是最直接,也是最容易想到的办法。
例如 字符串 abcabdb 它的子字符串是:
包含下标为0的 a ab abc abca abcad abcabdb
包含下标为1的 b bc bca bcab bcabd bcabdb
包含下标为2的 c ca cab cabd cabdb
包含下标为3的 a ab abd abdb
...
下面看代码:
public int lengthOfLongestSubstring(String s) {
int max = 0;
for(int i = 0 ; i < s.length() ; i++) {
List<Character> temp = new ArrayList<>();
for(int j = i ; j < s.length() ; j++) {
// 每一次的add操作,temp里面都会形成一个新的子字符串
temp.add(s.charAt(j));
// 判断子字符串是否包含重复字符
if(listIsUnique(temp)) {
max = Math.max(max, temp.size());
}
}
}
return max;
}
private static boolean listIsUnique(List<Character> list) {
Set<Character> set = new HashSet<>();
for(Character c : list) {
if(set.contains(c)) {
return false;
}else {
set.add(c);
}
}
return true;
}
复制代码
好的,提交到leetcode,看看结果
哈哈,最后一个用例耗时太长,不给通过。 粗略看看我们的暴力求解时间复杂度吧。外层两个for循坏,求出子字符串,然后又是一个for循环,判断是否重复。时间复杂度大概是O(N^3)。我们稍微优化下。
我们关心的是子字符串的长度,而不是具体的子字符串。所以我们可以不维护具体的子字符串,只关心长度。构造子字符串的方法还是两个for循环,但是判断是否重复时,可以使用hashSet替代一个for循环。看代码:
public int lengthOfLongestSubstring(String s) {
int max = 0;
for(int i = 0 ; i < s.length() ; i++) {
// 使用hasSet判断元素是否重复
Set<Character> temp = new HashSet<>();
for(int j = i ; j < s.length() ; j++) {
char ch = s.charAt(j);
// 因为是从下标为i的字符开始,依次添加字符构造子字符串。
// 所以当前元素重复,那么后面构造的子字符串必然也重复,所以跳出循环。
if(temp.contains(ch)) {
break;
}else {
temp.add(ch);
}
max = Math.max(max, temp.size());
}
}
return max;
}
复制代码
提交LeetCode,查看结果。
解法二:滑动窗口解法
想象一个窗口,窗口只能向右添加元素,从左删除元素。
我们维护一个list代表当前窗口中的元素。从字符串的第一个元素开始,向右遍历,如果窗口中不包含遍历的当前元素,则将当前元素添加到list,windowRight++,同时记录一个当前窗口的大小;如果窗口中包含遍历的当前元素,则从list移除最左边的元素,即下标为0的元素,windowLeft++ 。 循环条件:windowLeft 和windowRight 小于字符串长度。 每次添加元素时,窗口的最大值,即是我们要的结果。
看代码:
public int lengthOfLongestSubstring(String s) {
int windowLeft = 0;
int windowRight = 0;
LinkedList<Character> set = new LinkedList<>();
int sLength = s.length();
int max = 0;
while (windowRight < sLength && windowLeft < sLength) {
char ch = s.charAt(windowRight);
if (!set.contains(ch)) {
set.add(ch);
windowRight++;
max = Math.max(max, set.size());
} else {
set.remove(0);
windowLeft++;
}
}
return max;
}
复制代码
滑动窗口算法解释
1、定义窗口的作用
我们需要求字符不重复子字符串的最大长度,所以一定要先有子字符串,窗口所圈起来的范围就是子字符串。当窗口大小固定为1时,每向右滑动一次,便新得到一个长度为1的子字符串。当窗口长度为2时,没向右滑动一次,便得到一个长度为2的子字符串。 于是我们的问题便转换成了如何寻找一个最大的不含有重复元素的窗口
2、为什么当遇到新元素时,窗口向右扩张一个单位,而左边不动?
我们的目标是求最大的长度。如果当前窗口长度为1,向右滑动,得到的子字符串长度永远为1。但是,因为是新元素,所以我们至少可以得到一个1+1=2长度的不重复子字符串。
3、为什么遇到重复元素时,窗口要从左边缩小一个单位,而右边不动?
我们的窗口里的元素永远要是不重复的,因为我们是从左向右滑动,因为要保证窗口元素不重复,所以窗口要从左边缩小,直到没有重复元素,此时才可以进行下一轮的扩张。 最终,扩张期间,窗口的最大值即为我们所求。