1. 栈的基本概念与特性
栈(Stack)是一种特殊的线性表,它遵循"后进先出"(Last In First Out, LIFO)的原则。想象一下餐厅里叠放的餐盘——你总是从最上面取用餐盘,新洗好的餐盘也会放在最上面。这种结构在计算机科学中有着广泛的应用。
1.1 栈的核心特性
栈有两个基本操作端:
- 栈顶(Top):允许插入和删除元素的一端
- 栈底(Bottom):不允许操作的一端
栈的核心性质可以概括为:
- 只能在栈顶进行插入(入栈/Push)和删除(出栈/Pop)操作
- 中间位置的元素不可直接访问或操作
- 最后入栈的元素会最先出栈(LIFO原则)
注意:栈的"先进后出"特性与队列的"先进先出"形成鲜明对比,这是选择数据结构时需要重点考虑的因素。
1.2 栈的抽象数据类型
一个完整的栈ADT(抽象数据类型)通常包含以下基本操作:
- InitStack(): 初始化一个空栈
- Push(x): 将元素x压入栈顶
- Pop(): 弹出栈顶元素
- GetTop(): 获取栈顶元素(但不删除)
- IsEmpty(): 判断栈是否为空
- IsFull(): 判断栈是否已满(仅限顺序栈)
在实际应用中,我们可能还需要其他辅助操作,如获取栈大小、清空栈等,但这些都可以基于上述基本操作实现。
2. 顺序栈的实现
顺序栈使用数组作为底层存储结构,通过一个栈顶指针来跟踪当前栈顶位置。这种实现方式简单高效,但存在固定容量的限制。
2.1 顺序栈的结构设计
cpp复制#define MAXSIZE 100 // 栈的最大容量
typedef struct {
int *data; // 存储栈元素的数组
int top; // 栈顶指针
int capacity; // 栈的当前容量
} SeqStack;
这里有三种常见的栈顶指针设计方式:
-
top指向栈顶元素的下一个位置(初始top=0)
- 入栈:data[top++] = x
- 出栈:x = data[--top]
- 判空:top == 0
- 判满:top == capacity
-
top指向栈顶元素(初始top=-1)
- 入栈:data[++top] = x
- 出栈:x = data[top--]
- 判空:top == -1
- 判满:top == capacity-1
-
使用结构体封装(如上面的SeqStack)
- 更灵活,可以动态调整容量
- 可以添加其他辅助信息
提示:在实际工程中,建议使用第三种方式,因为它更灵活且易于扩展。第一种和第二种更适合教学示例或内存受限的环境。
2.2 顺序栈的基本操作实现
2.2.1 初始化栈
cpp复制SeqStack* InitSeqStack(int maxSize) {
SeqStack *s = (SeqStack*)malloc(sizeof(SeqStack));
if (!s) return NULL;
s->data = (int*)malloc(maxSize * sizeof(int));
if (!s->data) {
free(s);
return NULL;
}
s->top = 0;
s->capacity = maxSize;
return s;
}
2.2.2 入栈操作
cpp复制int Push(SeqStack *s, int x) {
if (s->top >= s->capacity) {
// 栈满,可以考虑动态扩容
int newCapacity = s->capacity * 2;
int *newData = (int*)realloc(s->data, newCapacity * sizeof(int));
if (!newData) return 0; // 扩容失败
s->data = newData;
s->capacity = newCapacity;
}
s->data[s->top++] = x;
return 1;
}
2.2.3 出栈操作
cpp复制int Pop(SeqStack *s, int *x) {
if (s->top <= 0) return 0; // 栈空
*x = s->data[--s->top];
return 1;
}
2.2.4 获取栈顶元素
cpp复制int GetTop(SeqStack *s, int *x) {
if (s->top <= 0) return 0;
*x = s->data[s->top - 1];
return 1;
}
2.3 顺序栈的优缺点分析
优点:
- 实现简单,逻辑清晰
- 存取速度快,时间复杂度O(1)
- 内存连续,缓存友好
缺点:
- 容量固定(除非实现动态扩容)
- 扩容时可能涉及数据搬移,性能开销大
- 可能造成内存浪费(分配过多但使用很少)
经验分享:在实际项目中,如果栈的大小可以预估且不会剧烈变化,顺序栈是首选。对于变化剧烈的场景,建议使用链式栈或实现动态扩容的顺序栈。
3. 链式栈的实现
链式栈使用链表作为存储结构,理论上可以无限扩展(受限于内存),但每个元素需要额外的指针空间。
3.1 链式栈的结构设计
cpp复制typedef struct StackNode {
int data;
struct StackNode *next;
} StackNode;
typedef struct {
StackNode *top; // 栈顶指针
int size; // 栈大小(可选)
} LinkStack;
链式栈通常不需要头节点,直接让top指针指向栈顶元素即可。添加size字段可以方便地获取栈的当前大小,但会增加一点维护开销。
3.2 链式栈的基本操作实现
3.2.1 初始化栈
cpp复制LinkStack* InitLinkStack() {
LinkStack *s = (LinkStack*)malloc(sizeof(LinkStack));
if (!s) return NULL;
s->top = NULL;
s->size = 0;
return s;
}
3.2.2 入栈操作
cpp复制int Push(LinkStack *s, int x) {
StackNode *newNode = (StackNode*)malloc(sizeof(StackNode));
if (!newNode) return 0;
newNode->data = x;
newNode->next = s->top;
s->top = newNode;
s->size++;
return 1;
}
3.2.3 出栈操作
cpp复制int Pop(LinkStack *s, int *x) {
if (s->top == NULL) return 0;
StackNode *temp = s->top;
*x = temp->data;
s->top = temp->next;
free(temp);
s->size--;
return 1;
}
3.2.4 获取栈顶元素
cpp复制int GetTop(LinkStack *s, int *x) {
if (s->top == NULL) return 0;
*x = s->top->data;
return 1;
}
3.3 链式栈的优缺点分析
优点:
- 理论上没有容量限制
- 插入删除非常高效,时间复杂度O(1)
- 内存利用率高,按需分配
缺点:
- 每个元素需要额外空间存储指针
- 内存不连续,缓存不友好
- 频繁的内存分配释放可能造成内存碎片
实操建议:在内存充足且栈大小变化大的场景下使用链式栈。对于嵌入式等内存受限环境,顺序栈更合适。
4. 栈的应用场景与实例
栈在计算机科学中有着广泛的应用,下面介绍几个典型场景。
4.1 函数调用栈
程序执行时的函数调用关系就是用栈来管理的。每次调用函数时,系统会将返回地址、参数和局部变量等信息压入调用栈;函数返回时,再将这些信息弹出。
cpp复制void funcA() {
// 一些操作...
funcB(); // 调用funcB
// 更多操作...
}
void funcB() {
// funcB的操作...
}
int main() {
funcA(); // 调用funcA
return 0;
}
调用过程:
- main调用funcA,将返回地址压栈
- funcA调用funcB,将返回地址压栈
- funcB执行完毕,弹出返回地址回到funcA
- funcA执行完毕,弹出返回地址回到main
4.2 表达式求值
栈可以用于计算算术表达式,特别是包含括号的复杂表达式。常见算法有:
- 中缀表达式转后缀表达式
- 后缀表达式求值
示例:中缀转后缀
cpp复制// 伪代码示例
string infixToPostfix(string infix) {
stack<char> s;
string postfix;
for (char c : infix) {
if (isOperand(c)) {
postfix += c;
} else if (c == '(') {
s.push(c);
} else if (c == ')') {
while (!s.empty() && s.top() != '(') {
postfix += s.top();
s.pop();
}
s.pop(); // 弹出'('
} else { // 运算符
while (!s.empty() && precedence(c) <= precedence(s.top())) {
postfix += s.top();
s.pop();
}
s.push(c);
}
}
while (!s.empty()) {
postfix += s.top();
s.pop();
}
return postfix;
}
4.3 括号匹配检查
栈非常适合检查各种括号(圆括号、方括号、花括号)的匹配情况。
cpp复制bool isBalanced(string expr) {
stack<char> s;
for (char c : expr) {
if (c == '(' || c == '[' || c == '{') {
s.push(c);
} else if (c == ')' || c == ']' || c == '}') {
if (s.empty()) return false;
char top = s.top();
s.pop();
if ((c == ')' && top != '(') ||
(c == ']' && top != '[') ||
(c == '}' && top != '{')) {
return false;
}
}
}
return s.empty();
}
4.4 浏览器的前进后退功能
浏览器使用两个栈来实现页面的前进后退导航:
- 后退栈:保存访问过的页面,点击后退时弹出并压入前进栈
- 前进栈:保存后退过的页面,点击前进时弹出并压入后退栈
4.5 深度优先搜索(DFS)
在图和树的遍历算法中,DFS通常使用栈(或递归调用栈)来实现:
cpp复制void DFS(Node* root) {
if (root == NULL) return;
stack<Node*> s;
s.push(root);
while (!s.empty()) {
Node* current = s.top();
s.pop();
visit(current);
// 将子节点按相反顺序压栈,保证从左到右访问
for (int i = current->children.size() - 1; i >= 0; --i) {
s.push(current->children[i]);
}
}
}
5. 栈的常见问题与优化
5.1 栈溢出问题
顺序栈的溢出:
- 上溢出(Overflow):栈已满时尝试入栈
- 下溢出(Underflow):栈已空时尝试出栈
解决方案:
- 动态扩容:当栈满时,分配更大的空间并复制数据
- 错误处理:检测溢出情况并返回错误码或抛出异常
cpp复制// 动态扩容示例
int Push(SeqStack *s, int x) {
if (s->top >= s->capacity) {
int newCapacity = s->capacity * 2;
int *newData = (int*)realloc(s->data, newCapacity * sizeof(int));
if (!newData) return 0;
s->data = newData;
s->capacity = newCapacity;
}
s->data[s->top++] = x;
return 1;
}
5.2 多栈共享空间
在某些场景下,可以设计一个数组来同时实现两个栈,提高空间利用率:
cpp复制typedef struct {
int *data;
int top1; // 栈1的栈顶指针
int top2; // 栈2的栈顶指针
int capacity;
} DoubleStack;
// 初始化
DoubleStack* InitDoubleStack(int size) {
DoubleStack *ds = (DoubleStack*)malloc(sizeof(DoubleStack));
ds->data = (int*)malloc(size * sizeof(int));
ds->top1 = -1;
ds->top2 = size;
ds->capacity = size;
return ds;
}
// 栈1的入栈
int Push1(DoubleStack *ds, int x) {
if (ds->top1 + 1 >= ds->top2) return 0; // 栈满
ds->data[++ds->top1] = x;
return 1;
}
// 栈2的入栈
int Push2(DoubleStack *ds, int x) {
if (ds->top2 - 1 <= ds->top1) return 0; // 栈满
ds->data[--ds->top2] = x;
return 1;
}
5.3 最小栈问题
设计一个栈,能够在O(1)时间内获取栈中的最小元素。解决方案是使用辅助栈:
cpp复制typedef struct {
stack<int> mainStack;
stack<int> minStack;
} MinStack;
void Push(MinStack *ms, int x) {
ms->mainStack.push(x);
if (ms->minStack.empty() || x <= ms->minStack.top()) {
ms->minStack.push(x);
}
}
int Pop(MinStack *ms) {
if (ms->mainStack.empty()) return -1; // 错误码
int x = ms->mainStack.top();
ms->mainStack.pop();
if (x == ms->minStack.top()) {
ms->minStack.pop();
}
return x;
}
int GetMin(MinStack *ms) {
if (ms->minStack.empty()) return -1;
return ms->minStack.top();
}
5.4 栈与递归的转换
递归函数本质上使用了系统调用栈,任何递归算法都可以用显式栈转换为非递归形式。
递归的阶乘函数:
cpp复制int factorial(int n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
}
转换为栈实现的非递归版本:
cpp复制int factorialIter(int n) {
stack<int> s;
int result = 1;
while (n > 1) {
s.push(n--);
}
while (!s.empty()) {
result *= s.top();
s.pop();
}
return result;
}
6. 栈的扩展应用与算法
6.1 使用栈实现队列
用两个栈可以实现一个队列的功能:
cpp复制typedef struct {
stack<int> inStack;
stack<int> outStack;
} MyQueue;
void Push(MyQueue *q, int x) {
q->inStack.push(x);
}
int Pop(MyQueue *q) {
if (q->outStack.empty()) {
while (!q->inStack.empty()) {
q->outStack.push(q->inStack.top());
q->inStack.pop();
}
}
int x = q->outStack.top();
q->outStack.pop();
return x;
}
bool Empty(MyQueue *q) {
return q->inStack.empty() && q->outStack.empty();
}
6.2 使用栈实现图的拓扑排序
拓扑排序可以用栈来实现:
cpp复制void topologicalSortUtil(int v, bool visited[], stack<int> &s, vector<int> adj[]) {
visited[v] = true;
for (int i : adj[v]) {
if (!visited[i]) {
topologicalSortUtil(i, visited, s, adj);
}
}
s.push(v);
}
vector<int> topologicalSort(int V, vector<int> adj[]) {
stack<int> s;
bool *visited = new bool[V]{false};
for (int i = 0; i < V; i++) {
if (!visited[i]) {
topologicalSortUtil(i, visited, s, adj);
}
}
vector<int> result;
while (!s.empty()) {
result.push_back(s.top());
s.pop();
}
delete[] visited;
return result;
}
6.3 使用栈计算直方图的最大矩形面积
这是一个经典的栈应用问题:
cpp复制int largestRectangleArea(vector<int>& heights) {
stack<int> s;
int maxArea = 0;
int n = heights.size();
for (int i = 0; i <= n; i++) {
int h = (i == n) ? 0 : heights[i];
while (!s.empty() && h < heights[s.top()]) {
int height = heights[s.top()];
s.pop();
int width = s.empty() ? i : i - s.top() - 1;
maxArea = max(maxArea, height * width);
}
s.push(i);
}
return maxArea;
}
6.4 使用栈进行树的非递归遍历
中序遍历的非递归实现:
cpp复制vector<int> inorderTraversal(TreeNode* root) {
vector<int> result;
stack<TreeNode*> s;
TreeNode* curr = root;
while (curr != NULL || !s.empty()) {
while (curr != NULL) {
s.push(curr);
curr = curr->left;
}
curr = s.top();
s.pop();
result.push_back(curr->val);
curr = curr->right;
}
return result;
}
7. 栈在不同编程语言中的实现
7.1 C++中的栈
C++标准库提供了stack容器适配器:
cpp复制#include <stack>
stack<int> s;
// 基本操作
s.push(10); // 入栈
int top = s.top(); // 获取栈顶元素
s.pop(); // 出栈
bool empty = s.empty(); // 判断是否为空
int size = s.size(); // 获取栈大小
7.2 Java中的栈
Java提供了Stack类,但更推荐使用Deque接口的实现:
java复制import java.util.Stack;
// 或者
import java.util.Deque;
import java.util.ArrayDeque;
// 使用Stack类
Stack<Integer> stack = new Stack<>();
stack.push(10);
int top = stack.peek();
stack.pop();
// 使用Deque(推荐)
Deque<Integer> stack = new ArrayDeque<>();
stack.push(10);
int top = stack.peek();
stack.pop();
7.3 Python中的栈
Python使用列表作为栈:
python复制stack = []
# 入栈
stack.append(10)
# 获取栈顶元素
top = stack[-1]
# 出栈
top = stack.pop()
# 判断是否为空
is_empty = len(stack) == 0
对于高性能需求,可以使用collections.deque:
python复制from collections import deque
stack = deque()
stack.append(10)
top = stack[-1]
top = stack.pop()
7.4 JavaScript中的栈
JavaScript使用数组作为栈:
javascript复制let stack = [];
// 入栈
stack.push(10);
// 获取栈顶元素
let top = stack[stack.length - 1];
// 出栈
let top = stack.pop();
// 判断是否为空
let isEmpty = stack.length === 0;
8. 栈的性能分析与优化
8.1 时间复杂度分析
栈的基本操作时间复杂度:
- 入栈(Push):O(1)
- 出栈(Pop):O(1)
- 获取栈顶(Peek):O(1)
- 判空(IsEmpty):O(1)
对于顺序栈的动态扩容操作,虽然单次扩容是O(n),但均摊分析下仍然是O(1)。
8.2 空间复杂度分析
- 顺序栈:O(n),n为栈容量
- 链式栈:O(n),每个节点需要额外指针空间
8.3 缓存友好性
顺序栈由于内存连续,缓存命中率高;链式栈内存分散,缓存不友好。对于性能敏感的应用,顺序栈通常是更好的选择。
8.4 多线程环境下的栈
在多线程环境下使用栈需要注意同步问题:
- 粗粒度锁:整个栈加锁,简单但并发度低
- 细粒度锁:如Treiber栈(基于CAS的无锁栈)
Treiber栈的简单实现:
cpp复制#include <atomic>
template<typename T>
class LockFreeStack {
private:
struct Node {
T data;
Node* next;
Node(const T& d) : data(d), next(nullptr) {}
};
std::atomic<Node*> head;
public:
void Push(const T& data) {
Node* newNode = new Node(data);
newNode->next = head.load(std::memory_order_relaxed);
while (!head.compare_exchange_weak(newNode->next, newNode,
std::memory_order_release,
std::memory_order_relaxed));
}
bool Pop(T& result) {
Node* oldHead = head.load(std::memory_order_relaxed);
if (!oldHead) return false;
while (!head.compare_exchange_weak(oldHead, oldHead->next,
std::memory_order_release,
std::memory_order_relaxed));
result = oldHead->data;
delete oldHead;
return true;
}
};
9. 栈的调试与测试技巧
9.1 栈的调试方法
- 打印栈内容:实现一个打印栈内容的辅助函数,方便调试
- 边界条件测试:空栈操作、满栈操作、单元素栈操作
- 序列化测试:一系列连续的入栈出栈操作
cpp复制void PrintStack(SeqStack *s) {
printf("Stack (top to bottom): ");
for (int i = s->top - 1; i >= 0; i--) {
printf("%d ", s->data[i]);
}
printf("\n");
}
void TestStack() {
SeqStack *s = InitSeqStack(5);
// 测试空栈操作
int val;
assert(Pop(s, &val) == 0);
assert(GetTop(s, &val) == 0);
// 测试基本操作
Push(s, 1); Push(s, 2); Push(s, 3);
assert(GetTop(s, &val) == 1 && val == 3);
Pop(s, &val); assert(val == 3);
Pop(s, &val); assert(val == 2);
Push(s, 4); Push(s, 5); Push(s, 6);
assert(GetTop(s, &val) == 1 && val == 6);
// 测试栈满
assert(Push(s, 7) == 0); // 应该失败
// 清理
FreeSeqStack(s);
}
9.2 内存泄漏检查
对于链式栈,特别注意内存泄漏问题:
- 确保每个Pop操作都释放了节点内存
- 实现销毁栈的函数,释放所有剩余节点
cpp复制void FreeLinkStack(LinkStack *s) {
while (s->top != NULL) {
StackNode *temp = s->top;
s->top = s->top->next;
free(temp);
}
free(s);
}
9.3 压力测试
对栈实现进行大规模数据测试:
cpp复制void StressTest() {
SeqStack *s = InitSeqStack(1000000);
// 大规模入栈
for (int i = 0; i < 1000000; i++) {
assert(Push(s, i) == 1);
}
// 大规模出栈
for (int i = 999999; i >= 0; i--) {
int val;
assert(Pop(s, &val) == 1);
assert(val == i);
}
assert(IsEmpty(s));
FreeSeqStack(s);
}
10. 栈的变体与扩展结构
10.1 双端栈
双端栈允许从两端进行入栈和出栈操作:
cpp复制typedef struct {
int *data;
int top1; // 栈1的栈顶指针
int top2; // 栈2的栈顶指针
int capacity;
} DoubleEndedStack;
DoubleEndedStack* InitDoubleEndedStack(int size) {
DoubleEndedStack *ds = (DoubleEndedStack*)malloc(sizeof(DoubleEndedStack));
ds->data = (int*)malloc(size * sizeof(int));
ds->top1 = -1;
ds->top2 = size;
ds->capacity = size;
return ds;
}
int Push1(DoubleEndedStack *ds, int x) {
if (ds->top1 + 1 >= ds->top2) return 0;
ds->data[++ds->top1] = x;
return 1;
}
int Push2(DoubleEndedStack *ds, int x) {
if (ds->top2 - 1 <= ds->top1) return 0;
ds->data[--ds->top2] = x;
return 1;
}
10.2 可持久化栈
可持久化栈支持访问历史版本,通常使用持久化数据结构技术实现:
cpp复制typedef struct PersistentStackNode {
int data;
struct PersistentStackNode *prev;
int refCount; // 引用计数用于垃圾回收
} PersistentStackNode;
typedef struct {
PersistentStackNode *top;
int size;
} PersistentStack;
PersistentStack* PushPersistent(PersistentStack *s, int x) {
PersistentStack *newVersion = (PersistentStack*)malloc(sizeof(PersistentStack));
PersistentStackNode *newNode = (PersistentStackNode*)malloc(sizeof(PersistentStackNode));
newNode->data = x;
newNode->prev = s->top;
newNode->refCount = 1;
if (s->top) s->top->refCount++;
newVersion->top = newNode;
newVersion->size = s->size + 1;
return newVersion;
}
10.3 单调栈
单调栈是指栈中的元素保持单调递增或递减的顺序,常用于解决一些特定问题:
cpp复制// 示例:下一个更大元素问题
vector<int> nextGreaterElement(vector<int>& nums) {
vector<int> result(nums.size(), -1);
stack<int> s; // 单调递减栈
for (int i = 0; i < nums.size(); i++) {
while (!s.empty() && nums[s.top()] < nums[i]) {
result[s.top()] = nums[i];
s.pop();
}
s.push(i);
}
return result;
}
10.4 硬件栈
在计算机体系结构中,硬件栈(如CPU的调用栈)具有以下特点:
- 通常有专门的寄存器(如x86的ESP/RSP)
- 栈指针向低地址增长(多数架构)
- 有专门的指令(PUSH/POP)
- 栈溢出可能导致严重安全问题
理解硬件栈对于系统编程和逆向工程非常重要。