算法实战(三)无重复字符的最长子串

一.前言

  今天开始第三题,这个题目有点绕,我一开始都看错了两次题目,最后面才弄清楚到底是要算什么。我自己先是想了一下思路,用的方法虽然和网上大部分用的不太一样,但是核心思想是一样的(我想到的也是优化的滑动窗口,但是我使用的时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 }

    熬夜写完了这篇文章,上面的方法可能还不是最优的,如果大家有什么疑问或者有更好的方法,欢迎留言讨论,谢谢!

  

  

posted @ 2019-08-21 01:05  litterCoder  阅读(1740)  评论(0编辑  收藏  举报