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

文章目录

题目

标题和出处

标题:字符串的排列

出处: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(∣Σ∣) 的空间。

相关推荐
童话名剑3 天前
目标检测(吴恩达深度学习笔记)
人工智能·目标检测·滑动窗口·目标定位·yolo算法·特征点检测
阿巴~阿巴~6 天前
从滑动窗口到拥塞控制:TCP高效可靠传输的三大支柱
服务器·网络·网络协议·tcp·滑动窗口·流量控制·拥塞控制
CodeAmaz9 天前
常用限流算法详解
滑动窗口·限流算法·令牌桶
Espresso Macchiato13 天前
Leetcode 3768. Minimum Inversion Count in Subarrays of Fixed Length
滑动窗口·leetcode hard·leetcode双周赛171·leetcode 3768
weixin_4617694013 天前
3. 无重复字符的最长子串
c++·算法·滑动窗口·最长字串
nju_spy14 天前
12月力扣每日一题(划分dp + 单调栈 + 堆 + 会议安排)
算法·leetcode·二分查找·动态规划·滑动窗口·单调栈·最大堆
2401_8414956416 天前
【LeetCode刷题】爬楼梯
数据结构·python·算法·leetcode·动态规划·滑动窗口·斐波那契数列
tang&23 天前
滑动窗口:双指针的优雅舞步,征服连续区间问题的利器
数据结构·算法·哈希算法·滑动窗口
Tisfy23 天前
LeetCode 3652.按策略买卖股票的最佳时机:滑动窗口
算法·leetcode·题解·滑动窗口
橘子真甜~1 个月前
C/C++ Linux网络编程14 - 传输层TCP协议详解(保证可靠传输)
linux·服务器·网络·网络协议·tcp/ip·滑动窗口·拥塞控制