1.LeetCode 20.有效的括号
题意:给定一个字符串s,内容只包括 { } ( ) [ ] 三种括号。要根据题意所定义的"有效的字符串",判定给定字符串是否为有效的。那么核心就是对有效字符串定义的解读。
那么,根据题意,左括号与同类型右括号闭合、左括号必须以正确的顺序闭合、每个右括号都有一个对应的相同类型的左括号。
此题用栈的方法去解决
那么,对于我这种菜鸡,第一眼看到这题是不会想到用栈的方式去解决,可能乱七八糟的想到用啥双指针呀什么从头尾遍历看看头尾元素是否相同这种无用功法(也可能能实现,但我菜不会)。
那为什么大佬能一眼(顶针)的(鉴定为)看出来是用栈的方式解决呢?
第一可能就是大佬有丰富的经验,第二可能就(有丰富的经验bushi)根据题意的条件去推导出来
那我们要学习的就是如何利用题意去推导使用栈的方法
题目简单来说也就是消消乐的游戏(左右括号消消乐),那么我们就应该想如何利用一个数据结构or算法来实现这个消消乐。蠢一点就一个个数据结构or算法去套试试(或者直接看看标签),或者再思考一下,拿一个右括号和左括号试试相不相匹配,也可以转化为右括号x转为左括号x1,看看转换后的右括号x1是否和要配对的左括号相同。那么综上所述我们就两个操作 ①存放转换后的括号②将转换后的元素拿出和当前遍历的元素匹配。
只实现上述两种操作,那一个栈是绰绰有余了,所以可以考虑用栈去实现该题。
那么搞定了为什么用栈的问题,就要思考用栈如何实现了。
因为字符串s括号顺序都是左括号到右括号,所以我们可以遇到左括号就转换为右括号存放到栈中,遇到右括号就取出当前栈顶元素对比一下是否相同,相同就继续遍历,不同就return false。如果退出遍历的循环同时栈为空,则说明已经将字符串中的元素都配对完了,则为有效字符,反之则为无效字符
下面是实现代码 C++版本
class Solution {
public:
bool isValid(string s) {
//s长度为奇数,则不能将字符平分两部分匹配,直接return false
if(s.size() % 2 == 1) {
return false;
}
int n = s.size();
stack<int> stk;
for(int i=0; i<n; i++) {
// 为左括号就存放右括号进栈中,当遍历到右括号时则直接和栈顶元素直接对比
if(s[i] == '(') stk.push(')');
else if(s[i] == '{') stk.push('}');
else if(s[i] == '[') stk.push(']');
else if(stk.empty() || stk.top() != s[i]) {
return false;
}
else {
stk.pop();
}
}
// 判断是否全部匹配完了的条件是栈是否为空
return stk.empty();
}
};
- 时间复杂度:O(N)。
- 空间复杂度:O(N)。
本题也可以使用哈希表的方法去辅助,原理相同
class Solution {
public:
bool isValid(string s) {
//s长度为奇数,则不能俩俩配对,直接return false
if(s.size() % 2 == 1) {
return false;
}
int n = s.size();
//
unordered_map<char, char> pairs = {
{']', '['},
{'}', '{'},
{')', '('}
};
stack<char> stk;
for(char ch : s) {
// 如果ch为hashtable中的值,即为右边,则进去对比
if(pairs.count(ch)) {
//如果栈中元素为空,
//或者栈顶元素(例如:'{' )不等于ch('}')所对应的哈希值('{'),则直接return false
if(stk.empty() || stk.top() != pairs[ch]) {
return false;
}
//将栈顶元素出栈,进行下一个栈中元素匹配
stk.pop();
}
// 将s中的左边都放进stk中
else {
stk.push(ch);
}
}
//当栈中元素为空时,则说明配对完成,否则return false,匹配不成功
return stk.empty();
}
};
2.LeetCode 84.柱状图中最大的矩形
题意:给n个长度为height[i]、宽度为1的柱子,问相邻的柱子(一个或多个)所形成的最大矩形的面积是多大。
此题用单调栈的方法解决。
那么我们同样来思考一下为什么要使用单调栈。
首先要了解一下单调栈是什么,应用于什么场景,原理是什么。
定义:单调栈就是一种栈中元素有序的栈,可以分为单调递减栈和单调递增栈(从栈底到栈顶元素的大小顺序)。
使用场景:在数组中寻找下一个更大(小)的元素(在一维数组中找第一个满足某种条件的数)。
原理:空间换时间,用额外的空间实现降维,时间复杂度降低。
了解了单调栈,回归思考为什么使用单调栈的问题
先模拟问题,尝试使用暴力解法去解决。
代码实现:
// 用宽进行模拟
class Solution {
public:
int largestRectangleArea(vector<int>& heights) {
int n = heights.size();
int ans = 0;
// 枚举左边界
for (int left = 0; left < n; ++left) {
int minHeight = INT_MAX;
// 枚举右边界
for (int right = left; right < n; ++right) {
// 确定高度
minHeight = min(minHeight, heights[right]);
// 计算面积
ans = max(ans, (right - left + 1) * minHeight);
}
}
return ans;
}
};
图解思路,延伸思路:
因为暴力解法的时间复杂度较高,可能导致超时,所以不适用暴力解法,但通过对暴力解法的解读,我们就应该知道了要实现两个操作
①使用一个结构存放单调递增的柱子的下标
②能取出柱子下标进行遍历
所以只要单向的一进一出,并且单调递增的结构,单调栈完全符合标准。
那么下面就是如何使用单调栈实现的思路:
使用单调栈,第一个要注意的点就是如何维护单调性,在什么条件下维护单调性
在该题中,即为如果当前遍历的元素要大于栈顶元素,则让该元素入栈,如果当前遍历元素要比栈顶元素小,则将栈中比它大的元素都出栈,以维护单调性。
那么如何在维护单调性的条件下求得要求得的max呢?
我们注意到当维护单调性时,当前遍历的元素比栈顶元素小时,会有将栈中比当前遍历元素大的元素弹出的操作,所以我们可以在该处去求得要求的max,即为在出栈操作时去求得max。
那么如何在出栈操作时求得要求的max呢?
就可以使用在分析暴力解法时的左右界的思路,将当前栈顶元素的下标为左界,以栈顶下面的元素的下标为右界,用每一次出栈时的高度乘以左界减右界的宽度,就可以得到当前遍历的柱形的矩形面积。
代码实现 C++
class Solution {
public:
int largestRectangleArea(vector<int>& heights) {
// 创建一个栈,用于实现单调栈
stack<int> stk;
// 小细节,在height数组中前后插入一个0,防止弹栈的时候,栈为空;
// 遍历完成以后,栈中还有元素;
heights.insert(heights.begin(), 0);
heights.push_back(0);
stk.push(0);
int result = 0;
for(int i = 1; i < heights.size(); i++) {
// 如果当前遍历的元素要比栈顶元素小,
//则将栈中元素弹出直至栈顶元素要小于当前遍历的元素
// 在弹出的过程总中,计算矩形的最大面积,stk.top()和i的差值为底边,
// 随栈顶元素弹出,底边不断增大,计算max面积
// 在该过程中,i始终为常量,cur为变量,所以能计算出变化的面积
while(heights[i] < heights[stk.top()]) {
int cur = stk.top();
stk.pop();
int left = stk.top();
int right = i;
int w = right - left - 1;
int h = heights[cur];
// 计算出的面积存放在result中,实时更新max
result = max(result, w*h);
}
stk.push(i);
}
return result;
}
};
-
时间复杂度:O(N)。
-
空间复杂度:O(N)。
3.LeetCode 85.最大矩形
题意:有一个只有0和1组成的二维数组,求它由1组成的最大矩形面积
本题其实和上一题的思路类似,只不过要在对二维数组进行处理后才会体现出来。
那么先对二维数组的信息进行处理
所以相对上一题要多一个将二维数组matrix转化为height数组的操作
实现该操作的代码
// 创建一个一维数组存放二维数组matrix中的信息
vector<int> line(matrix[0].size() + 2, 0);
for(int i=0; i<matrix.size(); i++) {
for(int j=0; j<matrix[0].size(); j++) {
// 此处的line数组从下标为1的地方开始存放,
// 避免了向line前后塞0的情况(在上一题有说明为什么要前后插0)
// 用line存放每列的高度,matrix[i][j]为'1'时,就用之前的高度再加一
line[j+1] = (matrix[i][j] == '0') ? 0 : line[j+1] + 1;
}
}
那求该题的解就在该操作的基础上,重复上一题的操作,用单调栈的解法
class Solution {
public:
int maximalRectangle(vector<vector<char>>& matrix) {
if(matrix.empty()) return 0;
int ans = 0;
// 创建一个一维数组存放二维数组matrix中的信息
vector<int> line(matrix[0].size() + 2, 0);
for(int i=0; i<matrix.size(); i++) {
for(int j=0; j<matrix[0].size(); j++) {
// 用line存放每列的高度,matrix[i][j]为'1'时,就用之前的高度再加一
line[j+1] = (matrix[i][j] == '0') ? 0 : line[j+1] + 1;
}
// 用ans存放要return的信息
// 将存放的每一列高度都调用函数往单调栈中判断,求得最大的矩形面积
ans = max(ans, largestRectangleArea(line));
}
return ans;
}
// 用单调栈求最大矩形的面积,和84题柱状图中最大的矩形解法相似
int largestRectangleArea(vector<int>& heights) {
int ans = 0;
// 相对于84题少了下列两行在数组头尾插入0的操作,
// 是因为在初始化height数组时,就将该数组前后都增加了一列
/*
heights.insert(heights.begin(), 0);
heights.push_back(0);
*/
// 用数组模拟一个栈,直接创建一个栈也OK
vector<int> st;
// 用单调栈求最大矩形面积
for(int i=0; i<heights.size(); i++) {
while(!st.empty() && heights[i] < heights[st.back()]) {
int cur = st.back();
st.pop_back();
int left = st.back() + 1;
int right = i - 1;
ans = max(ans, (right - left + 1) * heights[cur]);
}
st.push_back(i);
}
return ans;
}
};
时间复杂度:O(mn)O(mn),其中 mm 和 nn 分别是矩阵的行数和列数
空间复杂度:O(mn)O(mn),其中 mm 和 nn 分别是矩阵的行数和列数。
当然,对于该题也可以使用柱状图的优化暴力解法,也是同样增加将二维数组matrix转化为一维数组height后,再进行模拟宽或高
直接CV过来了
class Solution {
public:
int maximalRectangle(vector<vector<char>>& matrix) {
int m = matrix.size();
if (m == 0) {
return 0;
}
int n = matrix[0].size();
vector<vector<int>> left(m, vector<int>(n, 0));
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (matrix[i][j] == '1') {
left[i][j] = (j == 0 ? 0: left[i][j - 1]) + 1;
}
}
}
int ret = 0;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (matrix[i][j] == '0') {
continue;
}
int width = left[i][j];
int area = width;
for (int k = i - 1; k >= 0; k--) {
width = min(width, left[k][j]);
area = max(area, (i - k + 1) * width);
}
ret = max(ret, area);
}
}
return ret;
}
};
4.LeetCode 155.最小栈
题意:实现一个能实现push,pop,top,getMin()(能取得栈中最小值)的栈
看烂了也无非是实现几个函数来实现这些功能
push,pop,top这些C++自带的函数直接写里面调用就行。
主要是getMin函数的实现
获得一个栈中的最小值,那从源头解决,找数从哪来,在push函数中直接找min,所以直接去创建一个栈min_stk,该栈中只放push操作中的插入的最小值,完成后调用该getMin时就可以直接将min_stk的栈顶元素直接取出即可
综上所述,要实现俩个操作①创建一个存放最小值的栈min_stk,②在push函数中将最小值放进栈min_skt中。
代码实现 C++
class MinStack {
stack<int> stk;
stack<int> min_stk;
public:
MinStack() {
min_stk.push(INT_MAX);
}
void push(int val) {
stk.push(val);
// 在插入元素时,将插入元素和min_stk栈顶元素去比大小,
// 如果比栈顶元素要小,则直接将元素栈顶元素更新,反之不更新,再次插入栈顶元素
min_stk.push(min(min_stk.top(), val));
}
void pop() {
stk.pop();
min_stk.pop();
}
int top() {
int val = stk.top();
return val;
}
int getMin() {
// 直接将栈顶元素return出来就可以,因为栈顶元素是动态更新最小值的
return min_stk.top();
}
};
时间复杂度:O(1)。
空间复杂度:O(n)。
5.LeetCode 239.滑动窗口最大值
题意:给定一个大小为k的窗口,返回在该窗口中数组的最大值
第一眼看这题,害不就一滑动窗口吗,做过了,so easy,再一看,啥玩意,窗口固定,向右滑动,还求里面的最大值??!没见过啊多新鲜,然后啪的一下很快啊,打开题解,CV,提交,过啦,一套流程一气呵成。
不应该这样啊,咱们好好分析一下。
这题使用队列的方式解决。
那么回归老问题,为什么想到用队列?
如果使用栈,那么再更新窗口时,对于栈底的元素不能得到快速的更新,要反复出栈入栈较为麻烦,所以不考虑栈的方法。此时就可以注意到窗口的更新是双向插删的,那么我们就可以去考虑使用队列,但一般的队列(queue)只有入队和出队两个操作,不能满足双向插删,所以使用双端队列(deque),能进行头尾的删插。(如果没听过的小伙伴可以点击这)
那么就要考虑使用双端队列如何实现该题
该双端队列要实现push插入元素,pop删除元素,front返回最大值,显然不太现实,所以我们要自己定义一种双端队列,因为要能够通过front能直接得到最大值,所以该队列要为一个单调队列,队列尾元素始终为当前队列的最大值。
实现单调队列和实现单调栈原理其实也差不多,因为当当前遍历元素要大于队列尾时,就将队列元素都删除,将当前遍历元素放到队尾。
下列是代码实现要使用的单调队列
class MyQueue { //单调队列(从大到小)
public:
deque<int> que; // 使用deque来实现单调队列,能支持头尾插删
// 每次弹出的时候,比较当前要弹出的数值是否等于队列出口元素的数值,如果相等则弹出。
// 同时pop之前判断队列当前是否为空。
// 用于当窗口左界遍历到当前元素时删除
void pop(int value) {
if (!que.empty() && value == que.front()) {
que.pop_front();
}
}
// 如果push的数值大于入口元素的数值,那么就将队列后端的数值弹出,
// 直到push的数值小于等于队列入口元素的数值为止,
// 这样就保持了队列里的数值是单调从大到小的了。
void push(int value) {
while (!que.empty() && value > que.back()) {
que.pop_back();
}
que.push_back(value);
}
// 查询当前队列里的最大值 直接返回队列前端也就是front就可以了。
int front() {
return que.front();
}
};
下列是该题的实现代码 C++
class Solution {
private:
// 实现一个单调队列
class MyQueue {
public:
// 双向队列,即为队列的头和尾都可以进行pop和push的操作
deque<int> que;
// 该函数用于移动窗口时将最左边的元素删除
// 每次弹出的时候,比较当前要弹出的数值是否等于队列出口元素的数值,如果相等则弹出。
// 同时pop之前判断队列当前是否为空。
void pop(int value) {
if(!que.empty() && value == que.front()) {
que.pop_front();
}
}
// 该函数用于将元素入队,并维护队列的单调性(由大到小)
// 如果push的数值大于入口元素的数值,那么就将队列后端的数值弹出,
// 直到push的数值小于等于队列入口元素的数值为止。
// 这样就保持了队列里的数值是单调从大到小的了。
void push(int value) {
while(!que.empty() && value > que.back()) {
que.pop_back();
}
que.push_back(value);
}
// 用于得到队列的最大值,即为最左边的元素
int front() {
return que.front();
}
};
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
MyQueue que;
// 用于return
vector<int> result;
// 将第一个窗口的元素按单调队列的方式入队
for(int i=0; i<k; i++) {
que.push(nums[i]);
}
// result存放队列左边的最大值
result.push_back(que.front());
for(int i=k; i<nums.size(); i++) {
// 将窗口最左边的元素pop
que.pop(nums[i - k]);
// 将窗口最右边的元素push
que.push(nums[i]);
// 存放最大值
result.push_back(que.front());
}
return result;
}
};
时间复杂度:O(n)
空间复杂度:O(k)
该题还有其他如优先队列、分块 + 预处理的方法,本菜鸡就没水平继续解释了,有兴趣的小伙伴可以直接点这里。
6.LeetCode 394.字符串解码
题意:重复k倍的括号中的内容
这一题吧,本菜鸡思考了半天字符怎么乘,结果用要重复的字符累加k次就好了(捂脸哭)。
这题呢可以有递归和栈两种解法,分别解释
老问题,为什么要使用栈?
字符串中有(string)数字,字母,括号,(string)数字我们肯定要考虑变为(int)数字,所以我们考虑用两个箩筐去分别放数字和字母,括号作为判断的标准。那么这两个箩筐要实现什么功能呢?答:能放能出能删,一下不就想到了栈这一个箩筐了。所以该题考虑用两个栈,一个数字栈、一个字符栈实现。
那么如何使用这两个栈去实现呢
先考虑咋放
数字栈中的数字用s[i] - '0' 得到,但我们要考虑到k为多位数,所以我们要在前加一个*10,即为
// cnt为当前遍历括号前的数字字符
cnt = cnt * 10 + (s[i] - '0');
字符栈中的字符可以在遍历时定义一个string字符串,存放每次遍历时遇到的字符(直接和前面一个字符累加在一起即可)
那么现在就是对于括号如何作为判断的标准了:
对于左括号'[',当遍历到左括号时,则说明前面遍历的字符数字和字符字母应该放入栈中了,要开始进行下一个括号的遍历。
对于右括号']',当遍历到右括号时,则说明已经遍历完了一个括号,要进行重复字符的操作(进行的操作在代码块中解释)。
下面就是具体实现的代码 C++
class Solution {
public:
string decodeString(string s) {
// 临时存放s中的字符
string res = "";
// 用int类型的栈存放数字
stack <int> nums;
// string类型的栈存放字符
stack <string> strs;
// 临时存放s中的数字
int num = 0;
int len = s.size();
for(int i = 0; i < len; ++ i)
{
// 如果s[i]为数字,则存放入nums中
// 遇到1位以上的数字需要把前一个数字*10 ,
// 再加上当前的数字,比如遇到12这个数字,需要进行1 * 10 + 2才能得到12
if(s[i] >= '0' && s[i] <= '9')
{
num = num * 10 + s[i] - '0';
}
// 如果s[i]为字符,则加入res中
else if((s[i] >= 'a' && s[i] <= 'z') ||(s[i] >= 'A' && s[i] <= 'Z'))
{
res = res + s[i];
}
// 如果遇到左括号,则将之前遍历的数字存入数字栈中,字符存入字符栈中
else if(s[i] == '[') //将‘[’前的数字压入nums栈内, 字母字符串压入strs栈内
{
nums.push(num);
// 数字存入数字栈中,然后再重置,继续遍历
num = 0;
strs.push(res);
// 将字符存入字符栈中,然后重置,继续遍历
res = "";
}
// 遇到右括号,则将栈顶的元素pop
else //遇到‘]’时,操作与之相配的‘[’之间的字符,使用分配律
{
// times存放要重复多少次,即为s中的数字
int times = nums.top();
// 再将当前存放入times中的数字栈中的元素pop掉
nums.pop();
// 将字符栈中的元素取出来去增加tims倍,res中存放着未遇到"["存入栈中的s中的字符
for(int j = 0; j < times; ++ j)
strs.top() += res;
//之后若还是字母,就会直接加到res之后,因为它们是同一级的运算
res = strs.top();
//若是左括号,res会被压入strs栈,作为上一层的运算
strs.pop();
}
}
return res;
}
};
时间复杂度:O(s.size())
空间复杂度:O(s.size())
下面是递归方法的讲解:
为什么能想到递归?
该字符串说白了就是一个个括号组合而成,所以问题也可以分为一个个小括号中的问题去解决,所以可以考虑使用递归,下面是具体实现代码 C++
class Solution {
public:
string decodeString(string s) {
// 控制当前遍历的位置
int pos = 0;
return dfs(s, pos);
}
string dfs(string s, int &pos) {
// 存放要return的结果
string ans = "";
// 倍数
int cnt = 0;
while(pos < s.size()) {
// ch存放当前遍历的字符
char ch = s[pos];
// 如果当前遍历的为字符,则加入ans中
if(isalpha(ch)) {
ans += ch;
}
// 如果为数字,则转为int类型,存放进cnt中
else if(isdigit(ch)) {
cnt = cnt * 10 + (ch - '0');
}
// 如果为左括号,则继续往里面递归,
// 等于将一个大的括号分为了多个小括号之间的问题,继续递归下一个括号内的内容
else if(ch == '[') {
// 将pos++,跳过括号
pos++;
// 用sub存放递归的结果,即为内括号中的结果
string sub = dfs(s, pos);
// 外括号的结果和内括号的结果同时增加cnt倍
while(cnt--) {
ans += sub;
}
// 再将倍数重置
cnt = 0;
}
// 如果为右括号则说明当前的括号已经遍历完,可以直接结束此次的递归
else if(ch == ']') {
return ans;
}
// 如果都不满足,则继续向下遍历
pos++;
}
// 将结果return
return ans;
}
};
时间复杂度:O(s.size())
空间复杂度:O(s.size())
7.LeetCode 739.每日温度
题意:找到下一个比当前遍历温度要高的第一个温度。
该题用单调栈的方法去解决。
老问题,为什么用单调栈?
分析题目,找到比当前遍历温度要高的第一个温度,那是不是可以理解为在只要在递增,那比当前遍历温度要高的第一个温度是不是永远在下一个。
那么,我们是不是可以通过维护一个单调递增的玩意,如果当前遍历的元素要上一个放进去的元素要大,则将上一个放进去的元素(记为x)拿出来,将此时遍历的下标减去x的下标,就为x到下一个比它大的元素的距离。这不纯纯用单调栈去解决。
那么就应该思考用单调栈如何实现了
和之前的实现方法也类似,主要是在弹出栈时的操作如何完成题目的要求。
分析一下,当遍历到比栈顶元素要大的元素时,直接将栈顶元素弹出,将当前遍历的元素下标和栈顶元素的下标作差,存放,在循环,直至当前遍历的元素要小于栈顶元素。
下面是代码实现 C++
class Solution {
public:
vector<int> dailyTemperatures(vector<int>& temperatures) {
stack<int> stk;
// 要用于返回的数组
vector<int> ans(temperatures.size());
for(int i=0; i<temperatures.size(); i++) {
// 如果stk不为空,并且当前遍历元素要比栈顶元素要大
while(!stk.empty() && temperatures[i] > temperatures[stk.top()]) {
// 将栈顶元素取出
int prve = stk.top();
// i - stk.top() 得到要的结果
ans[prve] = i - prve;
stk.pop();
}
stk.push(i);
}
return ans;
}
};
时间复杂度:O(n)
空间复杂度:O(n)
此题也可以直接暴力解,和栈的解法类似,或者栈的解法就是由暴力解法优化来的,下面是实现代码,可以对照着看
class Solution {
public:
vector<int> dailyTemperatures(vector<int>& temperatures) {
int n = temperatures.size();
vector<int> ans(n), next(101, INT_MAX);
for (int i = n - 1; i >= 0; --i) {
int warmerIndex = INT_MAX;
for (int t = temperatures[i] + 1; t <= 100; ++t) {
warmerIndex = min(warmerIndex, next[t]);
}
if (warmerIndex != INT_MAX) {
ans[i] = warmerIndex - i;
}
next[temperatures[i]] = i;
}
return ans;
}
};