剑指Offer 48、最长不含重复字符的子字符串

152 阅读2分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第2天,点击查看活动详情

题目:请从字符串中找出一个最长的不包含重复字符的子字符串,计算该最长子字符串的长度。

解题思路

在做算法题时,我们可以首先不纠结最终的时间复杂度和空间复杂度,重要的是以自己可以理解的方式将此题先做出来,这样不管是对算法的优化还是其它方法的实现都会有一个更加清晰的认识。

解决本题需要关注三个问题:

  1. 子串的范围是如何确定。
  2. 子串如果存在重复字符如何判断。
  3. 子串出现了重复字符之后应该怎么办。

对于子串的范围,我们可以直接使用先固定一端再确定另一端的方式,即使用二层循环即可。判断重复元素最直接的方法是使用HashSet,若出现了重复字符,那本次的左端开始的索引即结束之后的判断。最终可得代码如下:

public int lengthOfLongestSubstring(String s) {
    if(s.length()<2) return s.length();
    int maxLen = 0;
    HashSet<Character> set = new HashSet<>();
    for(int i=0;i<s.length();i++){
        for(int j=i;j<s.length();j++){
            if(!set.contains(s.charAt(j))){
                set.add(s.charAt(j));
            }else {
                maxLen = Math.max(maxLen, set.size());
                set = new HashSet<>();
                break;
            }
        }
    }
    return maxLen;
}

时间复杂度还是很高的,为O(n2)O(n^2),最终耗时近90ms。很显然本题有着更简单的方法。

我们试着能不能对上面的代码进行优化,实际上上述代码存在很多重复计算,例如从0-n的字符串计算一遍,之后1-n又计算一遍,此时1-n已经在0-n的时候计算了,也就产生了重复计算。优化的思路是能不能只遍历一次,每当有字符和前面的子串重复时就将当前字符作为下一个字串的起点,也即滑动窗口的思想,我们可以首先固定左端,每次右移窗口,一旦右移的字符在左边已经出现就将左边窗口右移,右移的次数取决于窗口中是否还包含重复的字符,可得代码如下:

public int lengthOfLongestSubstring(String s) {
    int maxLen = 0;
    HashSet<Character> set = new HashSet<>();
    for(int i=0,j=0;i<s.length();i++){
        char c = s.charAt(i);
        while(set.contains(c)){ // 窗口一直移动,直到窗口中不包含和当前字符相同的字符
            set.remove(s.charAt(j++));
        }
        set.add(c);
        maxLen = Math.max(maxLen, i-j+1);
    }
    return maxLen;
}

最终时间复杂度为O(n)O(n)