C++ 标准库(STL, Standard Template Library)提供了种类丰富的容器,包括序列容器(如 vectordequelist)、关联容器(如 mapset)以及适配器容器(如 stackqueuepriority_queue)。在这些容器中,<stack> 是一种以“后进先出”(LIFO, Last In First Out)为核心特性的适配器容器,经常用于解决递归、括号匹配、路径回溯等问题。

作为一种容器适配器<stack> 并不直接存储数据,而是基于底层容器(如 dequevector)提供了一组符合 LIFO 特性的操作接口,这使它在设计层面更加灵活。本文将全面探讨 C++ 标准库中的 stack,从基础概念、API 用法、内部实现到性能分析,结合实际案例,帮助您深入理解和高效使用这一容器。


第一部分:什么是 <stack>

<stack> 是 C++ STL 中的一种容器适配器,它不直接存储元素,而是依赖底层容器(默认是 deque)实现功能。<stack> 的核心特性是 后进先出(LIFO),即最新添加的元素最先被移除。

1.1 栈的基本概念

栈是一种数据结构,遵循“后进先出”的规则,类似于一叠盘子,最先放入的盘子在最底层,最后放入的盘子在最顶层。栈只允许在一端(称为“栈顶”)进行插入和删除操作。

栈的基本操作包括:

  • 压栈(Push):在栈顶插入元素。
  • 出栈(Pop):从栈顶移除元素。
  • 访问栈顶元素(Top):查看但不移除栈顶的元素。
  • 检查栈是否为空(Empty)
  • 获取栈的大小(Size)

1.2 栈在 C++ 中的位置

在 C++ 中,stack 是一种 容器适配器,定义在头文件 <stack> 中。容器适配器是一种通过修改现有容器(比如 vectordeque)行为而构建的“适配器”,它只暴露特定的接口,并隐藏底层实现的复杂性。

底层容器选择

stack 可以使用以下容器作为其底层实现:

  • deque(默认):动态双端队列,支持高效的头尾插入和删除。
  • vector:动态数组,支持高效的尾部插入,但头部操作效率较低。
  • list:双向链表,适合频繁插入和删除操作。

开发者可以根据实际需求选择合适的底层容器:

#include <stack>
#include <vector>
#include <deque>
#include <list>

// 使用 vector 作为底层容器
std::stack<int, std::vector<int>> stackUsingVector;

// 使用 list 作为底层容器
std::stack<int, std::list<int>> stackUsingList;

栈的接口

stack 提供了一组有限的接口,专注于栈的核心功能:

函数

描述

push()

在栈顶插入一个新元素

pop()

移除栈顶元素

top()

返回栈顶元素的引用,但不移除

empty()

检查栈是否为空,返回一个布尔值

size()

返回栈中元素的数量

emplace()

原地构造一个元素并将其压入栈顶(C++11 引入)


第二部分:快速入门

以下是使用 <stack> 的基本示例代码。

2.1 初始化和基本操作

#include <iostream>
#include <stack>

int main() {
    std::stack<int> myStack; // 定义一个空栈

    // 压栈操作
    myStack.push(10);
    myStack.push(20);
    myStack.push(30);

    std::cout << "栈顶元素: " << myStack.top() << std::endl; // 输出: 30

    // 出栈操作
    myStack.pop(); // 移除栈顶元素
    std::cout << "栈顶元素: " << myStack.top() << std::endl; // 输出: 20

    // 检查栈是否为空
    if (!myStack.empty()) {
        std::cout << "栈不为空,大小为: " << myStack.size() << std::endl;
    }

    return 0;
}

运行结果:

栈顶元素: 30
栈顶元素: 20
栈不为空,大小为: 2

2.2 使用自定义底层容器

虽然 deque 是默认的底层容器,但您也可以选择 vectorlist 作为底层容器:

#include <iostream>
#include <stack>
#include <vector>
#include <list>

int main() {
    // 使用 vector 作为底层容器
    std::stack<int, std::vector<int>> stackWithVector;
    stackWithVector.push(1);
    stackWithVector.push(2);

    // 使用 list 作为底层容器
    std::stack<int, std::list<int>> stackWithList;
    stackWithList.push(3);
    stackWithList.push(4);

    std::cout << "stackWithVector 栈顶: " << stackWithVector.top() << std::endl;
    std::cout << "stackWithList 栈顶: " << stackWithList.top() << std::endl;

    return 0;
}

第三部分:高级特性

3.1 原地构造元素(emplace

C++11 引入了 emplace() 方法,允许直接在栈顶构造对象,避免了不必要的拷贝或移动操作。

#include <iostream>
#include <stack>
#include <string>

int main() {
    std::stack<std::string> myStack;

    // 使用 emplace 原地构造字符串
    myStack.emplace("Hello");
    myStack.emplace("World");

    std::cout << "栈顶元素: " << myStack.top() << std::endl; // 输出: World

    return 0;
}

3.2 自定义对象作为栈元素

stack 可以存储任何类型的对象,包括自定义的类对象。以下是一个示例:

#include <iostream>
#include <stack>

struct Point {
    int x, y;
    Point(int x, int y) : x(x), y(y) {}
};

int main() {
    std::stack<Point> pointStack;

    // 压栈
    pointStack.emplace(1, 2);
    pointStack.emplace(3, 4);

    // 访问栈顶元素
    Point topPoint = pointStack.top();
    std::cout << "栈顶点: (" << topPoint.x << ", " << topPoint.y << ")" << std::endl;

    return 0;
}

运行结果:

栈顶点: (3, 4)

3.3 与其他容器的转换

尽管 stack 是一个容器适配器,但您可以通过底层容器的特性来实现与其他容器的转换。例如,将 stack 数据导出到 vector

#include <iostream>
#include <stack>
#include <vector>

int main() {
    std::stack<int> myStack;

    // 压栈
    myStack.push(10);
    myStack.push(20);
    myStack.push(30);

    // 导出到 vector
    std::vector<int> myVector;
    while (!myStack.empty()) {
        myVector.push_back(myStack.top());
        myStack.pop();
    }

    // 输出 vector 中的数据
    for (int elem : myVector) {
        std::cout << elem << " ";
    }

    return 0;
}

运行结果:

30 20 10

第四部分:性能分析

4.1 时间复杂度

操作

时间复杂度

push()

O(1)

pop()

O(1)

top()

O(1)

empty()

O(1)

size()

O(1)

stack 的所有操作均具有常数时间复杂度,这使得它非常适合高效的栈操作。

4.2 底层容器对性能的影响

  • deque(默认):
    提供高效的头尾插入和删除操作,是实现 stack 的推荐选择。
  • vector
    尾部插入性能高,但在动态扩容时会有一定的额外开销。
  • list
    插入和删除性能高,但由于链表节点的额外指针存储开销,内存使用效率较低。

第五部分:实际案例

5.1 括号匹配

stack 是解决括号匹配问题的经典工具:

#include <iostream>
#include <stack>

bool isValid(const std::string& str) {
    std::stack<char> s;
    for (char ch : str) {
        if (ch == '(' || ch == '{' || ch == '[') {
            s.push(ch);
        } else {
            if (s.empty()) return false;
            char top = s.top();
            if ((ch == ')' && top == '(') ||
                (ch == '}' && top == '{') ||
                (ch == ']' && top == '[')) {
                s.pop();
            } else {
                return false;
            }
        }
    }
    return s.empty();
}

int main() {
    std::string input = "{[()]}";
    std::cout << (isValid(input) ? "合法" : "不合法") << std::endl;
    return 0;
}

运行结果:

合法

5.2 深度优先搜索(DFS)

stack 常用于实现图的深度优先搜索(DFS)算法。


第六部分:注意事项

6.1 不支持遍历

stack 没有提供迭代器接口,无法直接进行遍历。如果需要遍历栈中的元素,可以使用底层容器或临时变量存储。

6.2 选择合适的底层容器

根据性能需求选择合适的底层容器。例如,deque 是默认的选择,但如果操作过程非常简单且以较少的内存占用为目标,可以选择 vector


总结

C++ 标准库中的 <stack> 是一种功能强大且易于使用的容器适配器,通过它,开发者可以快速实现各种与“后进先出”相关的算法和数据结构。在实际使用中,了解底层容器的特性和选择非常重要,可以为项目的性能和内存效率带来显著提升。