題目
給定一個字符串,請你找出其中不含有重複字符的 最長子串 的長度。
示例 1:
輸入: "abcabcbb"
輸出: 3
解釋: 因爲無重複字符的最長子串是 "abc",所以其長度爲 3。
示例 2:
輸入: "bbbbb"
輸出: 1
解釋: 因爲無重複字符的最長子串是 "b",所以其長度爲 1。
示例 3:
輸入: "pwwkew"
輸出: 3
解釋: 因爲無重複字符的最長子串是 "wke",所以其長度爲 3。
請注意,你的答案必須是 子串 的長度,"pwke" 是一個子序列,不是子串。
方法1——暴力循環
我們可以將字符串S所有可能的子串都找出來遍歷,遍歷的過程中判斷當前子是否有重複的字符。設一個int型的變量ans來存放最長子串2的長度,初始化爲0,將每一個無重複字符的子串長度與ans進行比較,並將較大的值賦給ans,直到循環結束,找出最長無重複字符的子串的長度。
循環的話沒什麼好說的,雙重循環就可以找出所有的子串可能;而要判斷一個子串中是否有重複的字符,可以寫一個方法,使用HashSet來判斷是否有重複字符的存在,代碼如下:
public int lengthOfLongestSubstring(String s) {
int n = s.length();
int ans = 0;
int j=0;
for (int i = 0; i < n; i++){
for (j = i; j < n; j++){
System.out.print(s.charAt(j));
if (allUnique(s, i, j)){
ans = Math.max(ans, j - i + 1);
}
}
}
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;
}
方法2——滑動窗口
使用一個while循環來完成判斷無重複字符的最長子串。每次向右擴展前,判斷右邊界的下一個字符是否已經存在於set集合中,如果不存在,則直接向右擴展(將該字符add到set集合中);若已經存在,則從左邊界開始remove字符直到窗口內無重複字符爲止。
public int lengthOfLongestSubstring(String s) {
Set<Character> set = new HashSet<>();
int n = s.length();
int i = 0, j = 0, ans = 0;
while(i < n && j < n){
Character ch = s.charAt(j);
//如果該字符不存在於set集合中
if(!set.contains(ch)) {
//將該字符add到set中
set.add(ch);
//更新最長字串的長度
ans = Math.max(ans, j-i+1);
//繼續判斷下一個字符
j++;
}
//如果該字符已經存在於set集合中
else{
//從左邊界開始刪除字符
//直到窗口內不存在重複的字符
set.remove(s.charAt(i));
i++;
}
}
return ans;
}
方法3——優化的滑動窗口
其實可以用人的思維來思考這個問題。比如說有一個字符串"bcadace"(隨便寫的),如果使用方法2,當窗口滑動到 bcada 的時候,set中包含了a字符,這個時候應該將窗口左邊界向右移動,繼續判斷cada,直到da時方纔沒有重複字符的存在。但其實這樣做是沒有必要的,我們如果已經判斷出了a字符有重複,那麼直接從上一個a的下一個字符也就是d開始就可以了。而優化的滑動窗口就是這樣來進一步縮短代碼執行時間。
方法3將使用hashmap來存放字符以及字符所在的位置。之前使用set是因爲我們只需要判斷set中是否有重複的字符即可,並不需要知道字符所在的位置,然而這裏我們需要知道字符所在的位置,才能定位 i 的位置。
public int lengthOfLongestSubstring(String s) {
Map<Character, Integer> map = new HashMap<>();
int n = s.length();
int i = 0, j = 0, ans =0;
for(j = 0; j < n; j++){
//如果當前字符已經存在
if(map.containsKey(s.charAt(j))){
//如果字符在i,j區間的話,更新i爲字符所在位置的下一個
//如果字符不在i,j區間的話,i不變
i = Math.max(i, map.get(s.charAt(j))+1);
}
//更新長度
ans = Math.max(ans,j-i+1);
//將j字符put到map中
map.put(s.charAt(j),j);
}
return ans;
}
圖解:以abba爲例
1.i=0; j=0,此時map中無數據,直接更新ans,此時map:('a', 0)
2. i=0; j=1,此時map中不存在字符b,直接更新ans,此時map:('a', 0)('b', 1)
3. i= 0; j = 2,此時map中已經存在了字符b
使用map.get('b')獲取到上一個出現的b所在的位置爲1,此時窗口將兩個b都包含在內,將 i 更新爲上一個b所在位置加1,i = 2,
此時map:('a', 0), ('b', 2)
4. i = 2,j = 3,雖然此時滑動窗口內並未包含重複字符(直接觀察),但是在程序中,會判斷出map中已經存在a,所以要更新 i 的值,比較後發現 i 的值爲2,要大於上一個a出現的位置加1 (等於1),所以說此時 i 的值仍然爲2, 計算出ans = 2
其實在這個算法中,我比較難理解的就是 i 值的更新。在我自己完成練習的時候,就認爲直接將 i 的值更新爲上一次出現的位置+1就可以了,然而這樣是不行的。如果是abba的話,最後一個a進行判斷如果 i 更新爲2,會得出錯誤的答案:3。其實 i 的更新是需要滿足上一個出現的重複字符存在於滑動窗口內(也就是說,上一個出現的重複字符的位置要大於i,如果不大於i,相當於當前滑動窗口並沒有重複字符,我們只需要跟新map就可以了)