news 2026/7/28 9:06:45

C++栈数据结构:从原理到实战,掌握std::stack与经典算法

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C++栈数据结构:从原理到实战,掌握std::stack与经典算法

1. 项目概述:为什么“栈”是C++入门的必修课?

如果你刚开始学习C++,可能已经接触了变量、循环、函数这些基础概念,感觉编程世界的大门正在缓缓打开。但当你开始尝试写一些稍微复杂的程序,比如解析一个数学表达式、检查一段代码的括号是否匹配,或者想理解函数调用背后发生了什么时,你很快就会遇到一个瓶颈。这时,一个名为“栈”的数据结构就会成为你绕不开的核心课题。它不像“Hello World”那样直观,却是连接你所学的基础语法和真正解决实际问题能力之间的关键桥梁。我见过太多新手卡在这里,觉得抽象难懂,但一旦捅破这层窗户纸,你对程序运行的理解会立刻提升一个维度。

简单来说,栈是一种“后进先出”的数据集合,就像我们生活中叠放的盘子,你总是取走最上面的那个,也就是最后放上去的那个。在C++的世界里,栈的身影无处不在:函数调用时局部变量的存储、表达式求值、浏览器的前进后退功能,甚至是算法中经典的“括号匹配”问题,都离不开栈的支撑。理解栈,不仅仅是学会使用std::stack这个容器,更是理解计算机管理内存和执行逻辑的一种根本方式。对于初学者,掌握栈意味着你开始从“写代码”向“设计程序逻辑”迈进。接下来,我会带你从零开始,彻底搞懂C++中的栈,包括它的原理、标准库用法、自己动手实现,以及如何用它解决实际问题,避开我当年踩过的那些坑。

2. 栈的核心原理与抽象模型

2.1 “后进先出”的哲学与生活类比

栈最核心的特性就是LIFO,即“后进先出”。这个概念听起来有点学术,但其实在生活中比比皆是。最经典的例子就是一摞书或者一叠盘子:你只能从最顶部放入新的盘子,也只能从最顶部取走盘子。你无法直接抽走中间或底部的盘子,除非先把上面的都搬走。在编程中,这个“顶部”我们称之为“栈顶”,而底部则称为“栈底”。所有操作都只发生在栈顶。

另一个更贴近程序员的例子是“撤销”功能。你在文本编辑器里每输入一个字符,这个操作就被“压入”一个历史记录栈。当你按下Ctrl+Z时,编辑器就从栈顶“弹出”最近的一次操作并撤销它。连续按撤销,就会按倒序依次回退,这正是后进先出的体现。理解这个模型至关重要,因为它决定了栈的所有行为:你只能访问栈顶元素,想要处理下面的元素,必须先让上面的元素出栈。

2.2 栈的ADT:定义一套标准操作接口

在具体用代码实现之前,我们先用抽象数据类型来定义栈应该支持哪些操作。这就像定义一份“功能清单”,无论底层是用数组还是链表实现,这份清单不变:

  1. push(value): 入栈操作。将一个元素value添加到栈顶。类比为把一本新书放到书堆的最上面。
  2. pop(): 出栈操作。移除并返回栈顶的元素。注意,有些实现只移除不返回,标准库的std::stack就是如此,它用top()来获取。这相当于从书堆顶拿走一本书。
  3. top(): 获取栈顶元素。只查看栈顶是哪个元素,但不移除它。就像你看一眼最上面那本书的书名,但不拿走它。
  4. empty(): 判断栈是否为空。检查书堆里还有没有书。
  5. size(): 获取栈中当前元素的数量。数一数书堆有多高。

这套ADT是通用的。在C++标准库中,std::stack就是一个完美实现了这些操作的容器适配器。但作为初学者,我强烈建议你不要满足于直接调用std::stack,亲手用数组或链表实现一遍,是理解其内存管理和边界情况最有效的方式。我刚开始学的时候,觉得调用库函数就行,直到一次面试被要求白板实现一个栈并处理边界错误,才意识到亲手实现的重要性。

注意pop()操作在std::stack中有一个容易让人困惑的设计:它只移除栈顶元素,并不返回被移除的元素的值。你需要先用top()获取值,再调用pop()移除。这是为了避免因返回值拷贝可能引发的异常安全问题,是C++标准库设计中的一个经典取舍。

3. C++标准库中的栈:std::stack深度解析

3.1 容器适配器:std::stack的本质

很多新手会误以为std::stack是一个独立的容器,像std::vector一样自己管理内存。其实不然,它是一个“容器适配器”。这意味着它底层依赖于另一个容器(如std::deque,std::list,std::vector)来实际存储数据,它只是在这个底层容器之上,封装了一套严格的LIFO操作接口。

默认情况下,std::stack使用std::deque作为其底层容器。deque(双端队列)在头部和尾部进行插入删除的效率都很高,这很适合栈只在“一端”操作的需求。你可以通过模板的第二个参数来指定底层容器类型:

#include <stack> #include <vector> #include <list> int main() { // 默认,底层使用 std::deque<int> std::stack<int> stack1; // 显式指定底层容器为 std::vector<int> std::stack<int, std::vector<int>> stack2; // 显式指定底层容器为 std::list<int> std::stack<int, std::list<int>> stack3; return 0; }

选择不同的底层容器会带来细微的性能差异。std::vector在连续内存上操作,访问速度快,但当容量不足需要重新分配内存时,会有性能开销。std::list是链表,内存不连续,插入删除是常数时间,但元素访问可能慢一些。对于绝大多数入门和中级应用场景,使用默认的std::deque是完全足够且性能均衡的选择。除非你有极特殊的性能瓶颈需要优化,否则不必纠结于此。

3.2 基本操作实战与易错点

让我们通过一个完整的例子来演示std::stack的基本操作,并指出其中的关键细节。

#include <iostream> #include <stack> #include <string> int main() { std::stack<std::string> history; // 创建一个存储字符串的栈,模拟浏览器历史记录 // 1. 入栈操作 push history.push("www.homepage.com"); history.push("www.news.com"); history.push("www.shopping.com"); std::cout << "访问了三个网页后,历史记录栈大小: " << history.size() << std::endl; // 2. 查看栈顶 top std::cout << "当前所在页面(栈顶): " << history.top() << std::endl; // 输出: www.shopping.com // 3. 出栈操作 pop (模拟点击后退按钮) history.pop(); // 后退到 news.com std::cout << "点击后退后,当前页面: " << history.top() << std::endl; // 输出: www.news.com std::cout << "此时栈大小: " << history.size() << std::endl; // 输出: 2 // 4. 判断栈是否为空 empty while (!history.empty()) { std::cout << "正在后退,离开: " << history.top() << std::endl; history.pop(); } std::cout << "历史记录已清空,栈是否为空? " << (history.empty() ? "是" : "否") << std::endl; // !!! 危险操作:在空栈上调用 top() 或 pop() // std::cout << history.top(); // 未定义行为,程序可能崩溃或输出垃圾值 // history.pop(); // 同样,未定义行为 return 0; }

实操心得与避坑指南:

  1. 空栈检查是必须的:在调用top()pop()之前,永远要检查栈是否为空。对空栈进行这些操作会导致“未定义行为”,这意味着程序可能崩溃、产生错误结果或表现出任何奇怪的行为,这是C++程序中最难调试的错误之一。养成if (!stack.empty()) { ... }的条件反射。
  2. pop()不返回值:这是std::stack设计上故意为之,但很容易被忘记。如果你需要获取被移除的元素,必须遵循“先top(),后pop()”的模式。
    // 正确做法 int topValue = myStack.top(); // 先获取值 myStack.pop(); // 再移除 // 错误做法(编译不通过) // int value = myStack.pop();
  3. 栈没有迭代器:你不能像遍历vector那样用for (auto it = stack.begin(); ...)来遍历栈。因为栈的LIFO特性决定了你只能访问栈顶。如果你想遍历栈中的所有元素,通常需要将元素依次弹出到另一个辅助栈或容器中,这本身就是栈的典型应用场景之一。

4. 从零实现一个栈:数组与链表两种方案

理解了接口,我们来动手实现。这能让你透彻理解栈的底层机制和边界处理。我会分别用动态数组和单链表来实现。

4.1 基于动态数组的实现

用数组实现栈,我们需要维护一个数组(底层存储)、一个栈顶索引(指向下一个可插入位置)和总容量。

#include <iostream> #include <stdexcept> // 用于抛出标准异常 template <typename T> class ArrayStack { private: T* data; // 指向动态数组的指针 int topIndex; // 栈顶索引(指向下一个空位) int capacity; // 数组总容量 // 扩容函数(私有辅助函数) void resize(int newCapacity) { T* newData = new T[newCapacity]; for (int i = 0; i < topIndex; ++i) { newData[i] = data[i]; // 拷贝原有数据 } delete[] data; // 释放旧数组 data = newData; capacity = newCapacity; std::cout << "[调试] 栈已扩容,新容量: " << capacity << std::endl; } public: // 构造函数 ArrayStack(int initCapacity = 10) : capacity(initCapacity), topIndex(0) { data = new T[capacity]; } // 析构函数:释放动态内存 ~ArrayStack() { delete[] data; } // 拷贝构造函数和赋值运算符(规则三,此处为简化略过,但生产代码必须实现) // 入栈 void push(const T& value) { // 检查容量是否已满 if (topIndex == capacity) { resize(capacity * 2); // 经典策略:容量翻倍 } data[topIndex++] = value; // 存入数据,栈顶索引+1 } // 出栈 void pop() { if (empty()) { throw std::out_of_range("栈为空,无法执行pop操作"); } --topIndex; // 栈顶索引-1即可,逻辑上移除元素 // 可选:如果元素数远小于容量,可以缩容以节省空间 if (topIndex > 0 && topIndex == capacity / 4) { resize(capacity / 2); } } // 获取栈顶元素 T& top() { if (empty()) { throw std::out_of_range("栈为空,无法获取top元素"); } return data[topIndex - 1]; // 栈顶元素在 topIndex-1 的位置 } const T& top() const { // const版本,用于const对象 if (empty()) { throw std::out_of_range("栈为空,无法获取top元素"); } return data[topIndex - 1]; } // 判断栈是否为空 bool empty() const { return topIndex == 0; } // 获取栈大小 int size() const { return topIndex; } }; // 测试代码 int main() { ArrayStack<int> stack(5); // 初始容量5 for (int i = 1; i <= 10; ++i) { stack.push(i * 10); std::cout << "入栈: " << i * 10 << ", 栈大小: " << stack.size() << std::endl; } std::cout << "\n栈顶元素: " << stack.top() << std::endl; // 应为100 while (!stack.empty()) { std::cout << "出栈: " << stack.top() << std::endl; stack.pop(); } // 测试空栈异常 try { stack.pop(); } catch (const std::out_of_range& e) { std::cerr << "捕获异常: " << e.what() << std::endl; } return 0; }

数组实现的要点与陷阱:

  1. 动态扩容策略:这是核心。当数组满时,简单的做法是申请一个更大的新数组(通常是原容量的2倍),将旧数据拷贝过去,然后释放旧数组。翻倍扩容能在摊还分析下达到O(1)的平均时间复杂度。缩容策略(当元素很少时减少容量)可以节省内存,但操作要谨慎,避免在边界附近频繁扩容缩容。
  2. 栈顶指针的设计:我这里的topIndex指向“下一个空闲位置”。也有人设计成指向“当前栈顶元素”。两种都可以,但要保持所有操作逻辑一致。我更喜欢“指向下一个空闲位置”,因为这样初始状态topIndex=0很自然,size()直接返回topIndex
  3. 异常安全:在push中,如果new分配内存失败,会抛出std::bad_alloc异常。我们的代码在抛出异常时,栈的旧状态保持不变(因为先分配新内存,成功后再替换和删除旧的),这是比较好的做法。
  4. 内存管理:务必在析构函数中delete[] data,否则内存泄漏。对于更健壮的实现,还需要实现拷贝构造函数和赋值运算符(遵循“三法则”或“五法则”),防止浅拷贝导致重复释放内存。这里为简化示例省略了。

4.2 基于单链表的实现

链表实现不需要预先分配固定容量,每次入栈动态分配一个节点,理论上只要内存够就可以一直增长。

#include <iostream> #include <stdexcept> template <typename T> class LinkedListStack { private: // 链表节点定义 struct Node { T data; Node* next; Node(const T& val, Node* nxt = nullptr) : data(val), next(nxt) {} }; Node* topNode; // 指向栈顶节点的指针 int stackSize; public: LinkedListStack() : topNode(nullptr), stackSize(0) {} ~LinkedListStack() { // 析构时清空所有节点,防止内存泄漏 while (!empty()) { pop(); } } // 入栈:在链表头部插入新节点 void push(const T& value) { Node* newNode = new Node(value, topNode); // 新节点的next指向原栈顶 topNode = newNode; // 更新栈顶指针为新节点 ++stackSize; } // 出栈:删除链表头部节点 void pop() { if (empty()) { throw std::out_of_range("栈为空,无法执行pop操作"); } Node* nodeToDelete = topNode; topNode = topNode->next; // 栈顶指针下移 delete nodeToDelete; // 释放原栈顶节点内存 --stackSize; } // 获取栈顶元素 T& top() { if (empty()) { throw std::out_of_range("栈为空,无法获取top元素"); } return topNode->data; } const T& top() const { if (empty()) { throw std::out_of_range("栈为空,无法获取top元素"); } return topNode->data; } bool empty() const { return topNode == nullptr; // 栈顶指针为空即栈空 } int size() const { return stackSize; // 用一个变量维护大小,比遍历链表快 } }; // 测试代码与数组栈类似,此处省略

链表实现的要点与对比:

  1. 内存开销:每个元素都需要一个额外的节点对象(包含数据和next指针),内存开销比数组大。但对于元素本身很大的对象,这个开销占比相对变小。
  2. 操作复杂度:所有栈操作(push,pop,top,empty)都是严格的O(1)时间复杂度,且没有数组的扩容拷贝开销。
  3. 内存碎片:频繁的newdelete可能导致内存碎片。在实际项目中,对于性能敏感的栈,有时会使用内存池来管理节点。
  4. 选择建议:对于元素类型简单、数量可预估的场景,数组栈通常性能更好(缓存友好)。对于元素数量变化剧烈、或元素本身很大的场景,链表栈可以避免扩容拷贝的代价。作为学习,两种都实现一遍对理解指针和内存管理大有裨益。

5. 栈的经典应用场景与算法实战

理解了栈怎么用和怎么造,现在来看看它能解决哪些实际问题。这是将知识转化为能力的关键。

5.1 括号匹配检查器

这是栈最经典的教学案例。问题描述:给定一个只包含()[]{}的字符串,判断其中的括号是否匹配正确。例如,“([{}])”正确,“([)]”错误。

思路:遍历字符串,遇到左括号就入栈;遇到右括号,检查栈顶的左括号是否与之匹配,如果匹配则弹出栈顶,继续;如果不匹配或栈已空,则字符串无效。遍历结束后,如果栈为空,说明所有括号都正确匹配。

#include <iostream> #include <stack> #include <string> #include <unordered_map> bool isValidParentheses(const std::string& s) { std::stack<char> stk; // 用哈希表建立右括号到左括号的映射,方便匹配检查 std::unordered_map<char, char> pair = { {')', '('}, {']', '['}, {'}', '{'} }; for (char c : s) { if (c == '(' || c == '[' || c == '{') { // 左括号,入栈 stk.push(c); } else if (c == ')' || c == ']' || c == '}') { // 右括号,检查匹配 // 情况1:栈为空,说明没有对应的左括号 // 情况2:栈顶左括号与当前右括号不匹配 if (stk.empty() || stk.top() != pair[c]) { return false; } // 匹配成功,弹出栈顶左括号 stk.pop(); } // 其他字符可以忽略,或者根据题目要求处理 } // 最后栈必须为空,所有左括号都被匹配 return stk.empty(); } int main() { std::string test1 = "([{}])"; std::string test2 = "([)]"; std::string test3 = "((()))"; std::string test4 = "({[}])"; std::cout << test1 << " : " << (isValidParentheses(test1) ? "有效" : "无效") << std::endl; std::cout << test2 << " : " << (isValidParentheses(test2) ? "有效" : "无效") << std::endl; std::cout << test3 << " : " << (isValidParentheses(test3) ? "有效" : "无效") << std::endl; std::cout << test4 << " : " << (isValidParentheses(test4) ? "有效" : "无效") << std::endl; return 0; }

为什么栈是解决此问题的完美数据结构?因为有效的括号序列具有“最近相关性”。一个右括号必须与它前面最近的、未被匹配的左括号配对。栈的LIFO特性正好能让我们快速访问和移除这个“最近”的元素。

5.2 表达式求值(中缀转后缀)

计算像3 + 4 * 2 / ( 1 - 5 )这样的中缀表达式是栈的另一个王牌应用。直接计算中缀表达式需要考虑运算符优先级和括号,非常复杂。更优雅的方法是先将其转换为后缀表达式(逆波兰表达式),再求值。后缀表达式没有括号,运算符在操作数之后,如3 4 2 * 1 5 - / +,其求值规则非常简单,也天然适合栈来处理。

中缀转后缀算法(调度场算法)思路:

  1. 初始化一个操作数栈(或输出队列)和一个运算符栈。
  2. 从左到右扫描中缀表达式。
  3. 遇到数字,直接输出(加入操作数队列)。
  4. 遇到运算符(+ - * /):
    • 如果运算符栈为空,或栈顶是左括号(,则直接入栈。
    • 否则,比较当前运算符与栈顶运算符的优先级。只要栈顶运算符优先级不低于当前运算符,且栈顶不是左括号,就不断将栈顶运算符弹出并输出。最后将当前运算符入栈。
  5. 遇到左括号(,直接入栈。
  6. 遇到右括号),不断将运算符栈顶的运算符弹出并输出,直到遇到左括号(为止。将左括号弹出(不输出)。
  7. 表达式扫描完毕后,将运算符栈中剩余的所有运算符依次弹出并输出。

后缀表达式求值思路:

  1. 初始化一个操作数栈。
  2. 从左到右扫描后缀表达式。
  3. 遇到数字,入栈。
  4. 遇到运算符,从栈中弹出两个操作数(注意顺序:先弹出的是右操作数,后弹出的是左操作数),进行运算,将结果入栈。
  5. 扫描结束,栈中剩下的唯一数字就是表达式的结果。

由于实现代码较长,这里给出核心的运算符优先级比较和转换函数框架:

#include <stack> #include <string> #include <cctype> #include <vector> #include <iostream> #include <sstream> // 获取运算符优先级 int getPriority(char op) { if (op == '+' || op == '-') return 1; if (op == '*' || op == '/') return 2; return 0; // 其他字符,如括号 } // 中缀表达式字符串转后缀表达式(字符串向量) std::vector<std::string> infixToPostfix(const std::string& infix) { std::vector<std::string> postfix; // 存储后缀表达式 std::stack<char> opStack; // 运算符栈 std::istringstream iss(infix); std::string token; while (iss >> token) { // 假设表达式以空格分隔,简化处理 if (isdigit(token[0])) { // 是数字,直接输出 postfix.push_back(token); } else if (token == "(") { opStack.push('('); } else if (token == ")") { while (!opStack.empty() && opStack.top() != '(') { postfix.push_back(std::string(1, opStack.top())); opStack.pop(); } opStack.pop(); // 弹出左括号 } else { // 是运算符 + - * / while (!opStack.empty() && getPriority(opStack.top()) >= getPriority(token[0])) { postfix.push_back(std::string(1, opStack.top())); opStack.pop(); } opStack.push(token[0]); } } // 处理栈中剩余运算符 while (!opStack.empty()) { postfix.push_back(std::string(1, opStack.top())); opStack.pop(); } return postfix; } // 后缀表达式求值(需处理字符串到数字的转换) int evaluatePostfix(const std::vector<std::string>& postfix) { std::stack<int> valStack; for (const auto& token : postfix) { if (isdigit(token[0])) { valStack.push(std::stoi(token)); } else { int right = valStack.top(); valStack.pop(); int left = valStack.top(); valStack.pop(); switch (token[0]) { case '+': valStack.push(left + right); break; case '-': valStack.push(left - right); break; case '*': valStack.push(left * right); break; case '/': valStack.push(left / right); break; // 注意除零错误 } } } return valStack.top(); }

注意:这是一个简化版本,未处理负数、浮点数、多位数(需要更复杂的词法分析)以及除零等错误。但它清晰地展示了栈在表达式处理中的核心作用:运算符栈用于管理优先级和括号,操作数栈用于存储中间计算结果

5.3 函数调用栈与递归

这是栈在计算机系统层面最根本的应用,理解它对你调试程序至关重要。当你调用一个函数时,系统(或编译器)会自动维护一个“调用栈”:

  1. 调用时:将当前函数的返回地址、参数、局部变量等信息“压入”栈中,这个信息块称为“栈帧”或“活动记录”。
  2. 执行被调函数:函数在自己的栈帧空间内操作。
  3. 返回时:函数执行完毕,将其栈帧“弹出”,程序根据栈帧中保存的返回地址,跳回到调用者函数继续执行。

递归函数是这种机制的极致体现。每次递归调用都会压入一个新的栈帧。如果递归层数过深(比如没有终止条件或条件设置错误),就会导致“栈溢出”,因为系统的调用栈空间是有限的。

#include <iostream> void recursiveFunction(int n) { std::cout << "递归层数: " << n << std::endl; if (n == 0) return; // 基线条件,防止无限递归 recursiveFunction(n - 1); // 递归调用,新的栈帧被压入 std::cout << "返回层数: " << n << std::endl; } int main() { recursiveFunction(3); return 0; }

输出会是:

递归层数: 3 递归层数: 2 递归层数: 1 递归层数: 0 返回层数: 1 返回层数: 2 返回层数: 3

你可以清晰地看到“递”的过程(不断压栈)和“归”的过程(依次弹栈)。调试递归程序时,在脑海中模拟这个调用栈,是定位问题最快的方法。

6. 进阶话题:单调栈及其应用

当你对基础栈运用自如后,可以挑战一个强大的变种:单调栈。它常用于解决“下一个更大/更小元素”这类问题,能在O(n)时间复杂度内完成。

单调栈定义:栈内的元素(通常是索引)按照某种顺序(单调递增或单调递减)排列。

经典问题:每日温度。给定一个温度列表T,要求返回一个列表,表示对于每一天,你至少需要等待多少天才能等到一个更暖和的温度。如果之后都不会更暖和,则用0表示。

暴力解法是对于每一天i,向后遍历找到第一个T[j] > T[i],时间复杂度O(n²)。单调栈解法可以优化到O(n):

#include <vector> #include <stack> std::vector<int> dailyTemperatures(const std::vector<int>& T) { int n = T.size(); std::vector<int> answer(n, 0); std::stack<int> stk; // 栈里存的是下标,且下标对应的温度值是单调递减的 for (int i = 0; i < n; ++i) { // 当前温度 T[i] 比栈顶那天的温度高? // 如果是,说明对于栈顶那天来说,i 就是它等待的“更暖和”的一天 while (!stk.empty() && T[i] > T[stk.top()]) { int prevDay = stk.top(); stk.pop(); answer[prevDay] = i - prevDay; // 计算等待天数 } // 当前这天入栈,等待未来的某天比它更暖和 stk.push(i); } // 栈中剩余的日子,answer已经初始化为0,表示没有更暖和的日子 return answer; }

核心思想:维护一个温度值单调递减的栈(栈底到栈顶温度递减)。遍历每一天,如果当前温度高于栈顶那天的温度,就找到了栈顶那天的答案,弹出栈顶并计算天数差。重复此过程直到栈空或当前温度不再高于栈顶温度,然后将当前这天入栈。这样,每个元素最多入栈和出栈一次,时间复杂度O(n)。

单调栈的思路非常巧妙,是面试中的高频考点。理解它的关键在于,栈里存放的是“尚未找到答案”的元素的索引,并且它们保持着一种有序性,使得我们能用当前元素高效地更新这些“未解之谜”的答案。

7. 常见问题、调试技巧与性能考量

7.1 栈的常见使用误区

  1. 混淆stack.top()stack.pop():这是新手最常犯的错误。记住,top()只读,pop()只删。需要获取并移除时,必须分两步。
  2. 未检查空栈:在循环pop()或调用top()前,务必用empty()检查。这是防御性编程的基本功。
  3. 试图遍历栈std::stack没有迭代器。如果需要遍历,要么用辅助栈,要么考虑换用dequevector
  4. 误用栈解决所有问题:栈适合解决具有“后进先出”或“最近相关性”的问题。对于需要随机访问或先进先出的问题,应选择其他数据结构(如队列、向量)。

7.2 调试与性能分析

  • 可视化调试:在调试栈相关算法(如括号匹配、表达式求值)时,在关键步骤打印出栈的当前内容,是理解程序逻辑最直观的方法。你可以写一个辅助函数来打印栈(注意,打印需要拷贝栈,因为不能破坏原栈)。
  • 性能考量
    • 时间复杂度push,pop,top,empty,size在标准库实现和正确的自定义实现中都是O(1)。
    • 空间复杂度:除了存储元素本身,数组栈可能有未使用的预留空间,链表栈有节点指针开销。
    • 缓存友好性:基于数组(或std::vector/std::deque)实现的栈,其元素在内存中连续存储,对CPU缓存更友好,访问速度通常更快。链表栈的节点分散在内存中,缓存不命中率高,可能影响性能。
  • std::stack的底层容器选择:再次强调,默认的deque是通用选择。如果你需要频繁在栈中间进行访问(这违背栈的本意,但有时需要),或者对内存连续性有要求,可以考虑vector。但注意vector在扩容时可能导致迭代器失效。

7.3 栈溢出与递归深度

在函数调用或深度递归时,如果栈帧过多,超过系统或线程为调用栈分配的内存空间,就会发生“栈溢出”,程序会崩溃(如段错误)。在写递归算法时,务必确保有正确的终止条件(基线条件),并且对于可能深度很大的问题(如处理超深树或链表),考虑使用迭代+显式栈(手动模拟调用栈)来避免系统调用栈的溢出。

// 递归版本的二叉树前序遍历(可能导致栈溢出) void preorderRecursive(TreeNode* root) { if (!root) return; visit(root); preorderRecursive(root->left); preorderRecursive(root->right); } // 迭代版本,使用显式栈(更安全,可控) void preorderIterative(TreeNode* root) { if (!root) return; std::stack<TreeNode*> stk; stk.push(root); while (!stk.empty()) { TreeNode* node = stk.top(); stk.pop(); visit(node); // 注意入栈顺序:先右后左,保证出栈顺序是根->左->右 if (node->right) stk.push(node->right); if (node->left) stk.push(node->left); } }

从数组和链表的底层实现,到标准库的熟练使用,再到解决括号匹配、表达式求值等经典问题,最后触及单调栈和系统调用栈的深度,栈的学习路径清晰地展示了一个数据结构如何从抽象概念成长为解决实际问题的利器。我个人的体会是,学习栈最大的收获不是记住了pushpop,而是学会了用“后进先出”的视角去分析问题。当你再遇到需要处理“最近”、“嵌套”、“撤销”这类场景时,栈就会成为你思维工具箱里第一个被想到的选项。多写代码,多调试,亲手实现一遍,遇到问题多画图模拟栈的变化,这是掌握栈,乃至任何数据结构最扎实的方法。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/28 9:04:05

如何部署Not Quite RARBG:基于PM2的Node.js服务配置教程

如何部署Not Quite RARBG&#xff1a;基于PM2的Node.js服务配置教程 【免费下载链接】main Not Quite RARBGs main website. 项目地址: https://gitcode.com/gh_mirrors/main1/main Not Quite RARBG是一个基于Node.js构建的服务应用&#xff0c;通过PM2进行进程管理可以…

作者头像 李华
网站建设 2026/7/28 9:03:32

基于Arduino与Python的水结冰过程自动监测系统设计与实现

1. 项目概述&#xff1a;当创客思维遇上经典物理实验“水结冰了”这个现象&#xff0c;听起来像是小学自然课上的内容&#xff0c;但如果你用创客的视角重新审视它&#xff0c;就会发现一个充满探索乐趣和工程挑战的全新世界。这不仅仅是观察一个物态变化&#xff0c;而是将传感…

作者头像 李华
网站建设 2026/7/28 9:01:28

2026年6月北京市房山区二手房价格深度分析

一、报告概述 本报告基于2026年6月北京市房山区多个典型小区的实际二手房成交案例&#xff0c;从成交价格、户型结构、区域分布、市场趋势等维度进行深度分析&#xff0c;旨在为购房者、投资者及行业从业者提供真实、客观的市场参考。 核心结论&#xff1a;2026年6月房山区二…

作者头像 李华
网站建设 2026/7/28 9:01:16

CSDN技术社区高效使用指南与经验分享

1. 八年老用户的CSDN生存指南 2016年3月&#xff0c;我在解决一个诡异的Spring事务传播问题时第一次注册了CSDN账号。当时为了下载某个2积分的配置文件&#xff0c;不得不注册并完成了新手任务。没想到这个偶然的决定&#xff0c;让我在这个技术社区扎根了整整八年。作为早期用…

作者头像 李华
网站建设 2026/7/28 8:58:48

从微瓦到稳定供电:TI bq25570能量收集电源管理芯片评估与设计指南

1. 项目概述与核心价值 如果你正在为那些部署在偏远角落、难以更换电池的物联网传感器或者可穿戴设备寻找供电方案&#xff0c;那么能量收集技术很可能就是你需要的答案。这项技术的核心&#xff0c;就是从我们身边无处不在的微弱环境能量——比如室内光照、微小的温差、甚至机…

作者头像 李华