轉自:https://leetcode-cn.com/articles/longest-substring-without-repeating-characters/
解決方案
方法一:暴力法
思路
逐個檢查所有的子字符串,看它是否不含有重復的字符。
算法
假設我們有一個函數 boolean allUnique(String substring)
,如果子字符串中的字符都是唯一的,它會返回true,否則會返回false。 我們可以遍歷給定字符串 s
的所有可能的子字符串并調用函數 allUnique
。 如果事實證明返回值為true,那么我們將會更新無重復字符子串的最大長度的答案。
現在讓我們填補缺少的部分:
- 為了枚舉給定字符串的所有子字符串,我們需要枚舉它們開始和結束的索引。假設開始和結束的索引分別為
和
。那么我們有
(這里的結束索引
是按慣例排除的)。因此,使用
從0到
以及
從
到
這兩個嵌套的循環,我們可以枚舉出
s
的所有子字符串。 - 要檢查一個字符串是否有重復字符,我們可以使用集合。我們遍歷字符串中的所有字符,并將它們逐個放入
set
中。在放置一個字符之前,我們檢查該集合是否已經包含它。如果包含,我們會返回false
。循環結束后,我們返回true
。
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;
}
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;
}
}
復雜度分析
-
時間復雜度:
。
要驗證索引范圍在
內的字符是否都是唯一的,我們需要檢查該范圍中的所有字符。 因此,它將花費
的時間。
對于給定的
i
,對于所有所耗費的時間總和為:
因此,執行所有步驟耗去的時間總和為:
空間復雜度:
,我們需要
的空間來檢查子字符串中是否有重復字符,其中
表示
Set
的大小。而 Set 的大小取決于字符串的大小以及字符集/字母
的大小。
方法二:滑動窗口
算法
暴力法非常簡單。但它太慢了。那么我們該如何優化它呢?
在暴力法中,我們會反復檢查一個子字符串是否含有有重復的字符,但這是沒有必要的。如果從索引 到
之間的子字符串
已經被檢查為沒有重復字符。我們只需要檢查
對應的字符是否已經存在于子字符串
中。
要檢查一個字符是否已經在子字符串中,我們可以檢查整個子字符串,這將產生一個復雜度為 的算法,但我們可以做得更好。
通過使用 HashSet 作為滑動窗口,我們可以用 的時間來完成對字符是否在當前的子字符串中的檢查。
滑動窗口是數組/字符串問題中常用的抽象概念。 窗口通常是在數組/字符串中由開始和結束索引定義的一系列元素的集合,即 (左閉,右開)。而滑動窗口是可以將兩個邊界向某一方向“滑動”的窗口。例如,我們將
向右滑動 1個元素,則它將變為
(左閉,右開)。
回到我們的問題,我們使用 HashSet 將字符存儲在當前窗口(最初
)中。 然后我們向右側滑動索引
,如果它不在 HashSet 中,我們會繼續滑動
。直到
已經存在于 HashSet 中。此時,我們找到的沒有重復字符的最長子字符串將會以索引
開頭。如果我們對所有的
這樣做,就可以得到答案。
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;
}
}
復雜度分析
- 時間復雜度:
,在最糟糕的情況下,每個字符將被
和
訪問兩次。
- 空間復雜度:
,與之前的方法相同。滑動窗口法需要
的空間,其中
表示
Set
的大小。而Set的大小取決于字符串的大小以及字符集/字母
的大小。
方法三:優化的滑動窗口
上述的方法最多需要執行 2n 個步驟。事實上,它可以被進一步優化為僅需要 n 個步驟。我們可以定義字符到索引的映射,而不是使用集合來判斷一個字符是否存在。 當我們找到重復的字符時,我們可以立即跳過該窗口。
也就是說,如果 在
范圍內有與
重復的字符,我們不需要逐漸增加
。 我們可以直接跳過
范圍內的所有元素,并將
變為
。
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;
}
}
Java(假設字符集為 ASCII 128)
以前的我們都沒有對字符串 s
所使用的字符集進行假設。
當我們知道該字符集比較小的時侯,我們可以用一個整數數組作為直接訪問表來替換 Map
。
常用的表如下所示:
-
int [26]
用于字母 ‘a’ - ‘z’或 ‘A’ - ‘Z’ -
int [128]
用于ASCII碼 -
int [256]
用于擴展ASCII碼
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):
,與之前的方法相同。
- 空間復雜度(Table):
,
是字符集的大小。
自己做
最終代碼
class Solution:
def lengthOfLongestSubstring(self, s):
"""
:type s: str
:rtype: int
"""
arr = []
maxLen = 0
for i in s:
if i in arr:
index = arr.index(i)
arr = arr[index+1:]
arr.append(i)
if len(arr) > maxLen:
maxLen = len(arr)
return maxLen
1、"dvdf"失敗
原因:做法是遇到一個重復的字符就清空數組,這種情況下就有問題了。
解決方案:遇到重復時,刪掉重復的前面的所有字符
2、"aabaab!bb"失敗
原因:代碼寫錯了,
index = s.index(i) -> index = arr.index(i)