LeetCode系列之栈专题
1. 栈题目概述
https://leetcode-cn.com/tag/stack/
栈是一种操作受限的线性表。
2. 典型题目
2.1 字符串解码
https://leetcode-cn.com/problems/decode-string/
涉及《编译原理》,巴科斯范式(BNF)
2.2 有效的括号
https://leetcode-cn.com/problems/valid-parentheses/
bool isValid(string s) { if (s.size() % 2 != 0) return false; static const std::unordered_map<char, char> pairs = { {')', '('}, {']', '['}, {'}', '{'} }; std::stack<char> cstack; for (char ch : s) { if (pairs.count(ch)) { // right if (cstack.empty() || cstack.top() != pairs.at(ch)) { return false; } cstack.pop(); } else { // left cstack.push(ch); } } return cstack.empty(); }
有几点值得玩味:
- 合法的组合都应该是偶数个字符组成的,所以开头的判断可以优化
- 用unordered_map的count方法判断是否存在,看起来比find方法简洁(不知道效率如何???)
- 括号map的设计,是有括号到左括号的映射,以简化代码
时间复杂度O(N),空间复杂度O(N+Σ),N是栈中元素个数,Σ是pairs里符号的数量。
2.3 接雨水
https://leetcode-cn.com/problems/trapping-rain-water/
个人认为相比于使用栈的方法,动态规划更容易理解,所以才用了dp。
int trap(vector<int>& height) { int n = height.size(); vector<vector<int>> dp(2, vector<int>(n, 0)); int leftMax = 0; for (int i = 0; i < n; i++) { leftMax = max(height[i], leftMax); dp[0][i] = leftMax; } int rightMax = 0; for (int i = n-1; i >= 0; i--) { rightMax = max(height[i], rightMax); dp[1][i] = rightMax; } int sum = 0; for (int i = 0; i < n; i++) { sum += (min(dp[0][i], dp[1][i]) - height[i]); } return sum; }
时间复杂度O(N),空间复杂度O(N)。
2.4 柱状图中的最大矩形
https://leetcode-cn.com/problems/largest-rectangle-in-histogram/
int largestRectangleArea(vector<int>& heights) { int n = heights.size(); vector<int> left(n), right(n); stack<int> monoStack; for (int i = 0; i < n; i++) { while (!monoStack.empty() && heights[monoStack.top()] >= heights[i]) { monoStack.pop(); } left[i] = monoStack.empty() ? -1 : monoStack.top(); monoStack.push(i); } // clear the monoStack first stack<int>().swap(monoStack); for (int i = n - 1; i >= 0; i--) { while (!monoStack.empty() && heights[monoStack.top()] >= heights[i]) { monoStack.pop(); } right[i] = monoStack.empty() ? n : monoStack.top(); monoStack.push(i); } int maxArea = 0; for (int i = 0; i < n; i++) { maxArea = max(heights[i] * (right[i] - left[i] - 1), maxArea); } return maxArea; }
比较喜欢这种思路,类似于《接雨水》的整体思路,但是在计算辅助矩阵的时候,用到了单调栈。比视频题解里单纯用栈的方法,感觉更容易理解。
另外,清空栈的方法可以列举如下两种,stl没有为stack提供clear接口:
// 通过和空stack进行交换 stack<int>().swap(s); // 通过构造新的空的stack,然后赋值 s = stack<int>();
时间复杂度O(N),空间复杂度O(N)。
2.5 最小栈
https://leetcode-cn.com/problems/min-stack/
采用辅助栈的方法,简单有效。官方题解先入栈了一个INT_MAX,也是一个简化实现的好手。
class MinStack { public: /** initialize your data structure here. */ MinStack() { minStack.push(INT_MAX); } void push(int x) { dataStack.push(x); minStack.push(min(minStack.top(), x)); } void pop() { minStack.pop(); dataStack.pop(); } int top() { return dataStack.top(); } int getMin() { return minStack.top(); } private: std::stack<int> dataStack; std::stack<int> minStack; };
时间复杂度O(1),空间复杂度O(N)。
2.6 最大矩形
https://leetcode-cn.com/problems/maximal-rectangle/
2.7 下一个更大元素
https://leetcode-cn.com/problems/next-greater-element-i/
vector<int> nextGreaterElement(vector<int>& nums1, vector<int>& nums2) { unordered_map<int, int> ngeMap; vector<int> ret; stack<int> monoStack; for (int i : nums2) { while (!monoStack.empty() && i > monoStack.top()) { ngeMap[monoStack.top()] = i; monoStack.pop(); } monoStack.push(i); } while (!monoStack.empty()) { ngeMap[monoStack.top()] = -1; monoStack.pop(); } for (int i : nums1) { ret.push_back(ngeMap[i]); } return ret; }
假设nums1长度为M,nums2长度为N;
时间复杂度O(M+N),空间复杂度O(N)。
https://leetcode-cn.com/problems/next-greater-element-ii/
TODO
2.8 二叉树的中序遍历
https://leetcode-cn.com/problems/binary-tree-inorder-traversal/
递归解法:
vector<int> inorderTraversal(TreeNode* root) { vector<int> output; inorderTraversalRecursive(root, output); return output; } void inorderTraversalRecursive(TreeNode* root, vector<int>& output) { if (root == nullptr) return; inorderTraversalRecursive(root->left, output); output.push_back(root->val); inorderTraversalRecursive(root->right, output); }
时间复杂度O(N),空间复杂度最差O(N),此时二叉树蜕化成链表形式;平均是O(log N)。
迭代解法:
vector<int> inorderTraversal(TreeNode* root) { std::vector<int> ivec; std::stack<TreeNode *> s; while(root != NULL || !s.empty()) { if (root != NULL) { s.push(root); root = root->left; } else { root = s.top(); ivec.push_back(root->val); s.pop(); root = root->right; } } return ivec; }
发现几年前写的解法好像比官解要清晰一些。
时间复杂度O(N),空间复杂度O(N)。
2.9 二叉树的前序遍历
https://leetcode-cn.com/problems/binary-tree-preorder-traversal/
递归解法:
vector<int> preorderTraversal(TreeNode* root) { vector<int> output; preorderTraversalRecursive(root, output); return output; } void preorderTraversalRecursive(TreeNode* root, vector<int>& output) { if (root == nullptr) return; output.push_back(root->val); preorderTraversalRecursive(root->left, output); preorderTraversalRecursive(root->right, output); }
时间复杂度O(N),空间复杂度最差O(N),此时二叉树蜕化成链表形式;平均是O(log N)。
迭代解法:
vector<int> preorderTraversal(TreeNode* root) { vector<int> output; if (root == nullptr) return output; stack<TreeNode*> nodeStack; nodeStack.push(root); while (!nodeStack.empty()) { TreeNode* node = nodeStack.top(); output.push_back(node->val); nodeStack.pop(); // 注意这里的顺序 if (node->right) { nodeStack.push(node->right); } if (node->left) { nodeStack.push(node->left); } } return output; }
时间复杂度O(N),空间复杂度O(N)。
2.10 二叉树的后序遍历
https://leetcode-cn.com/problems/binary-tree-postorder-traversal/
递归解法:
vector<int> postorderTraversal(TreeNode* root) { vector<int> output; postorderTraversalRecursive(root, output); return output; } void postorderTraversalRecursive(TreeNode* root, vector<int>& output) { if (root == nullptr) return; postorderTraversalRecursive(root->left, output); postorderTraversalRecursive(root->right, output); output.push_back(root->val); }
时间复杂度O(N),空间复杂度最差O(N),此时二叉树蜕化成链表形式;平均是O(log N)。
迭代解法:
vector<int> postorderTraversal(TreeNode* root) { vector<int> output; if (root == nullptr) return output; stack<TreeNode*> nodeStack; nodeStack.push(root); while (!nodeStack.empty()) { TreeNode* node = nodeStack.top(); output.push_back(node->val); nodeStack.pop(); // 结合后面的reverse,注意顺序 if (node->left) { nodeStack.push(node->left); } if (node->right) { nodeStack.push(node->right); } } std::reverse(output.begin(), output.end()); return output; }
时间复杂度O(N),空间复杂度O(N)。
3. 总结
老师给出的建议:
- 不要忽视暴力解法,从暴力解法中找出优化的方向。
老师推荐的扩展题目:
- https://leetcode-cn.com/problems/trapping-rain-water/ 接雨水,见2.3
- https://leetcode-cn.com/problems/daily-temperatures/
- https://leetcode-cn.com/problems/next-greater-element-i/
- https://leetcode-cn.com/problems/remove-duplicate-letters/
- https://leetcode-cn.com/problems/online-stock-span/
- https://leetcode-cn.com/problems/remove-k-digits/
- https://leetcode-cn.com/problems/shortest-unsorted-continuous-subarray/
monotonic 单调的(单调递增、单调递减、单调栈)