找到字符串中所有字母异位词、串联所有单词的子串

目录

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

题目链接

题目描述

思路分析

代码实现

优化

串联所有单词的子串

题目链接

题目描述

思路分析

代码实现


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

题目链接

438. 找到字符串中所有字母异位词 - 力扣(LeetCode)

题目描述

给定两个字符串 s 和 p,找到 s 中所有 p 的 异位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。

示例 1:

输入: s = "cbaebabacd", p = "abc"
输出: [0,6]
解释:
起始索引等于 0 的子串是 "cba", 它是 "abc" 的异位词。
起始索引等于 6 的子串是 "bac", 它是 "abc" 的异位词。

 示例 2:

输入: s = "abab", p = "ab"
输出: [0,1,2]
解释:
起始索引等于 0 的子串是 "ab", 它是 "ab" 的异位词。
起始索引等于 1 的子串是 "ba", 它是 "ab" 的异位词。
起始索引等于 2 的子串是 "ab", 它是 "ab" 的异位词。

提示:

  • 1 <= s.length, p.length <= 3 * 104
  • s 和 p 仅包含小写字母

思路分析

题目要求我们在 s 字符串中找到 p 的异位词子串,并返回这些子串的起始索引

那么,什么是 异位词?

通过示例 1 中的例子:

起始索引等于 0 的子串是 "cba", 它是 "abc" 的异位词

可以看出 异位词 包含字符串 p 中的所有字母,字母位置不固定

如何判断一个子串是否是异位词呢?

异位词 需要包含 p 中所有的字母,即,我们只需要保证子串中的字母种类和数量与 p 中相同(不需要确定字母位置),因此,可以使用 哈希表

在哈希表 hash1 中存储 p 中的字母种类和数量

在哈希表 hash2 中存储 子串 的字母种类和数量

判断 hash2 中的字母种类和数量是否与 hash1 中的相同,若相同,则该子串为 p 的异位词 

由于s 和 p 中仅包含小写字母,因此,我们可以直接使用数组 int[26] 作为哈希表来进行存储

下标表示字母种类(a 对应 0,b 对应 1,c 对应 2...)

元素表示字母个数

在分析了如何判断一个子串是否是异位词之后,那么,该如何找到这些子串呢?

首先,字符串 p 的异位词长度一定与字符串 p 的长度相同,因此,要保证 s 中的子串长度与 p 的长度相同:

再判断是否该子串是否是异位词,接着向后移动,继续判断

因此,我们可以使用指针 left 和 right 维护一个长度为 p.length()滑动窗口,当窗口中每种字母的数量和字符串 p 中每种字符的数量相同时,则说明当前窗口为 p 的异位词

那么,如何维护这个滑动窗口呢? 

 首先定义 left 和 right 指向 s 的 0 下标位置

接着,让 right 下标元素进窗口(在 hash2 中存储 s.charAt(right)),并向右移动right(right++)

 然后判断当前窗口中的元素个数是否大于 p.length()

        若小于,什么都不需要做

        若等于,此时需要判断当前子串是否为异位词

        若大于,将左侧元素弹出窗口

由于滑动窗口是定长的,因此,只需要弹出一个元素就可以保证窗口中的元素等于 p.length(),此时,同样需要判断当前子串是否为异位词

重复上述 入窗口 和 判断操作,直到 right > s.length()

上述过程为:

(1)使用数组模拟的哈希表 hash1 和 hash2,hash1 中存储字符串 p 的字母种类和个数, hash2 存储 滑动窗口 中的字母种类和个数

(2)遍历 p,将 p 中的字母种类和个数存储在 hash1 中

(3)定义 left 和 right,指向 s 的 0 下标

(4)让 right 位置元素进窗口,并向后移动right

(5)判断 right - left + 1 是否大于 p.length(),若大于,则弹出左侧元素(弹出一次即可

(6)若等于,判断滑动窗口中的字母种类和个数(hash2)是否与 p 的字母种类和个数(hash1)相同,即 判断 hash1 是否与 hash2 相同

(7)重复(4)(5)直到 right > s.length()

理清了思路之后,我们就来尝试编写代码 

代码实现

class Solution {
    public List<Integer> findAnagrams(String s, String p) {
        List<Integer> ret = new ArrayList<>();
        int[] hash1 = new int[26];
        int[] hash2 = new int[26];
        for(int i = 0; i < p.length(); i++) {
            hash1[p.charAt(i) - 'a']++;
        }
        for(int left = 0, right = 0; right < s.length(); right++) {
            // 入窗口
            hash2[s.charAt(right) - 'a']++;
            // 判断
            if(right - left + 1 > p.length()) {
                // 出窗口
                hash2[s.charAt(left) - 'a']--;
                left++;
            }
            if(right - left + 1 == p.length()) {
                // 判断是否是异位词
                boolean flg = true;
                for(int i = 0; i < 26; i++) {
                    if(hash1[i] != hash2[i]) {
                        flg = false;
                        break;
                    }
                }
                if(flg) {
                    ret.add(left);
                }
            }
        }
        return ret;
    }
}

优化

每次判断子串是否为异位词时,需要进行遍历判断,比较耗时,能否进行优化呢?

 可以使用变量 count 来统计窗口中 有效字符 的个数,从而进行优化

什么是 有效字符?

有效字符指的是组成 p 的异位词所需要的字符,也就是 p 中的字符

我们通过一个例子来看:

将 c 加入窗口,c 是组成 p 异位词需要的字符,即为 有效字符,因此,count++

接着,向右移动right,继续添加新字母

  

将 新的 c 加入窗口,此时,虽然 c 为组成 p 异位词需要的字符,但是 p 中只需要一个 c ,窗口中有两个 c,即,新加入的 c 对于组成 p 没有作用,为无效字符,count 不进行统计

即:

加入字符 ch 后,hash2 中 ch 的个数 小于等于 hash1 中 ch 的个数,当前字符能够用于组成 p 的异位词,为有效字符,count++;

而若加入字符 ch 后,hash2 中 ch 的个数 大于 hash1 中 ch 的个数,当前字符不能用于组成 p 的异位词,为无效字符,count 不变

再向右移动right,继续添加新字母

将 b 加入到窗口中,b 为组成 p 异位词需要的字符,且窗口中b的个数为1,是 有效字符,count++

此时窗口大小为3,等于p的长度,需要判断子串是否为异位词

在添加了 count 之后,就不需要再进行遍历判断了,而是直接判断 count 是否等于 p 的长度,就可以判断出该子串是否为异位词

count = 2 < 3,因此,该子串不是异位词

为什么可以通过接判断 count 是否等于 p 的长度来判断异位词?

count 统计的是组成异位词的 有效字符个数, 当 count = p 时,说明 count 中包含组成异位词的所有字符,可以组成 p 的异位词

那么,是否会出现子串包含其他字母的情况?

我们是在 窗口小于等于 p.length() 的基础上进行判断的(若窗口长度大于 p.length(),则将左边元素弹出窗口),若子串中包含了其他字符,count 一定小于 p.length(),该子串一定不为异位词

继续向右移动 right:

将 a 加入窗口中,a 为组成 p 异位词需要的字符,为有效字符,count++

此时窗口长度大于 p 的长度,将左边元素弹出窗口:

 弹出字符 c 之前,c 的个数为2,而组成 p 的异位词只需要一个 c,因此,有一个 c 是无效字符,当弹出 c 之后,留下的 c 是组成 p 的有效字符,而弹出的 c 是无效字符,因此 count 的值不变

即:

在弹出字符 ch 之前,若 hash2 中 ch 的个数 大于 hash1 中 ch 的个数,则弹出的的字符 ch 不能够用于组成 p 的异位词,为无效字符,弹出后,count 的值不变

在弹出字符 ch 之前,若 hash2 中 ch 的个数 小于等于 hash1 中 ch 的个数,则弹出的的字符 ch 用于组成 p 的异位词,为有效字符,弹出后,count--

此时,count = p.length,此时的子串是异位词

因此,通过变量 count 来统计窗口中 有效字符 的长度,

在将元素加入窗口之后,判断加入的元素 ch 是否是有效字符,若为有效字符,count++

在将元素弹出窗口之前,判断弹出的元素 ch 是否为有效字符,若为有效字符,count--

当需要判断子串是否是异位词时,直接判断 count 是否等于 p.length(),若等于,则该子串为 p 的异位词

代码优化

class Solution {
    public List<Integer> findAnagrams(String s, String p) {
        List<Integer> ret = new ArrayList<>();
        int[] hash1 = new int[26];
        int[] hash2 = new int[26];
        for(int i = 0; i < p.length(); i++) {
            hash1[p.charAt(i) - 'a']++;
        }
        for(int left = 0, right = 0, count = 0; right < s.length(); right++) {
            // 入窗口
            int in = s.charAt(right) - 'a';
            hash2[in]++;
            // 更新 count
            if(hash2[in] <= hash1[in]) {
                count++;
            }
            // 判断
            if(right - left + 1 > p.length()) {
                // 更新 count
                int out = s.charAt(left) - 'a';
                if(hash2[out] <= hash1[out]) {
                    count--;
                }
                // 出窗口
                hash2[out]--;
                left++;
            }
            if(count == p.length()) {
                ret.add(left);
            }
        }
        return ret;
    }
}

串联所有单词的子串

题目链接

30. 串联所有单词的子串 - 力扣(LeetCode)

题目描述

给定一个字符串 s 和一个字符串数组 words words 中所有字符串 长度相同

 s 中的 串联子串 是指一个包含  words 中所有字符串以任意顺序排列连接起来的子串。

  • 例如,如果 words = ["ab","cd","ef"], 那么 "abcdef", "abefcd""cdabef", "cdefab""efabcd", 和 "efcdab" 都是串联子串。 "acdbef" 不是串联子串,因为他不是任何 words 排列的连接。

返回所有串联子串在 s 中的开始索引。你可以以 任意顺序 返回答案。

示例 1:

输入:s = "barfoothefoobarman", words = ["foo","bar"]
输出:[0,9]
解释:因为 words.length == 2 同时 words[i].length == 3,连接的子字符串的长度必须为 6。
子串 "barfoo" 开始位置是 0。它是 words 中以 ["bar","foo"] 顺序排列的连接。
子串 "foobar" 开始位置是 9。它是 words 中以 ["foo","bar"] 顺序排列的连接。
输出顺序无关紧要。返回 [9,0] 也是可以的。

示例 2:

输入:s = "wordgoodgoodgoodbestword", words = ["word","good","best","word"]
输出:[]
解释:因为 words.length == 4 并且 words[i].length == 4,所以串联子串的长度必须为 16。
s 中没有子串长度为 16 并且等于 words 的任何顺序排列的连接。
所以我们返回一个空数组。

示例 3:

输入:s = "barfoofoobarthefoobarman", words = ["bar","foo","the"]
输出:[6,9,12]
解释:因为 words.length == 3 并且 words[i].length == 3,所以串联子串的长度必须为 9。
子串 "foobarthe" 开始位置是 6。它是 words 中以 ["foo","bar","the"] 顺序排列的连接。
子串 "barthefoo" 开始位置是 9。它是 words 中以 ["bar","the","foo"] 顺序排列的连接。
子串 "thefoobar" 开始位置是 12。它是 words 中以 ["the","foo","bar"] 顺序排列的连接。

提示:

  • 1 <= s.length <= 104
  • 1 <= words.length <= 5000
  • 1 <= words[i].length <= 30
  • words[i] 和 s 由小写英文字母组成

思路分析

题目要求我们找到 s 中串联数组 words 中所有字符串的 子串,并返回子串的开始索引

在串联时,words 中的字符串顺序是任意的

且,words 中字符串的长度是相同的

将 s 按照 words 中字符串长度进行划分

我们将 words 中的一个字符串看做一个整体

此时,就将其转换成上述 找到字符串中所有字母异位词 一样的问题了

同样是使用 哈希表 + 滑动窗口 进行查找和判断,再使用变量 count 进行优化

但是,此时哈希表中需要存储的 key 为字符串,因此,不能再使用数组,而是需要使用 HashMap 进行存储

此外,在 找到字符串中所有字母异位词 中 left 和 right 每次移动一步,让新的字符加入或弹出窗口,但在本题中,每次需要加入或弹出一个字符串,因此,left 和 right 每次需要移动 len(words[0].length())步

上述我们从 0 位置开始划分字符串 s,那么,也可以从 1 下标、2下标开始划分:

但若从 3 下标开始划分:

 

和 从 0 下标开始划分的情况是一样的,

即,s 有 len 种划分方式,每一种划分都需要进行查找

在分析完思路之后,我们就可以尝试编写代码了 

代码实现

class Solution {
    public List<Integer> findSubstring(String s, String[] words) {
        List<Integer> ret = new ArrayList<>();
        Map<String, Integer> hash1 = new HashMap<>();
        int len = words[0].length();
        // 统计 words 中的字符串种类和数量
        for(String str: words) {
            hash1.put(str, hash1.getOrDefault(str, 0) + 1);
        }
        for(int i = 0; i < len; i++) {
            Map<String, Integer> hash2 = new HashMap<>();
            for(int left = i, right = i, count = 0; right + len <= s.length(); right += len) {
                // 入窗口
                String in = s.substring(right, right + len);
                hash2.put(in, hash2.getOrDefault(in, 0) + 1);
                // 更新 count
                if(hash2.get(in) <= hash1.getOrDefault(in, 0)) {
                    count++;
                }
                // 判断
                if(right - left + 1 > words.length * len) {
                    // 更新 count
                    String out = s.substring(left, left + len);
                    if(hash2.get(out) <= hash1.getOrDefault(out, 0)) {
                        count--;
                    }
                    // 出窗口
                    hash2.put(out, hash2.get(out) - 1);
                    left += len;
                }
                // 更新结果
                if(count == words.length) {
                    ret.add(left);
                }
            }
        }
        return ret;
    }
}
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

楠枬

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值