leetcode 经典题分类
- 链表
- 数组
- 字符串
- 哈希表
- 二分法
- 双指针
- 滑动窗口
- 递归/回溯
- 动态规划
- 二叉树
- 辅助栈
本系列专栏:点击进入 leetcode题目分类 关注走一波
前言:本系列文章初衷是为了按类别整理
出力扣(leetcode)最经典题目,一共100多道题,每道题都给出了非常详细的解题思路、算法步骤、代码实现。很多同学刚开始刷题都是按照力扣顺序刷题,其实这样对新手不太适用,刷题效果也很不好。因为力扣题目顺序是随机的,并没有按照算法分类,导致同一类型的算法强化训练不够,最后刷完也是迷迷糊糊的。所以本系列文章就是来帮你完成算法分类,针对每种算法做强化训练,保证让你以后遇到题目直接秒杀!
无重复字符的最长子串
【题目描述】
给定一个字符串 s ,请你找出其中不含有重复字符的最长子串的长度。(答案必须是子串的长度,不是子序列)
- 子串(Substring):指的是一个字符串中连续的一段子字符串。例如,在字符串 “abcd” 中,“bc” 和 “abcd” 都是它的子串。
- 子序列(Subsequence):指的是从字符串中按照原有顺序但不一定是连续的选取字符组成的新字符串。换句话说,子序列是通过原始字符串中选择某些字符而不改变它们的相对顺序,在不一定连续的情况下形成的新字符串。在字符串 “abcd” 中,“ac” 和 “bd” 都是它的子序列,但不是子串。
【输入输出实例】
示例 1:
输入: s = “abcabcbb”
输出: 3 (无重复字符的最长子串是 “abc”,所以其长度为 3)
示例 2:
输入: s = “bbbbb”
输出: 1
示例 3:
输入: s = “pwwkew”
输出: 3
【算法思路】
利用滑动窗口的思想,设置滑动窗口的左指针和右指针,通过对给定字符串s的遍历,利用右指针不断扩大滑动窗口的右边界,且要保证窗口内不出现重复字符。如果出现重复字符,就要缩小左边界,缩小到左右边界内的窗口不出现重复字符。
每次移动都需要重新计算窗口的大小,判断当前窗口长度是否大于长度最大值,若大于长度最大值,则更新长度最大值。
【算法描述】
int lengthOfLongestSubstring(string s)
{
int MaxStr = 0; //记录最长子串的长度
int right = 0; //右指针
int left = 0; //左指针
for(int i = 0; i < s.size(); i++) //遍历s
{
for(int j = left; j < right; j++) //遍历滑动窗口
{
if(s[j] == s[i]) //遇到重复字符时,滑动窗口左指针移到重复元素的下一位
{
left = j + 1;
break;
}
}
right++; //滑动窗口右指针每次都要右移
MaxStr = max(MaxStr, right - left); //找最长子串长度
}
return MaxStr;
}
串联所有单词的子串
【题目描述】
给定一个字符串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]
解释:子串"barfoo"开始位置是0。它是words中以 [“bar”,“foo”] 顺序排列的连接。子串"foobar"开始位置是9。它是words中以 [“foo”,“bar”] 顺序排列的连接。
示例 2:
输入:s = “wordgoodgoodgoodbestword”, words = [“word”,“good”,“best”,“word”]
输出:[]
示例 3:
输入:s = “barfoofoobarthefoobarman”, words = [“bar”,“foo”,“the”]
输出:[6,9,12]
【算法思路】
因为单词长度是固定的,我们可以计算出截取字符串的单词个数是否和words里相等,所以我们可以借用哈希表。
一个哈希表是word,用来存放words中的单词及出现的次数;一个哈希表hash是截取的字符串,即从 s 中不断划分单词,存放匹配成功的单词及出现的个数,再比较两个哈希是否相等。
先考虑如何能找到 s 划分的所有指定长度 n 的单词?从下标 i 开始把 s 划分为字母为 n 的单词,i 的取值为 0 ~ n-1,则只需要循环 n次就可以划分出所有长度为 n 的单词,因为从 i = n+1 开始划分与 i = 0 开始划分的单词是一样的。
从下标 i 开始划分 s,通过移动右指针right以间隔 n 来遍历 s。若此单词不在word中,表示匹配不成功,则要清除之前hash中记录的单词,并且移动窗口左指针到下一个单词继续匹配。若此单词在word中,表示匹配成功,则将该单词加入到hash中,并检查该单词是否匹配多次,即检查该单词在hash中出现的次数是否比word中高,若是则需要缩小窗口,也就是left右移,将移出窗口的单词在hash中–,直到hash中出现的次数小于word中次数。
最后只要成功匹配的单词数达到 m 时,则表示找到了一个串联子串,其left为该字串的起始下标,放入result即可。
【算法描述】
//滑动窗口
vector<int> findSubstring(string s, vector<string>& words)
{
vector<int> result; //存放结果
int m = words.size(); //单词数
int n = words[0].size(); //每个单词的字母数
int len = s.size();
unordered_map<string, int> word; //存放words中的单词及出现的个数
for(string str : words)
{
word[str]++;
}
//从下标i开始把s划分为字母为n的单词
//只需要循环n次就可以划分出所有长度为n的单词,因为从i = n+1开始划分与i = 0开始划分的单词是一样的
for(int i = 0; i < n; i++)
{
int left = i; //滑窗左指针
int right = i; //滑窗右指针
int count = 0; //记录已成功匹配的单词数
unordered_map<string, int> hash; //存放匹配成功的单词及出现的个数
while(right + n <= len)
{
string str(s.begin() + right, s.begin() + right + n); //左闭右开:划分单词
right += n; //窗口右边界右移一个单词的长度
if(word.find(str) == word.end()) //此单词不在words中,表示匹配不成功
{
count = 0; //重置,清除之前记录的单词
hash.clear();
left = right; //移动窗口左指针
}
else //此单词在words中,表示匹配成功
{
hash[str]++; //将单词加到hash中
count++;
while(hash[str] > word[str]) //一个单词匹配多次,需要缩小窗口,也就是left右移
{
string temp(s.begin() + left, s.begin() + left + n);
hash[temp]--;
count--;
left += n;
}
}
if(count == m) //成功匹配的单词数达到m时,表示找到了一个串联子串
{
result.push_back(left);
}
}
}
return result;
}
最小覆盖子串
【题目描述】
给你一个字符串 s
、一个字符串 t
。返回 s
中涵盖 t
所有字符的最小子串。如果 s
中不存在涵盖 t
所有字符的子串,则返回空字符串 ""
。
注意:
- 对于
t
中重复字符,我们寻找的子字符串中该字符数量必须不少于t
中该字符数量。 - 如果
s
中存在这样的子串,我们保证它是唯一的答案。
【输入输出实例】
示例 1:
输入:s = “ADOBECODEBANC”, t = “ABC”
输出:“BANC”
解释:最小覆盖子串 “BANC” 包含来自字符串 t 的 ‘A’、‘B’ 和 ‘C’。
示例 2:
输入:s = “a”, t = “a”
输出:“a”
解释:整个字符串 s 是最小覆盖子串。
示例 3:
输入: s = “a”, t = “aa”
输出: “”
解释: t 中两个字符 ‘a’ 均应包含在 s 的子串中,因此没有符合条件的子字符串,返回空字符串。
【算法思路】
我们在 s
上滑动窗口,通过移动 right
指针不断扩张窗口。当窗口包含 t
全部所需的字符后,就开始右移左指针 left
收缩窗口,如果能收缩,我们就收缩窗口直到得到最小窗口。
如何确定窗口内已经包含 t
全部字符?通过两个哈希表smap
、tmap
来存放s
、t
中字符出现次数,tmap
在初始就记录好,smap
在更新中逐渐记录,如果tmap
中记录字符都小于smap
中,说明s
窗口已经覆盖t
字符。
在扩大窗口时,右窗口新移入的字符,如果在t
中,才记录到smap
,每次扩大窗口都要检查当前窗口是否已经覆盖,如果覆盖就开始收缩左指针,缩小窗口,同时更新最小字串。
【算法描述】
string minWindow(string s, string t) {
// 记录t字符串的各字符次数
unordered_map<char, int> smap, tmap;
for(const auto& i : t) {
tmap[i]++;
}
int minNum = INT_MAX; // 记录子串最小长度
string result = ""; // 记录最小字串
int left = 0, right = 0; // 左右指针
while(right < s.size()) {
// 右窗口新加入字符是否为t中字符,若是则加入smap
if(tmap.find(s[right]) != tmap.end()) {
++smap[s[right]];
}
// 当前s窗口内子串已经覆盖了t,右移左指针,记录最小字串
while(check(tmap, smap)) {
if(minNum > right-left+1) {
minNum = right - left + 1;
result = s.substr(left, minNum);
}
if(tmap.find(s[left]) != tmap.end()) {
--smap[s[left]];
}
++left;
}
++right;
}
return result;
}
// 检查count中是否覆盖了origin中字符
bool check(unordered_map<char, int>& origin, unordered_map<char, int>& count) {
for(const auto& i : origin) {
if(count[i.first] < i.second) {
return false;
}
}
return true;
}
【知识点】
学会滑动窗口的思想:
滑动窗口是双指针的一种特例,可以称为左右指针,在任意时刻,只有一个指针运动,而另一个保持静止。滑动窗口路一般用于解决特定的序列中符合条件的连续的子序列的问题。
一般来说,我们面对的最多的两个序列就是数组与字符串。那么,滑动窗口解决的就是子序列与子数组的问题。
字符串类的滑动窗口问题
这类问题一般可以分为两类,单个字符串和两个字符串。两个字符串当中又可以按照连续序列与非连续序列进行划分。
对于两个字符串而言,一般是一个字符串作为母体,另一个字符串作为比较。
恭喜你全部读完啦!古人云:温故而知新。赶紧收藏关注起来,用之前再翻一翻吧~
📣推荐阅读
C/C++后端开发面试总结:点击进入 后端开发面经 关注走一波
C++重点知识:点击进入 C++重点知识 关注走一波
力扣(leetcode)题目分类:点击进入 leetcode题目分类 关注走一波