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

一.前言

  今天開始第三題,這個題目有點繞,我一開始都看錯了兩次題目,最後面才弄清楚究竟是要算什麼。我本身先是想了一下思路,用的方法雖然和網上大部分用的不太同樣,可是核心思想是同樣的(我想到的也是優化的滑動窗口,可是我使用的時StringBulider來存儲,沒有去使用map,list等,因此耗時更長),下面咱們就一塊兒來看看題目。ide

二.題目

  題目:給定一個字符串,請你找出其中不含有重複字符的 最長子串 的長度。測試

  示例1:輸入: "abcabcbb"優化

       輸出: 3spa

       解釋: 由於無重複字符的最長子串是 "abc",因此其長度爲 3。code

  示例2:輸入: "bbbbb"blog

       輸出: 1rem

       解釋: 由於無重複字符的最長子串是 "b",因此其長度爲 1。字符串

  示例3:輸入: "pwwkew"get

       輸出: 3string

       解釋: 由於無重複字符的最長子串是 "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 }

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

相關文章
相關標籤/搜索