每日算法练习:LeetCode 3. 无重复字符的最长子串 ✅

大家好,我是你们的算法小伙伴。今天我们来练习一道字符串与滑动窗口的经典经典题 ------LeetCode 3. 无重复字符的最长子串。这道题是面试中考察滑动窗口(双指针)哈希表的标杆题目,核心在于如何用线性时间复杂度动态维护一个 "无重复字符的窗口"。


题目描述

给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。

注意:子串是连续的字符序列,不同于子序列(不要求连续)。


示例 1:

复制代码
输入:s = "abcabcbb"

输出:3

解释:因为无重复字符的最长子串是 "abc",所以其长度为 3。

示例 2:

复制代码
输入:s = "bbbbb"

输出:1

解释:因为无重复字符的最长子串是 "b"。

示例 3:

复制代码
输入:s = "pwwkew"

输出:3

解释:因为无重复字符的最长子串是 "wke",注意 "pwke" 是子序列而不是子串。

提示:

  • 0 <= s.length <= 5 * 10⁴
  • s 由英文字母、数字、符号和空格组成

解题思路

核心矛盾

如何在遍历字符串的过程中,快速判断当前字符是否重复,并快速调整窗口左边界?

方法一:滑动窗口 + 哈希表(最优解,O (n))

核心思想 :用左右指针维护一个动态窗口 [left, right],保证窗口内字符唯一。

  1. 右指针:不断向右扩展,将新字符加入窗口。
  2. 哈希表(数组):记录每个字符最后一次出现的索引。
  3. 左指针调整:当右指针遇到重复字符时,将左指针直接跳转到 "重复字符的下一位"(需取当前左指针与历史位置的较大值,防止回退)。
  4. 更新长度:每次遍历都计算当前窗口长度,更新最大值。

为什么用数组? ASCII 码表共有 128 个字符,因此可以用一个长度为 128 的 int 数组代替 HashMap,将时间复杂度优化到极致,且空间复杂度仅为 O(1)。


代码实现

方法一:滑动窗口(最优解)

复制代码
class Solution {
    public int lengthOfLongestSubstring(String s) {
        int n = s.length();
        int maxLen = 0;
        // 定义数组记录字符最后出现的位置,初始为-1
        int[] lastPos = new int[128];
        Arrays.fill(lastPos, -1);
        
        int left = 0; // 窗口左边界
        
        // 右指针遍历字符串
        for (int right = 0; right < n; right++) {
            char c = s.charAt(right);
            // 关键步骤:如果当前字符出现过,且其位置在窗口内,更新左边界
            // Math.max是为了防止left回退(例如"abba"这种情况)
            left = Math.max(left, lastPos[c] + 1);
            
            // 更新当前字符的最新位置
            lastPos[c] = right;
            
            // 计算当前窗口长度并更新最大值
            maxLen = Math.max(maxLen, right - left + 1);
        }
        
        return maxLen;
    }
}

代码详解

1. 初始化

  • lastPos[128]:存储每个 ASCII 字符最后一次出现的索引,初始化为 -1(表示未出现)。
  • left = 0:窗口起始位置。
  • maxLen = 0:记录最长子串长度。

2. 遍历逻辑(右指针扩展)

  • char c = s.charAt(right):获取当前字符。
  • left = Math.max(left, lastPos[c] + 1)核心去重逻辑
    • 如果 c 是第一次出现,则 lastPos[c] = -1left 保持不变。
    • 如果 c 重复出现,则将左边界移动到该字符上一次出现位置的下一位,确保窗口内无重复。
    • 使用 Math.max 是为了处理如 "abba" 这种情况:当处理到第二个 'a' 时,lastPos['a'] 是 0,left 会变成 1;但当处理第二个 'b' 时,lastPos['b'] 是 2,如果不加 maxleft 会回退到 3,这是错误的,因此必须保证 left 只增不减。
  • lastPos[c] = right:更新该字符的最新位置。
  • maxLen = ...:计算窗口长度 right - left + 1 并取最大值。

示例 1 模拟:s = "abcabcbb"

表格

right char lastPos (初 - 1) left (更新) 窗口 [left, right] maxLen
0 a lastPos[97]=0 0 [0,0] 1
1 b lastPos[98]=1 0 [0,1] 2
2 c lastPos[99]=2 0 [0,2] 3
3 a lastPos[97]=3 max(0,0+1)=1 [1,3] 3
4 b lastPos[98]=4 max(1,1+1)=2 [2,4] 3
5 c lastPos[99]=5 max(2,2+1)=3 [3,5] 3
6 b lastPos[98]=6 max(3,3+1)=4 [4,6] 3
7 b lastPos[98]=7 max(4,4+1)=5 [5,7] 3
最终结果为 3

复杂度分析

表格

解法 时间复杂度 空间复杂度 优点
滑动窗口 + 数组 O(n) O(1) 每个字符仅遍历一次,常数级空间,效率最高
暴力枚举 O(n3) O(1) 思路直观,但绝对超时,不推荐
滑动窗口 + HashMap O(n) O(k) (k 为字符集大小) 通用性强,但对于本题数组更快

总结

  1. 核心考点 :本题是滑动窗口的经典应用,考察对 "双指针动态调整" 和 "重复字符快速定位" 的理解。
  2. 优化技巧 :使用 int[128] 数组代替 HashMap,是处理 ASCII 字符集问题的标准技巧,能显著提升运行效率。
  3. 易错点
    • left 的更新必须用 Math.max:这是最容易出错的地方,必须保证左指针只向右移动,不向左回退。
    • 窗口长度计算right - left + 1,不要忘记加 1。
    • 初始值设定:数组初始化为 -1 而非 0,因为索引 0 也是有效位置。

今天的每日算法练习就到这里,我们明天再见!👋

相关推荐
深邃-14 小时前
【数据结构与算法】-二叉树(1):树的概念与结构,二叉树的概念与结构
数据结构·算法·链表·二叉树··顺序表
风筝在晴天搁浅14 小时前
手撕归并排序
数据结构·算法·排序算法
lynnlovemin14 小时前
C++高精度加减乘除算法详解
开发语言·c++·算法·高精度
原来是猿14 小时前
算法中 cin/cout 超时?聊聊它与 printf/scanf 的性能差异
算法
老赵聊算法、大模型备案15 小时前
“清朗·整治AI应用乱象”专项行动深度解读:从资质合规视角看AI应用新规
大数据·人工智能·算法·安全·aigc
Hello.Reader15 小时前
算法基础(二)——算法为什么是一种核心技术
算法
rit843249915 小时前
电容层析成像(ECT)的ART算法MATLAB演示实例
开发语言·算法·matlab
故事和你9115 小时前
洛谷-算法2-4-字符串2
开发语言·数据结构·c++·算法·深度优先·动态规划·图论
cpp_250115 小时前
P3374 【模板】树状数组 1
数据结构·c++·算法·题解·洛谷·树状数组
郝学胜-神的一滴15 小时前
干货版《算法导论》 02 :算法效率核心解密
java·开发语言·数据结构·c++·python·算法