找到字符串中所有字母异位词、串联所有单词的子串
目录
找到字符串中所有字母异位词
题目链接
题目描述
思路分析
代码实现
优化
串联所有单词的子串
题目链接
题目描述
思路分析
代码实现
找到字符串中所有字母异位词
题目链接
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]++;// 更新 countif(hash2[in] <= hash1[in]) {count++;}// 判断if(right - left + 1 > p.length()) {// 更新 countint 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);// 更新 countif(hash2.get(in) <= hash1.getOrDefault(in, 0)) {count++;}// 判断if(right - left + 1 > words.length * len) {// 更新 countString 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;}
}