单调栈的“视线”魔法:统计「队列中可以看到的人数」
哈喽各位,我是前端小L。
我们的单调栈工具箱,已经能熟练处理“下一个/上一个 更大/更小”元素了。这些问题大多是寻找一个单一的目标。今天,我们的挑战升级了:我们要从一个“寻找者”变成一个“统计者”。
这道题是对单调栈“清算”逻辑的一次绝佳应用。我们将从右向左遍历,维护一个“天际线”,并巧妙地在 while 循环和 if 判断中,完成“能看到的所有矮个子”+“那个挡住你的高个子”的精妙计数。
力扣 1944. 队列中可以看到的人数
https://leetcode.cn/problems/number-of-people-visible-in-a-queue/

题目分析:
-
输入:一个身高数组
heights,代表一群人从左到右站成一排。 -
规则:你站在队列中(假设是
i位置),看向右边。你能看到j(j > i),当且仅当i和j之间的所有人,都比你俩都矮。-
规则的简化版:
i能看到j,当且仅当min(heights[i], heights[j]) > max(heights[k] for k in (i+1, j-1))。
-
-
目标:返回一个数组
ans,ans[i]表示第i个人能看到右边多少人。
例子: heights = [10, 6, 8, 5, 11, 9]
-
i=0 (10): 能看6(min(10,6)>max(empty)), 能看8(min(10,8)>max(6)), 不能看5(min(10,5)<max(6,8)), 能看11(min(10,11)>max(6,8,5)), 但11会挡住9。共 3 人。 -
i=1 (6): 能看8(min(6,8)>max(empty)), 但8会挡住5,11也比6高,但被8挡住了。共 1 人。 -
i=2 (8): 能看5(min(8,5)>max(empty)), 能看11(min(8,11)>max(5)), 但11会挡住9。共 2 人。 -
i=3 (5): 能看11(min(5,11)>max(empty)), 但11会挡住9。共 1 人。 -
i=4 (11): 能看9(min(11,9)>max(empty)). 共 1 人。 -
i=5 (9): 右边没人。共 0 人。
最终答案: [3, 1, 2, 1, 1, 0]
思路一:朴素的“视线检查” (O(n²))
最直观的方法,就是模拟这个过程:
-
遍历每个人
i。 -
再遍历
i右边的每个人j。 -
在
(i, j)之间,维护一个current_max_height。 -
如果
min(heights[i], heights[j]) > current_max_height,说明i能看到j,count++。 -
current_max_height = max(current_max_height, heights[j])(不对,应该是heights[j-1]?)。
这个逻辑很绕,很容易出错,而且时间复杂度至少是 O(n²)。我们需要更清晰的 O(n) 解法。
“Aha!”时刻:从右向左,维护“天际线”
这个“视线”问题,如果从左向右看,会非常复杂。但如果我们从右向左遍历,问题会变得异常简单。
我们维护一个单调递减的栈。这个栈代表了从“我”右边看过去的“天际线”。
算法流程:
-
初始化一个空栈
s(存储索引)和一个结果数组ans(长度n,初始全为0)。 -
从右向左遍历数组 (
i从n-1到0): a. 初始化当前人i的可见人数count = 0。 b. “清算矮个子” (while 循环):while (!s.empty() && heights[i] > heights[s.top()])* 当前的i比栈顶的人s.top()要高。 *i的视线可以“越过”s.top(),所以i肯定能看到s.top()。 *count++。 *s.pop()(这个“矮个子”被i看到了,并且他不会阻挡i看更远,所以弹出)。 c. “遇到高墙” (if 判断): *while循环结束时,栈顶(如果存在)的人s.top(),必然大于或等于heights[i]。 *i的视线到这里就被挡住了。 * 但是,i能看到这个“高墙”本身! *if (!s.empty()) { count++; }d. “加入天际线” (push): * 将当前人i的索引压入栈。他将成为左边的人(i-1)眼中的“天际线”的一部分。 *s.push(i);e. 记录答案:ans[i] = count; -
返回
ans。
代码实现 (单调递减栈)
#include <vector>
#include <stack>using namespace std;class Solution {
public:vector<int> canSeePersonsCount(vector<int>& heights) {int n = heights.size();vector<int> ans(n, 0);stack<int> s; // 存储索引的单调递减栈// 从右向左遍历for (int i = n - 1; i >= 0; --i) {int count = 0;// 1. 清算所有比我矮的(我能越过他们看)// 栈保持单调递减while (!s.empty() && heights[i] > heights[s.top()]) {count++; // 我能看到这个比我矮的s.pop();}// 2. 遇到第一个比我高(或等高)的“高墙”if (!s.empty()) {count++; // 我能看到这个“高墙”}// 3. 将我加入“天际线”s.push(i);// 4. 记录答案ans[i] = count;}return ans;}
};
复杂度分析
-
时间复杂度 O(n): 我们只从右向左遍历了数组一次。
while循环中的pop和push操作是关键。每个元素的索引i最多只入栈一次、出栈一次。因此,所有栈操作的总和是 O(n)(摊销分析)。总时间复杂度是 O(n)。 -
空间复杂度 O(n): 在最坏的情况下,例如一个严格递减的数组
[5, 4, 3, 2, 1],while循环永远不会执行,所有元素的索引都会被压入栈中。此时栈的空间占用为 O(n)。
总结:单调栈的“计数”模式
今天这道题,再次展现了单调栈在处理“视线”和“边界”问题上的强大能力。它和 LC 901(股票跨度)有异曲同工之妙,但又有所不同:
-
LC 901 (向左看):
while(price >= top.price),span += top.span。它在“吸收”矮个子的跨度。 -
LC 1944 (向右看):
while(height > top.height),count++。它在“清点”矮个子的数量。
这两个问题,都利用了单调栈(一个递增,一个递减)来高效地跳过那些“无关紧要”的中间元素,并在 O(n) 时间内完成了复杂的统计。
下期见!
