題目描述(Middle):
官方解答:https://leetcode.com/problems/longest-substring-without-repeating-characters/description/
方法一:暴力解法
一個一個地檢查子串看是否有重複字符
算法描述:
假設有一個函數 boolean allUnique(String substring) ,當子串的字符無重複時返回true,否則返回false。我們可以通過對給定字符串 s 的所有可能子串都調用該 allUnique 函數。如果返回爲true(說明無重複),則更新無重複的最長子串的長度。
具體如下:
- 假設子串的開頭標識爲 i ,結尾標識爲 j ,則 0 ≤ i < j ≤ n 。則使用兩個嵌套循環即可遍歷 s 的所有子串(i 從0~n-1,j 從i + 1到 n)
- 爲了檢查字符串中是否存在重複字符,可以使用集合來裝載。在放入一個字符前檢查該字符是否已存在於集合 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的子串 已經檢查到沒有重複字符了,那我們就只需要檢查 是否在 中就行了。
爲了檢查一個字符是否已經在這個子串中,我們可以循環掃描這個子串。但是複雜度爲。我們可以用滑動窗口方法進行優化,將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;
}
}