栈(Stack)是计算机科学中最基础且重要的数据结构之一,它的核心特性可以用"后进先出"(Last In First Out,LIFO)来概括。想象一下我们日常生活中给手枪装子弹的场景——最后压入弹匣的子弹总是最先被击发,这就是栈工作原理的完美类比。
在程序设计中,栈主要支持两种基本操作:
此外,通常还会提供:
注意:栈的操作永远只在栈顶进行,这是它与队列等其它线性结构的本质区别。这种受限的操作方式虽然看似简单,却能在许多场景中提供高效的解决方案。
用数组实现栈是最直观的方式之一。我们需要维护以下核心组件:
c复制#define MAX_SIZE 100 // 栈的最大容量
typedef struct {
int data[MAX_SIZE];
int top; // 栈顶指针
} ArrayStack;
初始化时,我们将top设置为-1,表示空栈。每次push操作时,top先自增,然后将元素存入data[top];pop操作则相反,先取出data[top],然后top自减。
压栈操作:
c复制void push(ArrayStack *stack, int value) {
if (stack->top >= MAX_SIZE - 1) {
printf("Stack overflow!\n");
return;
}
stack->data[++stack->top] = value;
}
出栈操作:
c复制int pop(ArrayStack *stack) {
if (stack->top < 0) {
printf("Stack underflow!\n");
return -1; // 错误码
}
return stack->data[stack->top--];
}
实操心得:数组实现的栈在访问元素时具有O(1)的时间复杂度,但需要注意以下几点:
- 必须预先分配固定大小的内存,可能导致空间浪费或溢出
- 边界检查至关重要,特别是pop时的栈空判断
- 在多线程环境下需要额外的同步机制
为了解决固定大小数组的限制,我们可以实现动态扩容的数组栈:
c复制typedef struct {
int *data; // 动态数组指针
int top; // 栈顶指针
int capacity; // 当前容量
} DynamicArrayStack;
当栈满时,我们可以按照一定策略(如双倍扩容)重新分配更大的内存:
c复制void resize(DynamicArrayStack *stack) {
int new_capacity = stack->capacity * 2;
int *new_data = (int *)realloc(stack->data, new_capacity * sizeof(int));
if (!new_data) {
printf("Memory allocation failed!\n");
return;
}
stack->data = new_data;
stack->capacity = new_capacity;
}
链式栈通过动态内存分配实现,每个元素都是一个独立的节点,通过指针连接:
c复制typedef struct StackNode {
int data;
struct StackNode *next;
} StackNode;
typedef struct {
StackNode *top;
int size;
} LinkedStack;
链式栈的优势在于:
压栈操作:
c复制void linked_push(LinkedStack *stack, int value) {
StackNode *newNode = (StackNode *)malloc(sizeof(StackNode));
if (!newNode) {
printf("Memory allocation failed!\n");
return;
}
newNode->data = value;
newNode->next = stack->top;
stack->top = newNode;
stack->size++;
}
出栈操作:
c复制int linked_pop(LinkedStack *stack) {
if (!stack->top) {
printf("Stack underflow!\n");
return -1;
}
StackNode *temp = stack->top;
int value = temp->data;
stack->top = temp->next;
free(temp);
stack->size--;
return value;
}
注意事项:链式栈虽然灵活,但每个节点都需要额外的指针空间,且频繁的内存分配/释放可能影响性能。在实际开发中,应根据具体场景选择实现方式。
| 特性 | 数组栈 | 链式栈 |
|---|---|---|
| 时间复杂度 | 所有操作O(1) | 所有操作O(1) |
| 空间开销 | 可能浪费或不足 | 每个节点额外指针空间 |
| 内存分配 | 一次性分配 | 动态分配 |
| 缓存友好性 | 好(连续内存) | 较差(内存不连续) |
| 实现复杂度 | 简单 | 中等 |
选择数组栈当:
选择链式栈当:
程序执行时,函数调用关系正是通过栈来管理的:
c复制int factorial(int n) {
if (n <= 1) return 1;
return n * factorial(n - 1); // 每次递归都会压栈
}
栈非常适合处理需要"回溯"的计算,如:
例如,计算后缀表达式"3 4 + 5 *"的算法:
浏览器的"后退"功能就是栈的典型应用:
现象:程序崩溃,报错"stack overflow"
可能原因:
解决方案:
现象:随机崩溃或数据损坏
调试技巧:
现象:偶发性的数据不一致
防护措施:
c复制pthread_mutex_t stack_mutex = PTHREAD_MUTEX_INITIALIZER;
void thread_safe_push(ArrayStack *stack, int value) {
pthread_mutex_lock(&stack_mutex);
push(stack, value);
pthread_mutex_unlock(&stack_mutex);
}
如何在O(1)时间内获取栈中的最小元素?我们可以使用辅助栈:
c复制typedef struct {
ArrayStack main_stack;
ArrayStack min_stack; // 同步记录当前最小值
} MinStack;
void minstack_push(MinStack *stack, int value) {
push(&stack->main_stack, value);
if (stack->min_stack.top < 0 || value <= peek(&stack->min_stack)) {
push(&stack->min_stack, value);
}
}
int minstack_getMin(MinStack *stack) {
return peek(&stack->min_stack);
}
任何递归算法都可以用栈转化为迭代实现。以斐波那契数列为例:
递归版本:
c复制int fib(int n) {
if (n <= 1) return n;
return fib(n-1) + fib(n-2);
}
迭代栈版本:
c复制int fib_stack(int n) {
LinkedStack stack;
init_linked_stack(&stack);
linked_push(&stack, n);
int sum = 0;
while (stack.size > 0) {
int current = linked_pop(&stack);
if (current <= 1) {
sum += current;
} else {
linked_push(&stack, current - 1);
linked_push(&stack, current - 2);
}
}
return sum;
}
现代CPU通常提供专门的硬件栈支持:
理解硬件栈有助于:
在实际项目中,我经常发现开发者忽视了栈的空间限制。特别是在嵌入式系统中,默认栈空间可能只有几KB,这时使用数组栈或控制递归深度就尤为重要。另一个经验是:当需要实现"撤销"功能时,栈往往是首选数据结构,但要注意限制历史记录的数量,防止内存耗尽。