算法基础 典型题 单调栈
算法基础练习总结入口:我的算法地图
文章目录
- 一、基本概念
- 二、场景分析
- 三、典型题目
一、基本概念
单调栈是一种栈内元素保持严格单调性(递增或递减) 的特殊栈结构,核心作用是高效寻找数组中元素的 “前后最近的更大 / 更小元素”,从而将暴力解法的 O (n²) 时间复杂度优化为 O (n)(每个元素最多入栈和出栈一次)。其思想简洁但应用灵活,是解决 “数组元素关系” 类问题的高频算法模式。
二、场景分析
1、常见题目类型
单调栈主要分为单调递增栈和单调递减栈,适用场景不同,核心是根据 “要找更大元素还是更小元素” 选择栈类型:
2、使用关键技巧
1)栈存储索引而非值:
索引包含位置信息,可用于计算元素之间的距离(如宽度i - st.top() - 1),而值无法直接获取位置,这是单调栈的核心技巧。
2)单调性的选择:
找 “更大元素”→ 用单调递减栈(当前元素比栈顶大时,栈顶的更大元素就是当前元素);
找 “更小元素”→ 用单调递增栈(当前元素比栈顶小时,栈顶的更小元素就是当前元素)。
3)边界处理:
结果数组初始化为 - 1(无目标元素时的默认值);
循环数组:通过i % n将数组 “翻倍”,避免单独处理尾部元素;
最大矩形 / 接雨水:在数组首尾加 0(或极小值),确保栈中所有元素都能被弹出处理。
4)弹出元素时的计算:
单调栈的核心逻辑在 “弹出栈顶元素” 时,此时需根据问题目标计算结果(如下一个更大元素的值、矩形的宽度、雨水的体积等),这一步是区分不同问题的关键。
三、典型题目
496. 下一个更大元素 I
nums1 中数字 x 的 下一个更大元素 是指 x 在 nums2 中对应位置 右侧 的 第一个 比 x 大的元素。给你两个 没有重复元素 的数组 nums1 和 nums2 ,下标从 0 开始计数,其中nums1 是 nums2 的子集。对于每个 0 <= i < nums1.length ,找出满足 nums1[i] == nums2[j] 的下标 j ,并且在 nums2 确定 nums2[j] 的 下一个更大元素 。如果不存在下一个更大元素,那么本次查询的答案是 -1 。返回一个长度为 nums1.length 的数组 ans 作为答案,满足 ans[i] 是如上所述的 下一个更大元素 。
vector<int> nextGreaterElement(vector<int>& nums1, vector<int>& nums2) {// 思路:单调栈统计 加 哈希表记录unordered_map <int, int> hashmap; // 记录结果,key=nums, value=下一个更大元素stack <int> indexstack; // 单调递减栈(存储索引)for (int i = 0; i < nums2.size(); i++) {// 调整栈:当前元素 > 栈顶元素,栈顶元素的下一个更大是当前元素 while (!indexstack.empty() && nums2[i] > nums2[indexstack.top()]) {int top_index = indexstack.top();indexstack.pop();hashmap[nums2[top_index]] = nums2[i];}indexstack.push(i);}vector<int> result;for (auto &num : nums1) {result.push_back(hashmap.count(num) ? hashmap[num] : -1);}return result;}
503. 下一个更大元素 II
给定一个循环数组 nums ( nums[nums.length - 1] 的下一个元素是 nums[0] ),返回 nums 中每个元素的 下一个更大元素 。数字 x 的 下一个更大的元素 是按数组遍历顺序,这个数字之后的第一个比它更大的数,这意味着你应该循环地搜索它的下一个更大的数。如果不存在,则输出 -1 。
vector<int> nextGreaterElements(vector<int>& nums) {// 思路:单调栈统计, 循环数组要操作两轮stack <int> indexstack; // 单调递减栈(存储索引)int len = nums.size();vector<int> result(len, -1);for (int i = 0; i < len * 2 - 1; i++) {// 调整栈:当前元素 > 栈顶元素,栈顶元素的下一个更大是当前元素 while (!indexstack.empty() && nums[i % len] > nums[indexstack.top()]) {int top_index = indexstack.top();indexstack.pop();result[top_index] = nums[i % len];}indexstack.push(i % len);}return result;}
1944. 队列中可以看到的人数
有 n 个人排成一个队列,从左到右 编号为 0 到 n - 1 。给你以一个整数数组 heights ,每个整数 互不相同,heights[i] 表示第 i 个人的高度。一个人能 看到 他右边另一个人的条件是这两人之间的所有人都比他们两人 矮 。更正式的,第 i 个人能看到第 j 个人的条件是 i < j 且min(heights[i], heights[j]) > max(heights[i+1], heights[i+2], …, heights[j-1]) 。请你返回一个长度为 n 的数组 answer ,其中 answer[i] 是第 i 个人在他右侧队列中能 看到 的 人数 。
vector<int> canSeePersonsCount(vector<int>& heights) {// 思路:单调栈,从右往左 维护可见高度// 基本逻辑,i要看到j(j > i),需要i到j之间的元素小于height[i]]// 更快方向,右边的人是否被看到,取决于他们与当前人的高度关系// 单调栈维护右边的 “可见高度”(栈内元素保持递增),这样可以快速判断当前人能看到多少右边的人int len = heights.size(); // 存储右边的人高度(栈顶→栈底递增)stack<int> heightstack;vector<int> result(len, 0);// 从右往左遍历(先处理右边的人,再处理左边的人)for (int i = len - 1; i >= 0; i--) {int h = heights[i];// 步骤1:弹出栈中所有比当前人矮的人(这些人能被当前人看到)while (!heightstack.empty() && heightstack.top() < h) {heightstack.pop();result[i]++;}// 步骤2:如果栈不为空,栈顶的人比当前人高(当前人能看到他,且他不会被挡住)if (!heightstack.empty()) {result[i]++;}// 步骤3:将当前人高度入栈,维护栈的单调性heightstack.push(h);}return result;}
84. 柱状图中最大的矩形
给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。求在该柱状图中,能够勾勒出来的矩形的最大面积。
int largestRectangleArea(vector<int>& heights) {// 思路:单调栈// 对于每个柱子最大面积(面积 = 高度 × 宽度,宽度由 “左右两边第一个比它矮的柱子” 决定)// 遍历整个数组,求得每个i位置height为高度矩形的最大面积,就能得到最大面积int len = heights.size();stack<int> stk; // 单调栈:存储柱子索引,栈内索引对应的高度严格递增int result = 0;// 末尾补0:确保最后所有柱子都能被弹出计算(0会触发剩余元素的处理)heights.push_back(0);// 遍历所有柱子(包括新增的0,索引从0到n)for (int right = 0; right <= len; right++) {// 核心逻辑:当前柱子高度 <= 栈顶柱子高度时,计算栈顶柱子的最大面积while (!stk.empty() && heights[stk.top()] >= heights[right]) {// 栈顶柱子的高度(当前计算的矩形高度)int h = heights[stk.top()];stk.pop(); // 弹出栈顶,此时栈顶变为左边界的候选// 左边界:栈为空则左边界是-1(左侧没有更矮的柱子),否则是新栈顶索引int left = stk.empty() ? -1 : stk.top(); // 计算面积:宽度 = right(右边界) - left(左边界) - 1result = max(result, (right - left - 1) * h);}stk.push(right);}return result;}
901. 股票价格跨度
设计一个算法收集某些股票的每日报价,并返回该股票当日价格的 跨度 。当日股票价格的 跨度 被定义为股票价格小于或等于今天价格的最大连续日数(从今天开始往回数,包括今天)。例如,如果未来 7 天股票的价格是 [100,80,60,70,60,75,85],那么股票跨度将是 [1,1,1,2,1,4,6] 。实现 StockSpanner 类:StockSpanner() 初始化类对象。int next(int price) 给出今天的股价 price ,返回该股票当日价格的 跨度 。
class StockSpanner {// 思路:单调栈 加额外保存连续日数量// 额外保存连续日的数量,如果今日价格不低于某个连续日,那么可以直接累加之前这个连续日的连续日。
public:StockSpanner() {}int next(int price) {int cnt = 1;while (!stk.empty() && stk.top().first <= price) {cnt += stk.top().second;stk.pop();}stk.push({price, cnt});return cnt; }
private:stack<pair<int, int>> stk;
};