Leet Code 3. Longest Substring Without Repeating Characters (最長的沒有重複字元的子字串)

LeonChen陳林峰發表於2018-07-24

轉載請註明出處:leonchen1024.com/2018/07/21/…

[Chinese ver]

3.最長的沒有重複字元的子字串

給你一個字串,得出最長的一個沒有重複字元的子字串的長度。

例子:

給定“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;
    }
}
複製程式碼

這程式碼。。沒臉見人

痛定思過,慘痛的教訓告訴我們要深思熟慮後再寫程式碼,這個程式碼就不分析了,寫出來的東西自己都要看不懂了,我連註釋都不好意思寫了,只是給自己留個教訓。。接下來會對這個方法做一個優化和重新構思。

二次解

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的敏感度還是不夠。

效率

分析 這個方法的原理就是將每一個字元進行遍歷,為了避免多重迴圈巢狀,我們使用了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;
    }
}
複製程式碼

扎心了老鐵

分析 這個方法的原理很簡單,就是最外層的迴圈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項和公式,太難打公式了,中間步驟就不詳細寫了。

等差數列前n項和

空間複雜度 : 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;
    }
}
複製程式碼

效率

分析 方法一重複檢查每一個子字串是否重複,其實沒有必要這樣,方法二就是避免了多次重複檢查,當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;
    }
}
複製程式碼

效率

分析 這個方法是對方法二的改進,我們使用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;
    }
}
複製程式碼

效率

效率最快的一種,在同樣的複雜度上為什麼會比上一種hashmap的方法快那麼多呢,簡單點說,其實集合大部分都是由陣列構成的,hashmap其實就是由陣列加連結串列封裝而成的,所以在速度上陣列查詢會優於hashmap查詢。

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

時間複雜度 : O(n) 空間複雜度 : O(m) m是字元表的大小

如果你有更好的辦法或者對我這裡的描述有其他看法,請聯絡我。謝謝

相關文章