题意描述:
给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。
示意:
输入: s = “abcabcbb”
输出: 3
解释: 因为无重复字符的最长子串是 “abc”,所以其长度为 3。
示例 2:
输入: s = “bbbbb”
输出: 1
解释: 因为无重复字符的最长子串是 “b”,所以其长度为 1。
示例 3:
输入: s = “pwwkew”
输出: 3
解释: 因为无重复字符的最长子串是 “wke”,所以其长度为 3。
请注意,你的答案必须是 子串 的长度,“pwke” 是一个子序列,不是子串
解题思路:
Alice: 这题应该是动态规划吧,你觉得动态规划和分治的区别是啥,不都是把一个大问题划分成小问题,然后通过解决一串小问题来解决大问题吗 ?
Bob: 不知道,分治应该没有递推公式吧,动态规划可能要复杂一些。别说那么多了,快看题吧。
Alice: 假设我们有一个数组 dp[i]
表示字符串 s
第 i
个位置之前的无重复最长字串,我们能求出 dp[i + 1]
吗 ?
Bob: 不行吧。dp[i]
表示字符串 s
第 i
个位置之前的无重复最长字串的话,我们并不知道那个最长无重复字串是不是以位置 i
结尾的,也不知道这个最长字串到底是什么。啥都不知道,怎么确定 dp[i + 1]
呢 ?
Alice: 对,看起来 dp[i]
一定要和 s[i ]
挂上关系。要不让 dp[i]
表示以 s[i]
为结尾的最长无重复字串的长度,那么这个最长字串应该是 s.slice(i - dp[i] + 1, i + 1)
。
Bob:这样应该就能推导递推公式了,如果 s[i + 1]
不在 s[i]
的字串里面,那 dp[i + 1] = dp[i] + 1
, 否则的话,否则的话…
Alice: 否则的话, dp[i+1]
应该和 dp[i]
没啥关系,直接从新暴力计算 dp[i + 1]
Bob:嗯,理论上应该可行。最后求出 dp
数组的最大值就是答案了。
Alice:还有一些边界条件要处理,不然也要 WA 几次。
–
Alice: 我怎么听说还有什么滑动窗口,什么双指针的解法 ?
Bob: 我也看到了,滑动窗口很好理解,我们要求的最长无重复字串就像是一个连续的大小可变的窗口在字符串 s 上划过,我们只需要求解滑动窗口的最大长度就行了。
Alice: 那双指针呢 ?
Bob: 双指针是滑动窗口思路的一种具体实现,就是用左右两个指针来确定窗口的大小,就这么回事。
Alice: 那不用双指针,用一个集合也能实现滑动窗口 ?
Bob: 是的,思路是一样的,具体实现使用的数据结构可以有多种。
代码:
JavaScript 动态规划
/**
* @param {string} s
* @return {number}
*/
var lengthOfLongestSubstring = function(s) {
// 从 index 开始前向查找最长无重复子串
const findLongestNoRepeatSubStrLengthFromIndex = (index) => {
const characters = [s[index]];
while (--index >= 0) {
const leftChar = s[index];
if (characters.includes(leftChar)){
break;
} else {
characters.push(leftChar);
}
}
return characters.length
}
if (!s) {
return 0;
}
// dp 数组默认都是 1
const dp = new Array(s.length).fill(1);
for(let i=1; i<s.length; ++i) {
// dp[i-1] 位置的最长无重复字串
const preLsrss = s.slice(i-1 - dp[i-1] + 1 , i-1 + 1);
if (!preLsrss.includes(s[i])) {
dp[i] = dp[i-1] + 1;
} else {
dp[i] = findLongestNoRepeatSubStrLengthFromIndex(i);
}
}
return Math.max(...dp);
};
JavaScript 滑动窗口 + 集合
/**
* @param {string} s
* @return {number}
*/
var lengthOfLongestSubstring = function(s) {
// 边界条件
if (!s) {
return 0;
}
// 一定包含 i 的最长无重复子串
const unique = new Set(s[0]);
let res = 1;
for (let i=1; i<s.length; ++i) {
if (!unique.has(s[i])) {
// 直接加一
unique.add(s[i]);
} else {
// 重新计算
unique.clear();
let j = i;
while(j > 0 && !unique.has(s[j])) {
unique.add(s[j]);
j--;
}
}
res = Math.max(res, unique.size);
}
return res;
};
JavaScript 滑动窗口双指针
/**
* @param {string} s
* @return {number}
*/
var lengthOfLongestSubstring = function(s) {
if (!s) {
return 0;
}
// 双指针指示滑动窗口
let left = 0;
let right = 0;
let res = 1;
// 注意这里的范围容易搞错
while (right < s.length - 1) {
// 右侧右移一位
right += 1;
// 维护滑动窗口
const repeat = s.slice(left, right).includes(s[right]);
if (repeat) {
const fromRight = [];
let i = right;
while ( i>= 0 && !fromRight.includes(s[i])) {
fromRight.push(s[i]);
i--;
}
left = i + 1;
}
res = Math.max(res, right - left + 1);
}
return res;
};
测试用例:
""
参考: