什么是栈
栈是只允许在一端插入和删除的线性表,特点是先进后出,后进先出。
允许操作的那一端叫栈顶,另一端叫栈底。元素只能从栈顶进、从栈顶出,想拿栈底的元素,必须先把上面的都弹走。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 栈顶 ↓ ┌──────────────┐ │ 5 │ ← 最后入栈,最先出栈 ├──────────────┤ │ 4 │ ├──────────────┤ │ 3 │ ├──────────────┤ │ 2 │ ├──────────────┤ │ 1 │ ← 最先入栈,最后出栈 └──────────────┘ 栈底
|
基本操作:
| 操作 |
说明 |
| 入栈 push |
把元素放到栈顶 |
| 出栈 pop |
把栈顶元素拿走 |
| 取栈顶 top |
看一眼栈顶元素,不拿走 |
栈可以用数组实现(顺序栈),也可以用链表实现(链式栈)。
一、顺序栈
1.1 结构定义、构造和析构
SeqStack/main.cpp1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| class SeqStack { public: SeqStack(int size = 10) : mtop(0), mcap(size) { mpStack = new int[mcap]; } ~SeqStack() { delete[]mpStack; mpStack = nullptr; } private: int* mpStack; int mtop; int mcap; };
|
三个成员:
mpStack 指向堆上申请的数组
mcap 是这块数组的总容量
mtop 是当前元素个数
mtop 有两个身份,它既是”元素个数”,也是”下一个可写入位置的下标“。因为数组下标从 0 开始,存了 3 个元素时,下一个该写的位置正好是下标 3。
1 2 3 4 5 6 7
| mpStack: ┌────┬────┬────┬────┬────┬────┬────┬────┬────┬────┐ │ 10 │ 11 │ 12 │ │ │ │ │ │ │ │ └────┴────┴────┴────┴────┴────┴────┴────┴────┴────┘ [0] [1] [2] [3] ... ↑ mtop = 3(下一个写入位置) 栈顶元素是 mpStack[mtop-1] = mpStack[2] = 12
|
1.2 入栈
SeqStack/main.cpp - Push1 2 3 4 5 6 7 8
| void Push(int val) { if (mtop == mcap) { expand(2 * mcap); } mpStack[mtop++] = val; }
|
mpStack[mtop++] = val 是两步:先把 val 写到下标 mtop 处,再把 mtop 加一。写完之后 mtop 又指向了新的空位。
mtop == mcap 是栈满的判断。容量为 10 时下标范围是 0~9,mtop 涨到 10 就说明 10 个位置全占满了。
1.3 扩容
SeqStack/main.cpp - expand1 2 3 4 5 6 7 8
| void expand(int size) { int* p = new int[size]; memcpy(p, mpStack, mtop * sizeof(int)); delete[]mpStack; mpStack = p; mcap = size; }
|
申请 → 拷贝 → 释放 → 换指针 → 改容量,顺序不能乱。
拷贝长度是 mtop * sizeof(int) 而不是 mcap * sizeof(int)。只有前 mtop 个位置是有效数据,后面的空位不用拷。
1.4 出栈与取栈顶
SeqStack/main.cpp - Pop / Top1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| void Pop() { if (mtop == 0) { throw "Stack is empty"; } mtop--; }
int Top() const { if (mtop == 0) { throw "Stack is empty"; } return mpStack[mtop - 1]; }
|
mtop--没有真的删除数据,因为 mtop 就是有效元素的边界,下标大于等于 mtop 的内存不会被访问到。出栈是 O(1),不需要移动任何元素。
1.5 获取元素个数、判断是否为空、打印元素
SeqStack/main.cpp - 其他1 2 3 4 5 6 7 8 9 10 11
| int Size() { return mtop; } bool Empty() { return mtop == 0; }
void Show() { for (int i = 0; i < mtop; i++) { cout << mpStack[i] << " "; } cout << endl; }
|
二、链式栈
2.1 结构定义、构造和析构
LinkStack/main.cpp1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| class LinkStack { public: LinkStack() : size_(0) { head_ = new Node(); } ~LinkStack() { Node* p = head_; while (p != nullptr) { head_ = head_->next; delete p; p = head_; } } private: struct Node { Node(int data = 0) : data_(data), next(nullptr) {} int data_; Node* next; }; Node* head_; int size_; };
|
链式栈就是带头节点的单链表,只不过所有操作都限制在一端。
链式栈的栈顶不在数组末尾,而是 head_->next。
1 2 3 4 5 6 7 8
| head_(哨兵) │ ▼ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │ next │──→ │ 19 │──→ │ 18 │──→ │ 17 │──→ nullptr └──────┘ └──────┘ └──────┘ └──────┘ ↑ 栈顶(最后入栈的)
|
2.2 入栈
LinkStack/main.cpp - push1 2 3 4 5 6 7
| void push(int val) { Node* p = new Node(val); p->next = head_->next; head_->next = p; size_++; }
|
入栈就是头插法,先让新节点接上原链,再让头节点改指向新节点。
头插之后,新节点成了第一个有效节点,也就是新的栈顶。
2.3 出栈与取栈顶
LinkStack/main.cpp - pop / top1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| void pop() { if (head_->next == nullptr) { throw "stack is empty"; } Node* p = head_->next; head_->next = p->next; delete p; size_--; }
int top() const { if (head_->next == nullptr) { throw "stack is empty"; } return head_->next->data_; }
|
2.4 遍历
LinkStack/main.cpp - main1 2 3 4 5 6 7 8 9 10 11
| LinkStack s; for (int i = 0; i < 10; i++) { s.push(i + 10); } while (!s.empty()) { cout << s.top() << " "; s.pop(); } cout << endl;
|
输出是 19 18 17 16 15 14 13 12 11 10,正好是入栈顺序的倒序。
三、两种实现对比
|
顺序栈 |
链式栈 |
| 底层结构 |
动态数组 |
带头节点的单链表 |
| 栈顶在哪 |
mpStack[mtop-1] |
head_->next |
| 入栈 |
尾部写入,满则扩容 |
头插 |
| 出栈 |
mtop-- |
删第一个节点 |
| 时间复杂度 |
入栈均摊 O(1),扩容那次 O(n) |
严格 O(1) |
| 额外开销 |
有预留空间(容量 ≥ 元素数) |
每个节点多一个 next 指针 |
| 内存布局 |
连续,缓存友好 |
离散 |
| 容量上限 |
受连续内存大小限制 |
有内存就能继续加 |
元素数量可预估、追求速度,选顺序栈。数组连续存储对 CPU 缓存友好,实际跑起来往往更快。
元素数量浮动大、不想浪费空间,选链式栈。不用扩容,用多少申请多少。
四、常见问题
- 栈的特点是什么? 后进先出(LIFO),只允许在栈顶一端插入和删除。
- 顺序栈和链式栈怎么选? 数量可预估、要速度选顺序栈;数量浮动大、不想预留空间选链式栈。
- 顺序栈为什么按 2 倍扩容? 均摊下来每次入栈是 O(1)。每次扩固定大小会让整体退化到 O(n²)。
- 链式栈为什么用头插? 头插 O(1),且头节点的 next 天然就是栈顶,不用额外维护尾指针。
- 出栈为什么不真的删数据?
mtop 就是有效元素的边界,越界部分不会被访问,清理是多余操作。
- 栈有什么实际应用? 函数调用栈、括号匹配、表达式求值(中缀转后缀)、深度优先搜索、编辑器的撤销功能。