【LeetCode 热题 100】3. 无重复字符的最长子串——滑动窗口

Problem: 3. 无重复字符的最长子串
题目:给定一个字符串 s ,请你找出其中不含有重复字符的 最长 子串 的长度。

整体思路

采用 滑动窗口 的方法来解决 “无重复字符的最长子串” 问题。核心目标是在给定的字符串 s 中,找到一个不包含任何重复字符的、最长的子字符串,并返回其长度。

算法的整体思路可以分解为以下几个核心步骤:

  1. 数据结构与指针初始化

    • 滑动窗口:算法在逻辑上维护一个“窗口”,该窗口由两个指针 leftright 界定,[left, right] 区间代表当前的子字符串。left 是窗口的左边界,right 是窗口的右边界。
    • 哈希表 (HashMap):代码使用一个 HashMap (map) 来存储当前窗口内每个字符及其出现的次数(频率)。这使得我们能以 O(1) 的平均时间复杂度快速判断一个字符是否在窗口内以及出现了多少次。
    • 变量初始化left 初始化为 0,ans(最终结果)初始化为 0。
  2. 窗口的扩展(遍历)

    • 代码通过一个 for 循环来移动 right 指针,从字符串的开始到结束,一次移动一个位置。
    • 每当 right 指针移动时,就相当于将一个新的字符 c[right] “移入”窗口。
    • 同时,更新 map 中该字符的频率。
  3. 窗口的收缩(处理重复)

    • 触发条件:在将新字符 c[right] 加入窗口后,代码会立即检查该字符在 map 中的频率是否大于 1。如果大于 1,则说明当前窗口内出现了重复字符,窗口变得“不合法”。
    • 收缩逻辑:为了使窗口重新“合法”,代码进入一个 while 循环,开始从左侧收缩窗口。
      • 它会减少窗口最左侧字符 c[left]map 中的频率。
      • 然后将 left 指针向右移动一位 (left++)。
    • 这个收缩过程会一直持续,直到 c[right] 这个重复字符在窗口内的频率降回到 1 为止。
  4. 结果更新

    • for 循环的每一次迭代中,在窗口调整(可能收缩)完毕后,此时的窗口 [left, right] 必然是一个不含重复字符的有效子串。
    • 代码会计算当前有效窗口的长度 right - left + 1,并与已记录的最大长度 ans进行比较,取较大者更新 ans
  5. 返回结果

    • right 指针遍历完整个字符串后,ans 中存储的就是整个过程中出现过的最长的、无重复字符的子串的长度。

总而言之,该算法通过 right 指针不断扩展窗口,并通过 left 指针在必要时收缩窗口,确保窗口内始终没有重复字符,同时动态更新找到的最大长度。

完整代码

import java.util.HashMap;
import java.util.Map;

class Solution {
    public int lengthOfLongestSubstring(String s) {
        // 将字符串转换为字符数组,可以略微提升后续字符访问的效率。
        char[] c = s.toCharArray();
        
        // 使用哈希表来存储当前滑动窗口内每个字符及其出现的次数(频率)。
        // Key: 字符, Value: 出现的次数。
        Map<Character, Integer> map = new HashMap<>();
        
        // a. 滑动窗口的左边界索引,初始时指向字符串的开头。
        int left = 0;
        // b. 用于记录和返回最长无重复子串的长度,初始化为0。
        int ans = 0;
        
        // c. for循环驱动滑动窗口的右边界`right`向右移动,遍历整个字符串。
        for (int right = 0; right < c.length; right++) {
            // 将右边界指向的字符加入窗口,并更新其在map中的频率。
            // getOrDefault(key, defaultValue)方法可以优雅地处理字符首次出现的情况。
            map.put(c[right], map.getOrDefault(c[right], 0) + 1);
            
            // 核心判断:检查窗口是否有效。
            // 如果新加入的字符 c[right] 在窗口中的数量大于1,说明窗口内出现了重复字符。
            // 此时需要从左侧收缩窗口,直到窗口再次变为有效(即 c[right] 的数量变回1)。
            while (map.get(c[right]) > 1) {
                // 将左边界指向的字符的频率减1,相当于将其移出窗口。
                map.put(c[left], map.get(c[left]) - 1);
                // 将左边界向右移动一位,即收缩窗口。
                left++;
            }
            
            // 在每次循环的最后(窗口调整完毕后),当前窗口 [left, right] 是一个有效的无重复子串。
            // 计算当前有效窗口的长度 (right - left + 1),并与已知的最大长度 ans 比较,更新为较大值。
            ans = Math.max(ans, right - left + 1);
        }
        
        // 遍历结束后,ans中存储的就是最终结果。
        return ans;
    }
}

时空复杂度

时间复杂度:O(N)
  1. 指针移动分析:算法使用了 leftright 两个指针。
    • right 指针从 0 线性扫描到 N-1,其中 N 是字符串 s 的长度。它总共移动了 N 次。
    • left 指针也从 0 开始移动,并且它 只向右移动,从不后退。它移动的总次数也不会超过 N 次。
  2. 摊还分析:每个字符 c[i] 最多被 right 指针访问一次,也最多被 left 指针访问一次。HashMapputget 操作的平均时间复杂度是 O(1)。因此,尽管存在嵌套的 while 循环,但整个过程的总操作数与 N 呈线性关系。

综合分析:两个指针共同完成了一次对字符串的遍历。因此,总的时间复杂度为 O(N)

空间复杂度:O(k)
  1. 主要存储开销:算法的额外空间主要来自于 HashMap (map)。
  2. 空间大小分析:这个 map 用来存储窗口内的字符及其频率。
    • 在最坏情况下,字符串中的所有字符都不同(例如 “abcdefg”),此时窗口会扩展到整个字符串,map 中会存储 N 个键值对。
    • 然而,map 中存储的键是字符,其数量受限于字符集的大小。例如,如果字符串只包含小写英文字母,map 的大小最多为 26。如果字符集是 ASCII,大小最多为 128 或 256。
  3. 结论:因此,空间复杂度取决于字符串中字符集的大小。我们可以表示为 O(k),其中 k 是字符集的大小(例如 ASCII 字符集大小为 128)。在很多情况下,k 是一个远小于 N 的常数,此时空间复杂度也可以看作是 O(1)。更严谨的表示是 O(min(N, k))。

综合分析:考虑到通用性,空间复杂度为 O(k),其中 k 是字符集的大小。

参考灵神

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

xumistore

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值