一.前言
今天开始第三题,这个题目有点绕,我一开始都看错了两次题目,最后面才弄清楚到底是要算什么。我自己先是想了一下思路,用的方法虽然和网上大部分用的不太一样,但是核心思想是一样的(我想到的也是优化的滑动窗口,但是我使用的时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的所有区间都是存在重复字符的,这个时候我们就没必要进行判断了,可以直接进行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 上面的滑动窗口法,虽然已经可以通过测试了,但是在计算一个字符串是否重复,要消耗O(n^2)的时间复杂度,于是我们可以使用一个全局的set来替代这个滑动窗口,让检查是否重复变成O(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 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),这时我们不当当需要记录字符串,还需要记录其对应的位置,所以我们需要使用一个map来保存,代码如下:
1 public class Solution {
2 public int lengthOfLongestSubstring(String s) {
3 int n = s.length(), ans = 0;
4 Map<Character, Integer> map = new HashMap<>(); // current index of character
5 // try to extend the range [i, j]
6 for (int j = 0, i = 0; j < n; j++) {
7 if (map.containsKey(s.charAt(j))) {
8 i = Math.max(map.get(s.charAt(j)), i);
9 }
10 ans = Math.max(ans, j - i + 1);
11 map.put(s.charAt(j), j + 1);
12 }
13 return ans;
14 }
15 }
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 }
熬夜写完了这篇文章,上面的方法可能还不是最优的,如果大家有什么疑问或者有更好的方法,欢迎留言讨论,谢谢!