一、LeetCode 3. 无重复字符的最长子串
题目介绍:
给定一个字符串 s
,请你找出其中不含有重复字符的 最长子串 的长度。
示例:
输入: s = "abcabcbb" 输出: 3 解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
注意:这道题目让输出的是不含有重复字符的最长子串的
长度
,而不是子串本身
二、解题思路
- 第一种解法:”老实人“又要上线了
题目中是要找不含有重复字符的子串,返回其长度。
第一时间我们可以想到找出所有的符合条件的子串,然后比较所有子串的长度,取最大的即可。
接下来就是考虑排列组合的问题。
在示例中,输入的是字符串”abca“
,可以组成的排列组合有:
// 字符串abca可以组成的排列组合子串 // 字符a开始 ab | abc | abca // 字符b开始 bc | bca // 字符c开始 ca
再次筛选不包含重复字符串的:
ab | abc | bc | bca | ca
由此可得到结果,不含有重复字符的最长子串的长度是3
/** * @param {string} s * @return {number} */ var lengthOfLongestSubstring = function(s) { // 记录最大子串长度 let maxLen = 0; // 当前子串 let currentSubStr = ''; // 提供每一种组合 for (let i = 0; i < s.length; i++) { // 每次遍历开始,初始化当前字符 currentSubStr = s[i]; // 开始排列组合 for (let j = i + 1; j < s.length; j++) { // 每次判断当前字符是否已经存在于currentSubStr中 // 如果存在说明当前的currentSubStr子串已经是目前最大的一个 if (currentSubStr.includes(s[j])) { // 将当前子串的长度与历史记录的子串长度进行比较,取二者最大值 maxLen = Math.max(currentSubStr.length, maxLen); // 将当前子串重置为当前字符,再次开始记录 currentSubStr = s[j]; } else { // 不重复字符,直接累积 currentSubStr += s[j]; } } // 最后一次比对 maxLen = Math.max(currentSubStr.length, maxLen); } return maxLen; }
执行测试用例:
OK,没问题我们就提交了!
执行结果是:超出时间限制
,说明我们这个实现是不理想的,消耗时间太长。
分析下这个算法的时间复杂度: O(n2)O(n^2)O(n2)。
在LeetCode的这个Case中,实际传入传入的字符串长度为 31000
!
So,这不是一个好算法!
- 瞅瞅第二种解法:
滑动窗口
滑动窗口是一个什么样的概念呢,大家可以想象为在横向平铺的字符串上,有一个可以滑动的框,框里面选中的是不重复的子串。
输入字符串:abcdbe
,在遍历字符串时,我们使用数组subStr
存储不重复的子串。
假定我们有两个指针:左指针和右指针,同时指向了不重复子串的首尾两个位置,在初始时都指向了字符串的第一个字符; 开始遍历时,右指针开始挪动,将不重复的字符依次放入到数组中。
如果遇到某个字符已经在数组subStr
中,则说明当前不重复子串已经是最长了,需要开启新的不重复子串了。
新的不重复子串从哪里开始记录呢?!
重点:
从该重复字符在不重复子串中的下一个位置开始记录。
在该case中,第一次记录的不重复子串为 abcd
。下一个字符b
开始重复,新的一个不重复子串开始位置为c
,即字符b
第一次出现的位置的下一个字符。
此刻的处理方式是移动左指针位置,即将数组subStr
中的元素从开头开始进行删除,直到移除重复字符b
。
将当前的字符b
存储到subStr
中。
比对当前不重复子串的长度与历史记录的最大长度,取最大值。
重复上述的过程,直到字符串遍历结束。
我们来看下代码上的实现
/** * @param {string} s * @return {number} */ var lengthOfLongestSubstring = function(s) { // subStr存储不重复子串 const subStr = []; // 记录不重复子串的最大值 let maxLen = 0; // 遍历字符串s for (let i = 0; i< s.length; i++) { // 判断当前字符s[i]是否已经存在 while (subStr.includes(s[i])) { // 如果存在,则移动指针,将该重复字符第一次出现的位置之前的所有字符清除,重新开始记录一个新的子串 subStr.shift(); } // 将当前字符存入 subStr.push(s[i]) // 比对当前子串的最大长度与历史记录最大长度,取最大值 maxLen = Math.max(subStr.length, maxLen) } // 返回最大值 return maxLen };
此算法的时间复杂度是O(n)O(n)O(n)
执行代码,测试用例通过,提交算法,看看结果如何。
嗯,还是非常理想的。
三、总结
今天我们接触了一个非常重要的概念:滑动窗口
,这是一个非常重要的概念和思维方式,希望各位小伙伴都能好好的理解一下,在后面的学习过程中我们还会多次碰到它。
从满足题目要求,到针对性能优化;从时间复杂度O(n2)O(n^2)O(n2)优化到时间复杂度O(n)O(n)O(n),算法总是一个探索、寻求最优解的过程。
除了使用滑动窗口
这种方式之外,你还有其他的实现方式吗,欢迎留言交流~
算法-从菜鸟开始,而无止境。与君共勉!