单调栈(Monotonic Stack)是一种特殊的栈结构,它在算法题解中扮演着重要角色。这种数据结构的特点是栈内元素始终保持单调递增或单调递减的顺序。我第一次接触这个概念是在解决"下一个更大元素"问题时,发现传统暴力解法O(n²)的时间复杂度在数据量大时完全无法接受。
单调栈的核心思想在于:通过维护元素的单调性,可以高效地找到每个元素左右两侧第一个满足特定条件的邻居。这种特性使其特别适合处理"临近元素比较"类问题。举个例子,当我们需要找数组中每个元素右边第一个比它大的数时,单调递减栈就能完美解决。
关键理解:单调栈不是一种独立的数据结构,而是对普通栈的特殊使用方式。就像给栈加上了一个排序规则,使其具有了特殊的解题能力。
从栈底到栈顶元素值严格递增的栈结构。这种栈适合处理"下一个更小元素"类问题。实际构建时,当新元素小于栈顶元素时,需要不断弹出栈顶元素直到满足递增条件。
python复制def increasing_stack(nums):
stack = []
for num in nums:
while stack and stack[-1] > num:
stack.pop()
stack.append(num)
return stack
与递增栈相反,从栈底到栈顶元素值严格递减。这是解决"下一个更大元素"的标准工具。构建过程中,遇到比栈顶大的新元素时需要弹出破坏单调性的元素。
python复制def decreasing_stack(nums):
stack = []
for num in nums:
while stack and stack[-1] < num:
stack.pop()
stack.append(num)
return stack
给定每日温度列表,要求返回需要等待多少天才能遇到更高温度的天数列表。这是单调栈最典型的应用场景。
python复制def dailyTemperatures(T):
stack = []
res = [0] * len(T)
for i, temp in enumerate(T):
while stack and T[stack[-1]] < temp:
prev = stack.pop()
res[prev] = i - prev
stack.append(i)
return res
这个解法的时间复杂度是O(n),因为每个元素最多入栈出栈各一次。空间复杂度O(n)来自栈的使用。
这个hard级别的问题可以通过单调栈高效解决。关键思路是找到每根柱子左右两边第一个比它矮的柱子,从而确定以当前柱子为高的最大矩形宽度。
python复制def largestRectangleArea(heights):
heights.append(0) # 哨兵节点
stack = [-1]
max_area = 0
for i in range(len(heights)):
while heights[i] < heights[stack[-1]]:
h = heights[stack.pop()]
w = i - stack[-1] - 1
max_area = max(max_area, h * w)
stack.append(i)
return max_area
当问题扩展到循环数组时,常规的单调栈解法需要调整。常见技巧是遍历两次数组,或者使用取模运算模拟循环。
python复制def nextGreaterElements(nums):
n = len(nums)
res = [-1] * n
stack = []
for i in range(2 * n):
while stack and nums[stack[-1] % n] < nums[i % n]:
res[stack.pop() % n] = nums[i % n]
stack.append(i)
return res
单调栈可以扩展到二维问题,如求矩阵中的最大矩形。通常需要先预处理每行的柱状图高度,然后对每行应用柱状图最大矩形算法。
初学者常犯的错误包括:
调试建议:在纸上手动模拟小规模输入的执行过程,特别关注边界元素(第一个和最后一个)的处理。
虽然单调栈已经是O(n)解法,但在实际编码比赛中还可以:
当遇到以下特征的问题时,考虑使用单调栈:
典型问题包括:
与暴力解法相比,单调栈将时间复杂度从O(n²)降低到O(n),这是质的飞跃。虽然需要额外的O(n)空间存储栈,但在大多数情况下这是可以接受的代价。
在实际应用中,我发现单调栈解法在LeetCode上的运行时间通常能击败95%以上的提交,这证明了其高效性。不过要注意,某些语言(如Python)的列表操作可能比专门设计的栈结构稍慢。