栈(Stack)是计算机科学中最基础且重要的数据结构之一,它遵循"后进先出"(LIFO, Last In First Out)的原则。想象一下餐厅里叠放的餐盘——你总是取走最上面的那个盘子,新洗好的盘子也会放在最上面。这种特性使得栈在程序设计中有着不可替代的作用。
栈作为一种受限的线性表,与普通线性表相比有两个关键区别:
栈的两个关键位置:
注意:栈的"指针"实际上是一个标记栈顶位置的变量,在顺序栈中通常是数组下标,在链式栈中则是结点指针。
顺序栈使用数组作为底层存储结构,通过一个变量(通常称为top)来标记栈顶位置。根据top指向的不同,有两种常见的实现方式。
c复制#define MAX_SIZE 100 // 栈的最大容量
typedef struct {
int data[MAX_SIZE];
int top; // 栈顶指针
} SeqStack;
// 初始化栈
void InitStack(SeqStack *S) {
S->top = -1; // 初始化为-1表示空栈
}
// 判空
int IsEmpty(SeqStack *S) {
return S->top == -1;
}
// 判满
int IsFull(SeqStack *S) {
return S->top == MAX_SIZE - 1;
}
// 入栈
void Push(SeqStack *S, int value) {
if (IsFull(S)) {
printf("栈已满,无法入栈\n");
return;
}
S->data[++S->top] = value; // 先移动top再赋值
}
// 出栈
int Pop(SeqStack *S) {
if (IsEmpty(S)) {
printf("栈为空,无法出栈\n");
return -1; // 错误码
}
return S->data[S->top--]; // 先取值再移动top
}
// 获取栈顶元素
int GetTop(SeqStack *S) {
if (IsEmpty(S)) {
printf("栈为空\n");
return -1;
}
return S->data[S->top];
}
实现细节解析:
top初始化为-1,表示栈为空top再赋值,保证top始终指向栈顶元素top,确保不会丢失数据top == MAX_SIZE - 1,因为数组下标从0开始c复制typedef struct {
int data[MAX_SIZE];
int top; // 指向下一个入栈位置
} SeqStack2;
void InitStack2(SeqStack2 *S) {
S->top = 0; // 初始化为0
}
int IsEmpty2(SeqStack2 *S) {
return S->top == 0;
}
int IsFull2(SeqStack2 *S) {
return S->top == MAX_SIZE;
}
void Push2(SeqStack2 *S, int value) {
if (IsFull2(S)) {
printf("栈已满\n");
return;
}
S->data[S->top++] = value; // 先赋值再移动top
}
int Pop2(SeqStack2 *S) {
if (IsEmpty2(S)) {
printf("栈为空\n");
return -1;
}
return S->data[--S->top]; // 先移动top再取值
}
int GetTop2(SeqStack2 *S) {
if (IsEmpty2(S)) {
printf("栈为空\n");
return -1;
}
return S->data[S->top - 1]; // top-1才是栈顶元素
}
两种实现方式的对比:
| 特性 | 方式一 (top指向元素) | 方式二 (top指向下一个位置) |
|---|---|---|
| 初始化top值 | -1 | 0 |
| 判空条件 | top == -1 | top == 0 |
| 判满条件 | top == MAX_SIZE-1 | top == MAX_SIZE |
| 入栈操作 | data[++top] = value | data[top++] = value |
| 出栈操作 | value = data[top--] | value = data[--top] |
| 获取栈顶 | data[top] | data[top-1] |
实际开发中,方式二更为常见,因为它与很多其他数据结构的索引方式一致(如C++ STL中的stack实现)
链式栈使用链表作为存储结构,通常采用带头结点的单链表实现。与顺序栈相比,链式栈的最大优势是没有容量限制(理论上只受内存大小限制),但每个元素需要额外的指针空间。
c复制typedef struct StackNode {
int data;
struct StackNode *next;
} StackNode;
typedef struct {
StackNode *top; // 栈顶指针
int size; // 栈当前大小(可选)
} LinkedStack;
// 初始化栈
void InitLinkedStack(LinkedStack *S) {
S->top = NULL;
S->size = 0;
}
// 判空
int IsEmptyLinked(LinkedStack *S) {
return S->top == NULL;
}
// 入栈(头插法)
void PushLinked(LinkedStack *S, int value) {
StackNode *newNode = (StackNode*)malloc(sizeof(StackNode));
if (!newNode) {
printf("内存分配失败\n");
exit(1);
}
newNode->data = value;
newNode->next = S->top;
S->top = newNode;
S->size++;
}
// 出栈
int PopLinked(LinkedStack *S) {
if (IsEmptyLinked(S)) {
printf("栈为空\n");
return -1;
}
StackNode *temp = S->top;
int value = temp->data;
S->top = temp->next;
free(temp);
S->size--;
return value;
}
// 获取栈顶元素
int GetTopLinked(LinkedStack *S) {
if (IsEmptyLinked(S)) {
printf("栈为空\n");
return -1;
}
return S->top->data;
}
// 销毁栈
void DestroyLinkedStack(LinkedStack *S) {
while (!IsEmptyLinked(S)) {
PopLinked(S);
}
}
关键设计要点:
| 特性 | 顺序栈 | 链式栈 |
|---|---|---|
| 存储结构 | 数组 | 链表 |
| 容量限制 | 固定大小 | 动态增长 |
| 内存效率 | 更高(无指针开销) | 较低(每个元素需要指针) |
| 访问速度 | 更快(连续内存) | 稍慢(可能缓存不命中) |
| 实现复杂度 | 简单 | 稍复杂(需处理指针) |
| 适用场景 | 大小可预估的情况 | 大小变化大的情况 |
实际选择时,如果栈的大小可以预估且不会剧烈变化,顺序栈通常是更好的选择;反之则考虑链式栈。
括号匹配检查器:
c复制int IsValidParentheses(char *s) {
SeqStack stack;
InitStack(&stack);
while (*s) {
if (*s == '(' || *s == '[' || *s == '{') {
Push(&stack, *s);
} else {
if (IsEmpty(&stack)) return 0;
char top = GetTop(&stack);
if ((*s == ')' && top != '(') ||
(*s == ']' && top != '[') ||
(*s == '}' && top != '{')) {
return 0;
}
Pop(&stack);
}
s++;
}
return IsEmpty(&stack);
}
中缀表达式转后缀表达式:
c复制// 运算符优先级判断
int GetPriority(char op) {
if (op == '+' || op == '-') return 1;
if (op == '*' || op == '/') return 2;
return 0;
}
void InfixToPostfix(char *infix, char *postfix) {
SeqStack opStack;
InitStack(&opStack);
int j = 0;
for (int i = 0; infix[i]; i++) {
if (isdigit(infix[i])) {
postfix[j++] = infix[i];
} else if (infix[i] == '(') {
Push(&opStack, infix[i]);
} else if (infix[i] == ')') {
while (!IsEmpty(&opStack) && GetTop(&opStack) != '(') {
postfix[j++] = Pop(&opStack);
}
Pop(&opStack); // 弹出左括号
} else {
while (!IsEmpty(&opStack) &&
GetPriority(GetTop(&opStack)) >= GetPriority(infix[i])) {
postfix[j++] = Pop(&opStack);
}
Push(&opStack, infix[i]);
}
}
while (!IsEmpty(&opStack)) {
postfix[j++] = Pop(&opStack);
}
postfix[j] = '\0';
}
栈溢出问题:
顺序栈上溢:当栈已满仍尝试入栈时发生
顺序栈下溢:当栈为空仍尝试出栈时发生
内存泄漏问题(链式栈):
多线程环境下的栈安全问题:
c复制pthread_mutex_t stack_mutex = PTHREAD_MUTEX_INITIALIZER;
void ThreadSafePush(LinkedStack *S, int value) {
pthread_mutex_lock(&stack_mutex);
PushLinked(S, value);
pthread_mutex_unlock(&stack_mutex);
}
int ThreadSafePop(LinkedStack *S) {
pthread_mutex_lock(&stack_mutex);
int value = PopLinked(S);
pthread_mutex_unlock(&stack_mutex);
return value;
}
c复制void DynamicPush(SeqStack *S, int value) {
if (IsFull(S)) {
int newSize = S->top * 2; // 双倍扩容
int *newData = (int*)realloc(S->data, newSize * sizeof(int));
if (!newData) {
printf("扩容失败\n");
exit(1);
}
S->data = newData;
MAX_SIZE = newSize;
}
Push(S, value);
}
在某些场景下,可能需要同时管理多个栈。如果使用固定大小的顺序栈,可能会导致某些栈空间不足而其他栈空间闲置。多栈共享技术可以更灵活地利用内存。
双向栈实现:
c复制#define SHARED_SIZE 200
typedef struct {
int data[SHARED_SIZE];
int top1; // 栈1的栈顶指针
int top2; // 栈2的栈顶指针
} DoubleStack;
void InitDoubleStack(DoubleStack *S) {
S->top1 = -1;
S->top2 = SHARED_SIZE;
}
int IsFullDouble(DoubleStack *S) {
return S->top1 + 1 == S->top2;
}
// 栈1的入栈
void Push1(DoubleStack *S, int value) {
if (IsFullDouble(S)) {
printf("共享空间已满\n");
return;
}
S->data[++S->top1] = value;
}
// 栈2的入栈
void Push2(DoubleStack *S, int value) {
if (IsFullDouble(S)) {
printf("共享空间已满\n");
return;
}
S->data[--S->top2] = value;
}
这种技术可以扩展到更多栈的共享,核心思想是将多个栈的栈底分别放在共享空间的两端或特定位置,让它们相向生长。
深度优先搜索(DFS):
c复制void DFS(int graph[MAX][MAX], int start, int n) {
int visited[MAX] = {0};
SeqStack S;
InitStack(&S);
Push(&S, start);
visited[start] = 1;
while (!IsEmpty(&S)) {
int v = Pop(&S);
printf("%d ", v);
for (int i = n-1; i >= 0; i--) { // 逆序保证访问顺序正确
if (graph[v][i] && !visited[i]) {
Push(&S, i);
visited[i] = 1;
}
}
}
}
非递归的二叉树遍历:
c复制void InOrderTraversal(TreeNode *root) {
LinkedStack S;
InitLinkedStack(&S);
TreeNode *p = root;
while (p || !IsEmptyLinked(&S)) {
if (p) {
PushLinked(&S, (int)p); // 实际应使用void*指针栈
p = p->left;
} else {
p = (TreeNode*)PopLinked(&S);
printf("%d ", p->val);
p = p->right;
}
}
}
当函数被调用时,系统会在栈中创建一个栈帧(Stack Frame),包含:
例如对于函数调用func(a, b):
c复制int func(int x, int y) {
int z = x + y;
return z;
}
对应的栈帧可能如下布局(从高地址到低地址):
code复制| 返回地址 |
| 参数y |
| 参数x |
| 局部变量z|
理解系统栈的工作原理对于调试递归函数和解决栈溢出问题非常有帮助。当递归深度太大时,会导致系统栈空间耗尽,这时可以考虑:
编写完备的测试用例是确保栈实现正确性的关键。以下是一些基本的测试场景:
c复制void TestSeqStack() {
SeqStack S;
InitStack(&S);
// 测试空栈操作
assert(IsEmpty(&S) == 1);
assert(Pop(&S) == -1); // 空栈出栈应返回错误
// 测试入栈和出栈
for (int i = 0; i < MAX_SIZE; i++) {
Push(&S, i);
assert(GetTop(&S) == i);
}
assert(IsFull(&S) == 1);
assert(Push(&S, 100) == -1); // 满栈入栈应失败
// 测试出栈顺序
for (int i = MAX_SIZE-1; i >= 0; i--) {
assert(Pop(&S) == i);
}
assert(IsEmpty(&S) == 1);
printf("顺序栈测试通过!\n");
}
c复制void TestLinkedStack() {
LinkedStack S;
InitLinkedStack(&S);
// 测试空栈操作
assert(IsEmptyLinked(&S) == 1);
assert(PopLinked(&S) == -1);
// 测试入栈和出栈
for (int i = 0; i < 1000; i++) { // 测试大量数据
PushLinked(&S, i);
assert(GetTopLinked(&S) == i);
}
// 测试出栈顺序
for (int i = 999; i >= 0; i--) {
assert(PopLinked(&S) == i);
}
assert(IsEmptyLinked(&S) == 1);
DestroyLinkedStack(&S); // 测试销毁
printf("链式栈测试通过!\n");
}
c复制void TestAlternateOperations() {
SeqStack S;
InitStack(&S);
for (int i = 0; i < 100; i++) {
Push(&S, i);
assert(Pop(&S) == i);
assert(IsEmpty(&S) == 1);
}
printf("交替操作测试通过!\n");
}
c复制void TestRandomOperations() {
LinkedStack S;
InitLinkedStack(&S);
int expectedTop = -1;
srand(time(NULL));
for (int i = 0; i < 10000; i++) {
int op = rand() % 2;
if (op == 0 || expectedTop == -1) { // 入栈
int value = rand() % 1000;
PushLinked(&S, value);
expectedTop = value;
} else { // 出栈
assert(PopLinked(&S) == expectedTop);
expectedTop = IsEmptyLinked(&S) ? -1 : GetTopLinked(&S);
}
if (!IsEmptyLinked(&S)) {
assert(GetTopLinked(&S) == expectedTop);
}
}
DestroyLinkedStack(&S);
printf("随机操作测试通过!\n");
}
在实际项目中,还应该考虑:
除了基本的顺序栈和链式栈,还有一些栈的变体结构可以解决特定问题。
最小栈可以在O(1)时间内获取栈中的最小元素。
c复制typedef struct {
SeqStack mainStack;
SeqStack minStack; // 辅助栈,存储当前最小值
} MinStack;
void InitMinStack(MinStack *S) {
InitStack(&S->mainStack);
InitStack(&S->minStack);
}
void PushMin(MinStack *S, int value) {
Push(&S->mainStack, value);
if (IsEmpty(&S->minStack) || value <= GetTop(&S->minStack)) {
Push(&S->minStack, value);
}
}
int PopMin(MinStack *S) {
int value = Pop(&S->mainStack);
if (value == GetTop(&S->minStack)) {
Pop(&S->minStack);
}
return value;
}
int GetMin(MinStack *S) {
return GetTop(&S->minStack);
}
用栈实现队列:
c复制typedef struct {
SeqStack inStack;
SeqStack outStack;
} StackQueue;
void Enqueue(StackQueue *Q, int value) {
Push(&Q->inStack, value);
}
int Dequeue(StackQueue *Q) {
if (IsEmpty(&Q->outStack)) {
while (!IsEmpty(&Q->inStack)) {
Push(&Q->outStack, Pop(&Q->inStack));
}
}
return Pop(&Q->outStack);
}
用队列实现栈:
c复制typedef struct {
Queue q1;
Queue q2;
} QueueStack;
void PushQueueStack(QueueStack *S, int value) {
Enqueue(&S->q1, value);
}
int PopQueueStack(QueueStack *S) {
while (QueueSize(&S->q1) > 1) {
Enqueue(&S->q2, Dequeue(&S->q1));
}
int value = Dequeue(&S->q1);
// 交换q1和q2
Queue temp = S->q1;
S->q1 = S->q2;
S->q2 = temp;
return value;
}
单调栈是指栈中的元素保持单调递增或递减的顺序,常用于解决"下一个更大元素"类问题。
下一个更大元素问题:
c复制void NextGreaterElement(int nums[], int n, int result[]) {
LinkedStack S;
InitLinkedStack(&S);
for (int i = n - 1; i >= 0; i--) {
while (!IsEmptyLinked(&S) && GetTopLinked(&S) <= nums[i]) {
PopLinked(&S);
}
result[i] = IsEmptyLinked(&S) ? -1 : GetTopLinked(&S);
PushLinked(&S, nums[i]);
}
DestroyLinkedStack(&S);
}
这个算法的时间复杂度是O(n),因为每个元素最多入栈和出栈一次。类似的问题还包括:
虽然我们主要讨论了C语言实现,但了解其他语言中的栈实现也很有价值。
cpp复制#include <stack>
void TestSTLStack() {
std::stack<int> s;
// 入栈
s.push(1);
s.push(2);
s.push(3);
// 访问栈顶
std::cout << "Top: " << s.top() << std::endl; // 3
// 出栈
s.pop();
std::cout << "Top after pop: " << s.top() << std::endl; // 2
// 大小和判空
std::cout << "Size: " << s.size() << std::endl;
std::cout << "Empty: " << s.empty() << std::endl;
}
STL stack默认基于deque实现,也可以指定底层容器:
cpp复制std::stack<int, std::vector<int>> vStack; // 基于vector
std::stack<int, std::list<int>> lStack; // 基于list
java复制import java.util.Stack;
public class Main {
public static void main(String[] args) {
Stack<Integer> stack = new Stack<>();
stack.push(1);
stack.push(2);
stack.push(3);
System.out.println("Top: " + stack.peek()); // 3
System.out.println("Pop: " + stack.pop()); // 3
System.out.println("Top: " + stack.peek()); // 2
System.out.println("Size: " + stack.size());
System.out.println("Empty: " + stack.empty());
}
}
注意:Java的Stack类是继承自Vector的,由于设计上的问题,官方推荐使用Deque接口的实现类来代替:
java复制Deque<Integer> stack = new ArrayDeque<>();
Python没有专门的栈类,但列表可以完美实现栈的功能:
python复制stack = []
# 入栈
stack.append(1)
stack.append(2)
stack.append(3)
# 访问栈顶
print(stack[-1]) # 3
# 出栈
print(stack.pop()) # 3
print(stack.pop()) # 2
# 大小和判空
print(len(stack))
print(not stack)
对于高性能需求,可以使用collections.deque:
python复制from collections import deque
stack = deque()
理解栈的底层实现细节对于编写高性能代码至关重要。
顺序栈在内存中是连续存储的,这带来了几个优势:
典型的顺序栈内存布局:
code复制低地址 -> | data[0] | data[1] | ... | data[top] | ... | data[MAX-1] | <- 高地址
链式栈的每个结点需要额外存储next指针,这导致:
优化策略:
下面是一个简单的性能对比测试框架:
c复制#include <time.h>
#define TEST_COUNT 1000000
void TestSeqStackPerformance() {
SeqStack S;
InitStack(&S);
clock_t start = clock();
for (int i = 0; i < TEST_COUNT; i++) {
Push(&S, i);
}
for (int i = 0; i < TEST_COUNT; i++) {
Pop(&S);
}
clock_t end = clock();
printf("顺序栈耗时: %.2f ms\n", (double)(end - start) * 1000 / CLOCKS_PER_SEC);
}
void TestLinkedStackPerformance() {
LinkedStack S;
InitLinkedStack(&S);
clock_t start = clock();
for (int i = 0; i < TEST_COUNT; i++) {
PushLinked(&S, i);
}
for (int i = 0; i < TEST_COUNT; i++) {
PopLinked(&S);
}
clock_t end = clock();
printf("链式栈耗时: %.2f ms\n", (double)(end - start) * 1000 / CLOCKS_PER_SEC);
DestroyLinkedStack(&S);
}
实际测试结果可能会显示:
在实际项目中使用栈时,有一些经验教训值得分享。
预估数据量大小:
性能需求:
内存限制:
开发效率:
忘记检查栈空/满:
c复制int SafePop(SeqStack *S) {
if (IsEmpty(S)) {
// 记录错误日志
return ERROR_CODE;
}
return Pop(S);
}
内存泄漏(链式栈):
多线程竞争条件:
栈溢出攻击防范:
c复制void PrintStack(SeqStack *S) {
printf("Stack (top to bottom): ");
for (int i = S->top; i >= 0; i--) {
printf("%d ", S->data[i]);
}
printf("\n");
}
c复制int CheckStackIntegrity(LinkedStack *S) {
int count = 0;
StackNode *p = S->top;
while (p) {
count++;
p = p->next;
}
assert(count == S->size); // 如果维护了size变量
return count;
}
使用调试器观察栈:
bt查看调用栈记录栈操作日志:
c复制void LogPush(SeqStack *S, int value, const char *file, int line) {
Push(S, value);
printf("Pushed %d at %s:%d\n", value, file, line);
PrintStack(S);
}
#define LOG_PUSH(S, val) LogPush(S, val, __FILE__, __LINE__)
在资源受限的嵌入式系统中,使用栈需要额外注意:
c复制#define MAX_STACK_SIZE 64
typedef struct {
int data[MAX_STACK_SIZE];
int top;
} StaticStack;
// 避免动态内存分配
StaticStack stack = { .top = -1 };
c复制int GetStackUsage(StaticStack *S) {
return (S->top + 1) * 100 / MAX_STACK_SIZE;
}
避免递归:使用显式栈替代递归调用
考虑内存对齐:确保栈数据对齐以提高访问效率
在实际嵌入式项目中,栈的使用往往需要与整个系统的内存管理策略相结合,可能需要实现自定义的内存池和分配策略。