一.前言
今天開始第三題,這個題目有點繞,我一開始都看錯了兩次題目,最后面才弄清楚到底是要算什么。我自己先是想了一下思路,用的方法雖然和網上大部分用的不太一樣,但是核心思想是一樣的(我想到的也是優化的滑動窗口,但是我使用的時StringBulider來存儲,沒有去使用map,list等,所以耗時更長),下面我們就一起來看看題目。
二.題目
題目:給定一個字符串,請你找出其中不含有重復字符的 最長子串 的長度。
示例1:輸入: "abcabcbb"
輸出: 3
解釋: 因為無重復字符的最長子串是 "abc",所以其長度為 3。
示例2:輸入: "bbbbb"
輸出: 1
解釋: 因為無重復字符的最長子串是 "b",所以其長度為 1。
示例3:輸入: "pwwkew"
輸出: 3
解釋: 因為無重復字符的最長子串是 "wke",所以其長度為 3。
請注意,你的答案必須是 子串 的長度,"pwke" 是一個子序列,不是子串。
三.解題思路
首先要明白這個題的意思,就是要我們求字符串的不重復的連續子串,首先必須是連續的,不能斷開,其次子串中不能有一個字母是重復的。弄清楚了題目的意思后,讓我們來看看解法。
1)暴力法:暴力法總是最容易讓人明白的方法,首先我們拿到字符串的所有子串,這個通過兩次循環可以實現拿到所有字串的區間,這里我們采用[i,j)(左閉右開)的區間。然后我們實現一個方法,這個方法用來判斷傳入的字符串在一段區間內是否存在重復,這個可以借助set來實習。假設這段區間內的字串沒有重復字符,那么我們就要去對比更新最大字串的值。代碼如下:
1 public class Solution { 2 public int lengthOfLongestSubstring(String s) { 3 int sum = 0; 4 int length = s.length(); 5 for(int i = 0; i < length; i++){ 6 for(int j = i + 1; j <= length; j++){ 7 sum = uniqueStr(s, i , j) && sum < j - i ? sum = j - i : sum; 8 } 9 } 10 return sum; 11 } 12 13 public boolean uniqueStr(String s, int i, int j){ 14 Set<Character> set = new HashSet(); 15 for(int n = i; n < j; n++){ 16 if(set.contains(s.charAt(n))){ 17 return false; 18 } 19 set.add(s.charAt(n)); 20 } 21 return true; 22 } 23 }
2)2.1 滑動窗口法:這里我們要引入一個概念---窗口,窗口其實就是類似於上面提到的左閉右開的一個區間。那么為什么叫做滑動窗口呢?大家可以看到,上面的暴力法,我們做了很多無用的計算,在區間[i,j)中,假如我們判斷是存在重復字符,那么以i開頭,並且結尾大於j的所有區間都是存在重復字符的。舉個例子,在【0,5)這個區間是存在重復字符的,那么在【0,6),【0,7)...一直到末尾的所有區間都會是存在重復字符的。這個時候我們就沒必要進行判斷了,可以直接進行i+1的操作,所以大家會發現,每次操作要么j+1,要么i+1,整個區間就像是在滑動一樣,所以就要滑動窗口。代碼如下:
1 public class Solution { 2 public int lengthOfLongestSubstring(String s) { 3 int sum = 0; 4 int length = s.length(); 5 int begin = 0; 6 int end = 0; 7 while(begin < length && end < length){ 8 if(uniqueStr(s, begin, ++end)){ 9 sum = sum < end - begin ? end - begin : sum; 10 }else{ 11 begin++; 12 } 13 14 } 15 return sum; 16 } 17 18 public boolean uniqueStr(String s, int i, int j){ 19 Set<Character> set = new HashSet(); 20 for(int n = i; n < j; n++){ 21 if(set.contains(s.charAt(n))){ 22 return false; 23 } 24 set.add(s.charAt(n)); 25 } 26 return true; 27 } 28 }
2.2 上面的滑動窗口法,雖然已經可以通過測試了,但是還是耗費的時間比較久。因為如果在【i,j)上是沒有重復字符的,在計算j位置的時候,我們只需要計算j是否在【i,j)上存在就行,於是我們可以使用一個全局的set來替代這個滑動窗口,讓檢查是否重復變成O(1)的操作,在區間左邊界發生變化的時候,我們只需要移除set中begin所指向的字符,從而達到滑動窗口到效果。舉個例子,我們已經判斷了在【0,5)這個區間是沒有重復字符的,在判斷【0,6)這個區間的時間,又要一個一個加入到set來進行判斷,這樣就占用了大量時間,如果用一個全局set保存的話,第5個字符進來,我們只需要在set中判斷一下是否存在就行了。代碼如下:
1 public class Solution { 2 public int lengthOfLongestSubstring(String s) { 3 int sum = 0; 4 int length = s.length(); 5 int begin = 0; 6 int end = 0; 7 Set<Character> set = new HashSet(); 8 while(begin < length && end < length){ 9 if(!set.contains(s.charAt(end))){ 10 set.add(s.charAt(end++)); 11 sum = sum < end - begin ? end - begin : sum; 12 }else{ 13 set.remove(s.charAt(begin++)); 14 } 15 } 16 return sum; 17 } 18 }
2.3 優化版滑動窗口:其實有人可能已經發現了,還可以繼續優化。當我們在【i,j)區間是無重復字符區間,檢測到第J個字符在這個區間是已經存在的,假設重復的位置是K(i <= k < j),那么其實我們可以直接將窗口跳至【k+1,j),而不是【i+1,j)。舉個例子,如果字符串是“abcdhcef”,我們在【0,5)這個區間上並沒有發現字符重復,而當第5個字符進行判斷時,我們會發現c這個字符已經存在,如果我們向之前那樣,將區間的左邊界進行加1,變成【1,5),再將第五個元素進行判斷,其實還是重復的,假設我們找到c之前存在到位置2,然后之間將窗口滑動到【2,5),這樣就大量節省了時間。這時我們不僅僅需要記錄字符串,還需要記錄其對應的位置,所以我們需要使用一個map來保存,代碼如下:
1 public class Solution { 2 public int lengthOfLongestSubstring(String s) { 3 //字符串的長度 4 int n = s.length(); 5 //定義最大字串長度sum 6 int sum = 0; 7 //創建一個map 8 Map<Character, Integer> map = new HashMap<>(); 9 for(int begin = 0, end = 0; end < n; end++) { 10 //獲取end位置的字符位置 11 Integer index = map.get(s.charAt(end)); 12 //如果為空,表示之前不存在重復字符 13 //如果不為空,則要判斷字符的位置是否大於等於begin的位置,因為我們的區間是【begin,end) 14 //而我們進行窗口滑動時候,只是將begin移動到了重復字符之后,並沒有在map中刪除重復字符之前到所有元素 15 //所以,我們要判斷重復的字符是否在我們有效的區間內 16 if (index != null && index >= begin) { 17 //區間內重復,則將begin滑動到重復字符的下一個元素 18 begin = index + 1; 19 } 20 //將字符串放入map,或更新它的位置 21 map.put(s.charAt(end), end); 22 //計算sum 23 sum = Math.max(sum, end - begin + 1); 24 } 25 return sum; 26 } 27 }
2.4 因為map在滑動窗口左邊界移動時,不好刪除左邊界之前的元素,所以這個窗口需要我們從邏輯上理解,為了更直觀的簡直,我們還可以使用LinkedList來替代map,代碼如下:
1 class Solution 2 { 3 public int lengthOfLongestSubstring(String s) 4 { 5 int sum = 0;//記錄最長子串長度 6 LinkedList<Character> temp = new LinkedList<>(); 7 8 for (int i = 0; i < s.length(); i++ ) 9 { 10 Character curCh = s.charAt(i); 11 if (!temp.contains(curCh)){ 12 temp.add(curCh); 13 sum = sum < temp.size() ? temp.size() : sum; 14 } 15 else//如果新增字符與原子串中字符有重復的,刪除原子串中重復字符及在它之前的字符,與新增字符組成新的子串 16 { 17 int index = temp.indexOf(curCh); 18 for (int j = 0;j <= index;j++ ){ 19 temp.remove(); 20 } 21 temp.add(curCh); 22 } 23 } 24 return sum; 25 } 26 }
熬夜寫完了這篇文章,上面的方法可能還不是最優的,如果大家有什么疑問或者有更好的方法,歡迎留言討論,謝謝!