LeetCode3. Longest Substring Without Repeating Characters

[Chinese ver]

3.最長的沒有重復(fù)字符的子字符串

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

給你一個字符串,得出最長的一個沒有重復(fù)字符的子字符串的長度。

例子:

給定“abcabcbb”,答案是“abc”,長度為3。

給定“bbbbb”,答案是“b”,長度為1。

給定“pwwkew”,答案是“wke”,長度為3.

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

先來一個極其繁瑣的算法,一開始沒有經(jīng)過太多的思考,導(dǎo)致不斷有沒考慮到情況發(fā)生,不斷的修修改改,成了一個極其冗余的代碼。。

初解

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)]

痛定思過,慘痛的教訓(xùn)告訴我們要深思熟慮后再寫代碼,這個代碼就不分析了,寫出來的東西自己都要看不懂了,我連注釋都不好意思寫了,只是給自己留個教訓(xùn)。。接下來會對這個方法做一個優(yōu)化和重新構(gòu)思。

二次解

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

優(yōu)化之后的代碼就很清晰了。重新構(gòu)思的時候才想到還有一個Math.max()的方法可以用,對api的敏感度還是不夠。

[圖片上傳失敗...(image-c8ed22-1532413554986)]

分析
這個方法的原理就是將每一個字符進(jìn)行遍歷,為了避免多重循環(huán)嵌套,我們使用了hashmap來將時間成本換成空間成本,通過hashmap來判斷前面是否有這個字符,并存儲他的序號信息。那怎么得到最長的子字符串呢?其實(shí)可以分成以下幾種情況:
我們在前一個重復(fù)的字符加上(p)來表示,后一個重復(fù)的字符加上(n)來表示
1.這個字符是第一個重復(fù)的字符a,長度應(yīng)該是從開始到a(n)的前一位。
2.這個字符不是第一個重復(fù)的字符a,上一個重復(fù)的字符為b,如果a(p)在b(p)的后面那么長度是兩個a之間的長度,如果a(p)在b(p)的前面,那么長度是a(n)到b(p)之間(因?yàn)閍(n)到a(p)之間包含了兩個b,這樣的字符串是不合規(guī)矩的)
3.這個字符沒有出現(xiàn)重復(fù)的情況,長度就是這個字符到上一個重復(fù)字符a(p)出現(xiàn)的位置加一。

整理一下現(xiàn)在的情況,其實(shí)以上的條件都可以整理成一種方式來計算,因?yàn)樽幼址仨毷沁B續(xù)的,所以就是計算當(dāng)前字符到上一個任意重復(fù)字符點(diǎn)的長度,然后取出最大的一個。

時間復(fù)雜度 : O(n) .
空間復(fù)雜度 : O(n) .

接下來我們看一下這類題目的幾類解法。

方法一:暴力循環(huá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)]

分析
這個方法的原理很簡單,就是最外層的循環(huán)L1遍歷每一個字符a,嵌套一個遍歷a之后所有字符b的循環(huán)L2,在L2中又用了一個循環(huán)L3用hashset的方式來遍歷a到b間以a為開頭的所有字符串,并判斷是否是沒有重復(fù)值的字符串。嵌套了三層的循環(huán)!!!直接導(dǎo)致了Time Limit Exceeded 的結(jié)果,表示你這個方法太耗時了,不予通過。
時間復(fù)雜度 : O(n^3) . 首先是最內(nèi)層的[i,j)次的循環(huán),時間復(fù)雜度為O(j-i)。然后是每一個j的循環(huán)[i+1,n),

n
∑ O(j?i)
i+1

最后算上最外層的a的循環(huán)[0,n).時間復(fù)雜度為

  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

這個轉(zhuǎn)化過程呢其實(shí)就是利用的等差數(shù)列的n項(xiàng)和公式,太難打公式了,中間步驟就不詳細(xì)寫了。
[圖片上傳失敗...(image-fc9556-1532413554986)]

空間復(fù)雜度 : O(min(n,m)) . n就是字符串的長度,m是字母表的字符集的值。因?yàn)槲覀冏疃嗑涂赡苡衜個不重復(fù)的值,所以hashset的size最大也只會是m.

這個方法在邏輯上可以稍微做一個優(yōu)化,主要的思路就是其實(shí)如果a到b已經(jīng)是包含重復(fù)的字符了,那么a到b后面的字符也一定是包含重復(fù)字符的。可以省略了那些比較。

方法二:滑窗算法(也稱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)]

分析
方法一重復(fù)檢查每一個子字符串是否重復(fù),其實(shí)沒有必要這樣,方法二就是避免了多次重復(fù)檢查,當(dāng)i~j沒有重復(fù)時,我們只需要檢測s[j + 1]是否和i~j中的字符重復(fù)。直到j(luò)+1的字符重復(fù)了,我們就得到了i字符的最長不重復(fù)字符。當(dāng)j不小于n時就可以停止計算了,因?yàn)榇藭r的[i,j)肯定要長于[i+1,j)。

時間復(fù)雜度 : O(2n) = O(n)
空間復(fù)雜度 : O(min(m,n)) n就是字符串的長度,m是字母表的字符集的值。因?yàn)槲覀冏疃嗑涂赡苡衜個不重復(fù)的值,所以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)]

分析
這個方法是對方法二的改進(jìn),我們使用HashMap來保存出現(xiàn)的字符和他的位置,當(dāng)出現(xiàn)重復(fù)字符時,我們可以直接定位到重復(fù)字符的位置(注意這里的位置和程序上的序號是不同的,他的第一位就是1而不是我們程序上的0.),對比以前的i取較大的值賦值為新的i。為什么取大值可以看上面的二次解

時間復(fù)雜度 : O(n)
空間復(fù)雜度 : 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)]

效率最快的一種,在同樣的復(fù)雜度上為什么會比上一種hashmap的方法快那么多呢,簡單點(diǎn)說,其實(shí)集合大部分都是由數(shù)組構(gòu)成的,hashmap其實(shí)就是由數(shù)組加鏈表封裝而成的,所以在速度上數(shù)組查找會優(yōu)于hashmap查找。

分析
這個方法原理和上一個使用hashmap的方法大致相同,但是它使用的是int[]來存儲鍵值對,使用字符來做下標(biāo)(是的,字符可以做下標(biāo),準(zhǔn)確的來說是整型字符常量可以做下標(biāo),會被解析為他的ASCII碼值),位置來做值。

時間復(fù)雜度 : O(n)
空間復(fù)雜度 : O(m) m是字符表的大小

如果你有更好的辦法或者對我這里的描述有其他看法,請聯(lián)系我。謝謝

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,923評論 6 535
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,740評論 3 420
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,856評論 0 380
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,175評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,931評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,321評論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,383評論 3 443
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,533評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,082評論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,891評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,067評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,618評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,319評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,732評論 0 27
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,987評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,794評論 3 394
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,076評論 2 375

推薦閱讀更多精彩內(nèi)容