Leetcode-3无重复字符的最长子串

4次阅读

共计 495 个字符,预计需要花费 2 分钟才能阅读完成。

思路

使用一个 idx 指明当前子串的开始位置

class Solution:
    def lengthOfLongestSubstring(self, s: str) -> int:
        m = dict()

        '''
        对于每一个 char c 如果没出现 continue
        出现了:
            在当前字串的索引之前:continue
            之后: map 出现位置之后一位
        '''
        idx = 0
        res = 0
        for i , c in enumerate(s):
            if c not in m:
                pass
            else:
                if m < idx:
                    pass

                else:
                    res = max(res , i - idx)
                    idx = m + 1
            m = i
            print(res , i - idx + 1)
            res = max(res , i - idx + 1)
        return res

优化

提取有用的条件分支

class Solution:
    def lengthOfLongestSubstring(self, s: str) -> int:
        m = dict()
        
        idx = 0
        res = 0
        for i , c in enumerate(s):
            if c in m and m >= idx:
                res = max(res , i - idx)
                idx = m + 1
                
            m = i
            res = max(res , i - idx + 1)
        return res

正文完
 0

leetcode3无重复字符的最长子串

4次阅读

共计 2258 个字符,预计需要花费 6 分钟才能阅读完成。

欢迎跟着夏老师一起学习算法,这方面我自己的基础很薄弱,所以解题方法和思路也会讲的很”小白“,不需要什么基础就能看懂。

关注公众号可以进行交流和加入微信群,群内定期有系列文章分享噢!

Question

给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。

示例 1:

输入: "abcabcbb"
输出: 3 
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。

示例 2:

输入: "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。

示例 3:

输入: "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。

遍历法

最容易想到的一种算法,也是效率最低的一种算法

  1. 通过两次遍历得到所有可能的 子字符串 列表
  2. 将每个字符串传入一个函数检测是否包含重复字符,如果不包含则更新最长子串的长度
// 判断给定的子串是否包含重复字符
function isUnique(str, start, end) {const chars = [];
  for(let i = start; i < end; i++) {const char = str[i];
    if(chars.indexOf(char) !== -1) { // 字符已存在,本字符串不符合条件
      return false;
    }
    chars.push(char); // 添加字符
  }
  return true;
}

// 获取字符串最长子串长度
function lengthOfLongestSubstring(s) {
  let max = 0;
  for(let i = 0; i < s.length; i++) {for(let j = i+1; j <= s.length; j++) {if(isUnique(s, i, j)) { // 判断子串是否唯一
        max = Math.max(max, j - i); // j - i 为当前子串长度
      }
    }
  }
  return max;
}

时间复杂度 $O(n^3)$

i 循环,j 循环,isUnquie 中的循环,3 次循还嵌套

空间复杂度 $O(min(n,m))$

isUnique 函数中定义了一个数组来存储不重复的子串字符,长度为 $k$,$k$ 的长度取决于字符串 $s$ 的大小 $n$ 以及 字符串 $s$ 包含的不重复字符数大小 $m$

滑动窗口法

暴力法中我们会重复检查一个子串是否包含重复的字符,如果从 $i$ ~ $j-1$ 之间的子串已经被检查过没有重复字符了,那么只需要检查 $s[j]$ 是否在这个子串就行了。

子串使用 js 自带的数据结构 Set 存储

如果不在该子串,那么子串长度 +1,$j+1$,继续往后走

如果在这个子串,证明出现了重复,我们需要将 $s[i]$ 移出来之后 $i+1$,继续往后走

function lengthOfLongestSubstring(s) {const set = new Set();
  const max = 0;
  let i = 0;
  let j = 0;
  
  while(i < s.length && j < s.length) {if(!set.has(s[j])) {// j 不在 set 中,set 中添加 s[j],j 后移,同时更新最大子串长度
      set.add(s[j]);
         j++;
      max = Math.max(max, j - i);
    } else {set.delete(s[i]); // 移除 set 左边的数据,i 后移一位
      i++;
    }
  }
  return max;
}

时间复杂度 $O(2n) \approx O(n)$

最好的情况是 j 一次走完没有出现重复,最坏的情况是 i 和 j 都走到了末尾

空间复杂度 $O(min(n,m))$

与暴力法相似,也需要一个 Set 存储不重复字符,$n$ 是字符串 $s$ 长度,$m$ 是字符串 $s$ 中不重复的字母个数

优化的滑动窗口

在滑动窗口解法中,$i$ 的后移可以优化一下,如果 s$[j]$ 在 s[$i$] ~ s[$j$] 内与字符 $c$ (随便取的名字)重复,$i$ 不需要一步一步 $i$++,直接把 $i$ 定位到 $c$ + 1 的位置即可。这样可以将算法时间复杂度稳定在 $O(n)$

function lengthOfLongestSubstring(s) {const map = {}; // 保存 字符和下标的映射关系,如果字符重复,从 map 拿到位置,i 直接跳到这个位置
  let max = 0;
  
  for(let i = 0, j = 0;j < s.length;j++) {const char = s[j];
    if(map[char] !== undefined) { // 当前字符存在重复,需要将 i 更新
      i = Math.max(i, map[char]); // 如果 i 的当前位置大于 map[char],不能更新为 map[char]
    }
    max = Math.max(max, j - i + 1); // 由于 j 最大是 s.length-1,所以最大子串长度需要 +1
    map[char] = j + 1; // 保存映射关系
  }
  return max;
}

时间复杂度 $O(n)$

只遍历了 j

空间复杂度 $O(min(n,m))$

与之前的方法相同

Q: 为什么第 8 行的 i = Math.max(i, map[char]) 不能直接是 i = map[char]?

A: $i$ 的位置比 map[char] 大的情况下如果直接赋值会导致 $i$ 往前面走,会导致返回的子串长度大于实际的子串长度

错误例子 abba

i j s[j] s[i] ~ s[j] Max
0 0 a a 1
0 1 b ab 2
2(map 中没有 s[j],所以这里的位置直接是当前 j 的值) 2 b b 2
1(map 中有 s[j],第 1 个字符就是 a,直接拿来用) 3 a bba 3

可以看到第 4 次循环中 i 的位置已经出现了问题,把位置 1 的 a 拿过来进行计算了,窗口的起始左边也从 2 变成了 1,往回走了。

正文完
 0

leetcode3-无重复字符的最长子串

4次阅读

共计 689 个字符,预计需要花费 2 分钟才能阅读完成。

给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。

示例 1:

 输入: "abcabcbb"
输出: 3 
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。

示例 2:

 输入: "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。

示例 3:

 输入: "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。

实现思路:

一个简单的滑动块的操作,利用一个队列作为滑块,循环过程中元素不断进入这个队列,作为子串,一旦遇到有重复的元素,就将其本身及左边的元素移出。完成循环后取队列中出现的最大长度即可。

我实现中有一个的小技巧,在剔除重复子串的操作中,我没有将队列进行循环剔除,而是记录重复子串出现的位置,利用 slice 将队列进行一次性切割,提高算法性能。

我的实现:

/**
 * @param {string} s
 * @return {number}
 */
var lengthOfLongestSubstring = function(s) {let queue = []
  let result = 0
  const sLen = s.length
  for (let i = 0; i < sLen; ++i) {const item = s[i]
    const index = queue.indexOf(item)
    queue.push(item)
    if (-1 !== index) queue = queue.slice(index + 1)
    else result = Math.max(result, queue.length)
  }
  return result
};

成绩:

正文完
 0

LeetCode3无重复字符的最长子串

4次阅读

共计 2814 个字符,预计需要花费 8 分钟才能阅读完成。

longest-substring-without-repeating-characters

题目描述

Given a string, find the length of the longest substring without repeating characters.

给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。

示例 1:

输入: "abcabcbb"
输出: 3 
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。

示例 2:

输入: "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。

示例 3:

输入: "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。请注意,你的答案必须是 ** 子串 ** 的长度,"pwke" 是一个子序列,不是子串。

<!–more–>

我的垃圾思路

One

  1. 刚开始想耍一些 ” 小聪明 ”,看有没有巧妙的方法解决,当时用了类似于 Node 的前后 ’ 指针 ’ 的方式发现有线用例是过不了的
  2. 后面用到了类似 ” 滑窗 ” 的方法,碰到重复字符则将滑窗宽度归零,且位置回到 被重复 字符的下一位置。
  3. 但会出现死循环,因为没把之前被重复的字符 remove 掉 — 后面发现只 remove 掉那个重复的字符的话,有些没有重复的字符在回退之后再次计算的时候,会发生混乱 <font color=grey size=2>(回退后再次走到之前不重复的字符时候,因为 hash 表中已经在第一次 put 过了,所以这次一定会发生重复情况)</font>
  4. 所以上面把滑窗归零的时候是真正的归零,包括存数据的 hash 表

上面方法应该是 $ O(n^2) $ , 因为会发生例如 abcdea 在最后 a 发生重复,就会完全回退到 b 位置 —so low;提交记录耗时大概 80+ms

Two

  1. 第二个方法是也两个指针类似滑窗,k 指针一直前进,发生重复时 j 指针移动到 被重复 字符的下一位置,但是只能往右移动,不能回退
  2. map<Character,Integer> 中存放的之前被重复字符的 value(即字符所在的索引)换为当前发生重复的字符位置 — 不是被重复字符
  3. 循环中一直保持 max 最大
  4. 当有其中一个指针到达终点时,就可以退出了;由于 j,k 代表的都是索引,所以最后结果 为max+1

Three

  1. 第二种方法发现 k 一直在 ++,其实就相当于 for 循环的 i++,所以就换成 for 循环了 — 复杂度应该是 $ O(n) $

Two 和 Three 提交的耗时 6ms, 占用内存 35M– 占用内存竟然超过了 100% 的 java 用户ヽ (°◇°) ノ

我的垃圾代码

package com.dasnnj.practice.medium;

import java.util.HashMap;
import java.util.Map;

/**
 * Description <p> TODO:
 * 给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。* <p>
 * 示例 1:
 * <p>
 * 输入: "abcabcbb"
 * 输出: 3
 * 解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。* 示例 2:
 * <p>
 * 输入: "bbbbb"
 * 输出: 1
 * 解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。* 示例 3:
 * <p>
 * 输入: "pwwkew"
 * 输出: 3
 * 解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。* 请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。</p>
 *
 * @author DASNNJ <a href="mailto:dasnnj@outlook.com.com"> dasnnj@outlook.com </a>
 * @date 2019-05-08 13:17
 */
public class LongestSubstringWithoutRepeatingCharacters {public static void main(String[] args) {LongestSubstringWithoutRepeatingCharacters l = new LongestSubstringWithoutRepeatingCharacters();
        System.out.println(l.lengthOfLongestSubstring(""));
    }

    public int lengthOfLongestSubstring(String s) {
        //One
        /*char[] chars = s.toCharArray();
        Map<Character, Integer> map = new HashMap<>(8);
        // 计算非重复字符的长度
        Integer len = 0;
        int max = 0;
        // 索引
        Integer index;
        for (int i = 0; i < chars.length; i++) {
            // 如果是正常进行
            // 如果重复
            if ((index = map.get(chars[i])) != null) {
                // 回退到重复的位置,从重复位置的下一位重新算,相当于舍弃两个重复字符中的第一个
                i = index;
                // 长度归零
                len = 0;
                // 将 map 清空,从重复位置的下一位置重新计算 -- 清空是因为第一个重复的在上面提到是相当于舍弃,不清空的话会影响下次循环的判断
                map.clear();} else {
                // 没重复当然正常 put  正常 ++
                map.put(chars[i], i);
                len++;
            }
            // 每次循环都保持 max 最大
            if (len > max) {max = len;}
        }
        return max;*/

        if ("".equals(s)) {return 0;}
        char[] chars = s.toCharArray();
        //j k, 用于 Two 方法的两个指针 --- 后面发现直接用 for 循环即可
        int j = 0, k = 0, max = 0;
        Integer ele;
        Map<Character, Integer> sets = new HashMap<>(16);

        //Three
        for (int m = 0; m < chars.length; m++) {
            // 如果发生重复
            if ((ele = sets.get(chars[m])) != null) {
                // j 指针指向两个重复字符中的第一个的下一位置,j 指针不能后退,只能前进,所以下面有个判断
                if (j < ele + 1) {j = ele + 1;}
            }
            // 不重复则是正常 put,重复情况 1. 将 j 指针指向原字符的下一位 2. 用新字符替换掉 map 中原字符(主要是为了替换 map 中 key 为此字符 的 value 值也就是索引)sets.put(chars[m], m);
            // 每次循环保持 max 最大
            if (max < m - j) {max = m - j;}
        }

        //Two  原理同 Three
        /*while (j < chars.length && k < chars.length) {if ((ele = sets.get(chars[k])) != null) {if (j < ele + 1) {j = ele + 1;}
            }
            sets.put(chars[k], k);
            k++;

            if (max < k - j) {max = k - j;}
        }*/
        return max + 1;
    }
}

正文完
 0

LeetCode.3 无重复字符的最长子串

4次阅读

共计 2511 个字符,预计需要花费 7 分钟才能阅读完成。

先跳到第三题是因为第二题一眼没读懂
一、题目
无重复字符的最长子串:

给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。
示例 1
输入: “abcabcbb” 输出: 3 解释: 因为无重复字符的最长子串是 “abc”,所以其长度为 3。
示例 2
输入: “bbbbb” 输出: 1 解释: 因为无重复字符的最长子串是 “b”,所以其长度为 1。
示例 3
输入: “pwwkew” 输出: 3 解释: 因为无重复字符的最长子串是 “wke”,所以其长度为 3。
请注意,你的答案必须是 子串 的长度,”pwke” 是一个子序列,不是子串。

二、我的答案
因为这道题之前在学校刷题的时候见过类似的,大概记得解法,就先放自己的思路
v1.0
/**
* @param {string} s
* @return {number}
*/
var lengthOfLongestSubstring = function(s) {
let resultLength = 0
function hasEchoChar(str){
return new Set(str).size !== str.length ? true : false
}
let begin = 0, end = 1;
while(end <= s.length) {
if (!hasEchoChar(s.slice(begin, end))) {
end – begin > resultLength ? resultLength = end – begin : null;
end++
} else {
begin++
}
}
return resultLength
};
讲解

两个指针用来遍历字符串,begin 指向当前字符串的头,end 指向当前字符串的下一位
let begin = 0, end = 1

如果当前字符串不包含重复字符串,则判断是否更新结果 (resultLength) 且 end++,如果包含,begin++
if (!hasEchoChar(s.slice(begin, end))) {
end – begin > resultLength ? resultLength = end – begin : null;
end++
} else {
begin++
}
       上文也说了之前碰到过类似的题,当时理解这部分用了好久,因为常写的循环都是只有一个指针变量的,像 for(let i = 0; i < num; i++),如果按照惯用思路肯定是循环套循环遍历所有子串,麻烦的丫批,上面这种写法妙就妙在一次循环中要么更新 begin 要么更新 end,缩小了子串的筛选范围。
       以示例 3 中的 pwwkew 为例,遍历的过程如下图

v2.0
写完 v1.0 代码,开心提交,通过是通过了,但是       我:???怎么肥四,一顿乱吹然后战胜 8%,神仙打架?仔细分析了一下,发现是判断是否包含重复字符的函数 hasEchoChar 的问题,在 v1.0 代码中我把子串转化成 set 结构然后对比前后 length 是否相等,因为 set 自动去重的特性,如果相等则没有重复字符。以此来实现判断是否包含重复字符。(这么写的原因是上一题做完后去看了 es6 中 Set 和 Map 一章,上一题分析连接: 我是链接)
function hasEchoChar(str){
return new Set(str).size !== str.length ? true : false
}
       估计耗时就是耗在了转化成 set 结构了,这样写本身并没错,但是他的作用是判断一个任意字符串是否含有重复字符,我们每次判断的是任意字符串吗?并不是,那就不应该继续采用这种方法。聚光灯打到上文中展示遍历过程那张图,有重复字符的是 pw->pww 和 wke->wkew,也就是说,只有每次 end++ 时才有可能产生重复字符,也即是说我们只需要判断上次遍历的字符串是否包含这次新添加的字符即可,思路理清,代码如下
/**
* @param {string} s
* @return {number}
*/
var lengthOfLongestSubstring = function (s) {
let resultLength = 0
let begin = 0, end = 1
while (end <= s.length) {
let index = s.slice(begin, end – 1).indexOf(s[end – 1])
if (index !== -1) {
begin += (index + 1)
end++
} else {
end – begin > resultLength ? resultLength = end – begin : null;
end++
}
}
return resultLength
};
       同时做出优化的部分还有找到相同字串的情况
if (index !== -1) {
begin += (index + 1)
end++
}
       在 v1.0 的代码中,每次找到相同字符就将 begin 指到下一个位置,如果还有呢,就再指向下一个位置,很明显 begin 可以一次指到位,即指到与当前子串末位相同字符位置的下一位,同时 end++,开始下一轮的崭新循环。
       至此这道题我已经竭尽所能,执行用时从 v1.0 的 956ms 降低到 v2.0 的 132ms,击败提交也从 8.77% 涨到 71.88%。但是贪婪的我并不能满足于 71.88%,于是去复制了一份耗时最低的答案自己提交,发现耗时 140ms 甚至比我的耗时还要多?去查了一下(查询结果),可能原因为:1、服务器负载;2、测试用例增加。奥~~ 这样啊       那对不起,我的就是最佳答案
三、优秀答案
/**
* @param {string} s
* @return {number}
*/
var lengthOfLongestSubstring = function(s) {
let substr = ”, maxLength = 0;
for (var i = 0; i < s.length; i++) {
let findIndex = substr.indexOf(s[i]);
if (~findIndex) {
substr = substr.substring(findIndex + 1);
}
substr += s[i];
if (substr.length > maxLength) {
maxLength = substr.length;
}
}
return maxLength;
};
四、路漫漫其修远兮

正文完
 0