跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客我的书AI学习GitHub 精选镜像AI工具UI配色美学关于
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
C++算法

栈的合法出栈序列判断与卡特兰数规律

探讨栈的弹出序列合法性判断问题。介绍模拟栈操作的常规解法,时间复杂度为 O(n)。总结基于“元素后小元素严格逆序”的快速判断规律。分析 n 个元素的合法出栈序列总数,揭示其与卡特兰数的数学本质关系,并列举括号匹配、二叉树计数等卡特兰数的其他应用场景。

锁机制发布于 2026/3/21更新于 2026/10/26123 浏览
栈的合法出栈序列判断与卡特兰数规律

一、基础问题:怎么判断弹出序列是否合法?

先从一道经典算法题入手,题目描述如下:

输入两个整数序列,第一个是栈的压入顺序(所有数字不重复),请判断第二个序列是否可能是该栈的弹出顺序。 示例 1:压入 [1,2,3,4,5],弹出 [4,5,3,2,1] → 合法(返回 true) 示例 2:压入 [1,2,3,4,5],弹出 [4,3,5,1,2] → 不合法(返回 false)

1.1 常规解法:模拟栈操作

最直观的思路是'复现'栈的压入和弹出过程,步骤很简单:

  1. 遍历压入序列,把元素逐个压入栈;
  2. 每次压入后,检查栈顶元素是否和当前弹出序列的'待弹出元素'(用指针标记)一致;
  3. 如果一致,就弹出栈顶,并移动弹出指针;
  4. 遍历结束后,若栈为空,说明所有元素都按顺序弹出,序列合法;否则不合法。
代码实现(C++)
#include <vector>
#include <stack>
using namespace std;

bool IsPoporder(vector<int>& pushV, vector<int>& popV) {
    size_t popIdx = 0;
    stack<int> st;
    for (auto& num : pushV) {
        st.push(num);
        while (!st.empty() && st.top() == popV[popIdx]) {
            st.pop();
            popIdx++;
        }
    }
    return st.empty();
}
1.2 模拟过程示意图

以示例 1 压入 [1,2,3,4,5],弹出 [4,5,3,2,1] 为例,展示每一步的变化:

步骤压入元素栈内元素弹出指针 popIdx栈顶是否匹配 popV[popIdx]操作后栈内元素操作后 popIdx
11[1]01≠4 → 不匹配[1]0
22[1,2]02≠4 → 不匹配[1,2]0
33[1,2,3]03≠4 → 不匹配[1,2,3]0
44[1,2,3,4]04=4 → 匹配[1,2,3]1
55[1,2,3,5]15=5 → 匹配[1,2,3]2
6压入结束[1,2,3]23=3 → 匹配[1,2]3
7-[1,2]32=2 → 匹配[1]4
8-[1]41=1 → 匹配[]5

最终栈为空,返回 true。如果是示例 2,最后栈会残留元素,返回 false。

二、深入观察:出栈序列的隐藏规律

模拟法能解决问题,但如果笔试时没有编译器,能不能快速口算判断?通过分析多个按照顺序入栈的例子,发现了一个关键规律:

如果入栈是顺序入栈,出栈序列中,任意元素后面比它小的元素,必须保持严格逆序。

2.1 用例子验证规律
例子 1:合法序列 [4,5,3,2,1]
  • 看元素 4:后面比它小的元素是 [3,2,1] → 逆序(3>2>1),符合;
  • 看元素 5:后面比它小的元素是 [3,2,1] → 逆序,符合;
  • 看元素 3:后面比它小的是 [2,1] → 逆序,符合;
  • 所有元素都满足,所以合法。
例子 2:不合法序列 [4,3,5,1,2]
  • 重点看元素 5:后面比它小的元素是 [1,2] → 顺序(1<2),不符合规律;
  • 因此直接判断不合法,和题目结果一致。
再举个合法例子:[3,4,5,2,1]
  • 元素 3 后面比它小的是 [2,1] → 逆序;
  • 元素 4 后面比它小的是 [2,1] → 逆序;
  • 元素 5 后面比它小的是 [2,1] → 逆序;
  • 符合规律,确实是合法出栈序列。
2.2 规律的本质:为什么会这样?

栈的核心是'先进后出'(LIFO)。假设元素 A 比元素 B 先压入栈,若 A 还没弹出时 B 就弹出了,那么 A 必须在 B 之后弹出(因为 A 在 B 下面)。

对应到规律:如果元素 X 后面有比它小的元素 Y(Y 比 X 先压入),那么所有比 X 小且在 Y 之后压入的元素 Z,必须在 Y 之前弹出 —— 否则就违反了'先进后出',这就是'严格逆序'的由来。

三、基于规律的判断代码实现

既然规律成立,我们可以换一种思路写判断代码:先把压入序列的'数值'映射到'压入顺序下标'(比如压入 [2,3,4,5,1],则 2 的下标是 0,3 是 1,…,1 是 4),再检查弹出序列对应的下标是否满足'任意元素后比它小的元素严格逆序'。

代码实现(C++)
#include <vector>
#include <unordered_map>
using namespace std;

bool IsPoporderByRule(vector<int>& pushV, vector<int>& popV) {
    // 1. 建立'数值→压入下标'的映射,将问题转为'有序下标'的判断
    unordered_map<int, int> valToIdx;
    for (int i = 0; i < pushV.size(); ++i) {
        valToIdx[pushV[i]] = i;
        if (valToIdx.find(popV[i]) == valToIdx.end()) {
            return false;
        }
    }
    // 2. 把弹出序列转为对应的'压入下标'序列
    vector<int> popIdx;
    for (int num : popV) {
        popIdx.push_back(valToIdx[num]);
    }
    // 3. 检查规律:任意元素后比它小的元素是否严格逆序
    int n = popIdx.size();
    for (int i = 0; i < n; ++i) {
        for (int j = i + 1; j < n; ++j) {
            if (popIdx[j] < popIdx[i]) {
                for (int k = j + 1; k < n; ++k) {
                    if (popIdx[k] < popIdx[i] && popIdx[k] > popIdx[j]) {
                        return false;
                    }
                }
            }
        }
    }
    return true;
}

代码逻辑很简单:通过下标映射,把任意压入序列的问题,转化为'1,2,3,…n'有序压入的问题,再用规律验证即可。

四、探索出栈序列总数:邂逅卡特兰数

解决了'判断合法性',新问题来了:n 个元素的合法出栈序列有多少种?

比如:

  • n=1 → 1 种([1]);
  • n=2 → 2 种([1,2]、[2,1]);
  • n=3 → 5 种([123,132,213,231,321]);
  • n=4 → 14 种;n=5 → 42 种…
4.1 推导尝试

曾尝试基于'严格逆序'规律推导总数:以 n=5 为例,把最大元素 5 放在不同位置,认为 5 后面的元素必须逆序,前面的元素符合卡特兰数。

比如假设 5 在第 3 位,前面有 2 个元素(符合卡特兰数 C₂=2),从 4 个元素选 2 个放在前面(组合数 C₄²=6),总数就是 2×6=12。但实际计算时发现,这样会多算不合法的序列(如 [3,1,5,4,2],1 和 2 的顺序破坏了逆序),推导需修正。

4.2 卡特兰数:答案的数学本质

这个问题的答案已被数学家卡特兰解决:n 个元素的合法出栈序列数,等于第 n 个卡特兰数,其通项公式为:

C_n = 1/(n+1) * C(2n, n)

其中 C(2n, n) 是组合数,表示从 2n 个元素中选 n 个的方案数。

我们验证一下:

  • n=1:结果是 1 → 正确;
  • n=2:结果是 2 → 正确;
  • n=3:结果是 5 → 正确;
  • n=5:结果是 42 → 正确。
4.3 卡特兰数的更多应用(拓展)

卡特兰数不只是栈的专利,它还出现在很多场景:

  • 括号匹配:n 对括号的合法匹配数(如 n=2:()()、(()) → 2 种);
  • 二叉树计数:n 个节点的不同二叉树结构数(n=2:2 种);
  • 路径规划:从 (0,0) 到 (n,n) 不穿过对角线的路径数。

五、总结

  1. 判断弹出序列合法:两种方法
    • 模拟栈操作:效率 O(n),适合编程实现;
    • 规律判断:基于'元素后小元素严格逆序',适合快速口算。
  2. 出栈序列总数:第 n 个卡特兰数,公式是 C_n = 1/(n+1) * C(2n, n)。
  3. 数学本质:栈的'先进后出'规则,最终转化为卡特兰数的计数问题,体现了算法与数学的紧密联系。

目录

  1. 一、基础问题:怎么判断弹出序列是否合法?
  2. 1.1 常规解法:模拟栈操作
  3. 代码实现(C++)
  4. 1.2 模拟过程示意图
  5. 二、深入观察:出栈序列的隐藏规律
  6. 2.1 用例子验证规律
  7. 例子 1:合法序列 [4,5,3,2,1]
  8. 例子 2:不合法序列 [4,3,5,1,2]
  9. 再举个合法例子:[3,4,5,2,1]
  10. 2.2 规律的本质:为什么会这样?
  11. 三、基于规律的判断代码实现
  12. 代码实现(C++)
  13. 四、探索出栈序列总数:邂逅卡特兰数
  14. 4.1 推导尝试
  15. 4.2 卡特兰数:答案的数学本质
  16. 4.3 卡特兰数的更多应用(拓展)
  17. 五、总结

更多推荐文章

查看全部
  • 大模型加速智能终端升级:海尔、小米、OPPO 等实践案例
  • Spring MVC 快速入门:响应处理与报文格式设置
  • C++ 二叉搜索树:概念、性能分析与核心实现
  • Python FastAPI 入门指南:从零构建生产级 RESTful API
  • 前端高频面试题:TypeScript 核心考点与实战
  • Ubuntu 远程桌面安装配置指南
  • PX4+Gazebo+MAVROS 自定义无人机仿真控制实战
  • Llama-3.2V-11B 部署实战:GPU 显存优化与 Batch Size 调优
  • 使用 Java 将 PDF 转换为 Excel:基础转换与高级设置
  • Java 串口通信库 jSerialComm 跨平台开发指南
  • Qt Creator 配置 GitHub Copilot AI 辅助编程插件指南
  • Rust WebAssembly 与 Three.js 结合的 3D 数据可视化实战:高性能粒子系统
  • Python cv2 库:OpenCV 图像处理与计算机视觉基础
  • AI Agent 架构:基础组成模块深度解析
  • DiT(Diffusion Transformer)详解:架构与核心模块分析
  • 华为诺亚发布 PixArt-Σ:基于 DiT 架构的 0.6B 4K 文生图模型
  • Neo4j 图数据库核心特性与 K8s 集群部署实战
  • 常见黑客攻击方法及入侵流程解析
  • 职场经验谈:若无必要离职原因,建议谨慎跳槽
  • AI 赋能原则 10 解读:政府 2.0 与公共智能系统建设

相关免费在线工具

  • 加密/解密文本

    使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online

  • Gemini 图片去水印

    基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

  • HTML转Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online