LeetCode刷题第六天(字符串的排列)


题目

给你两个字符串 s1s2 ,写一个函数来判断 s2 是否包含 s1 的排列。
换句话说,s1 的排列之一是 s2 的 子串 。
示例一:

输入:s1 = "ab" s2 = "eidbaooo"
输出:true
解释:s2 包含 s1 的排列之一 ("ba").

示例二:

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

方法一:滑动窗口

思想

由于排列不会改变字符串中每个字符的个数,所以只有当两个字符串每个字符的个数均相等时,一个字符串才是另一个字符串的排列。

根据这一性质,记 s1 的长度为 n,我们可以遍历 s2中的每个长度为 n 的子串,判断子串和 s1 中每个字符的个数是否相等,若相等则说明该子串是 s1​ 的一个排列。

使用两个数组 cnt1 和 cnt2​,cnt1\textit{cnt}_1cnt1​ 统计 s1中各个字符的个数,cnt2​ 统计当前遍历的子串中各个字符的个数。

由于需要遍历的子串长度均为 n,我们可以使用一个固定长度为 n 的滑动窗口来维护 cnt2:滑动窗口每向右滑动一次,就多统计一次进入窗口的字符,少统计一次离开窗口的字符。然后,判断 cnt1​ 是否与 cnt2相等,若相等则意味着 s1s_1s1​ 的排列之一是 s2 的子串。

JAVA代码

class Solution {
    public boolean checkInclusion(String s1, String s2) {
        int n = s1.length(), m = s2.length();
        if (n > m) {
            return false;
        }
        int[] cnt1 = new int[26];
        int[] cnt2 = new int[26];
        for (int i = 0; i < n; ++i) {
            ++cnt1[s1.charAt(i) - 'a'];
            ++cnt2[s2.charAt(i) - 'a'];
        }
        if (Arrays.equals(cnt1, cnt2)) {
            return true;
        }
        for (int i = n; i < m; ++i) {
            ++cnt2[s2.charAt(i) - 'a'];
            --cnt2[s2.charAt(i - n) - 'a'];
            if (Arrays.equals(cnt1, cnt2)) {
                return true;
            }
        }
        return false;
    }
}

方法二:优化滑动窗口

优化思想

注意到每次窗口滑动时,只统计了一进一出两个字符,却比较了整个 cnt1 和 cnt2 数组。

从这个角度出发,我们可以用一个变量 diff 来记录 cnt1​ 与 cnt2​ 的不同值的个数,这样判断 cnt1和 cnt2 是否相等就转换成了判断 diff 是否为 0.

每次窗口滑动,记一进一出两个字符为 x 和 y。

若 x=y 则对 cnt2 无影响,可以直接跳过。

若 x≠y,对于字符 x,在修改 cnt2 之前若有 cnt2[x]=cnt1[x],则将 diff 加一;在修改 cnt2\textit{cnt}_2cnt2​ 之后若有 cnt2[x]=cnt1[x],则将 diff 减一。字符 y 同理。

此外,为简化上述逻辑,我们可以只用一个数组 cnt,其中 cnt[x]=cnt2[x]−cnt1[x],将 cnt1[x] 与 cnt2[x] 的比较替换成 cnt[x] 与 0 的比较。

JAVA代码

class Solution {
    public boolean checkInclusion(String s1, String s2) {
        int n = s1.length(), m = s2.length();
        if (n > m) {
            return false;
        }
        int[] cnt = new int[26];
        for (int i = 0; i < n; ++i) {
            --cnt[s1.charAt(i) - 'a'];
            ++cnt[s2.charAt(i) - 'a'];
        }
        int diff = 0;
        for (int c : cnt) {
            if (c != 0) {
                ++diff;
            }
        }
        if (diff == 0) {
            return true;
        }
        for (int i = n; i < m; ++i) {
            int x = s2.charAt(i) - 'a', y = s2.charAt(i - n) - 'a';
            if (x == y) {
                continue;
            }
            if (cnt[x] == 0) {
                ++diff;
            }
            ++cnt[x];
            if (cnt[x] == 0) {
                --diff;
            }
            if (cnt[y] == 0) {
                ++diff;
            }
            --cnt[y];
            if (cnt[y] == 0) {
                --diff;
            }
            if (diff == 0) {
                return true;
            }
        }
        return false;
    }
}

复杂度分析

时间复杂度:O(n+m+∣Σ∣),其中 n 是字符串 s1​ 的长度,m 是字符串 s2​ 的长度,Σ 是字符集,这道题中的字符集是小写字母,∣Σ∣=26。
空间复杂度:O(∣Σ∣)。

方法三:双指针

算法思想

回顾方法一的思路,我们在保证区间长度为 n 的情况下,去考察是否存在一个区间使得 cnt的值全为 0。

反过来,还可以在保证 cnt 的值不为正的情况下,去考察是否存在一个区间,其长度恰好为 n。

初始时,仅统计 s1​ 中的字符,则 cnt 的值均不为正,且元素值之和为 −n。

然后用两个指针 left 和 right 表示考察的区间 [left,right]。right 每向右移动一次,就统计一次进入区间的字符 x。为保证 cnt 的值不为正,若此时 cnt[x]>0,则向右移动左指针,减少离开区间的字符的 cnt 值直到 cnt[x]≤0。

注意到 [left,right]的长度每增加 1,cnt 的元素值之和就增加 1。当 [left,right]的长度恰好为 n时,就意味着 cnt 的元素值之和为 0。由于 cnt 的值不为正,元素值之和为 0 就意味着所有元素均为 0,这样我们就找到了一个目标子串。

JAVA代码

class Solution {
    public boolean checkInclusion(String s1, String s2) {
        int n = s1.length(), m = s2.length();
        if (n > m) {
            return false;
        }
        int[] cnt = new int[26];
        for (int i = 0; i < n; ++i) {
            --cnt[s1.charAt(i) - 'a'];
        }
        int left = 0;
        for (int right = 0; right < m; ++right) {
            int x = s2.charAt(right) - 'a';
            ++cnt[x];
            while (cnt[x] > 0) {
                --cnt[s2.charAt(left) - 'a'];
                ++left;
            }
            if (right - left + 1 == n) {
                return true;
            }
        }
        return false;
    }
}

复杂度分析

时间复杂度:O(n+m+∣Σ∣)。
创建 cnt 需要 O(∣Σ∣) 的时间。
遍历 s1​ 需要 O(n) 的时间。
双指针遍历 s2​ 时,由于 left 和 right 都只会向右移动,故这一部分需要 O(m) 的时间。
空间复杂度:O(∣Σ∣)。

链接:https://leetcode-cn.com/problems/permutation-in-string/solution/zi-fu-chuan-de-pai-lie-by-leetcode-solut-7k7u/
来源:力扣(LeetCode)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值