滑动窗口题目:字符串的排列

文章目录

题目

标题和出处

标题:字符串的排列

出处:567. 字符串的排列

难度

4 级

题目描述

要求

给定两个字符串 s1 \texttt{s1} s1 和 s2 \texttt{s2} s2,如果 s2 \texttt{s2} s2 包含 s1 \texttt{s1} s1 的一个排列则返回 true \texttt{true} true,否则返回 false \texttt{false} false。

换言之,如果 s1 \texttt{s1} s1 的排列之一是 s2 \texttt{s2} s2 的子串则返回 true \texttt{true} true。

示例

示例 1:

输入: s1 = "ab", s2 = "eidbaooo" \texttt{s1 = "ab", s2 = "eidbaooo"} s1 = "ab", s2 = "eidbaooo"

输出: true \texttt{true} true

解释: s2 \texttt{s2} s2 包含 s1 \texttt{s1} s1 的排列之一( "ba" \texttt{"ba"} "ba")。

示例 2:

输入: s1 = "ab", s2 = "eidboaoo" \texttt{s1 = "ab", s2 = "eidboaoo"} s1 = "ab", s2 = "eidboaoo"

输出: false \texttt{false} false

数据范围

  • 1 ≤ s1.length, s2.length ≤ 10 4 \texttt{1} \le \texttt{s1.length, s2.length} \le \texttt{10}^\texttt{4} 1≤s1.length, s2.length≤104
  • s1 \texttt{s1} s1 和 s2 \texttt{s2} s2 仅包含小写英语字母

解法一

思路和算法

如果字符串 s 2 s_2 s2 包含字符串 s 1 s_1 s1 的一个排列,则 s 1 s_1 s1 的长度必须小于或等于 s 2 s_2 s2 的长度。如果 s 1 s_1 s1 的长度大于 s 2 s_2 s2 的长度,则 s 2 s_2 s2 一定不包含 s 1 s_1 s1 的排列,返回 false \text{false} false。

当 s 1 s_1 s1 的长度小于或等于 s 2 s_2 s2 的长度时,需要遍历 s 2 s_2 s2 的每个长度为 s 1 s_1 s1 长度的子字符串,判断是否存在子字符串是 s 1 s_1 s1 的一个排列。字符串 s 1 s_1 s1 的任意排列都满足排列中的每个字符的出现次数与 s 1 s_1 s1 中的每个字符的出现次数相同,因此可以比较每个字符的出现次数,判断每个子字符串是否为 s 1 s_1 s1 的排列。以下所说的 s 2 s_2 s2 的子字符串均表示长度为 s 1 s_1 s1 长度的子字符串。

首先遍历 s 1 s_1 s1 计算每个字符的出现次数,然后遍历 s 2 s_2 s2 的首个子字符串并计算每个字符的出现次数。如果 s 2 s_2 s2 的首个子字符串中的每个字符的出现次数与 s 1 s_1 s1 中的每个字符的出现次数相同,则 s 2 s_2 s2 的首个子字符串即为 s 1 s_1 s1 的排列,返回 true \text{true} true。

如果 s 2 s_2 s2 的首个子字符串不是 s 1 s_1 s1 的排列,则需要继续遍历其余的子字符串并判断是否为 s 1 s_1 s1 的排列。每次将子字符串的下标范围向右移动一位,则有一个字符移出子字符串,有一个字符移入子字符串,更新子字符串中这两个字符的出现次数之后,比较子字符串中的每个字符的出现次数是否与 s 1 s_1 s1 中的每个字符的出现次数相同。如果遇到一个子字符串的每个字符的出现次数与 s 1 s_1 s1 中的每个字符的出现次数相同,则该子字符串即为 s 1 s_1 s1 的排列,返回 true \text{true} true。

如果遍历结束之后没有在 s 2 s_2 s2 中找到 s 1 s_1 s1 的排列,返回 false \text{false} false。

代码

java 复制代码
class Solution {
    public boolean checkInclusion(String s1, String s2) {
        int length1 = s1.length(), length2 = s2.length();
        if (length1 > length2) {
            return false;
        }
        int[] counts1 = new int[26];
        int[] counts2 = new int[26];
        for (int i = 0; i < length1; i++) {
            char c1 = s1.charAt(i);
            counts1[c1 - 'a']++;
            char c2 = s2.charAt(i);
            counts2[c2 - 'a']++;
        }
        if (checkEqual(counts1, counts2)) {
            return true;
        }
        for (int i = length1; i < length2; i++) {
            char prev = s2.charAt(i - length1);
            counts2[prev - 'a']--;
            char curr = s2.charAt(i);
            counts2[curr - 'a']++;
            if (checkEqual(counts1, counts2)) {
                return true;
            }
        }
        return false;
    }

    public boolean checkEqual(int[] counts1, int[] counts2) {
        for (int i = 0; i < 26; i++) {
            if (counts1[i] != counts2[i]) {
                return false;
            }
        }
        return true;
    }
}

复杂度分析

  • 时间复杂度: O ( ( m + n ) × ∣ Σ ∣ ) O((m + n) \times |\Sigma|) O((m+n)×∣Σ∣),其中 m m m 和 n n n 分别是字符串 s 1 s_1 s1 和 s 2 s_2 s2 的长度, Σ \Sigma Σ 是字符集,这道题中 Σ \Sigma Σ 是全部小写英语字母, ∣ Σ ∣ = 26 |\Sigma| = 26 ∣Σ∣=26。只有当 s 1 s_1 s1 的长度小于等于 s 2 s_2 s2 的长度时才需要遍历字符串 s 2 s_2 s2 寻找 s 1 s_1 s1 的排列,需要遍历字符串 s 1 s_1 s1 和 s 2 s_2 s2 各一次,对于每个子字符串需要 O ( ∣ Σ ∣ ) O(|\Sigma|) O(∣Σ∣) 的时间判断是否为 s 1 s_1 s1 的排列。

  • 空间复杂度: O ( ∣ Σ ∣ ) O(|\Sigma|) O(∣Σ∣),其中 Σ \Sigma Σ 是字符集,这道题中 Σ \Sigma Σ 是全部小写英语字母, ∣ Σ ∣ = 26 |\Sigma| = 26 ∣Σ∣=26。记录 s 1 s_1 s1 和 s 2 s_2 s2 中每个字母的出现次数需要 O ( ∣ Σ ∣ ) O(|\Sigma|) O(∣Σ∣) 的空间。

解法二

思路和算法

解法一对于 s 2 s_2 s2 的每个子字符串都需要遍历所有字母的计数。由于每次将子字符串的下标范围向右移动一位时,最多只会有两个字符的出现次数有变化,因此可以将每个子字符串的判断时间降低到 O ( 1 ) O(1) O(1)。

同时遍历 s 1 s_1 s1 以及 s 2 s_2 s2 的首个子字符串,计算每个字符在 s 2 s_2 s2 的子字符串与 s 1 s_1 s1 中的出现次数之差,并记录出现次数之差不为 0 0 0 的字符个数。如果每个字符在 s 2 s_2 s2 的首个子字符串与 s 1 s_1 s1 中的出现次数之差都是 0 0 0,则 s 2 s_2 s2 的首个子字符串即为 s 1 s_1 s1 的排列,返回 true \text{true} true。

如果 s 2 s_2 s2 的首个子字符串不是 s 1 s_1 s1 的排列,则需要继续遍历其余的子字符串并判断是否为 s 1 s_1 s1 的排列。每次将子字符串的下标范围向右移动一位,则有一个字符移出子字符串,有一个字符移入子字符串,将移出子字符串的字符记为 prev \textit{prev} prev,将移入子字符串的字符记为 curr \textit{curr} curr。如果 prev = curr \textit{prev} = \textit{curr} prev=curr,则子字符串中的每个字符的出现次数没有发生变化,因此不执行任何操作。如果 prev ≠ curr \textit{prev} \ne \textit{curr} prev=curr,则将 prev \textit{prev} prev 的出现次数之差减 1 1 1,将 curr \textit{curr} curr 的出现次数之差加 1 1 1,更新字符的出现次数之差之后,更新出现次数之差不为 0 0 0 的字符个数,更新方法如下。

  • 如果 prev \textit{prev} prev 的出现次数之差为 − 1 -1 −1,则将出现次数之差不为 0 0 0 的字符个数加 1 1 1;如果 prev \textit{prev} prev 的出现次数之差为 0 0 0,则将出现次数之差不为 0 0 0 的字符个数减 1 1 1。

  • 如果 curr \textit{curr} curr 的出现次数之差为 1 1 1,则将出现次数之差不为 0 0 0 的字符个数加 1 1 1;如果 curr \textit{curr} curr 的出现次数之差为 0 0 0,则将出现次数之差不为 0 0 0 的字符个数减 1 1 1。

更新字符的出现次数之差以及出现次数之差不为 0 0 0 的字符个数之后,如果出现次数之差不为 0 0 0 的字符个数为 0 0 0,则当前子字符串为 s 1 s_1 s1 的排列,返回 true \text{true} true。

如果遍历结束之后没有在 s 2 s_2 s2 中找到 s 1 s_1 s1 的排列,返回 false \text{false} false。

代码

java 复制代码
class Solution {
    public boolean checkInclusion(String s1, String s2) {
        int length1 = s1.length(), length2 = s2.length();
        if (length1 > length2) {
            return false;
        }
        int[] counts = new int[26];
        for (int i = 0; i < length1; i++) {
            char c1 = s1.charAt(i), c2 = s2.charAt(i);
            if (c1 == c2) {
                continue;
            }
            counts[c1 - 'a']--;
            counts[c2 - 'a']++;
        }
        int diffs = 0;
        for (int i = 0; i < 26; i++) {
            if (counts[i] != 0) {
                diffs++;
            }
        }
        if (diffs == 0) {
            return true;
        }
        for (int i = length1; i < length2; i++) {
            char prev = s2.charAt(i - length1), curr = s2.charAt(i);
            if (prev == curr) {
                continue;
            }
            counts[prev - 'a']--;
            counts[curr - 'a']++;
            if (counts[prev - 'a'] == -1) {
                diffs++;
            } else if (counts[prev - 'a'] == 0) {
                diffs--;
            }
            if (counts[curr - 'a'] == 1) {
                diffs++;
            } else if (counts[curr - 'a'] == 0) {
                diffs--;
            }
            if (diffs == 0) {
                return true;
            }
        }
        return false;
    }
}

复杂度分析

  • 时间复杂度: O ( m + n + ∣ Σ ∣ ) O(m + n + |\Sigma|) O(m+n+∣Σ∣),其中 m m m 和 n n n 分别是字符串 s 1 s_1 s1 和 s 2 s_2 s2 的长度, Σ \Sigma Σ 是字符集,这道题中 Σ \Sigma Σ 是全部小写英语字母, ∣ Σ ∣ = 26 |\Sigma| = 26 ∣Σ∣=26。只有当 s 1 s_1 s1 的长度小于等于 s 2 s_2 s2 的长度时才需要遍历字符串 s 2 s_2 s2 寻找 s 1 s_1 s1 的排列,需要遍历字符串 s 1 s_1 s1 和 s 2 s_2 s2 各一次,对于 s 2 s_2 s2 的首个子字符串需要 O ( ∣ Σ ∣ ) O(|\Sigma|) O(∣Σ∣) 的时间判断是否为 s 1 s_1 s1 的排列,对于 s 2 s_2 s2 的其余每个子字符串需要 O ( 1 ) O(1) O(1) 的时间判断是否为 s 1 s_1 s1 的排列。

  • 空间复杂度: O ( ∣ Σ ∣ ) O(|\Sigma|) O(∣Σ∣),其中 Σ \Sigma Σ 是字符集,这道题中 Σ \Sigma Σ 是全部小写英语字母, ∣ Σ ∣ = 26 |\Sigma| = 26 ∣Σ∣=26。记录 s 1 s_1 s1 和 s 2 s_2 s2 中每个字母的出现次数需要 O ( ∣ Σ ∣ ) O(|\Sigma|) O(∣Σ∣) 的空间。

相关推荐
KarrySmile2 小时前
Day8--滑动窗口与双指针--1004. 最大连续1的个数 III,1658. 将 x 减到 0 的最小操作数,3641. 最长半重复子数组
数据结构·算法·双指针·滑动窗口·不定长滑动窗口·最大连续1的个数·最长子数组
崎岖Qiu10 天前
leetcode1343:大小为K的子数组(定长滑动窗口)
java·算法·leetcode·力扣·滑动窗口
崎岖Qiu12 天前
leetcode643:子数组最大平均数 I(滑动窗口入门之定长滑动窗口)
java·算法·leetcode·力扣·双指针·滑动窗口
✿ ༺ ོIT技术༻19 天前
剑指offer第2版:双指针+排序+分治+滑动窗口
算法·排序算法·剑指offer·双指针·滑动窗口·分治
Tisfy25 天前
LeetCode 1695.删除子数组的最大得分:滑动窗口(哈希表)
算法·leetcode·散列表·题解·双指针·滑动窗口·哈希表
GEEK零零七3 个月前
Leetcode 159. 至多包含两个不同字符的最长子串
算法·leetcode·滑动窗口
卷卷的小趴菜学编程3 个月前
算法篇-----滑动窗口
数据结构·算法·双指针·滑动窗口·哈希表·数组相关
满怀10154 个月前
【数据通信完全指南】从物理层到协议栈的深度解析
5g·滑动窗口·数据封装·crc校验·qam调制
阳洞洞4 个月前
滑动窗口leetcode 209和76
算法·leetcode·双指针·滑动窗口