567. 字符串的排列
问题描述
给你两个字符串 s1 和 s2,写一个函数来判断 s2 是否包含 s1 的排列。
换句话说,s1 的排列之一是 s2 的子串。
示例:
输入: s1 = "ab", s2 = "eidbaooo"
输出: true
解释: s2 包含 s1 的排列之一 ("ba").
输入: s1 = "ab", s2 = "eidboaoo"
输出: false
输入: s1 = "adc", s2 = "dcda"
输出: true
解释: s2 包含 s1 的排列之一 ("dca" 或 "cda").
算法思路
滑动窗口 + 字符频次匹配:
- 如果
s2包含s1的排列,则存在一个长度为s1.length()的子串,其字符频次与s1完全相同 - 使用固定大小的滑动窗口(窗口大小 = s1.length())
- 维护窗口内字符频次数组和 s1 的字符频次数组
- 当两个频次数组相等时,找到 s1 的排列
- 滑动窗口:移除左边界字符,添加右边界字符,更新频次
优化(单数组差值法):
- 使用一个数组记录 s1 频次与窗口频次的差值
- 维护一个计数器,记录差值为 0 的字符种类数
- 当计数器等于 26(或实际使用的字符种类数)时,找到排列
- 滑动时更新差值和计数器
代码实现
方法一:双数组比较法
class Solution {
/**
* 判断s2是否包含s1的排列 - 双数组比较法
*
* @param s1 目标字符串
* @param s2 主字符串
* @return 如果s2包含s1的排列则返回true,否则返回false
*/
public boolean checkInclusion(String s1, String s2) {
// 边界条件检查
if (s1 == null || s2 == null || s1.length() > s2.length()) {
return false;
}
int[] s1Count = new int[26]; // s1中各字符的频次
int[] windowCount = new int[26]; // 当前窗口中各字符的频次
int s1Len = s1.length();
// 初始化:计算s1的字符频次,并初始化第一个窗口
for (int i = 0; i < s1Len; i++) {
s1Count[s1.charAt(i) - 'a']++;
windowCount[s2.charAt(i) - 'a']++;
}
// 检查第一个窗口
if (Arrays.equals(s1Count, windowCount)) {
return true;
}
// 滑动窗口:从第2个位置开始
for (int i = s1Len; i < s2.length(); i++) {
// 移除左边界字符(窗口左移)
windowCount[s2.charAt(i - s1Len) - 'a']--;
// 添加右边界字符
windowCount[s2.charAt(i) - 'a']++;
// 检查当前窗口
if (Arrays.equals(s1Count, windowCount)) {
return true;
}
}
return false;
}
}
方法二:单数组差值法(优化)
class Solution {
/**
* 判断s2是否包含s1的排列 - 单数组差值法(优化)
* 使用一个数组记录差值,避免每次比较整个数组
*
* @param s1 目标字符串
* @param s2 主字符串
* @return 如果s2包含s1的排列则返回true,否则返回false
*/
public boolean checkInclusion(String s1, String s2) {
// 边界条件检查
if (s1 == null || s2 == null || s1.length() > s2.length()) {
return false;
}
int[] diff = new int[26]; // 记录s1频次与窗口频次的差值
int s1Len = s1.length();
int count = 0; // 记录差值为0的字符种类数
// 初始化:计算初始差值
for (int i = 0; i < s1Len; i++) {
diff[s1.charAt(i) - 'a']++;
diff[s2.charAt(i) - 'a']--;
}
// 统计初始差值为0的字符种类数
for (int i = 0; i < 26; i++) {
if (diff[i] == 0) {
count++;
}
}
// 检查第一个窗口
if (count == 26) {
return true;
}
// 滑动窗口
for (int i = s1Len; i < s2.length(); i++) {
// 准备移除的字符(左边界)
char leftChar = s2.charAt(i - s1Len);
// 准备添加的字符(右边界)
char rightChar = s2.charAt(i);
// 处理移除左边界字符
if (diff[leftChar - 'a'] == 0) {
count--; // 移除前差值为0,移除后不为0
}
diff[leftChar - 'a']++;
if (diff[leftChar - 'a'] == 0) {
count++; // 移除后差值变为0
}
// 处理添加右边界字符
if (diff[rightChar - 'a'] == 0) {
count--; // 添加前差值为0,添加后不为0
}
diff[rightChar - 'a']--;
if (diff[rightChar - 'a'] == 0) {
count++; // 添加后差值变为0
}
// 检查当前窗口
if (count == 26) {
return true;
}
}
return false;
}
}
方法三:简化版单数组法
class Solution {
/**
* 简化版单数组法
* 直接维护目标频次,窗口移动时更新
*
* @param s1 目标字符串
* @param s2 主字符串
* @return 如果s2包含s1的排列则返回true,否则返回false
*/
public boolean checkInclusion(String s1, String s2) {
// 边界条件检查
if (s1 == null || s2 == null || s1.length() > s2.length()) {
return false;
}
int[] target = new int[26];
int s1Len = s1.length();
// 初始化目标频次
for (char c : s1.toCharArray()) {
target[c - 'a']++;
}
// 初始化第一个窗口
for (int i = 0; i < s1Len; i++) {
target[s2.charAt(i) - 'a']--;
}
// 检查第一个窗口
if (isAllZero(target)) {
return true;
}
// 滑动窗口
for (int i = s1Len; i < s2.length(); i++) {
// 移除左边界字符
target[s2.charAt(i - s1Len) - 'a']++;
// 添加右边界字符
target[s2.charAt(i) - 'a']--;
// 检查当前窗口
if (isAllZero(target)) {
return true;
}
}
return false;
}
/**
* 检查数组是否全为0
*/
private boolean isAllZero(int[] arr) {
for (int val : arr) {
if (val != 0) {
return false;
}
}
return true;
}
}
算法分析
- 时间复杂度:
- 方法一:O(n × 26) = O(n),每次比较数组需要O(26)
- 方法二:O(n),每次滑动窗口操作是O(1)
- 方法三:O(n × 26) = O(n),每次检查数组需要O(26)
- 空间复杂度:O(1)
- 所有方法都只使用固定大小的数组(26个元素)
- 方法对比:
- 方法一:逻辑清晰,易于理解
- 方法二:效率最高,避免了数组比较
- 方法三:代码简洁,但效率略低于方法二
算法过程
输入:s1 = "ab", s2 = "eidbaooo"
方法一执行过程:
- 初始化:
s1Count = [1,1,0,0,...](a,b各1个)- 第一个窗口 “ei” →
windowCount = [0,0,0,0,1,0,0,0,1,0,...](e,i各1个) Arrays.equals(s1Count, windowCount)为 false
i=2:窗口 “id”- 移除 ‘e’ →
windowCount[e] = 0 - 添加 ‘d’ →
windowCount[d] = 1 windowCount = [0,0,0,1,0,0,0,0,1,0,...]≠s1Count→ false
- 移除 ‘e’ →
i=3:窗口 “db”- 移除 ‘i’ →
windowCount[i] = 0 - 添加 ‘b’ →
windowCount[b] = 1 windowCount = [0,1,0,1,0,0,0,0,0,0,...]≠s1Count→ false
- 移除 ‘i’ →
i=4:窗口 “ba”- 移除 ‘d’ →
windowCount[d] = 0 - 添加 ‘a’ →
windowCount[a] = 1 windowCount = [1,1,0,0,0,0,0,0,0,0,...]=s1Count→ return true
- 移除 ‘d’ →
测试用例
public static void main(String[] args) {
Solution solution = new Solution();
// 测试用例1:标准示例
System.out.println("Test 1: " + solution.checkInclusion("ab", "eidbaooo")); // true
// 测试用例2:标准示例
System.out.println("Test 2: " + solution.checkInclusion("ab", "eidboaoo")); // false
// 测试用例3:包含排列
System.out.println("Test 3: " + solution.checkInclusion("adc", "dcda")); // true
// 测试用例4:相同字符串
System.out.println("Test 4: " + solution.checkInclusion("abc", "abc")); // true
// 测试用例5:s1比s2长
System.out.println("Test 5: " + solution.checkInclusion("abcd", "abc")); // false
// 测试用例6:空字符串
System.out.println("Test 6: " + solution.checkInclusion("", "abc")); // true (空字符串的排列是空字符串)
System.out.println("Test 7: " + solution.checkInclusion("a", "")); // false
// 测试用例7:单字符
System.out.println("Test 8: " + solution.checkInclusion("a", "a")); // true
System.out.println("Test 9: " + solution.checkInclusion("a", "b")); // false
// 测试用例8:重复字符
System.out.println("Test 10: " + solution.checkInclusion("aab", "eidbaaoo")); // true
// 测试用例9:无匹配
System.out.println("Test 11: " + solution.checkInclusion("hello", "ooolleoooleh")); // false
// 测试用例10:长字符串
System.out.println("Test 12: " + solution.checkInclusion("abc", "ccccbbbbaaaa")); // false
}
关键点
-
排列定义:
- 两个字符串包含相同的字符,且每个字符的出现次数相同
- 顺序可以不同
-
滑动窗口大小:
- 固定大小 = s1.length()
- 只有长度相等的子串才可能是排列
-
字符频次匹配:
- 核心思想:排列 ⇔ 字符频次完全相同
- 使用数组记录26个小写字母的频次
-
优化:
- 方法二避免了每次比较整个数组,通过维护计数器提高效率
- 滑动窗口时,只需更新移除和添加的字符频次
-
边界处理:
- s1为null或长度大于s2时,直接返回false
- s1为空字符串时,应返回true(空字符串的排列是空字符串,任何字符串都包含空字符串)
常见问题
-
为什么方法二中要检查
diff[char] == 0前后的情况?- 因为只有当差值从0变为非0,或从非0变为0时,才会影响计数器
- 如果差值从1变为2,或从-1变为-2,计数器不需要变化
-
如果字符串包含大写字母怎么办?
- 题目说明只包含小写字母
- 如果需要处理大小写,可以扩展数组大小到52,或统一转换为小写
-
方法二中的
count == 26是否总是成立?- 是的,因为数组大小为26,当所有位置差值都为0时,count=26
- 即使某些字母在s1和窗口中都没出现(差值为0),也会计入count
-
与438题的区别是什么?
- 438题要求返回所有起始索引,本题只需判断是否存在
- 438题需要收集结果,本题找到即返回true
- 算法核心完全相同,都是滑动窗口+字符频次匹配
-
如何扩展支持任意字符(包括Unicode)?
- 使用HashMap代替数组
- 记录实际出现的字符种类,而不是固定26个
- 判断条件改为
count == map.size()(实际使用的字符种类数)
171万+

被折叠的 条评论
为什么被折叠?



