leetcode 567. 字符串的排列

LeetCode 链接

字符串的排列


题目描述

给定两个字符串 s1s2,判断 s2 中是否存在一个连续子串,其字符组成与 s1 完全相同(顺序无关)。例如,若 s1 = "ab",则合法的子串可以是 "ab""ba" 或其他任意排列组合。

​示例 1​​:

输入:s1 = "ab"s2 = "eidbaooo"

输出:true

解释:s2 中存在子串 "ba",其字符组成与 s1 相同。

​示例 2​​:

输入:s1 = "ab"s2 = "eidboaoo"

输出:false

解释:s2 中所有长度为 2 的子串均不满足字符组成要求。


核心思路

本题的关键在于高效判断 s2 中是否存在与 s1 字符组成相同的子串。由于直接暴力枚举所有可能的子串会导致时间复杂度过高(如 s2 长度为 1e4 时,子串数量可达百万级),因此需要借助滑动窗口和字符计数的技巧来优化。


解法一:暴力枚举(直观但低效)

​思路​​:

  1. 统计 s1 中各字符的出现次数。
  2. 遍历 s2 的所有可能子串(长度等于 s1),逐一比对字符计数是否一致。

​代码实现​​:

class Solution {
    public boolean checkInclusion(String s1, String s2) {
        if (s1.length() > s2.length()) return false;
        
        // 统计s1的字符频率
        Map<Character, Integer> target = new HashMap<>();
        for (char c : s1.toCharArray()) {
            target.put(c, target.getOrDefault(c, 0) + 1);
        }
        
        // 枚举s2的所有长度为s1.length()的子串
        for (int i = 0; i <= s2.length() - s1.length(); i++) {
            Map<Character, Integer> window = new HashMap<>();
            for (int j = i; j < i + s1.length(); j++) {
                char c = s2.charAt(j);
                window.put(c, window.getOrDefault(c, 0) + 1);
            }
            // 判断当前窗口是否与目标完全匹配
            if (target.equals(window)) return true;
        }
        return false;
    }
}

​特点​​:

✅ 代码逻辑简单直观

❌ 时间复杂度为 O(m*n)mn 分别为 s1s2 的长度),无法通过大数据量测试


解法二:滑动窗口 + 动态计数(最优解)

​核心思想​​:

通过维护一个固定大小的窗口(长度等于 s1),逐步向右滑动窗口,实时更新窗口内的字符计数,并与 s1 的计数进行比对。当窗口计数完全匹配时,立即返回结果。

​关键优化点​​:

  1. ​滑动窗口​​:避免重复计算,每次只需处理新增和移出的字符。
  2. ​数组代替哈希表​​:利用 ASCII 码的特性,用长度为 26 的数组存储字符计数,提升查询效率。

​代码实现​​:

class Solution {
    public boolean checkInclusion(String s1, String s2) {
        if (s1.length() > s2.length()) return false;
        
        int[] target = new int[26];  // s1的字符计数
        int[] window = new int[26]; // 当前窗口的字符计数
        
        // 初始化窗口和目标计数
        for (int i = 0; i < s1.length(); i++) {
            target[s1.charAt(i) - 'a']++;
            window[s2.charAt(i) - 'a']++;
        }
        
        // 初步比对
        if (Arrays.equals(target, window)) return true;
        
        // 滑动窗口
        for (int i = s1.length(); i < s2.length(); i++) {
            // 添加新字符
            char newChar = s2.charAt(i);
            window[newChar - 'a']++;
            
            // 移出旧字符
            char oldChar = s2.charAt(i - s1.length());
            window[oldChar - 'a']--;
            
            // 判断是否匹配
            if (Arrays.equals(target, window)) return true;
        }
        return false;
    }
}

​特点​​:

✅ 时间复杂度优化至 O(n)(仅需遍历一次 s2

✅ 空间复杂度优化至 O(1)(固定大小数组)

❌ 需要处理字符计数的动态增减逻辑


代码对比与选择建议

方法时间复杂度空间复杂度适用场景
暴力枚举O(m*n)O(m)数据量极小(如面试题)
滑动窗口O(n)O(1)实际工程场景

​总结​​:滑动窗口方案通过巧妙的动态计数和窗口滑动策略,将时间复杂度从 O(m*n) 降低到线性级别,是本题的最优解。建议优先掌握该方法,并理解其核心思想——通过维护固定窗口减少重复计算。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

daxiang12092205

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

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

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

打赏作者

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

抵扣说明:

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

余额充值