找到字符串中所有字母异位词

题目介绍

力扣438题:https://leetcode-cn.com/problems/find-all-anagrams-in-a-string/
给定一个字符串 s 和一个非空字符串 p,找到 s 中所有是 p 的字母异位词的子串,返回这些子串的起始索引。

字符串只包含小写英文字母,并且字符串 s 和 p 的长度都不超过 20100。
说明:

  • 字母异位词指字母相同,但排列不同的字符串。
  • 不考虑答案输出的顺序。

示例 1:

分析

“字母异位词”,指“字母相同,但排列不同的字符串”。注意这里所说的“排列不同”,是所有字母异位词彼此之间而言的,并不是说要和目标字符串p不同。另外,我们同样应该考虑,p中可能有重复字母。

方法一:暴力法

最简单的想法,自然还是暴力法。就是直接遍历s中每一个字符,把它当作子串的起始,判断长度为p.length()的子串是否是p的字母异位词就可以了。

考虑到子串和p中都可能有重复字母,我们还是用一个额外的数据结构,来保存每个字母的出现频次。由于本题的字符串限定只包含小写字母,所以我们可以简单地用一个长度为26的int类型数组来表示,每个位置存放的分别是字母a~z的出现个数。
代码演示如下:

// 方法一:暴力法,枚举所有的长度为p.length()的子串
public List<Integer> findAnagrams1(String s, String p){
    // 定义一个结果列表
    ArrayList<Integer> result = new ArrayList<>();

    // 1. 统计p中所有字符频次
    int[] pCharCounts = new int[26];

    for (int i = 0; i < p.length(); i++){
        pCharCounts[p.charAt(i) - 'a'] ++;
    }

    // 2. 遍历s,以每一个字符作为起始,考察长度为p.length()的子串
    for (int i = 0; i <= s.length() - p.length(); i++){
        // 3. 判断当前子串是否为p的字母异位词
        boolean isMatched = true;

        // 定义一个数组,统计子串中所有字符频次
        int[] subStrCharCounts = new int[26];

        for (int j = i; j < i + p.length(); j++){
            subStrCharCounts[s.charAt(j) - 'a'] ++;

            // 判断当前字符频次,如果超过了p中的频次,就一定不符合要求
            if (subStrCharCounts[s.charAt(j) - 'a'] > pCharCounts[s.charAt(j) - 'a']){
                isMatched = false;
                break;
            }
        }
        if (isMatched)
            result.add(i);
    }
    return result;
}

复杂度分析

  • 时间复杂度:O(|s| * |p|),其中|s|表示s的长度,|p|表示p的长度。时间开销主要来自双层循环,循环的迭代次数分别是(s.length-p.length+1)和
    p.length, 所以时间复杂度为O((|s|-|p|+1) * |p|), 去除低阶复杂度,最终的算法复杂度为 O(|s| * |p|)。
  • 空间复杂度:O(1)。需要两个大小为 26的计数数组,分别保存p和当前子串的字母个数。尽管循环迭代过程中在不断申请新的空间,但是上一次申请的数组空间应该可以得到复用,所以实际上一共花费了2个数组的空间,因为数组大小是常数,所以空间复杂度为O(1)。

方法二:滑动窗口(双指针)

暴力法的缺点是显而易见的:时间复杂度较大,运行耗时较长。

我们在暴力求解的时候,其实对于很多字母是做了多次统计的。子串可以看作字符串上开窗截取的结果,自然想到,可以定义左右指针向右移动,实现滑动窗口的作用。在指针移动的过程中,字符只会被遍历一次,时间复杂度就可以大大降低。
代码演示如下:

// 方法二:滑动窗口法,分别移动起始和结束位置
public List<Integer> findAnagrams(String s, String p){
    // 定义一个结果列表
    ArrayList<Integer> result = new ArrayList<>();

    // 1. 统计p中所有字符频次
    int[] pCharCounts = new int[26];

    for (int i = 0; i < p.length(); i++){
        pCharCounts[p.charAt(i) - 'a'] ++;
    }

    // 统计子串中所有字符出现频次的数组
    int[] subStrCharCounts = new int[26];
    // 定义双指针,指向窗口的起始和结束位置
    int start = 0, end = 1;

    // 2. 移动指针,总是截取字符出现频次全部小于等于p中字符频次的子串
    while (end <= s.length()){
        // 当前新增字符
        char newChar = s.charAt(end - 1);
        subStrCharCounts[newChar - 'a'] ++;

        // 3. 判断当前子串是否符合要求
        // 如果新增字符导致子串中频次超出了p中频次,那么移动start,消除新增字符的影响
        while ( subStrCharCounts[newChar - 'a'] > pCharCounts[newChar - 'a'] && start < end){
            char removedChar = s.charAt(start);
            subStrCharCounts[removedChar - 'a'] --;
            start ++;
        }

        // 如果当前子串长度等于p的长度,那么就是一个字母异位词
        if (end - start == p.length())
            result.add(start);

        end ++;
    }
    return result;
}

复杂度分析

  • 时间复杂度:O(|s|)。 窗口的左右指针最多都到达 s 串结尾,s 串每个字符最多被左右指针都经过一次,所以时间复杂度为O(|s|)。
  • 空间复杂度:O(1)。只需要两个大小为 26 的计数数组,大小均是确定的常量,所以空间复杂度为O(1)。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值