问题描述
给定一个字符串,找出不含有重复字符的最长子串的长度。
示例1:
输入: “abcabcbb”
输出: 3
解释: 无重复字符的最长子串是 “abc”,其长度为 3。
示例2:
输入: “bbbbb”
输出: 1
解释: 无重复字符的最长子串是 “b”,其长度为 1。
示例3:
输入: “pwwkew”
输出: 3
解释: 无重复字符的最长子串是 “wke”,其长度为 3。
请注意,答案必须是一个子串,“pwke” 是一个子序列 而不是子串。
我的答案
思考
初步:想到的是遍历一遍字符串,定义两个StringBuffer,1和2如果没有相同的就向1append添加,如果有就将1的值赋给2,然后将1置空,每一次遍历比较1和2的长度,若1的长度大则将1的值赋给2,遍历完成后则会返回最长字符串。但是如果输入的是"dfvda"这样子的字符串,它将会输出"da",而不是预期的"fvda"。
改进:出现问题后想到的是出现相同的字符后截取第一次出现字符到第二次相同字符的字串,也就是说,当遇到第二个d时,将2赋值为"fvd",然后每一次遍历时进行长度比较,最后可以达到预期效果。
代码
class Solution {
public int lengthOfLongestSubstring(String s) {
if(s.equals("")){
return 0;
}
if(s.equals(" ")){
return 1;
}
StringBuffer subString = new StringBuffer();
StringBuffer stringBuf = new StringBuffer();
for(int i = 0; i < s.length(); i++){
String ch1 = s.substring(i,i+1);
if(stringBuf.indexOf(ch1)<0){
stringBuf.append(ch1);
}else{
stringBuf.replace(0,stringBuf.length(),stringBuf.substring(stringBuf.indexOf(ch1)+1,stringBuf.length()));
stringBuf.append(ch1);
System.out.println(stringBuf);
}
if(stringBuf.length()>=subString.length()){
subString.replace(0,subString.length(),stringBuf.toString());
}
}
System.out.println(subString);
return subString.length();
}
}
复杂度分析
虽然看起来整个字符串只遍历了一遍,但是其中字符的搜索使得耗时增加,也就是说时间复杂度为O(n)~O(n^2)之间,完全取决于运气,所以这个算法并不好。
官方答案
优化后
算法思想
官方给出了三种答案,第一种是暴力法,很愚蠢,第二种就是类似于自己写的答案,也就是滑动窗口,而字符串的查找使用的是hashset,每一次的查找时间就可以缩减为O(1),在取出每一个字符时,对字符进行判断,如果没有则加入set,并将滑动窗口的右坐标加一,如果有,则去除前一个重复元素,并将滑动窗口的左坐标加一,最后每一次加入元素时需要进行判断,如果右坐标-左坐标>set长度,则最长字串为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++)); //如果没有则加入,右坐标+1
ans = Math.max(ans, j - i); //比较,取大的为ans
}
else {
set.remove(s.charAt(i++)); //如果有则去除最左,并将左坐标+1
}
}
return ans;
}
}
复杂度分析
- 时间复杂度:O(2n)=O(n),在最糟糕的情况下,每个字符将被 ii 和 jj 访问两次。
- 空间复杂度:O(min(m, n)),与之前的方法相同。滑动窗口法需要 O(k) 的空间,其中 k 表示 Set 的大小。而Set的大小取决于字符串 nn 的大小以及字符集/字母 m 的大小。
最优解
算法思想
上述的方法最多需要执行 2n 个步骤。事实上,它可以被进一步优化为仅需要 n 个步骤。我们可以定义字符到索引的映射,而不是使用集合来判断一个字符是否存在。 当我们找到重复的字符时,我们可以立即跳过该窗口。
也就是说,如果s[j]在[i,j)范围内有与j’重复的字符,我们不需要逐渐增加 i 。 我们可以直接跳过[i,j’] 范围内的所有元素,并将 i 变为j’ +1。
代码
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;
}
}
复杂度分析
- 时间复杂度:O(n),索引 j 将会迭代 n 次。
- 空间复杂度(HashMap):O(min(m,n)),与之前的方法相同。
- 空间复杂度(Table):O(m),m 是字符集的大小。