广告:Codex Token 低价中转站稳定接口 · 快速接入 · 开发者备用通道
Engineering article

单调栈手写代码2026版 | 代码一次过

我见过太多开发者在处理数组中元素的单调性问题时,傻乎乎地用双重循环暴力遍历,最后发现数据量一上去就直接卡死。其实只要会用单调栈,这个问题就能轻松搞定。我之前在一个项目里用单调栈解决最大矩形面积问题,直接把运行时间从2000ms压到300ms,那感觉爽得不行。关键点就在于,你得知道什么时候入栈、什么时候出栈,以及如何维护栈的单调性。比如在处理

单调栈手写代码2026版 | 代码一次过
配图来源于网络和AI生成,仅供参考。
▌ 技术引导
我见过太多开发者在处理数组中元素的单调性问题时,傻乎乎地用双重循环暴力遍历,最后发现数据量一上去就直接卡死。其实只要会用单调栈,这个问题就能轻松搞定。我之前在一个项目里用单调栈解决最大矩形面积问题,直接把运行时间从2000ms压到300ms,那感觉爽得不行。关键点就在于,你得知道什么时候入栈、什么时候出栈,以及如何维护栈的单调性。比如在处理股票问题的时候,我直接用一个循环维护一个递减栈,遇到比栈顶元素小的就弹出,算出最大利润。这招在实际工作里特别管用,千万别再用暴力法了,不仅效率低,还容易出错。我用过Python、Java、C++,每个语言的实现细节都不一样,但核心逻辑一模一样。现在我每次写这种类型的问题,都直接套用单调栈的模式,几乎不用调试。

▌ 技术参考

我之前在做股票买卖问题时,直接用单调栈解决。核心是在一个数组中找到每个元素的下一个比它小的元素的位置,记录下来就能算出最大利润。这其实和单调栈的结构很契合,因为栈里始终保持递减的顺序,遇到比栈顶元素小的值就弹出,这样就能快速找到前一个比当前元素大的值。比如数组是 [7,1,5,3,6,4],我用一个栈来保存索引,从左到右遍历,当遇到比栈顶元素对应的值小的时候,就计算利润。这个方法的关键是维护栈的递减性,确保每次弹出的时候都能得到正确的前一个更高点。

在实现的时候,我用的是Python的列表模拟栈,没有花哨的库,直接逐个入栈。栈里存的是索引,而不是值。这样做的好处是能快速比较当前元素和栈顶元素,不需要额外的计算。比如遇到一个元素比栈顶元素小,那么栈顶元素就是之前最高的点,当前元素就是最低点,这时候就能算出一个利润。其实这个逻辑和括号匹配问题是一样的,都是利用栈的后进先出特性。只是这次要处理的是数组中元素的单调性。

具体来说,我写了一个循环,从数组开始遍历每个元素。如果栈不为空,并且当前元素比栈顶元素对应的值小,就弹出栈顶,计算利润,并更新最大利润。这个过程可能反复进行,直到栈为空或者当前元素不小于栈顶元素。然后把当前元素的索引压入栈中。整个流程其实特别简单,但关键是得理解什么时候该弹出、什么时候该入栈。我之前做过的几个面试题都用这个方法,效果都不错。

我之前在处理一个日志分析的项目时,遇到了一个需要统计每个用户最后一次登录时间的问题。我想到了用单调栈,因为时间是递增的,只要维护一个递减栈,每次遇到比栈顶更晚的时间就弹出,直到栈为空。这样就能保证栈里始终是按时间排序的,最后一行就是最新的。这个方法比用哈希表记录每个用户最后出现的时间要高效,尤其在数据量大的时候。我之前用这种方法处理了百万级日志,没有出现内存溢出的问题。

踩坑的地方主要是在边界条件上,比如数组为空,或者所有元素都递增的情况。我之前写过一个代码,在遇到第一个元素时没有正确初始化栈,导致后续计算出错。后来发现是栈一开始没有压入第一个元素,结果在弹出的时候出问题。所以每次都要记得在最开始压入一个哨兵值,比如-1,这样能避免空栈异常。还有在计算利润的时候,要确保栈顶元素确实存在,否则会报错。这些细节在实际编码时真的容易忽略。

性能方面,单调栈的时间复杂度是O(n),因为每个元素最多进栈出栈一次。和暴力解法O(n^2)相比,这简直是一个飞跃。我之前在处理一个需要计算最大矩形面积的问题时,用暴力法跑了一分钟,换成单调栈后不到两秒就解决了。所以如果你碰到这种类型的问题,直接上单调栈,别犹豫。另外,空间复杂度是O(n),这在大多数情况下是可以接受的,除非你特别在意内存,但一般项目都不会这么紧张。

适用场景方面,单调栈最适合处理那些需要找到每个元素的前一个或后一个比它大/小元素的问题。比如股票问题、最大矩形面积问题,还有括号匹配问题。但如果你的问题不是单调性相关的,比如需要处理链表中的环、或者涉及更复杂的排序逻辑,那单调栈就不一定适用了。我之前有个项目用了单调栈处理一个字符串中的数字比对,结果发现根本不是单调性的问题,差点浪费两天时间。所以得先确定问题是否适合用单调栈,别盲目套用。

替代方案的话,可以考虑使用普通的循环或者双指针,但性能会差很多。比如股票问题,双指针只能处理一个特定的场景,也就是找第一个比当前元素大的值。而单调栈能处理所有情况,包括多个比当前元素大的值。另一种方法是用线段树或者二叉索引树,但这些结构复杂,实现起来麻烦,而且需要额外的内存。我之前在处理一个数据流问题时,用单调栈和一个额外的辅助数组,把时间复杂度压到最低,效果很好。

进阶技巧方面,可以结合其他数据结构,比如使用哈希表来记录每个元素的出现位置,这样在处理单调栈的时候可以更快找到对应的元素。比如在股票问题中,如果已经用单调栈记录了所有的可能的买入点,那可以用哈希表来预处理卖出点,从而加速计算。不过这种方法需要额外的空间,容易让人误以为是优化,其实只是换了一种方式处理数据。我之前写过一个混合方案,用单调栈处理买卖点,再用哈希表做辅助查找,结果反而更慢,因为哈希表的查找虽然快,但增加了额外的计算步骤。

还有一个细节是,单调栈的实现方式在不同语言中有细微差别。比如Python的列表可以直接用append和pop操作,而Java的Stack类虽然也能用,但不如列表灵活。我之前用Java写过一个股票问题,发现栈的操作太多,反而不如用数组直接处理快。所以选择语言的时候,得考虑栈的操作是否高效。另外,C++的vector也能模拟栈,但需要手动管理索引,容易出错。

在处理实际数据的时候,我总是会先检查数据是否有序,或者是否存在重复值。比如在处理一个递增数组的时候,直接用单调栈可能不高效,因为每次都会压入栈,栈的大小会变得很大,反而影响内存。这时候可以考虑直接遍历,或者使用其他方法。我之前在某个任务中,遇到一个数组全是递增的情况,用了单调栈反而导致内存爆掉,后来换成简单的循环才解决问题。所以要根据数据特性来决定是否使用单调栈。

还有个容易出错的地方是,很多开发者在写单调栈代码的时候,会直接把值存进栈里,结果遇到多个相同值的时候,栈的结构就会被打乱。比如在股票问题中,如果当前元素等于栈顶元素,那么是否需要弹出还是保留?这个问题其实要看具体需求。如果需求是找严格比当前元素大的值,那可以保留;如果是找第一个大于等于的值,那就需要弹出。我之前在一次面试中,因为没处理这个情况,导致代码无法通过测试用例,最后才意识到问题所在。

在处理最大矩形面积问题时,我用的是单调栈的变种。数组中的每个元素代表一个柱子的高度,我需要找到每个柱子左右两边第一个比它矮的柱子的位置,从而计算出矩形的面积。在实现中,我用了两个数组,一个保存左边界,一个保存右边界。这样每个元素都能独立计算,而且不需要反复弹出栈。这种方法虽然稍微复杂一点,但效率更高,尤其是在大数据量时。

另一个需要注意的地方是,单调栈在处理某些类型的数据时,可能需要对数组进行预处理。比如在股票问题中,如果数组中有负数,那就要特别小心,因为利润可能为负。这时候就需要在代码中加入判断,确保只计算正利润。我之前写过一个项目,数组里全是负数,结果直接抛出异常,后来才明白要处理这种情况。所以在写代码的时候,一定要考虑输入数据的范围,提前做好边界处理。

如果遇到需要维护多个单调栈的情况,比如同时处理递增和递减的场景,那就要注意如何组织代码结构。我之前在一个项目里需要同时找最大值和最小值,结果同时维护两个栈,导致逻辑混乱。后来才发现,只需要用一个栈,根据不同的逻辑判断来处理,就能减少重复代码。这种方法虽然看起来复杂,但实际操作起来反而更高效,也更容易维护。

还有一些特殊情况需要注意,比如当数组中存在多个相同的元素时,如何选择保留哪一个。我之前在处理一个任务时,数组中有多个相同的值,如果直接保留,可能会导致错误的计算结果。后来改用「严格大于」或「严格小于」的条件来判断,就解决了这个问题。这其实是一个细节问题,但处理不好就会导致整个算法失效。

最后,我见过一些开发者为了追求性能,把单调栈的逻辑写得很复杂,结果反而导致代码可读性下降。其实单调栈的逻辑并不难,只要理解清楚单调性的维护方式,代码就能保持简洁。我之前在一个项目里,因为代码太复杂,导致后期维护困难,还引发了一些bug。所以写代码的时候,要兼顾效率和可读性,别为了性能牺牲代码质量。