LeetCode3. Longest Substring Without Repeating Characters

[Chinese ver]

3.最长的没有重复字符的子字符串

原文出处http://leonchen1024.com/2018/07/21/3-Longest-Substring-Without-Repeating-Characters/Record[Chinese-ver]/

给你一个字符串,得出最长的一个没有重复字符的子字符串的长度。

例子:

给定“abcabcbb”,答案是“abc”,长度为3。

给定“bbbbb”,答案是“b”,长度为1。

给定“pwwkew”,答案是“wke”,长度为3.

注意答案必须是一个子字符串,“pwke”是一个子序列,而不是一个子字符串。

先来一个极其繁琐的算法,一开始没有经过太多的思考,导致不断有没考虑到情况发生,不断的修修改改,成了一个极其冗余的代码。。

初解

public class Solution {
    public int lengthOfLongestSubstring(String s) {
        HashMap<String,Integer> map = new HashMap<>();
        int maxSubLength = 0;
        int tempLength = 0;
        boolean isFirstRepeat = true;
        String beginSub = "";
        int bigStartNum = 0;
        for (int i=0;i<s.length();i++){
            beginSub = String.valueOf(s.charAt(i));
            if(map.containsKey(beginSub)){
                if(map.get(beginSub)>bigStartNum){
                    tempLength = i - map.get(beginSub);
                    bigStartNum = map.get(beginSub);
                }else{
                    tempLength = i - bigStartNum;
                }
                if(tempLength>maxSubLength){
                    maxSubLength = tempLength;
                }
                if(isFirstRepeat){
                    tempLength = i;
                    isFirstRepeat =false;
                    if(tempLength>maxSubLength){
                    maxSubLength = tempLength;
                }
                }
            }else{
                 tempLength = i - bigStartNum;
                   if(tempLength>maxSubLength){
                    maxSubLength = tempLength;
                }
            }
            map.put(beginSub,i);
        }
        if((s.length()-bigStartNum)>maxSubLength){
            maxSubLength = (s.length()-bigStartNum-1);
        }
        if(isFirstRepeat){
            return s.length();
        }

        return maxSubLength;
    }
}

[图片上传失败...(image-dff03e-1532413554986)]

痛定思过,惨痛的教训告诉我们要深思熟虑后再写代码,这个代码就不分析了,写出来的东西自己都要看不懂了,我连注释都不好意思写了,只是给自己留个教训。。接下来会对这个方法做一个优化和重新构思。

二次解

public class Solution {
    public int lengthOfLongestSubstring(String s) {
        HashMap<String,Integer> map = new HashMap<>();
        //max substring's length
        int maxSubLength = 0;
        //the sub character now
        String nowSub = "";
        //the bigger index which the string can start
        int bigStartNum = -1;
        for (int i=0;i<s.length();i++){
            nowSub = String.valueOf(s.charAt(i));
            if(map.containsKey(nowSub)){
                //if the character repeat,you can start the string from the larger one of the nowSub occur before this time and bigStartNum.
                bigStartNum = Math.max(map.get(nowSub), bigStartNum);
            }
            //the maxSubLength is the maximum number of the length of bigStartNum to i and old maxSubLength
            maxSubLength = Math.max(i - bigStartNum, maxSubLength);
            map.put(nowSub,i);
        }
        return maxSubLength;
    }
}

优化之后的代码就很清晰了。重新构思的时候才想到还有一个Math.max()的方法可以用,对api的敏感度还是不够。

[图片上传失败...(image-c8ed22-1532413554986)]

分析
这个方法的原理就是将每一个字符进行遍历,为了避免多重循环嵌套,我们使用了hashmap来将时间成本换成空间成本,通过hashmap来判断前面是否有这个字符,并存储他的序号信息。那怎么得到最长的子字符串呢?其实可以分成以下几种情况:
我们在前一个重复的字符加上(p)来表示,后一个重复的字符加上(n)来表示
1.这个字符是第一个重复的字符a,长度应该是从开始到a(n)的前一位。
2.这个字符不是第一个重复的字符a,上一个重复的字符为b,如果a(p)在b(p)的后面那么长度是两个a之间的长度,如果a(p)在b(p)的前面,那么长度是a(n)到b(p)之间(因为a(n)到a(p)之间包含了两个b,这样的字符串是不合规矩的)
3.这个字符没有出现重复的情况,长度就是这个字符到上一个重复字符a(p)出现的位置加一。

整理一下现在的情况,其实以上的条件都可以整理成一种方式来计算,因为子字符串必须是连续的,所以就是计算当前字符到上一个任意重复字符点的长度,然后取出最大的一个。

时间复杂度 : O(n) .
空间复杂度 : O(n) .

接下来我们看一下这类题目的几类解法。

方法一:暴力循环

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;
    }

    //judge that all the characters in s is unique.
    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;
    }
}

[图片上传失败...(image-caf983-1532413554986)]

分析
这个方法的原理很简单,就是最外层的循环L1遍历每一个字符a,嵌套一个遍历a之后所有字符b的循环L2,在L2中又用了一个循环L3用hashset的方式来遍历a到b间以a为开头的所有字符串,并判断是否是没有重复值的字符串。嵌套了三层的循环!!!直接导致了Time Limit Exceeded 的结果,表示你这个方法太耗时了,不予通过。
时间复杂度 : O(n^3) . 首先是最内层的[i,j)次的循环,时间复杂度为O(j-i)。然后是每一个j的循环[i+1,n),

n
∑ O(j−i)
i+1

最后算上最外层的a的循环[0,n).时间复杂度为

  n-1   n             n-1   (1+n-i)(n-i)
O( ∑  ( ∑ (j-i))) = O(∑    —————————————— ) = O(n^3)
  i=0  j=i+1          i=0         2

这个转化过程呢其实就是利用的等差数列的n项和公式,太难打公式了,中间步骤就不详细写了。
[图片上传失败...(image-fc9556-1532413554986)]

空间复杂度 : O(min(n,m)) . n就是字符串的长度,m是字母表的字符集的值。因为我们最多就可能有m个不重复的值,所以hashset的size最大也只会是m.

这个方法在逻辑上可以稍微做一个优化,主要的思路就是其实如果a到b已经是包含重复的字符了,那么a到b后面的字符也一定是包含重复字符的。可以省略了那些比较。

方法二:滑窗算法(也称K近邻算法)

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;
    }
}

[图片上传失败...(image-9075e1-1532413554986)]

分析
方法一重复检查每一个子字符串是否重复,其实没有必要这样,方法二就是避免了多次重复检查,当i~j没有重复时,我们只需要检测s[j + 1]是否和i~j中的字符重复。直到j+1的字符重复了,我们就得到了i字符的最长不重复字符。当j不小于n时就可以停止计算了,因为此时的[i,j)肯定要长于[i+1,j)。

时间复杂度 : O(2n) = O(n)
空间复杂度 : O(min(m,n)) n就是字符串的长度,m是字母表的字符集的值。因为我们最多就可能有m个不重复的值,所以hashset的size最大也只会是m.

方法三:改良后的滑窗算法

HashMap

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;
    }
}

[图片上传失败...(image-9b1bf5-1532413554986)]

分析
这个方法是对方法二的改进,我们使用HashMap来保存出现的字符和他的位置,当出现重复字符时,我们可以直接定位到重复字符的位置(注意这里的位置和程序上的序号是不同的,他的第一位就是1而不是我们程序上的0.),对比以前的i取较大的值赋值为新的i。为什么取大值可以看上面的二次解

时间复杂度 : O(n)
空间复杂度 : O(min(m,n)) 和方法二相同

ASCII 128

public class Solution {
    public int lengthOfLongestSubstring(String s) {
        int n = s.length(), ans = 0;
        int[] index = new int[128]; // current index of character
        // try to extend the range [i, j]
        for (int j = 0, i = 0; j < n; j++) {
            i = Math.max(index[s.charAt(j)], i);
            ans = Math.max(ans, j - i + 1);
            index[s.charAt(j)] = j + 1;
        }
        return ans;
    }
}

[图片上传失败...(image-4cff11-1532413554986)]

效率最快的一种,在同样的复杂度上为什么会比上一种hashmap的方法快那么多呢,简单点说,其实集合大部分都是由数组构成的,hashmap其实就是由数组加链表封装而成的,所以在速度上数组查找会优于hashmap查找。

分析
这个方法原理和上一个使用hashmap的方法大致相同,但是它使用的是int[]来存储键值对,使用字符来做下标(是的,字符可以做下标,准确的来说是整型字符常量可以做下标,会被解析为他的ASCII码值),位置来做值。

时间复杂度 : O(n)
空间复杂度 : O(m) m是字符表的大小

如果你有更好的办法或者对我这里的描述有其他看法,请联系我。谢谢

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,519评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,842评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,544评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,742评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,646评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,027评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,513评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,169评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,324评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,268评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,299评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,996评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,591评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,667评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,911评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,288评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,871评论 2 341

推荐阅读更多精彩内容