1、有效的括号
题目链接:有效的括号
题目描述:
给定一个只包括 '(', ')', '{', '}', '[', ']' 的字符串 s ,判断字符串是否有效。
有效字符串需满足:
- 左括号必须用相同类型的右括号闭合。
- 左括号必须以正确的顺序闭合。
- 每个右括号都有一个对应的相同类型的左括号。
解答
方法一:最原始的解答
这道题目应该是学习数据结构栈的时候学到的第一个应用情景,括号是否匹配。思路也是非常简单的,遇到左括号就入栈,遇到右括号就和栈顶元素比较是否配对,要是配对则弹出栈顶元素,开启下一个元素的比较,要是不配对表示括号无效。最后所有元素扫描后,要是栈空,则表示括号是有效的,要是栈非空,则表示括号是无效的。
于是代码编写就比较简单了:
cpp
class Solution {
public:
bool isValid(string s) {
int len = s.size();
if (len % 2 == 1)
return false; // 奇数肯定不符合条件
stack<char> stk;
for (char c : s) {
if (c == '(' || c == '[' || c == '{')
stk.push(c); // 左括号入栈
else {
if (stk.empty())
return false; // 来了一个右括号,但栈中没有匹配的项
else {
// 需要配对才能弹栈
if ((c == ')' && stk.top() == '(') ||
(c == ']' && stk.top() == '[') ||
(c == '}' && stk.top() == '{'))
stk.pop();
else // 不配对就报错
return false;
}
}
}
// 最后看看栈中元素是否为空
return stk.empty() == true ? true : false;
}
};
方法二:简化一下方法一
方法一中的:
cpp
if ((c == ')' && stk.top() == '(') ||
(c == ']' && stk.top() == '[') ||
(c == '}' && stk.top() == '{'))
实际上在 C + + C++ C++ 中可以使用:
cpp
unordered_map<char, char> pairs = {{'}', '{'}, {']', '['}, {')', '('}};
进行存储。且最后的结尾直接写 return stk.empty()
即可
于是代码可以优化如下:
cpp
class Solution {
public:
bool isValid(string s) {
int len = s.size();
if (len % 2 == 1)
return false; // 奇数肯定不符合条件
unordered_map<char, char> pairs = {{'}', '{'}, {']', '['}, {')', '('}};
stack<char> stk;
for (char c : s) {
// c 是右括号
if (pairs.count(c)) {
if (stk.empty() || stk.top() != pairs[c])
return false;
stk.pop();
} else
// c 是左括号(非法字符也执行这一条,但是这道题目没有非法字符)
stk.push(c);
}
// 最后看看栈中元素是否为空
return stk.empty();
}
};
2、最小栈
题目链接:最小栈
题目描述:
设计一个支持 push
,pop
,top
操作,并能在常数时间内检索到最小元素的栈。
实现 MinStack
类:
MinStack()
初始化堆栈对象。void push(int val)
将元素val
推入堆栈。void pop()
删除堆栈顶部的元素。int top()
获取堆栈顶部的元素。int getMin()
获取堆栈中的最小元素。
解答
要想实现常数时间内对最小元素的检索,一种思路就是开辟一个配对栈,每次压入一个元素,同时配对压入已压入所有元素的最小值,这样在查找元素的时候就可以实现常数时间内的查找。
cpp
class MinStack {
private:
stack<pair<int, int>> stk;
public:
MinStack() {}
void push(int val) {
if (stk.empty()) {
stk.push({val, val});
} else {
stk.push({val, min(val, stk.top().second)});
}
}
void pop() { stk.pop(); }
int top() { return stk.top().first; }
int getMin() { return stk.top().second; }
};
或者不使用配对栈,而是使用两个栈,另外一个充当配对栈即可:
cpp
class MinStack {
private:
stack<int> stk1;
stack<int> stk2;
public:
MinStack() {}
void push(int val) {
// if (stk1.empty()) {
// stk1.push(val);
// stk2.push(val);
// } else {
// stk1.push(val);
// stk2.push(min(val, stk2.top()));
// }
stk1.empty() ? stk2.push(val) : stk2.push(min(val, stk2.top()));
stk1.push(val);
}
void pop() {
stk1.pop();
stk2.pop();
}
int top() { return stk1.top(); }
int getMin() { return stk2.top(); }
};
3、字符串解码
题目链接:字符串解码
题目描述:
给定一个经过编码的字符串,返回它解码后的字符串。
编码规则为: k[encoded_string]
,表示其中方括号内部的 encoded_string
正好重复 k
次。注意 k
保证为正整数。
你可以认为输入字符串总是有效的;输入字符串中没有额外的空格,且输入的方括号总是符合格式要求的。
此外,你可以认为原始数据不包含数字,所有的数字只表示重复的次数 k
,例如不会出现像 3a
或 2[4]
的输入。
测试用例保证输出的长度不会超过 10 5 10^5 105。
解答
方法一:双栈法
读题可知,可以使用两个栈来解决这个问题,其中一个栈存储的是字符串中的数字,也就是倍数,另一个栈存储的是需要处理的字符串。遍历该字符串,
- 每次遇到数字的时候,使用
k = k * 10 + (ch - '0');
来进行处理。 - 遇到左括号
[
,则数字进数字栈,然后当前数字置0
;字符串进字符串栈,然后字符串置为空。 - 遇到右括号
]
,取出数字栈中顶端的数字,并将栈顶元素进行弹栈,取出字符串栈中栈顶元素,并弹栈,然后根据数字栈中弹出的数字确定当前字符串需要复制几次,然后加在从字符串中弹出的元素后面,等待后面的使用。 - 遇到字符,则直接加到当前字符串上。
根据上述思路,可以编写代码如下:
cpp
class Solution {
public:
string decodeString(string s) {
stack<int> countStack; // 数字栈
stack<string> stringStack; // 字符串栈
string currentString = ""; // 当前字符串解码
int k = 0; // 当前的倍数
for (char ch : s) {
if (isdigit(ch)) {
k = k * 10 + (ch - '0'); // 可能会出现多位数,需要处理
} else if (ch == '[') {
countStack.push(k);
stringStack.push(currentString);
currentString = ""; // 重置字符串
k = 0; // 重置数字
} else if (ch == ']') {
string temp = stringStack.top();
stringStack.pop();
int repeatTimes = countStack.top();
countStack.pop();
// 重复几次,由 k 决定
for (int i = 1; i <= repeatTimes; i++)
temp += currentString;
currentString = temp;
} else {
// 若是字母,则直接加入到 currentString 上即可
currentString += ch;
}
}
return currentString;
}
};
举个例子:对于字符串 s = "3[a2[c]]"
,其栈操作动态图示如下:
bash
初始:
countStack: []
stringStack: []
currentString: ""
读取 '3':
countStack: []
stringStack: []
currentString: ""
k = 3
读取 '[':
countStack: [3]
stringStack: [""]
currentString: ""
k = 0
读取 'a':
countStack: [3]
stringStack: [""]
currentString: "a"
k = 0
读取 '2':
countStack: [3]
stringStack: [""]
currentString: "a"
k = 2
读取 '[':
countStack: [2,3]
stringStack: ["a",""]
currentString: ""
k = 0
读取 'c':
countStack: [2,3]
stringStack: ["a",""]
currentString: "c"
k = 0
读取 ']':
弹出2和"a" → currentString = "acc"
countStack: [3]
stringStack: [""]
currentString: "acc"
k = 0
读取 ']':
弹出3和"" → currentString = "accaccacc"
countStack: []
stringStack: []
currentString: "accaccacc"
k = 0
方法二:单栈法
只使用一个栈,这个栈中存储数字,字母,左括号。
cpp
class Solution {
public:
string getDigits(string& s, size_t& ptr) {
string ret = "";
while (ptr < s.size() && isdigit(s[ptr])) {
ret.push_back(s[ptr++]);
}
return ret;
}
string getString(vector<string>& v) {
string ret;
for (auto& str : v)
ret += str;
return ret;
}
string decodeString(string s) {
vector<string> stk; // 栈:存数字、字母、左括号
size_t ptr = 0;
while (ptr < s.size()) {
char cur = s[ptr];
if (isdigit(cur)) {
// 提取完整数字
string digits = getDigits(s, ptr);
stk.push_back(digits);
} else if (isalpha(cur) || cur == '[') {
// 普通字母或左括号
stk.push_back(string(1, s[ptr++]));
} else {
// 遇到 ']'
++ptr;
vector<string> sub; // 用来临时保存一个完整的括号内字符串
while (stk.back() != "[") {
sub.push_back(stk.back());
stk.pop_back();
}
reverse(sub.begin(), sub.end());
stk.pop_back(); // 弹出 '['
// 弹出数字(当前重复次数)
int repTime = stoi(stk.back());
stk.pop_back();
string o = getString(sub);
string t;
while (repTime--)
t += o;
stk.push_back(t);
}
}
return getString(stk);
}
};
方法三:递归下降法
详细思路见 :官方解答方法二:递归
cpp
class Solution {
private:
string str; // 存储输入字符串,作为成员变量,递归时方便访问
size_t ptr; // 当前解析的位置索引
public:
// 读取一个完整的数字(可能是多位数)
int getDigits() {
int res = 0;
// 当指针未到结尾并且当前字符是数字
while (ptr < str.size() && isdigit(str[ptr])) {
// 累积数字:'0'->0, '1'->1 ...
res = res * 10 + (str[ptr++] - '0');
}
return res;
}
// 核心递归函数:解析并返回一个解码后的字符串片段
string getString() {
// 递归终止条件:
// 1. 到达字符串结尾
// 2. 碰到右括号 ']' (表示当前递归段结束)
if (ptr == str.size() || str[ptr] == ']')
return "";
char cur = str[ptr]; // 当前字符
int repTime = 1; // 重复次数默认 1(字母直接拼接时用)
string ret; // 当前返回的字符串片段
if (isdigit(cur)) {
// 情况1:遇到数字,说明接下来是 k[encoded_string] 的结构
repTime = getDigits(); // 获取数字 k
++ptr; // 跳过 '['
// 递归解析括号内部的字符串
string inner = getString();
++ptr; // 跳过 ']'
// 将解析出来的 inner 重复 repTime 次
while (repTime--)
ret += inner;
} else if (isalpha(cur)) {
// 情况2:遇到普通字母,直接取出并向后移动指针
ret = string(1, str[ptr++]);
}
// 递归解析后续的字符串片段,并拼接返回
return ret + getString();
}
// 主函数入口:初始化全局变量并调用递归
string decodeString(string s) {
str = s; // 保存输入字符串
ptr = 0; // 初始化指针
return getString(); // 从头开始解析
}
};
上述代码的整体逻辑是按语法规则递归解析:
bash
String -> EPS
\| Digits '\[' String ']' String
\| Char String
-
当遇到数字时:
- 先解析出数字
k
; - 进入括号递归解析子串;
- 出递归后将子串重复
k
次。
- 先解析出数字
-
当遇到字母时:
- 直接拼接进结果。
-
遇到右括号或结尾时:
- 返回空串,递归回溯。
举个例子:输入 "3[a2[c]]"
ptr=0
→ 读到数字3
→getDigits()
→repTime=3
- 跳过
[
→ 递归解析"a2[c]"
- 读到字母
a
→ret="a"
- 读到数字
2
→ 递归解析"c"
→inner="c"
→ 重复两次 →"cc"
- 拼接结果
"a" + "cc" = "acc"
- 读到字母
- 回到外层 → 重复
"acc"
三次 →"accaccacc"
4、每日温度
题目链接:每日温度
题目描述:给定一个整数数组 temperatures
,表示每天的温度,返回一个数组 answer
,其中 answer[i]
是指对于第 i
天,下一个更高温度出现在几天后。如果气温在这之后都不会升高,请在该位置用 0
来代替。
解答
超时解法:暴力破解
直接上两层循环,找到对应解,于是代码编写如下:
cpp
class Solution {
public:
vector<int> dailyTemperatures(vector<int>& temperatures) {
int len = temperatures.size();
vector<int> result(len, 0);
for (int i = 0; i < len; i++) {
for (int j = i + 1; j < len; j++) {
if (temperatures[i] < temperatures[j]) {
result[i] = j - i;
break;
}
}
}
return result;
}
};
很显然,代码肯定是超时的。
于是暴力解法肯定是行不通的,这时候就需要寻求其他的优化方法了。
或者使用比较好的暴力方法:
cpp
class Solution {
public:
vector<int> dailyTemperatures(vector<int>& temperatures) {
int len = temperatures.size(); // 获取温度数组的长度
vector<int> result(len); // 结果数组,result[i] 表示第 i 天需要等待多少天才有更热的天气
vector<int> next(101, INT_MAX); // next[t] 表示温度 t 在未来(右侧)第一次出现的最小索引
// 初始化为 INT_MAX 表示"尚未出现"或"不可达"
// 温度范围假设为 0~100,所以数组大小为 101
// 从数组末尾向前遍历(倒序),因为我们要利用"右边的信息"来更新当前结果
for (int i = len - 1; i >= 0; --i) {
int warmerIndex = INT_MAX; // 用于记录比当前温度更高的天气中,最早出现的那一天的索引
// 遍历所有比当前温度更高的温度值(从 temperatures[i]+1 到 100)
for (int t = temperatures[i] + 1; t <= 100; ++t) {
// 查看温度 t 是否在右侧出现过
// 取所有更高温度中,最早出现的那一天(即 next[t] 最小的那个)
warmerIndex = min(warmerIndex, next[t]);
}
// 如果找到了一个更热的天气(warmerIndex 不是 INT_MAX)
if (warmerIndex != INT_MAX) {
result[i] = warmerIndex - i; // 等待天数 = 更热天气的索引 - 当前索引
}
// 如果没找到,result[i] 保持为 0(默认值),表示没有更热的天气
// 更新 next 数组:当前温度 temperatures[i] 在位置 i 出现
// 因为是倒序遍历,i 越小越晚处理,所以这里更新的是"目前为止最左边的出现位置"
// 但 next[t] 存的是"从右往左看最早出现的位置",所以直接赋值 i 是正确的
next[temperatures[i]] = i;
}
return result; // 返回结果数组
}
};
方法:单调栈
这道题目归属于栈系列,肯定就是想能否使用栈的相关知识来进行题目的求解。我分析后发现:
可以使用单调栈(具体为单调递减栈)来高效解决"下一个更高温度"的问题。栈中存储的是温度值及其对应索引的二元组,维护一个温度单调递减的序列。当遍历到一个更高的温度时,不断与栈顶元素比较,弹出所有温度小于当前值的元素,并根据索引差计算等待天数,从而确保每个元素只被入栈和出栈一次,达到较高的时间效率。
cpp
class Solution {
public:
vector<int> dailyTemperatures(vector<int>& temperatures) {
int len = temperatures.size();
vector<int> result(len, 0);
stack<pair<int, int>> stk;
stk.push({temperatures[0], 0});
for (int i = 1; i < len; i++) {
if (temperatures[i] < stk.top().first)
stk.push({temperatures[i], i});
else {
while (!stk.empty() && stk.top().first < temperatures[i]) {
result[stk.top().second] = i - stk.top().second;
stk.pop();
}
stk.push({temperatures[i], i});
}
}
return result;
}
};
或者可以优化一下写法:
cpp
class Solution {
public:
vector<int> dailyTemperatures(vector<int>& temperatures) {
int len = temperatures.size(); // 获取温度数组的长度
vector<int> result(len, 0); // 初始化结果数组,默认等待天数为0
stack<pair<int, int>> stk; // 单调栈,存储 {温度, 索引},维护温度的递减顺序
for (int i = 0; i < len; i++) { // 遍历每一天的温度
// 当栈不为空且当前温度高于栈顶温度时,说明找到了更热的一天
while (!stk.empty() && temperatures[i] > stk.top().first) {
auto [temp, idx] = stk.top(); // 取出栈顶的温度和对应索引
stk.pop(); // 弹出已处理的元素
result[idx] = i - idx; // 计算等待天数并更新结果
}
stk.push({temperatures[i], i}); // 将当前温度和索引压入栈
}
return result; // 返回结果数组
}
};
5、柱状图中最大的矩形
题目链接:柱状图中最大的矩形
题目描述:
给定 n
个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1
。
求在该柱状图中,能够勾勒出来的矩形的最大面积。
解答
超时代码(枚举宽)
直接上两个循环,然后依次遍历宽即可
cpp
class Solution {
public:
int largestRectangleArea(vector<int>& heights) {
int len = heights.size();
int ans = 0;
for (int left = 0; left < len; left++) {
int minHeight = INT_MAX;
for (int right = left; right < len; right++) {
minHeight = min(minHeight, heights[right]);
ans = max(ans, (right - left + 1) * minHeight);
}
}
return ans;
}
};
超时代码(枚举高)
cpp
class Solution {
public:
int largestRectangleArea(vector<int>& heights) {
int n = heights.size();
int ans = 0;
for (int mid = 0; mid < n; ++mid) {
// 枚举高
int height = heights[mid];
int left = mid, right = mid;
// 确定左右边界
while (left - 1 >= 0 && heights[left - 1] >= height) {
--left;
}
while (right + 1 < n && heights[right + 1] >= height) {
++right;
}
// 计算面积
ans = max(ans, (right - left + 1) * height);
}
return ans;
}
};
原始单调栈
看到这题目的第一眼,我就觉得还是需要使用单调栈进行求解,但是我写的最开始的代码比较的冗长:
cpp
class Solution {
public:
int largestRectangleArea(vector<int>& heights) {
int len = heights.size();
int ans = 0;
stack<pair<int, int>> stk;
for (int i = 0; i < len; i++) {
if (stk.empty() || stk.top().first <= heights[i]) {
stk.push({heights[i], i});
} else {
while (!stk.empty() && heights[i] < stk.top().first) {
int h = stk.top().first;
stk.pop();
// 可能弹栈之后,栈顶元素为空,需要判定一下
// 弹栈之前的栈顶元素和弹栈之后的栈顶元素可能在柱状图中不是连续的
// 因此,只能先弹栈之后再取栈顶元素,这样才知道宽度
// 为啥这个宽度区间中的最小高度就是弹栈之前元素的高度呢?
// 因为这个栈是单调栈,这个宽度区间只有大于后面高度的才会被弹栈,小于的会直接压入栈中
int left_bound = stk.empty() ? -1 : stk.top().second;
int w = i - left_bound - 1;
ans = max(ans, h * w);
}
stk.push({heights[i], i});
}
}
while (!stk.empty()) {
int h = stk.top().first;
stk.pop();
int left_bound = stk.empty() ? -1 : stk.top().second;
// 为哈这里的右边界是 len
// 因为从当前栈顶元素到 len 的话,这里面最小的高度就是当前栈顶元素的高度
// 否则,在前面维护单调栈的时候就不对了。
int w = len - left_bound - 1;
ans = max(ans, h * w);
}
return ans;
}
};
优化单调栈
上述代码写的还是比较的冗长,实际上还是有可以优化的地方,比如上述的 if-else
完全没有必要写,实际上,对于上述最好的优化引入一个哨兵。这样就可以免去上述 for
循环后还需要判断栈中是否为空这一步。
cpp
class Solution {
public:
int largestRectangleArea(vector<int>& heights) {
int len = heights.size();
stack<int> stk;
int ans = 0;
for (int i = 0; i <= len; i++) {
int h = (i == len) ? 0 : heights[i]; // 最后增加一个高度为 0 的哨兵,这样必定能保证 for 循环之后栈中为空
while (!stk.empty() && h < heights[stk.top()]) {
int height = heights[stk.top()];
stk.pop();
int width = stk.empty() ? i : i - stk.top() - 1;
ans = max(ans, height * width);
}
stk.push(i);
}
return ans;
}
};
官方还有一些解法,可以查看 官方解答