1 题目
提示
给定一个字符串 s ,请你找出其中不含有重复字符的 最长 子串 的长度。
示例 1:
输入: s = "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。注意 "bca" 和 "cab" 也是正确答案。
示例 2:
输入: s = "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。
示例 3:
输入: s = "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。
提示:
0 <= s.length <= 5 * 104s由英文字母、数字、符号和空格组成
2 代码实现
c++
cpp
class Solution {
public:
int lengthOfLongestSubstring(string s) {
int left = 0 ;
int right = 0 ;
int maxLen = 0 ;
unordered_map <char , int > res ;
while(right < s.size() ){
char cur = s[right];
if (res.find(cur) == res.end() || res[cur] < left){
res[cur] = right ;
maxLen = max(maxLen , right - left + 1 );
right ++;
}else {
left = res[cur] + 1 ;
}
}
return maxLen ;
}
};
js
javascript
/**
* @param {string} s
* @return {number}
*/
var lengthOfLongestSubstring = function(s) {
let maxLen = 0 ;
let left = 0 ;
let right = 0 ;
const map = new Map();
while (right < s.length){
const c = s[right];
if (!map.has(c) || map.get(c) < left ){
map.set(c, right);
maxLen = Math.max(maxLen , right - left + 1 );
right ++ ;
}else {
left = map.get(c) + 1 ;
}
}
return maxLen ;
};
思考
不知道应该怎么去维护这个...这个和滑窗的关系是?。。。因为子串的定义是要连续的,所以这里滑窗的作用是为了是连续的字串,是否有重复呢,那就用哈希表来维护一下,比如是否在表里面存过,c++里面是unordered_map , js里面是map,但其是具体的api还不知。。。。。
自己乱写了一通
cpp
class Solution {
public:
int lengthOfLongestSubstring(string s) {
int left = 0 ;
int right = s.size() - 1 ;
unordered_map <int , int > res ;
while (left <= right){
if ()//没见过的,而且没重复,加进来 ; else 左边界收缩
//前进的状态一直是右边界往前走,左边界依据是否见过而收缩
//同时要维护一个最终的答案 len ,是cur还是之前规定好的len ,采用max函数
}
}
};
题目描述
给定一个字符串 s,请你找出其中不含有重复字符的最长子串的长度。
提示:0 ≤ s.length ≤ 5 * 10⁴,s 由英文字母、数字、符号和空格组成。
示例
-
示例 1:输入 s = "abcabcbb",输出 3(最长子串为 "abc"、"bca" 等);
-
示例 2:输入 s = "bbbbb",输出 1(最长子串为 "b");
-
示例 3:输入 s = "pwwkew",输出 3(最长子串为 "wke",注意 "pwke" 是子序列而非子串)。
解题思路(滑动窗口 + 哈希表)
核心思路与你最初的想法完全一致:用滑动窗口保证子串的连续性,用哈希表判断窗口内是否有重复字符,具体逻辑如下:
-
滑动窗口定义:用 left(左边界)和 right(右边界)表示当前无重复字符的连续子串,窗口范围为 [left, right];
-
右指针移动:right 从 0 开始,依次遍历字符串的每个字符,负责"扩大窗口";
-
哈希表作用:记录每个字符最后一次出现的下标(重点解决你疑惑的 key/value 问题);
-
左指针移动:当遇到重复字符时,将 left 跳到重复字符的下一位,负责"收缩窗口",保证窗口内始终无重复;
-
记录最大值:每次扩大或调整窗口后,计算窗口长度,用 max 函数保留最长长度。
代码实现
cpp
class Solution {
public:
int lengthOfLongestSubstring(string s) {
int left = 0; // 滑动窗口左边界(你最初定义的变量,保留不变)
int right = 0; // 修正:右边界从0开始遍历,而非s.size()-1
int maxLen = 0; // 维护最终答案:最长无重复子串的长度
// 哈希表:key=字符(char),value=该字符最后出现的下标(int)(解决你疑惑的key/value)
unordered_map<char, int> res;
// 你最初写的while循环,逻辑完全保留,仅补全内部判断
while (right < s.size()) {
char c = s[right]; // 当前遍历到的字符(c就是哈希表的key)
// 核心判断(重点讲解,解决你的疑惑)
// 条件含义:当前字符没出现过,或者出现过但不在当前窗口内 → 无重复
if (res.find(c) == res.end() || res[c] < left) {
// 重点:res[c] = right → key是字符c,value是当前字符的下标right
// 作用:记录当前字符的最新位置,方便后续判断是否重复
res[c] = right;
// 计算当前窗口长度(right - left + 1),更新最长长度
maxLen = max(maxLen, right - left + 1);
// 无重复 → 右指针继续右移,扩大窗口(符合你"右边界一直往前走"的思路)
right++;
} else {
// 否则:当前字符在窗口内重复了 → 左边界收缩(你最初的思路)
// res[c]是该字符上次出现的下标,left跳到它的下一位,保证窗口无重复
left = res[c] + 1;
}
}
return maxLen;
}
};
关键代码详解(针对你的疑惑点)
1. 哈希表 res 的 key 和 value(res[c] = right)
你疑惑的 res[c] = right; 是整个题解的核心,拆解如下:
-
res 是 unordered_map<char, int> 类型,规定了 key 必须是字符(char),value 必须是整数(int);
-
c 是当前遍历到的字符(比如 s = "abc",right=0 时,c = 'a'),所以 key = c(当前字符);
-
right 是当前字符 c 在字符串 s 中的下标(比如 c='a' 时,right=0),所以 value = right(当前字符的下标);
-
这句话的作用:把"当前字符"和"它的位置"存进哈希表,方便后续遇到重复字符时,快速找到它上次出现的位置。
2. 核心判断条件(res.find(c) == res.end() || res[c] < left)
这句话是判断"当前字符是否在窗口内重复"的关键,拆成两部分讲,全程大白话:
-
第一部分:
res.find(c) == res.end()-
res.find(c):在哈希表中查找当前字符 c;
-
res.end():哈希表的"末尾",表示"没找到";
-
整体含义:当前字符 c 从来没出现过,肯定不重复,可以加入窗口。
-
-
第二部分:
res[c] < left-
res[c]:通过 key(字符 c)找到 value(c 上次出现的下标);
-
left:当前滑动窗口的左边界;
-
整体含义:字符 c 以前出现过,但它上次出现的位置在当前窗口左边(不在窗口内),不算重复,也可以加入窗口。
-
总结:两个条件满足任意一个,就说明当前字符无重复,可扩大窗口;两个条件都不满足,说明字符在窗口内重复,需要收缩左边界。
复杂度分析
-
时间复杂度:O(n),n 是字符串 s 的长度,每个字符只被 right 遍历一次,left 只移动不回溯,整体遍历一次即可;
-
空间复杂度:O(min(m, n)),m 是字符集的大小(英文字母、数字、符号等的总数量),哈希表最多存储 m 个不同字符,最坏情况下存储 n 个字符(所有字符都不重复)。
示例演示(帮助理解窗口移动)
以 s = "abcabcbb" 为例,简化演示关键步骤,重点看哈希表和窗口变化:
-
right=0(c='a'):res 中无 'a' → 存 res['a']=0,窗口 [0,0],maxLen=1,right++;
-
right=1(c='b'):res 中无 'b' → 存 res['b']=1,窗口 [0,1],maxLen=2,right++;
-
right=2(c='c'):res 中无 'c' → 存 res['c']=2,窗口 [0,2],maxLen=3,right++;
-
right=3(c='a'):res['a']=0 ≥ left=0(在窗口内)→ left=0+1=1,不移动 right;
-
right=3(c='a'):再次判断,res['a']=0 < left=1(不在窗口内)→ 存 res['a']=3,窗口 [1,3],maxLen 仍为3,right++;
-
后续继续遍历,窗口始终维护无重复,最终 maxLen=3,与示例输出一致。
总结
本题核心是"滑动窗口 + 哈希表",你最初的代码框架完全正确,仅需补充3个关键细节:
-
右指针 right 从 0 开始遍历,而非字符串末尾;
-
哈希表 res 的 key 是字符、value 是下标,用 res[c] = right 记录字符位置;
-
用 res.find(c) == res.end() || res[c] < left 判断是否重复,重复则收缩左指针。
上述代码可直接提交通过,且完全贴合你的思考逻辑,重点标注了你之前不懂的两个核心点,可对照代码反复理解。
没问题!我1:1 把你这段 C++ 代码翻译成 JavaScript ,逻辑完全一样 、结构完全一样 、变量名几乎一样,你一眼就能对应上!
JavaScript 实现
1. 哈希表
C++:
cpp
unordered_map<char, int> res;
res[c] = right;
res.find(c) == res.end();
res[c] < left;
JS:
javascript
const map = new Map();
map.set(c, right);
!map.has(c)
map.get(c) < left
2. 核心判断(逻辑 100% 一样)
C++:
cpp
if (res.find(c) == res.end() || res[c] < left)
JS:
javascript
if (!map.has(c) || map.get(c) < left)
3. 存字符位置
C++:
cpp
res[c] = right;
JS:
javascript
map.set(c, right);
4. 重复时收缩左指针
C++:
cpp
left = res[c] + 1;
JS:
javascript
left = map.get(c) + 1;
1. key 和 value 是谁?
javascript
map.set(c, right)
key = c(字符)
value = right(下标)
和你 C++ 的 res[c] = right 完全一样。
2. 判断条件什么意思?
javascript
!map.has(c) // 从没出现过
||
map.get(c) < left // 出现过,但不在窗口里
- 双指针滑动窗口
- 右指针一直走
- 哈希表存字符位置
- 重复就跳左指针
- 维护最大长度
3 题目
给定一个含有 n个正整数的数组和一个正整数 target。
找出该数组中满足其总和大于等于target的长度最小的 子数组 [numsl, numsl+1, ..., numsr-1, numsr] ,并返回其长度**。** 如果不存在符合条件的子数组,返回 0 。(这里的l,r都是下标)
示例 1:
输入:target = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3] 是该条件下的长度最小的子数组。
示例 2:
输入:target = 4, nums = [1,4,4]
输出:1
示例 3:
输入:target = 11, nums = [1,1,1,1,1,1,1,1]
输出:0
提示:
1 <= target <= 1091 <= nums.length <= 1051 <= nums[i] <= 104
进阶:
- 如果你已经实现
O(n)时间复杂度的解法, 请尝试设计一个O(n log(n))时间复杂度的解法。
4 代码实现
c++
cpp
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
int minLen = INT_MAX ;
int left = 0 ;
int right = 0 ;
int sum = 0 ;
while (right < nums.size()){
sum += nums[right];
while(sum >= target){
minLen = min(minLen , right - left + 1 );
sum -= nums[left];
left ++ ;
}
right ++ ;
}
return minLen == INT_MAX ? 0 : minLen ;
}
};
js
javascript
/**
* @param {number} target
* @param {number[]} nums
* @return {number}
*/
var minSubArrayLen = function(target, nums) {
let minLen = Infinity ;
let left = 0 ;
let right = 0 ;
let sum = 0 ;
while (right < nums.length){
sum += nums[right];
while(sum >= target){
minLen = Math.min(right - left + 1 , minLen);
sum -= nums[left];
left ++ ;
}
right ++ ;
}
return minLen === Infinity ? 0 : minLen ;
};
思考
蛤,毫无头绪啊,这怎么搞的啊????
1.base case 遍历所有,sum看看是否小于 , 那就是0 ; 好像没这个必要。。
- 先排序吗,然后长度最小肯定是要从最大数字往最小数字走,这样维护一个滑窗?不是的!!!
因为题目要的是 连续子数组排序会破坏连续性,直接错误!!!那就是正常滑窗就ok了
我服了我写不出来
cpp
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
int minLen = 0 ;
int left = 0 ;
int right = 0 ;
int sum = 0 ;
while (right < nums.size() && left <= right ){
if (sum < target){
right ++;
sum += nums[right];
}else{
left ++;
sum -= nums[left];
}
minLen = min(right - left + 1 , minLen);
}
return minLen ;
}
};
题解
1. 为什么不能先排序?
你自己已经悟了:题目要连续子数组,排序会破坏顺序 → 直接错!
2. 滑动窗口核心(和上一题一模一样)
- right 一直往前走
- 把数字加入窗口 sum
- 一旦 sum ≥ target → 尝试缩 left,让窗口尽量短
- 每次缩的时候,更新最短长度
3. 你代码的 3 个小错误(超级好改)
-
minLen 初始不能是 0 应该是无穷大
INT_MAX -
不能先移动指针,再加减必须:
cppsum += nums[right] right++你写反了
-
更新最小长度必须在 sum >= target 里面不是每次循环都更新
最关键的:为什么用 while 不是 if?
因为:加完一个数可能满足 sum >= target 缩 left 一次后可能 still >= target 还能再缩!
所以必须用 while 缩到不能缩为止。
你能记住的终极口诀(两道题通用)
cpp
right 一直走
加入窗口
while(满足条件) {
更新答案
缩 left
}
上一题满足条件:无重复 这一题满足条件:sum >= target
cpp
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
int left = 0; // 完全保留:滑动窗口左边界
int right = 0; // 完全保留:右边界从0开始
int minLen = INT_MAX; // 最小改动:maxLen → minLen,初始无穷大
// 最小改动:去掉哈希表,加一个 sum 记录窗口和
int sum = 0;
// 完全保留:你最爱的 while 循环!
while (right < nums.size()) {
// 最小改动:把当前数字加入窗口 sum
sum += nums[right];
// ==================== 最小改动核心 ====================
// 把你的"字符不重复判断" 换成 "sum >= target"
// 并且用 while 缩到不能缩为止
while (sum >= target) {
// 最小改动:更新最小长度
minLen = min(minLen, right - left + 1);
// 最小改动:缩左指针,减去左边数字
sum -= nums[left];
left++;
}
// ======================================================
// 完全保留:右边界一直往前走
right++;
}
// 最小改动:找不到返回 0
return minLen == INT_MAX ? 0 : minLen;
}
};
5 小结
滑窗这么简单太久没做也还是会忘记,特别要注意 js 的语法,特别要注意这个!!!