算法實戰(三)無重復字符的最長子串


一.前言

  今天開始第三題,這個題目有點繞,我一開始都看錯了兩次題目,最后面才弄清楚到底是要算什么。我自己先是想了一下思路,用的方法雖然和網上大部分用的不太一樣,但是核心思想是一樣的(我想到的也是優化的滑動窗口,但是我使用的時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 }

    熬夜寫完了這篇文章,上面的方法可能還不是最優的,如果大家有什么疑問或者有更好的方法,歡迎留言討論,謝謝!

  

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM