栈
20. 有效的括号
思路:当遇到左括号时将对应的右括号压入栈中,当遇到非左括号时,若栈为空或栈顶对应字符与当前字符不同,则返回false。
class Solution {
public:
bool isValid(string s) {
stack<char> stk;
for (int i = 0; i < s.size(); i++){
if (s[i] == '{') stk.push('}');
else if (s[i] == '[') stk.push(']');
else if (s[i] == '(') stk.push(')');
else if (stk.empty() || s[i] != stk.top()) return false;
else stk.pop();
}
if (!stk.empty()) return false;
return true;
}
};
标准答案中在最开始先判断字符串的长度,若为奇数,则直接返回false。加入提前终止判断后程序开销大幅降低。另外,最后两行可以合并为一行:return st.empty()
。
1047. 删除字符串中的所有相邻重复项
思路:栈的一大功能就是在遍历某个容器时,记录上一个遍历的元素是什么。
class Solution {
public:
string removeDuplicates(string s) {
stack<char> stk;
for (int i = 0; i < s.size(); i++){
if (!stk.empty() && s[i] == stk.top())
stk.pop();
else
stk.push(s[i]);
}
string result = "";
while (!stk.empty()) {
result += stk.top();
stk.pop();
}
reverse (result.begin(), result.end());
return result;
}
};
此外,在由栈生成字符串的代码实现方面,学习string的初始化、拼接时使用加和操作,并复习reverse函数的使用方法。
上面代码的效率不是最优的,在力扣官方解答中,直接使用了字符串自带的push_back、pop_back函数:
class Solution {
public:
string removeDuplicates(string s) {
string stk;
for (char ch : s) {
if (!stk.empty() && stk.back() == ch) {
stk.pop_back();
} else {
stk.push_back(ch);
}
}
return stk;
}
};
150. 逆波兰表达式求值
思路:遇到数字则入栈;遇到算符则取出栈顶两个数字进行计算,并将结果压入栈中。
class Solution {
public:
int evalRPN(vector<string>& tokens) {
stack<int> stk;
for (int i = 0; i < tokens.size(); i++) {
if (tokens[i] == "+" || tokens[i] == "-" || tokens[i] == "*" || tokens[i] == "/") {
int num1 = stk.top();
stk.pop();
int num2 = stk.top();
stk.pop();
if (tokens[i] == "+") stk.push(num2 + num1);
if (tokens[i] == "-") stk.push(num2 - num1);
if (tokens[i] == "*") stk.push(num2 * num1);
if (tokens[i] == "/") stk.push(num2 / num1);
} else {
stk.push(stoi(tokens[i]));
}
}
int result = stk.top();
stk.pop();
return result;
}
};
这里调用了现成的函数来实现字符串向整型的转化,根据具体转化的类型int、long、long long分别有对应的库函数stoi()、stol()、stoll()。
队列
239. 滑动窗口最大值
思路:一开始的思路是维护一个最大值的索引,每次入队的值和最大值做比较,如果最大值索引超出窗口范围,则重新排序。这种做法的问题是当最大值超出索引范围后,定位新的最大值仍需要重新排序。在数组是单调递减的情况下,每一次都需要重新排序获取最大值下标,时间复杂度和暴力解法相同,为O(nk)。
那么如何在弹出最大值之后确定下一个最大值对应的索引位置?此时需要利用的一个关键性质是:对于滑动窗口里的两元素nums[i]和nums[j],如果 i < j 且 nums[i] < nums[j],则 i 必不可能是最大值对应的索引。也就是说,每次新来的元素并不需要和现有窗口里所有的元素做比较,只需要和有希望未来成为最大值的元素做比较,并在这些元素中淘汰掉比它小的元素。使用这种思路构建队列,储存的是元素的索引值,索引是单调递增的,而其在数组里对应的值是单调递减的。这种队列称为单调队列。
力扣官方的代码实现如下,其中deque是一个双向队列,C++中deque是stack和queue默认的底层实现容器。
class Solution {
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
int n = nums.size();
deque<int> q;
for (int i = 0; i < k; ++i) {
while (!q.empty() && nums[i] >= nums[q.back()]) {
q.pop_back();
}
q.push_back(i);
}
vector<int> ans = {nums[q.front()]};
for (int i = k; i < n; ++i) {
while (!q.empty() && nums[i] >= nums[q.back()]) {
q.pop_back();
}
q.push_back(i);
//这里判断当下最大元素是否超出窗口范围,力扣官方给的是while判断,实际上由于for循环每次只推进一格,用if判断一次即可。
while (q.front() <= i - k) {
q.pop_front();
}
ans.push_back(nums[q.front()]);
}
return ans;
}
};
347. 前 K 个高频元素
思路:基本思路是使用unordered_map统计各元素出现的频次,再想办法将这些频次排序,取前k个元素返回。总的时间复杂度为O(nlgn)。而实际上题中只需要知道前k个有哪些,并不需要知道具体的大小顺序,所以可以借助一个大小为k的小顶堆进行排序。
力扣官方的代码如下,需要进一步学习小顶堆的优先队列实现。
class Solution {
public:
static bool cmp(pair<int, int>& m, pair<int, int>& n) {
return m.second > n.second;
}
vector<int> topKFrequent(vector<int>& nums, int k) {
unordered_map<int, int> occurrences;
for (auto& v : nums) {
occurrences[v]++;
}
// pair 的第一个元素代表数组的值,第二个元素代表了该值出现的次数
priority_queue<pair<int, int>, vector<pair<int, int>>, decltype(&cmp)> q(cmp);
for (auto& [num, count] : occurrences) {
if (q.size() == k) {
if (q.top().second < count) {
q.pop();
q.emplace(num, count);
}
} else {
q.emplace(num, count);
}
}
vector<int> ret;
while (!q.empty()) {
ret.emplace_back(q.top().first);
q.pop();
}
return ret;
}
};