【LeetCode每日一题】3. 无重复字符的最长子串 560. 和为 K 的子数组

每日一题

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
}
相关推荐
代码地平线1 小时前
【排序】C语言实现八大排序算法(含完整源码与性能测试)
c语言·算法·排序算法
承渊政道1 小时前
【贪心算法】(经典实战应用解析(一):柠檬水找零、将数组和减半的最少操作次数、最大数、摆动序列)
数据结构·c++·学习·算法·leetcode·贪心算法·排序算法
初心未改HD1 小时前
机器学习之支持向量机SVM详解
算法·机器学习·支持向量机
he___H1 小时前
子串----
java·数据结构·算法·leetcode
05候补工程师2 小时前
【ROS 2 避坑指南】从 SLAM 实时建图到 Nav2 导航算法深度调优全过程
算法·ubuntu·机器人
Dlrb12112 小时前
C语言-函数传参
c语言·数据结构·算法
洛水水10 小时前
【力扣100题】18.随机链表的复制
算法·leetcode·链表
南宫萧幕10 小时前
规则基 EMS 仿真实战:SOC 区间划分与 Simulink 闭环建模全解
算法·matlab·控制
多加点辣也没关系10 小时前
数据结构与算法|第二十三章:高级数据结构
数据结构·算法