一.前言
今天开始第三题,这个题目有点绕,我一开始都看错了两次题目,最后面才弄清楚到底是要算什么。我自己先是想了一下思路,用的方法虽然和网上大部分用的不太一样,但是核心思想是一样的(我想到的也是优化的滑动窗口,但是我使用的时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 }
熬夜写完了这篇文章,上面的方法可能还不是最优的,如果大家有什么疑问或者有更好的方法,欢迎留言讨论,谢谢!