1.无重复字符的最长子串(3 - 中/剑指48)
题目描述:给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。
示例 :
输入: s = "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
思路:本题是面试的高频题目,也是hash表的一个具体应用。思路是维持一个队列(窗口),保持队列中的元素满足题目要求(元素不重复)。
具体实现是:使用hashmap记录每个字符的索引位置,便于更新下一个窗口的左边界,遍历字符串。
- 如果窗口中含有这个元素,移动移除左边界元素(左边索引+1);
- 否则加入窗口,更新最长子串长度。
代码实现:
public int lengthOfLongestSubstring(String s) {
int n = s.length();
if (s == null || n == 0) {
return 0;
}
Map<Character, Integer> map = new HashMap<>();
int max = 0;
int left = 0;
for (int i = 0; i < n; ++i) {
char c = s.charAt(i);
if (map.containsKey(c)) {
left = Math.max(left, map.getOrDefault(c, 0) + 1);
}
map.put(c, i);
max = Math.max(max, i - left + 1);
}
return max;
}
2.串联所有单词的子串(30 - 难)
题目描述:给定一个字符串 s 和一些长度相同的单词 words。找出 s 中恰好可以由 words 中所有单词串联形成的子串的起始位置。
注意:子串要与 words 中的单词完全匹配,中间不能有其他字符,但不需要考虑 words 中单词串联的顺序。
示例 :
输入:
s = "barfoothefoobarman",
words = ["foo","bar"]
输出:[0,9]
解释:
从索引 0 和 9 开始的子串分别是 "barfoo" 和 "foobar" 。
输出的顺序不重要, [9,0] 也是有效答案。
思路:由于单词长度固定,我们维护一个长度为所有元素长度总和的队列(窗口)。本题也是考察hash表,键为存储的单词,值存的单词出现的次数。具体实现见代码。注意:
-
可以用indexOf(),判断一个字符或者字符串是否存在一个字符串中,不存在返回负数;否则将这个单词加入哈希表;
-
当单词数组长度超过字符串长度,直接剔除。
-
对遍历的每一个窗口,用一个subMap记录,left和right记录左右边界(更新的话,是先将单词从字符串中截取出来,再在subMap中删除);
-
通过移动右边界right,依次得到一个单词。如果wordMap中没有这个单词,更新左边界,清除这次记录(包括subMap和单词数量记录数count);否则,加入subMap,注意count符合要求统计左边界,超过限制删除左边界。
代码实现:
public static List<Integer> solution2(String s, String[] words) {
List<Integer> res = new ArrayList<>();
Map<String, Integer> wordsMap = new HashMap<>();
if (s.length() == 0 || words.length == 0) return res;
for (String word: words) {
// 主串s中没有这个单词,直接返回空
if (s.indexOf(word) < 0) return res;
wordsMap.put(word, wordsMap.getOrDefault(word, 0) + 1);
}
int oneLen = words[0].length(), wordsLen = oneLen * words.length;
if (wordsLen > s.length()) return res;
for (int i = 0; i < oneLen; ++i) {
int left = i, right = i, count = 0;
// 统计每个符合要求的word
Map<String, Integer> subMap = new HashMap<>();
// 右窗口不能超出主串长度
while (right + oneLen <= s.length()) {
// 得到一个单词
String word = s.substring(right, right + oneLen);
right += oneLen;
// words[]中没有这个单词,那么当前窗口肯定匹配失败,直接右移到这个单词后面
if (!wordsMap.containsKey(word)) {
left = right;
subMap.clear();
count = 0;
} else {
subMap.put(word, subMap.getOrDefault(word, 0) + 1);
++count;
// 如果这个单词出现的次数大于words[]中它对应的次数,又由于每次匹配和words长度相等的子串
// 如 ["foo","bar","foo","the"] "| foobarfoobar| foothe"
// 第二个bar虽然是words[]中的单词,但是次数抄了,那么右移一个单词长度后 "|barfoobarfoo|the"
// bar还是不符合,所以直接从这个不符合的bar之后开始匹配,也就是将这个不符合的bar和它之前的单词(串)全移出去
while (subMap.getOrDefault(word, 0) > wordsMap.getOrDefault(word, 0)) {
// 从当前窗口字符统计map中删除从左窗口开始到数量超限的所有单词(次数减一)
String w = s.substring(left, left + oneLen);
subMap.put(w, subMap.getOrDefault(w, 0) - 1);
// 符合的单词数减一
--count;
// 左窗口位置右移
left += oneLen;
}
// 当前窗口字符串满足要求
if (count == words.length) res.add(left);
}
}
}
return res;
}
3.最小覆盖子串(76 - 难)
题目描述:给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 "" 。要求:时间复杂度O(N)。
注意:如果 s
中存在这样的子串,我们保证它是唯一的答案,两个字符串只含有英文字符。
示例 :
输入:s = "ADOBECODEBANC", t = "ABC"
输出:"BANC"
思路:可以通过滑动窗口解决,注意:最小覆盖子串长度不唯一,但至少与t相同。我们可以定义两个变量:start(最小覆盖子串的起始位置),size(最小覆盖子串的长度)
关键:如何判断窗口中含有t的所有元素?可以使用数组或者哈希表记录字符出现的次数。
-
使用数组记录t中(我们需要寻找的)字符的个数,cnt记录要找字符总数。
-
当我们遍历s字符串时,遇到需要的字符我们就将他出现的次数 - 1。总次数cnt == 0,此时找到一个覆盖子串。
-
上述还不是一次寻找的最小覆盖子串,需要找左边界,如何找呢?还是通过出现的次数,方法:在遍历过程中,s中出现每个的字符在need数组中-1,这样我们在找左边界是为负数,一定要恢复!
-
这时,可以统计长度size,更新起始索引start;
-
最终,移动窗口,如何移动(删除左边界元素)呢?将其加入need数组,更新左边界和cnt个数 + 1,下次遍历出现这个字符就可以删除(也可以理解为替换)。
代码实现:
public String minWindow(String s, String t) {
if (s == null || s.length() == 0 || t == null || t.length() == 0 || s.length() < t.length()) {
return "";
}
// 记录需要字符的个数
int[] need = new int[128];
for (int i = 0; i < t.length(); i++) {
need[t.charAt(i)]++;
}
int left = 0;
int size = Integer.MAX_VALUE, cnt = t.length();
int start = 0;
for (int i = 0; i < s.length(); i++) {
if (need[s.charAt(i)] > 0) cnt--;
need[s.charAt(i)]--;
// 窗口中已经包含所有需要的字符
if (cnt == 0) {
while (left < i && need[s.charAt(left)] < 0) {
need[s.charAt(left)]++;
left++;
}
if (i - left + 1 < size) {
size = i - left + 1;
start = left;
}
// 窗口移动,注意更新need数组和cnt值
need[s.charAt(left)]++;
left++;
cnt++;
}
}
return size == Integer.MAX_VALUE ? "" : s.substring(start, start + size);
}
4.找到字符串中所有字母的异位词(438 - 中)
题目描述:
给定一个字符串 s 和一个非空字符串 p,找到 s 中所有是 p 的字母异位词的子串,返回这些子串的起始索引。字符串只包含小写英文字母,并且字符串 s 和 p 的长度都不超过 20100。
说明:
- 字母异位词指字母相同,但排列不同的字符串。
- 不考虑答案输出的顺序。
示例 :
输入:
s: "cbaebabacd" p: "abc"
输出:
[0, 6]
解释:
起始索引等于 0 的子串是 "cba", 它是 "abc" 的字母异位词。
起始索引等于 6 的子串是 "bac", 它是 "abc" 的字母异位词。
思路:显然滑窗,窗口大小为p字符串的长度,关键:那么如何比较两部分的元素是否是异位词呢?
-
可以直接调库函数
Arrays.equals()
,判断两个字符串是否是异位词(即两个数组是否相同)。 -
本题仍使用数组存储字符出现次数,但是本题两个串申请了两个空间,方便进行上边的对比。
-
这里判断与上题相比简单了许多,只要长度满足我们就进行判断,两部分异构,记录起始索引;否则,移除左边界,这里直接在sMap删除。
代码实现:
public List<Integer> findAnagrams(String s, String p) {
List<Integer> ans = new ArrayList<>();
if (s.length() < p.length()) return ans;
int[] pMap = new int[26];
for (int i = 0; i < p.length(); i++) {
pMap[p.charAt(i) - 'a']++;
}
int[] sMap = new int[26];
int left = 0;
for (int i = 0; i < s.length(); i++) {
sMap[s.charAt(i) - 'a']++;
if (i - left + 1 == p.length()) {
if (Arrays.equals(sMap, pMap)) {
ans.add(left);
}
sMap[s.charAt(left) - 'a']--;
left++;
}
}
return ans;
}
5.至多包含两个不同字符的最长子串(159 - 中)
题目描述:给定一个字符串 s ,找出 至多 包含两个不同字符的最长子串 t ,并返回该子串的长度。
示例 :
示例 1:
输入: "eceba"
输出: 3
解释: t 是 "ece",长度为3。
示例 2:
输入: "ccaabbb"
输出: 5
解释: t 是 "aabbb",长度为5。
思路:维护一个窗口,元素类型至多两种。本题关键:如何判断窗口中的元素类型大于2了呢?
这里使用Hashmap记录当前字符在窗口中最靠右的索引值,为什么这么做呢?这样我们可以控制窗口的移动,right每右移一步:
-
若已经存在,更新map中当前元素最靠右的位置
-
若不存在,添加到map唯一存在,索引也是最靠右的
这样就好办了,如果我们元素类型超了2,即map的大小等于3。
-
记录最小索引值,用于下边删除和更新左边界。
-
删除操作是删除索引最小的那些或那个元素,因为记录的是右边界,这样我们也不用担心左边界问题。
代码实现:
public int lengthOfLongestSubstringTwoDistinct(String s) {
int strLength = s.length();
if (strLength < 3) return strLength;
int left = 0, right = 0;
HashMap<Character, Integer> map = new HashMap<>();
int maxLen = 2;
while (right < strLength) {
map.put(s.charAt(right), right);
right++;
if (map.size() == 3) {
int minIdx = Collections.min(map.values());
map.remove(s.charAt(minIdx));
left = minIdx + 1;
}
maxLen = Math.max(maxLen, right - left);
}
return maxLen;
}
-
ps:map.values()显示map中所有的值,返回值类型Collection<Integer>,用min()获取最小值。
-
map.remove(key):删除map中键为key的元素
6.至多包含k个不同字符的最长子串(340 - 难)
题目描述:给定一个字符串 s ,找出 至多 包含k个不同字符的最长子串 t ,并返回该子串的长度。
示例 :
示例 1:
输入: "eceba" 2
输出: 3
解释: t 是 "ece",长度为3。
示例 2:
输入: "aa", 1
输出: 2
解释: t 是 "aa",长度为2。
思路:维护一个窗口,元素类型至多k种。具体实现:类比上一题,解决方案还是使用hash表记录字符出现的最右索引。
代码实现:
public int lengthOfLongestSubstringKDistinct(String s, int k) {
int strLength = s.length();
if (strLength < k + 1) return strLength;
if (strLength * k == 0) return 0;
int left = 0, right = 0;
HashMap<Character, Integer> map = new HashMap<>();
int maxLen = k;
while (right < strLength) {
map.put(s.charAt(right), right);
right++;
if (map.size() == k + 1) {
int minIdx = Collections.min(map.values());
map.remove(s.charAt(minIdx));
left = minIdx + 1;
}
maxLen = Math.max(maxLen, right - left);
}
return maxLen;
}
7.替换后最长重复字符(424 - 中)
题目描述:给你一个仅由大写英文字母组成的字符串,你可以将任意位置上的字符替换成另外的字符,总共可最多替换 k 次。
在执行上述操作后,找到包含重复字母的最长子串的长度。
示例 :
输入:s = "ABAB", k = 2
输出:4
解释:用两个'A'替换为两个'B',反之亦然。
思路:可以想一下k = 0
的情况(不替换字符),即求字符串的最长重复元素。关键点:如何判断一个字符串改变了k个字符就变成了一条连续的子串?
只要保证【当前子串的长度 <= 当前子串出现字符最多的个数 + k】,这样一定可以将当前子串替换为一条连续的子串(元素都相同)。
-
当满足上述条件,右边界扩张,并更新之前出现重复字母的最多个数(这个个数用一个数组记录);
-
一旦不满足上述条件,我们移除左边界(直接在数组中将左边界对应的次数 - 1);
注意:我们窗口维护了可能的最长子串,所以返回值为窗口大小,即s.length() - left
。
代码实现:
public int characterReplacement(String s, int k) {
int n = s.length();
if (s == null || n < 2 || n < k) return n;
int[] map = new int[26];
int left = 0;
int ans = 0, preMax = 0;
for (int i = 0; i < n; i++) {
int index = s.charAt(i) - 'A';
map[index]++;
preMax = Math.max(preMax, map[index]);
if (i - left + 1 > preMax + k) {
map[s.charAt(left) - 'A']--;
left++;
}
}
return n - left;
}
网友评论