LeetCode100天Day9-无重复字符的最长子串与赎金信

LeetCode100天Day9-无重复字符的最长子串与赎金信:滑动窗口与计数数组

摘要:本文详细解析了LeetCode中两道经典题目------"无重复字符的最长子串"和"赎金信"。通过滑动窗口算法查找最长子串,以及使用计数数组统计字符频率,帮助读者掌握字符串处理的高效技巧。

目录

文章目录

1. 无重复字符的最长子串(Longest Substring Without Repeating Characters)

1.1 题目描述

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

示例 1

复制代码
输入: s = "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
注意 "bca" 和 "cab" 也是正确答案。

示例 2

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

示例 3

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

1.2 解题思路

这道题使用滑动窗口算法,核心思想是:

  1. 维护一个窗口,窗口内没有重复字符
  2. 使用HashMap记录每个字符最后出现的位置
  3. 遇到重复字符时,移动窗口左边界
  4. 不断更新最大长度

解题步骤

  1. 创建HashMap记录字符位置
  2. 使用左右指针维护滑动窗口
  3. 遍历字符串,检查字符是否重复
  4. 如果重复,移动左指针到重复字符的下一个位置
  5. 更新最大长度

1.3 代码实现

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

        for(int i = 0; i < len; i++){
            char temp = s.charAt(i);

            // 检查字符是否在窗口内重复
            if(map.containsKey(temp) && map.get(temp) >= left){
                left = map.get(temp) + 1;
            }

            // 更新字符位置
            map.put(temp, i);

            // 更新最大长度
            ans = Math.max(ans, i - left + 1);
        }

        return ans;
    }
}

1.4 代码逐行解释

第一部分:变量初始化
java 复制代码
int len = s.length();           // 字符串长度
int ans = 0;                    // 最长子串长度
int left = 0;                   // 滑动窗口左边界
Map<Character, Integer> map = new HashMap<>();  // 记录字符位置

变量作用

变量 类型 作用
len int 字符串长度
ans int 记录最大长度
left int 滑动窗口左边界
map Map 记录每个字符最后出现的位置
第二部分:遍历字符串
java 复制代码
for(int i = 0; i < len; i++){
    char temp = s.charAt(i);

    // 检查重复并更新窗口
    if(map.containsKey(temp) && map.get(temp) >= left){
        left = map.get(temp) + 1;
    }

    // 更新字符位置
    map.put(temp, i);

    // 更新最大长度
    ans = Math.max(ans, i - left + 1);
}
第三部分:重复检查逻辑
java 复制代码
if(map.containsKey(temp) && map.get(temp) >= left){
    left = map.get(temp) + 1;
}

为什么要两个条件

条件 作用
map.containsKey(temp) 检查字符是否出现过
map.get(temp) >= left 检查字符是否在当前窗口内

示例

复制代码
字符串: "abcabcbb"

第1次遍历:
  i=0, temp='a'
  map不包含'a'
  map = {'a': 0}
  ans = max(0, 0-0+1) = 1

第2次遍历:
  i=1, temp='b'
  map不包含'b'
  map = {'a': 0, 'b': 1}
  ans = max(1, 1-0+1) = 2

第3次遍历:
  i=2, temp='c'
  map不包含'c'
  map = {'a': 0, 'b': 1, 'c': 2}
  ans = max(2, 2-0+1) = 3

第4次遍历:
  i=3, temp='a'
  map包含'a',位置0
  0 >= left(0)? 是
  left = 0 + 1 = 1  ← 移动左边界
  map = {'a': 3, 'b': 1, 'c': 2}
  ans = max(3, 3-1+1) = 3

...继续遍历

1.5 执行流程详解

示例1s = "abcabcbb"

复制代码
i=0, temp='a':
  map不包含'a'
  map = {'a': 0}, left=0
  ans = max(0, 0-0+1) = 1
  窗口: [a]

i=1, temp='b':
  map不包含'b'
  map = {'a': 0, 'b': 1}, left=0
  ans = max(1, 1-0+1) = 2
  窗口: [a, b]

i=2, temp='c':
  map不包含'c'
  map = {'a': 0, 'b': 1, 'c': 2}, left=0
  ans = max(2, 2-0+1) = 3
  窗口: [a, b, c]

i=3, temp='a':
  map包含'a',位置0 >= left(0)
  left = 0 + 1 = 1
  map = {'a': 3, 'b': 1, 'c': 2}
  ans = max(3, 3-1+1) = 3
  窗口: [b, c, a]

i=4, temp='b':
  map包含'b',位置1 >= left(1)
  left = 1 + 1 = 2
  map = {'a': 3, 'b': 4, 'c': 2}
  ans = max(3, 4-2+1) = 3
  窗口: [c, a, b]

i=5, temp='c':
  map包含'c',位置2 >= left(2)
  left = 2 + 1 = 3
  map = {'a': 3, 'b': 4, 'c': 5}
  ans = max(3, 5-3+1) = 3
  窗口: [a, b, c]

i=6, temp='b':
  map包含'b',位置4 >= left(3)
  left = 4 + 1 = 5
  map = {'a': 3, 'b': 6, 'c': 5}
  ans = max(3, 6-5+1) = 3
  窗口: [c, b]

i=7, temp='b':
  map包含'b',位置6 >= left(5)
  left = 6 + 1 = 7
  map = {'a': 3, 'b': 7, 'c': 5}
  ans = max(3, 7-7+1) = 3
  窗口: [b]

最终输出: 3

1.6 滑动窗口图解

复制代码
字符串: a b c a b c b b
索引:   0 1 2 3 4 5 6 7

步骤1: i=0
窗口: [a]
      ↑
     left=0
ans=1

步骤2: i=1
窗口: [a b]
      ↑ ↑
     left=0
ans=2

步骤3: i=2
窗口: [a b c]
      ↑   ↑
     left=0
ans=3

步骤4: i=3, 遇到重复的'a'
窗口:   [b c a]
          ↑ ↑
        left=1
ans=3

步骤5: i=4, 遇到重复的'b'
窗口:     [c a b]
            ↑ ↑
          left=2
ans=3

1.7 复杂度分析

分析维度 复杂度 说明
时间复杂度 O(n) 每个字符最多访问两次
空间复杂度 O(min(m,n)) m是字符集大小,n是字符串长度

1.8 边界情况

输入 说明 输出
"" 空字符串 0
"a" 单个字符 1
"aa" 全部重复 1
"ab" 无重复 2

2. 赎金信(Ransom Note)

2.1 题目描述

给你两个字符串:ransomNotemagazine,判断 ransomNote 能不能由 magazine 里面的字符构成。

如果可以,返回 true;否则返回 false

magazine 中的每个字符只能在 ransomNote 中使用一次。

示例 1

复制代码
输入:ransomNote = "a", magazine = "b"
输出:false

示例 2

复制代码
输入:ransomNote = "aa", magazine = "ab"
输出:false

示例 3

复制代码
输入:ransomNote = "aa", magazine = "aab"
输出:true

2.2 解题思路

这道题的核心是统计字符频率:

  1. 统计magazine中每个字符出现的次数
  2. 遍历ransomNote,对每个字符减1
  3. 如果某个字符的计数变为负数,说明magazine中字符不够

解题步骤

  1. 创建长度为26的计数数组(假设只有小写字母)
  2. 遍历magazine,统计每个字符的出现次数
  3. 遍历ransomNote,对每个字符减1
  4. 如果计数变为负数,返回false
  5. 遍历结束,返回true

2.3 代码实现

java 复制代码
class Solution {
    public boolean canConstruct(String ransomNote, String magazine) {
        int[] cnt = new int[26];

        // 统计magazine中的字符
        for(int i = 0; i < magazine.length(); i++){
            cnt[magazine.charAt(i) - 'a']++;
        }

        // 检查ransomNote
        for(int i = 0; i < ransomNote.length(); i++){
            cnt[ransomNote.charAt(i) - 'a']--;
            if(cnt[ransomNote.charAt(i) - 'a'] < 0){
                return false;
            }
        }

        return true;
    }
}

2.4 代码逐行解释

第一部分:创建计数数组
java 复制代码
int[] cnt = new int[26];

数组结构

复制代码
索引: 0  1  2  3  ... 25
字符: a  b  c  d  ...  z

cnt[0] 记录 'a' 的数量
cnt[1] 记录 'b' 的数量
cnt[2] 记录 'c' 的数量
...
cnt[25] 记录 'z' 的数量
第二部分:统计magazine字符
java 复制代码
for(int i = 0; i < magazine.length(); i++){
    cnt[magazine.charAt(i) - 'a']++;
}

字符到索引的转换

java 复制代码
char c = 'a';
int index = c - 'a';  // 97 - 97 = 0

char c = 'b';
int index = c - 'a';  // 98 - 97 = 1

char c = 'z';
int index = c - 'a';  // 122 - 97 = 25

示例

复制代码
magazine = "aab"

i=0: magazine.charAt(0) = 'a'
     cnt['a' - 'a'] = cnt[0]++
     cnt = [1, 0, 0, ..., 0]

i=1: magazine.charAt(1) = 'a'
     cnt[0]++
     cnt = [2, 0, 0, ..., 0]

i=2: magazine.charAt(2) = 'b'
     cnt['b' - 'a'] = cnt[1]++
     cnt = [2, 1, 0, ..., 0]
第三部分:检查ransomNote
java 复制代码
for(int i = 0; i < ransomNote.length(); i++){
    cnt[ransomNote.charAt(i) - 'a']--;
    if(cnt[ransomNote.charAt(i) - 'a'] < 0){
        return false;
    }
}

检查逻辑

复制代码
magazine统计后: cnt = [2, 1, 0, ...]
                  a  b

ransomNote = "aa"

i=0: ransomNote.charAt(0) = 'a'
     cnt[0]-- → cnt[0] = 1
     cnt[0] < 0? 否,继续

i=1: ransomNote.charAt(1) = 'a'
     cnt[0]-- → cnt[0] = 0
     cnt[0] < 0? 否,继续

循环结束,返回 true

2.5 执行流程详解

示例1ransomNote = "a", magazine = "b"

复制代码
步骤1:统计magazine
magazine = "b"
cnt['b' - 'a']++ → cnt[1]++
cnt = [0, 1, 0, 0, ..., 0]

步骤2:检查ransomNote
ransomNote = "a"
cnt['a' - 'a']-- → cnt[0]--
cnt[0] = -1
-1 < 0? 是,返回 false

输出: false

示例2ransomNote = "aa", magazine = "ab"

复制代码
步骤1:统计magazine
magazine = "ab"
cnt[0]++ → cnt = [1, 0, 0, ...]
cnt[1]++ → cnt = [1, 1, 0, ...]

步骤2:检查ransomNote
ransomNote = "aa"
i=0: cnt[0]-- → cnt[0] = 0
i=1: cnt[0]-- → cnt[0] = -1
     -1 < 0? 是,返回 false

输出: false

示例3ransomNote = "aa", magazine = "aab"

复制代码
步骤1:统计magazine
magazine = "aab"
cnt[0]++ → cnt = [1, 0, 0, ...]
cnt[0]++ → cnt = [2, 0, 0, ...]
cnt[1]++ → cnt = [2, 1, 0, ...]

步骤2:检查ransomNote
ransomNote = "aa"
i=0: cnt[0]-- → cnt[0] = 1
i=1: cnt[0]-- → cnt[0] = 0

循环结束,返回 true

输出: true

2.6 算法图解

复制代码
magazine = "aab"
ransomNote = "aa"

步骤1:统计magazine
        a   a   b
        ↓   ↓   ↓
cnt: [2, 1, 0, 0, ..., 0]
       a  b

步骤2:消耗ransomNote
        a       a
        ↓       ↓
cnt: [0, 1, 0, 0, ..., 0]
       a  b

结果:所有cnt都 >= 0,返回 true

2.7 复杂度分析

分析维度 复杂度 说明
时间复杂度 O(m + n) m是magazine长度,n是ransomNote长度
空间复杂度 O(1) 固定大小的数组(26)

2.8 边界情况

ransomNote magazine 说明 输出
"a" "a" 刚好够 true
"a" "b" 字符不匹配 false
"aa" "a" 数量不够 false
"" "a" 空字符串 true

3. 两题对比与总结

3.1 算法对比

对比项 无重复字符的最长子串 赎金信
核心算法 滑动窗口 计数数组
数据结构 HashMap 数组
时间复杂度 O(n) O(m + n)
空间复杂度 O(min(m,n)) O(1)
应用场景 查找最长子串 字符匹配

3.2 滑动窗口模板

java 复制代码
// 滑动窗口标准模板
int left = 0;
int max = 0;

for(int right = 0; right < length; right++){
    // 1. 将right指向的元素加入窗口

    // 2. 如果窗口不满足条件,移动left
    while(窗口不满足条件){
        // 移除left指向的元素
        left++;
    }

    // 3. 更新结果
    max = Math.max(max, right - left + 1);
}

return max;

3.3 计数数组应用

何时使用计数数组

  1. 字符范围有限(如26个小写字母)
  2. 需要统计字符频率
  3. 需要比较字符数量

计数数组的优点

  • 空间复杂度O(1)
  • 访问速度快O(1)
  • 实现简单

3.4 HashMap vs 数组

对比项 HashMap 数组
空间复杂度 O(n) O(1)固定大小
访问速度 O(1)平均 O(1)
适用范围 任意字符 有限字符集
灵活性

4. 总结

今天我们学习了两道字符串处理题目:

  1. 无重复字符的最长子串:掌握滑动窗口算法,理解HashMap在记录字符位置中的作用
  2. 赎金信:掌握计数数组的使用,理解字符频率统计的方法

核心收获

  • 滑动窗口是解决子串问题的利器
  • HashMap可以记录字符位置,辅助窗口移动
  • 计数数组适合有限字符集的频率统计
  • 字符与数字的转换是常用技巧

练习建议

  1. 尝试用数组代替HashMap解决最长子串问题(假设字符集是ASCII)
  2. 思考如何找到最长无重复子串本身,而不是长度
  3. 尝试用HashMap解决赎金信问题(不限制字符集)

参考资源

文章标签

#LeetCode #算法 #Java #字符串 #滑动窗口

喜欢这篇文章吗?别忘了点赞、收藏和分享!你的支持是我创作的最大动力!

相关推荐
white-persist2 小时前
【内网运维】Netstat与Wireshark:内网运维溯源实战解析
运维·网络·数据结构·测试工具·算法·网络安全·wireshark
wjs20242 小时前
Go 语言类型转换
开发语言
努力学算法的蒟蒻2 小时前
day52(1.2)——leetcode面试经典150
算法·leetcode·面试
菩提祖师_2 小时前
基于Java的物联网智能交通灯控制系统
java·开发语言·物联网
java修仙传2 小时前
力扣hot100:字符串解码
算法·leetcode·职场和发展
公众号:ITIL之家2 小时前
服务价值体系重构:在变化中寻找不变的运维本质
java·运维·开发语言·数据库·重构
梭七y2 小时前
【力扣hot100题】(116)矩阵置零
算法·leetcode·矩阵
自在极意功。2 小时前
Spring 中 Bean 的生命周期
java·spring·bean生命周期
zhaokuner2 小时前
01-领域与问题空间-DDD领域驱动设计
java·开发语言·设计模式·架构