每日一题
2026.5.12
3. 无重复字符的最长子串
题目
给定一个字符串 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 * 104
s 由英文字母、数字、符号和空格组成
总体思路
暴力求解
- 时间复杂度:O(n²) 外层 n 次 × 内层最多 n 次 = n²
- 空间 复杂度:O(min(m, n)) charSet 最多存字符集大小 m 或字符串长度 n
移动窗口
- 核心算法思想:
使用滑动窗口技巧维护一个无重复字符的窗口
窗口由左右指针 left 和 right 表示
移动右指针扩展窗口,遇到重复字符时移动左指针收缩窗口
使用哈希表(Set)记录窗口中的字符
每次滑动后更新最大长度 - 时间复杂度
时间复杂度:O(n)
每个字符最多被访问两次(一次进窗口,一次出窗口)
整体线性扫描字符串 - 空间复杂度:O(min(m, n))
m:字符集大小(如ASCII为128,Unicode更大)
n:字符串长度
最坏情况需要存储所有不同的字符
代码
go
//暴力求解
func lengthOfLongestSubstring(s string) int {
res := 0
for left := 0; left < len(s); left++ {
charSet := make(map[byte]bool)
for right := left; right < len(s); right++ {
if charSet[s[right]] {
break
}
charSet[s[right]] = true
if right-left+1 > res {
res = right -left +1
}
}
}
return res
}
//移动窗口求解
func lengthOfLongestSubstring(s string) int {
charSet := make(map[byte]bool)
res := 0
left := 0
for right:=0; right < len(s); right++ {
for left < right && charSet[s[right]] {
delete(charSet,s[left])
left++
}
charSet[s[right]] = true
if right-left+1 > res {
res = right-left+1
}
}
return res
}
//
func lengthOfLongestSubstring(s string) int {
lastIndex := make(map[byte]int)
maxLen := 0
left := 0
for right := 0; right < len(s); right++ {
ch := s[right]
if idx, ok := lastIndex[ch]; ok && idx >= left {
left = idx + 1
}
lastIndex[ch] = right
if right-left+1 > maxLen {
maxLen = right - left + 1
}
}
return maxLen
}
go
//详细注释代码
func lengthOfLongestSubstring(s string) int {
// res 存储全局最大长度,初始为 0(空字符串时返回 0)
res := 0
// 【外层循环】枚举所有可能的子串左端点 left
// left 从 0 开始,到 len(s)-1 结束
for left := 0; left < len(s); left++ {
// charSet 用于记录当前子串 [left, right] 中出现过的字符
// 每次 left 右移时,都新建一个空集合,因为子串起点变了
charSet := make(map[byte]bool)
// 【内层循环】枚举右端点 right,从 left 开始向右扩展子串
for right := left; right < len(s); right++ {
// 【重复判断】如果 s[right] 已经在当前子串中出现过
// 说明从 left 开始、包含 s[right] 的子串存在重复字符
// 不符合"无重复字符"要求,必须终止当前 left 的扩展
if charSet[s[right]] {
break // 跳出内层循环,尝试下一个 left
}
// 【入集合】s[right] 是首次出现,加入当前子串的字符集合
charSet[s[right]] = true
// 【更新答案】计算当前无重复子串的长度:right - left + 1
// 如果比历史最大值 res 更大,则更新 res
// +1 是因为下标从 0 开始,长度 = 右下标 - 左下标 + 1
if right-left+1 > res {
res = right - left + 1
}
}
}
// 返回最终找到的最长无重复子串长度
return res
}
func lengthOfLongestSubstring(s string) int {
// charSet 模拟哈希集合,记录当前窗口 [left, right] 内的字符
// key: 字符(byte), value: true(bool 只用来占位,表示存在)
charSet := make(map[byte]bool)
// res 存储全局最大长度,初始为 0(空字符串时返回 0)
res := 0
// left 是滑动窗口的左边界,初始为 0
// 窗口范围是 [left, right],包含两端
left := 0
// 【主循环】right 作为滑动窗口的右边界,从左到右遍历字符串
for right := 0; right < len(s); right++ {
// 【收缩阶段】
// 如果 s[right] 已经在当前窗口中(charSet[s[right]] == true)
// 说明出现了重复字符,必须不断右移 left,缩小窗口
// 直到把重复的字符移出窗口,才能将 s[right] 安全加入
//
// left < right 是保护条件:确保 left 不会越过 right
for left < right && charSet[s[right]] {
delete(charSet, s[left]) // 将左边界字符移出窗口(从集合删除)
left++ // 左边界右移,窗口缩小
}
// 【扩展阶段】
// 此时窗口内已经没有 s[right] 的重复字符
// 将当前字符加入窗口(集合)
charSet[s[right]] = true
// 【更新答案】
// 计算当前窗口长度:right - left + 1
// 如果比历史最大值 res 更大,则更新 res
if right-left+1 > res {
res = right - left + 1
}
}
// 返回最终找到的最长无重复子串长度
return res
}
func lengthOfLongestSubstring(s string) int {
// 用 map 记录每个字符上一次出现的下标
// key: 字符 (byte 表示 ASCII 字符)
// value: 该字符上次出现的位置
lastIndex := make(map[byte]int)
maxLen := 0 // 记录最长子串长度
left := 0 // 左指针,表示窗口的起始位置
// 遍历字符串,right 是右指针
for right := 0; right < len(s); right++ {
ch := s[right] // 当前字符
// 如果当前字符 ch 在 map 中出现过,并且上次出现的位置 idx
// 在当前窗口的范围 [left, right] 内
if idx, ok := lastIndex[ch]; ok && idx >= left {
// 将左指针移动到重复字符的下一个位置
// 这样可以保证窗口内不包含重复字符
left = idx + 1
}
// 记录当前字符的最新位置(右指针的位置)
lastIndex[ch] = right
// 计算当前窗口长度
// 窗口长度 = right - left + 1
if right-left+1 > maxLen {
maxLen = right - left + 1
}
}
return maxLen
}
560. 和为 K 的子数组
题目
给你一个整数数组 nums 和一个整数 k ,请你统计并返回 该数组中和为 k 的子数组的个数 。
子数组是数组中元素的连续非空序列。
示例 1:
输入:nums = [1,1,1], k = 2
输出:2
示例 2:
输入:nums = [1,2,3], k = 3
输出:2
提示:
1 <= nums.length <= 2 * 104
-1000 <= nums[i] <= 1000
-107 <= k <= 107
总体思路
暴力求解
- 枚举所有子数组:
外层循环固定子数组的起点 i。
内层循环枚举子数组的终点 j。 - 逐步累加:
从 i 到 j 的元素累加求和,看是否等于 k。 - 计数:
如果和等于 k,就让 count++。
时间复杂度为O(n²)。
前缀和+哈希
- 前缀和定义:
pre[i] = nums[0] + nums[1] + ... + nums[i]。
任意子数组和 nums[l...r] = pre[r] - pre[l-1]。
所以我们只要知道某个前缀和 pre[r]-k 之前出现过几次,就能知道有多少个子数组以 r 为结尾,和等于 k。 - 哈希表 m:
key:某个前缀和的值
value:这个前缀和出现的次数 - 初始化:
m[0] = 1,表示"前缀和 0 出现过 1 次",这样当 pre == k 时,能正确计数从 0 开始的子数组。 - 遍历数组:
更新当前前缀和:pre += nums[i]
在哈希表里查找:pre-k 出现过多少次,把次数加到答案上。
把当前前缀和 pre 存入哈希表(出现次数 +1)。 - 时间复杂度
遍历一次数组,每次操作哈希表是 O(1),所以总体 O(n),比暴力法快得多。
代码
golang
go
// 暴力求解
func subarraySum(nums []int, k int) int {
n := len(nums)
count := 0
for i:=0; i<n; i++ {
sum := nums[i]
if sum == k {
count++ // 单个元素也可能刚好等于 k
}
for j:=i+1; j<n; j++ {
sum += nums[j] // 把 nums[j] 累加进来
if sum == k {
count++
}
}
}
return count
}
// 前缀和+哈希表
// 统计和为 k 的子数组个数:前缀和 + 哈希表
func subarraySum(nums []int, k int) int {
n := len(nums)
count, pre := 0, 0
m := map[int]int{0: 1}
for i := 0; i < n; i++ {
pre += nums[i] // 维护当前前缀和
// 需要的"目标前缀和"是 pre-k,出现几次就能形成几个子数组
count += m[pre-k] // 若不存在,m[pre-k] 默认是 0(见下文语法点)
/*if _, ok := m[pre - k]; ok {
count += m[pre - k]
}*/ //不必要写这一步
m[pre] = m[pre] + 1 // 记录当前前缀和出现次数 +1
}
return count
}
func subarraySum(nums []int, k int) int {
n := len(nums) // 数组长度
count, pre := 0, 0 // count: 满足条件的子数组个数;pre: 当前前缀和
// m 是哈希表,key: 前缀和的值,value: 该前缀和出现的次数
// 初始化 {0: 1} 是核心技巧:表示"前缀和为 0"出现过 1 次
// 这处理了从数组开头开始的子数组情况(即 pre - k == 0 时)
m := map[int]int{0: 1}
// 遍历数组,计算每个位置的前缀和
for i := 0; i < n; i++ {
pre += nums[i] // 累加当前元素,维护前缀和 pre = nums[0] + ... + nums[i]
// 【核心逻辑】
// 要找和为 k 的子数组,等价于找:存在某个 j < i,使得
// pre[i] - pre[j-1] == k
// 即 pre[j-1] == pre[i] - k
//
// m[pre-k] 表示"前缀和等于 pre-k"的出现次数
// 每出现一次,就代表有一个以 i 结尾、和为 k 的子数组
count += m[pre-k]
// 等价写法(上面的简写):
// if _, ok := m[pre-k]; ok {
// count += m[pre-k]
// }
// Go 中 map 访问不存在的 key 返回零值(int 为 0),所以可直接写 count += m[pre-k]
// 将当前前缀和 pre 的出现次数 +1,供后面的位置查询使用
m[pre] = m[pre] + 1
}
return count
}