【LeetCode系列】 無重複字符的最長子串 Longest Substring Without Repeating Characters

題目描述(Middle):

官方解答:https://leetcode.com/problems/longest-substring-without-repeating-characters/description/

方法一:暴力解法

一個一個地檢查子串看是否有重複字符

算法描述:

假設有一個函數  boolean allUnique(String substring)  ,當子串的字符無重複時返回true,否則返回false。我們可以通過對給定字符串  s  的所有可能子串都調用該  allUnique  函數。如果返回爲true(說明無重複),則更新無重複的最長子串的長度。

具體如下:

  1. 假設子串的開頭標識爲 i ,結尾標識爲 j ,則 0 ≤ i < j ≤ n 。則使用兩個嵌套循環即可遍歷  s  的所有子串(i 從0~n-1,j 從i + 1到 n)
  2. 爲了檢查字符串中是否存在重複字符,可以使用集合來裝載。在放入一個字符前檢查該字符是否已存在於集合 set 中。如果存在則返回false。循環結束後返回true。

代碼如下:

public class Solution{
    public int lengthOfLongestSubString(String s){
        int n = s.length(), 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
    }
    //判斷字符串是否全是單一的函數,是返回True,否則返回False
    public boolean allUnique(String s, int start, int end){
        Set<Character> set = new HashSet<>();    //集合set存儲字符
        for(int i = start; i < end; i++){    //從開頭位置遍歷到結尾位置
            Character ch = s.charAt(i);      //獲取i位置的字符
            if(set.contains(ch))             //判斷字符是否在集合set中
                return false;                //若在集合set中表示有重複,非單一
            set.add(ch);                     //在集合中增加新字符
        }
        return true;
    }
}

 方法二:滑動窗口

算法描述:

在上一方法中,我們一直重複地檢查一個子串,看它是否有重複字符,但實際上是沒有必要的。如果一個從索引 i 到 j - 1的子串  s_i_{j}已經檢查到沒有重複字符了,那我們就只需要檢查 s\left [ j \right ] 是否在 s_i_{j} 中就行了。

爲了檢查一個字符是否已經在這個子串中,我們可以循環掃描這個子串。但是複雜度爲O\left ( n^{2} \right )。我們可以用滑動窗口方法進行優化,將HashSet作爲一個滑動窗口。

滑動窗口指的是在數組或字符串中某一範圍的元素,比如:[i, j) 下標範圍內的元素。如果我們將 [i, j) 向右移動一個元素則變成了 [i+1, j+1)。

我們用HashSet存儲現有窗口 [i, j) 的字符(初始時 i = j)。然後我們將索引 j 向右滑動, 如果(指向的新的字符)不在HashSet中則繼續向右滑動,直到 s[j] 已經在HashSet中爲止。當然,無重複字符的最長子串是從索引 i 開始的。對所有的 i 進行此操作就可以得到答案。

代碼如下:

public class Solution{
    public int lengthOfLongestSubString(String s){
        int n = s.length(), ans = 0;
        Set<Character> set = new HashSet<>();
        int i = 0, j = 0;
        while(i < n && j < n){
            if(!set.contains(s.charAt(j))){    //若集合set中不包含索引j的字符
                set.add(s.charAt(j++));        //將該字符存入集合set中,j++窗口右移
                and = Math.max(ans, j - i);    //更新結果
            }
            else{                              //若集合set中包含索引j的字符
                set.remove(s.charAt(i++));     //將索引i的字符移出集合set,i++繼續循環判斷
            }
        }
        return ans;
    }
}

方法三:窗口移動的優化 

在方法二中可以很明顯看到弊端,即當遇到有 j 指向的字符存在集合中時需要一點一點移動 i 。實際上假設 s[j] 在 [i, j) 窗口範圍內有重複的字符且其索引爲 j' ,我們可以跳過 [i, j'] 範圍內的所有元素,直接置 i 爲 j' + 1 。

因此可以用HashMap將每個字符和其所在位置一一對應起來。

代碼如下:

class Solution {
    public int lengthOfLongestSubstring(String s) {
        int ans = 0, n = s.length();
        Map<Character, Integer> map = new HashMap<>();  //HashMap存儲(不重複的)字符和字符下標対
        for(int i = 0, j = 0; j < n; j ++){
            if(map.containsKey(s.charAt(j))){    //若新的字符已經在HashMap中
                i = Math.max(map.get(s.charAt(j)), i);    //將窗口的左端i移動到該字符的最大值下標
            }
            ans = Math.max(ans, j - i + 1);    //計算窗口大小
            map.put(s.charAt(j), j + 1);        //將新的字符放入HashMap中
        }
        return ans;
    }
}

上述算法都沒有對字符串  s  使用的字符集進行 假設。我們知道字符集是很小的,我們可以將 Map 換爲整形數組來直接訪問。

如 index[65] 表示字符 'A' 的索引值。(注:在ASCII碼錶中 'A' 是 65)

常用表示如下:

  • int[26]    用於字符 'a' - 'z' 或者 'A' - 'Z'
  • int[128]  用於所有的ASCII碼(ASCII碼錶中只有128個字符)
  • int[256]  用於擴展的ASCII碼

可修改代碼如下:

public class Solution{
    public int lengthOfLongestSubstring(String s){
        int n = s.length(), ans = 0;
        int[] index = new index[128];    //當前字符的索引數組
        for(int i = 0, j = 0; j < n; j++){
            i = Math.max(index[s.charAt(j)], i);    //找到j的字符索引和i的最大值,即如果有重複取i最大的那個
            ans = Math.max(ans, j - i + 1);
            index[s.charAt(j)] = j + 1;    //存儲j的字符的索引爲j+1
        }
        return ans;
    }
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章