Leetcode3.无重复字符的最长子串 HashSet+HashMap 【hot100算法个人笔记】【java写法】

算法刷题打卡 | 今天终于刷到了中等难度的题 ------LeetCode 3. 无重复字符的最长子串,这道题是滑动窗口的经典应用,之前做的都是链表的基础题,今天换字符串的题练练手,把 HashSet 和 HashMap 两种滑动窗口的写法都理清楚了,做个笔记记录一下。

题目回顾

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

示例

示例 1:

Plain 复制代码
输入: s = "abcabcbb"
输出: 3 
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。

示例 2:

Plain 复制代码
输入: s = "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。

示例 3:

Plain 复制代码
输入: s = "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。

示例 4:

Plain 复制代码
输入: s = ""
输出: 0

核心思路:滑动窗口

这道题的核心就是滑动窗口 的思想:我们维护一个窗口 [left, right],保证这个窗口里的字符都是不重复的。然后我们不断移动右指针扩大窗口,遇到重复字符的时候,就移动左指针缩小窗口,直到窗口里没有重复字符,全程记录最大的窗口长度就可以了。

相比暴力枚举所有子串的 O (n²) 解法,滑动窗口可以把时间复杂度降到 O (n),每个字符最多被访问两次,效率高了很多。

解法一:HashSet 实现滑动窗口

这是滑动窗口的基础写法,比较直观,容易理解,适合新手入门。

步骤拆解

我自己整理的操作要点,每一步都不能乱:

  1. 初始化指针:左指针left和右指针right都从 0 开始,temp记录当前窗口的长度,res记录最大的长度。

  2. 初始化 HashSet:注意 Set 的类型是Character不是char,因为 Java 的泛型不能用基本类型,用来存当前窗口里的字符。

  3. 循环条件:只要右指针小于字符串的长度,就继续处理。

  4. 拿到当前右指针指向的字符cur

  • 如果 Set 里已经包含了cur,说明窗口里有重复了,我们要移动左指针缩小窗口:把左指针指向的字符从 Set 里删掉,左指针右移一位,当前窗口长度temp减一。

  • 如果 Set 里不包含cur,说明可以扩大窗口:把cur加入 Set,右指针右移一位,当前窗口长度temp加一,然后更新resmax(res, temp),记录最大的长度。

    复制代码
    TIP:更新 res 的操作必须放在 else 块里!不然重复的时候也会更新,会导致结果错误。
  1. 最后返回res就可以了。

代码实现

java 复制代码
class Solution {
    public int lengthOfLongestSubstring(String s) {
        int left=0,right=0,temp=0,res=0;
        HashSet<Character> set = new HashSet<>();
        while(right<s.length()){
            char cur=s.charAt(right);
            if(set.contains(cur)){
                set.remove(s.charAt(left));
                left++;
                temp--;
            }else{
                set.add(cur);
                right++;
                temp++;
                res=Math.max(temp,res);
            }
        }
        return res;
    }
}

踩坑感悟

一开始写这个的时候,我踩了两个小坑:

  1. 一开始把 Set 的泛型写成了char,结果编译器直接报错,才想起来 Java 的泛型不支持基本类型,必须用包装类Character,这个细节差点卡了我半天。

  2. 一开始把更新 res 的操作放在了循环的最后,不管有没有重复都更新,结果遇到重复的时候,窗口缩小了,res 反而被更新成了更小的数,结果错了,后来才明白,只有扩大窗口的时候才需要更新 res,所以必须放在 else 里。

这个写法的好处是非常直观,很容易理解滑动窗口的移动过程,但是缺点是左指针是一步一步移动的,遇到重复的时候要移很多步,效率稍微低一点。

解法二:HashMap 优化滑动窗口

这个是滑动窗口的优化写法,我们可以用 HashMap 记录每个字符最后一次出现的位置,这样遇到重复的时候,左指针可以直接跳到重复字符的下一个位置,不用一步一步移了,效率更高。

步骤拆解

我整理的 HashMap 版的要点:

  1. Map 的定义:Key 存储字符,Value 存储这个字符最后一次出现的下标位置。

  2. 初始化左指针left从 0 开始,res记录最大长度。

  3. 遍历右指针right,拿到当前的字符cur

  • 如果 Map 里已经存在cur,说明这个字符之前出现过,那我们的左指针就要跳到这个字符上次出现的位置的下一位,这样窗口里就没有重复了。

  • 注意这里要取max(left, map.get(cur)+1),因为有可能这个字符上次出现的位置在左指针的左边,也就是不在当前窗口里,这时候我们不能把左指针往回跳,不然窗口就乱了。

  1. 然后把当前字符和它的下标更新到 Map 里,不管之前有没有,都要更新成最新的位置。

  2. 然后更新resmax(res, right-left+1),也就是当前窗口的长度,记录最大的。

代码实现

java 复制代码
class Solution {
    public int lengthOfLongestSubstring(String s) {
        int left=0,res=0;
        HashMap<Character,Integer> map = new HashMap<>();

        for(int right=0;right<s.length();right++){
            char cur = s.charAt(right);
            if(map.containsKey(cur)){
                left = Math.max(left,map.get(cur)+1);
            }
            map.put(cur,right);
            res = Math.max(res,right-left+1);
        }
        return res;
    }
}

踩坑感悟

这个写法我踩了一个超级经典的坑:一开始我没加Math.max,直接写了left = map.get(cur)+1,结果测试abba这个用例的时候直接错了!

  • 处理到第二个 b 的时候,left 变成了 2,没问题。

  • 然后处理到第二个 a 的时候,map 里 a 的位置是 0,这时候如果直接left=0+1=1,就把 left 往回跳了,窗口变成了[1,3],也就是bba,这时候就有重复了!

    后来才明白,必须用 max,保证 left 只会往右走,不会往左跳,这样才不会出错,这个坑真的太多人踩了,我调试了半天才发现问题所在。

这个写法的好处是左指针可以直接跳,不用一步一步移,所以效率比 HashSet 的版本高很多,尤其是长字符串的时候,差距很明显。

复杂度分析

两种解法的时间复杂度都是 O (n),n 是字符串的长度,每个字符最多被左右指针各访问一次,不会重复处理。

空间复杂度都是 O (min (m, n)),m 是字符集的大小,比如如果是英文字母的话,最多也就 26 个字符,所以空间其实很小,就算字符串很长,空间也不会太大。

刷题笔记与感悟

做完这道题,最大的收获就是彻底搞懂了滑动窗口的思想,之前一直觉得滑动窗口很抽象,现在做完这道题才明白,其实就是维护一个窗口,保证窗口里满足我们的条件,然后移动指针,动态调整窗口的大小,全程记录我们要的结果,这个思想真的太通用了,很多字符串、数组的题都能用。

而且这道题也让我明白,优化的思路就是:能不能把一步一步的移动,改成直接跳转到目标位置?用 HashMap 记录位置,就可以把 O (n) 的左指针移动,变成 O (1) 的跳转,虽然时间复杂度还是 O (n),但是实际运行效率高了很多。

另外边界情况也都测试了:空字符串、全重复的字符串、所有字符都不重复的字符串,两种方法都能正确处理,鲁棒性还是很好的。

总结

这道中等难度的题,核心就是滑动窗口,两种解法各有优劣:

  • HashSet 版:写法直观,容易理解,适合新手入门,帮你理解滑动窗口的移动过程。

  • HashMap 优化版:效率更高,代码更简洁,是面试的时候推荐写的版本,体现了你对优化的理解,也能体现你踩过那些经典的坑(比如 abba 的 max 问题)。

搞定了这道经典的滑动窗口题,之后的其他滑动窗口的题,就有很好的基础了!

相关推荐
Binary-Jeff2 小时前
Maven 依赖作用域详解:compile、provided、runtime、test
java·spring·spring cloud·servlet·java-ee·maven
QH_ShareHub2 小时前
Rstudio 与 R 打开 Rdata (压缩文件) 差异
java·前端·r语言
鹭天2 小时前
目标检测学习笔记
笔记·学习·目标检测
spencer_tseng2 小时前
apache-maven-3.9.6
java·maven
MR.P_H_2 小时前
QT创建新工程,无法正常编译(Kit套件无法正常配置)
开发语言·qt
-许平安-2 小时前
MCP项目笔记五(PluginAPI)
c++·笔记·rpc·json·mcp·pluginapi
MicroTech20252 小时前
微算法科技(NASDAQ: MLGO)支持区块链的工业物联网隐私保护新方案:基于格的可链接环签名技术
科技·算法·区块链
zhenxin01222 小时前
SpringMVC 请求参数接收
前端·javascript·算法
没有蛀牙lm2 小时前
windows下快速安装android studio(预估30min)
开发语言·javascript·webpack