栈(Stack)是一种基本的数据结构,遵循“后进先出”(Last-In-First-Out, LIFO)的原则。栈在计算机科学中有广泛的应用,如函数调用管理、表达式求值、撤销操作等。在C语言中,栈可以通过数组或链表来实现。本文将详细介绍如何使用链表在C语言中实现栈,包括其基本结构、操作、示例代码以及优缺点。
栈的基本概念
-
后进先出(LIFO)原则:最新压入栈的元素最先被弹出。
-
主要操作:
-
压栈(Push):将元素添加到栈顶。
-
弹栈(Pop):移除并返回栈顶元素。
-
查看栈顶(Peek 或 Top):查看栈顶元素但不移除。
-
检查栈是否为空(IsEmpty)。
-
使用链表实现栈
使用链表实现栈时,通常将链表的头部作为栈顶,这样可以在常数时间内进行压栈和弹栈操作。
栈节点的结构体定义
首先,定义栈节点的结构体:
#include <stdio.h>
#include <stdlib.h>
// 定义栈节点结构体
typedef struct StackNode {
int data; // 数据域
struct StackNode* next; // 指向下一个节点的指针
} StackNode;
创建新节点
创建一个新的栈节点:
// 创建一个新节点并返回指针
StackNode* createNode(int data) {
StackNode* newNode = (StackNode*)malloc(sizeof(StackNode));
if (!newNode) {
printf("内存分配失败\n");
exit(1);
}
newNode->data = data; // 初始化数据
newNode->next = NULL; // 初始化指针
return newNode;
}
栈的基本操作
1. 压栈(Push)
将元素添加到栈顶:
void push(StackNode** top, int data) {
StackNode* newNode = createNode(data); // 创建新节点
newNode->next = *top; // 新节点指向当前栈顶
*top = newNode; // 更新栈顶指针
printf("压栈: %d\n", data);
}
2. 弹栈(Pop)
移除并返回栈顶元素:
int pop(StackNode** top) {
if (*top == NULL) {
printf("栈为空,无法弹栈\n");
exit(1); // 或者返回一个错误码
}
StackNode* temp = *top;
int popped = temp->data;
*top = (*top)->next; // 更新栈顶指针
free(temp); // 释放弹出的节点
printf("弹栈: %d\n", popped);
return popped;
}
3. 查看栈顶元素(Peek)
查看栈顶元素但不移除:
int peek(StackNode* top) {
if (top == NULL) {
printf("栈为空\n");
exit(1); // 或者返回一个错误码
}
return top->data;
}
4. 检查栈是否为空(IsEmpty)
判断栈是否为空:
int isEmpty(StackNode* top) {
return top == NULL;
}
5. 遍历栈
遍历并显示栈中的所有元素:
void traverseStack(StackNode* top) {
StackNode* temp = top;
printf("栈内容: ");
while (temp != NULL) {
printf("%d -> ", temp->data);
temp = temp->next;
}
printf("NULL\n");
}
完整示例程序
下面是一个完整的示例程序,展示了如何使用链表实现栈,并执行基本操作:
#include <stdio.h>
#include <stdlib.h>
// 栈节点结构体定义
typedef struct StackNode {
int data;
struct StackNode* next;
} StackNode;
// 创建新节点
StackNode* createNode(int data) {
StackNode* newNode = (StackNode*)malloc(sizeof(StackNode));
if (!newNode) {
printf("内存分配失败\n");
exit(1);
}
newNode->data = data;
newNode->next = NULL;
return newNode;
}
// 压栈操作
void push(StackNode** top, int data) {
StackNode* newNode = createNode(data);
newNode->next = *top;
*top = newNode;
printf("压栈: %d\n", data);
}
// 弹栈操作
int pop(StackNode** top) {
if (*top == NULL) {
printf("栈为空,无法弹栈\n");
exit(1);
}
StackNode* temp = *top;
int popped = temp->data;
*top = (*top)->next;
free(temp);
printf("弹栈: %d\n", popped);
return popped;
}
// 查看栈顶元素
int peek(StackNode* top) {
if (top == NULL) {
printf("栈为空\n");
exit(1);
}
return top->data;
}
// 检查栈是否为空
int isEmpty(StackNode* top) {
return top == NULL;
}
// 遍历栈
void traverseStack(StackNode* top) {
StackNode* temp = top;
printf("栈内容: ");
while (temp != NULL) {
printf("%d -> ", temp->data);
temp = temp->next;
}
printf("NULL\n");
}
// 主函数示例
int main() {
StackNode* stack = NULL; // 初始化空栈
// 压栈操作
push(&stack, 10);
push(&stack, 20);
push(&stack, 30);
// 遍历栈
traverseStack(stack); // 输出: 30 -> 20 -> 10 -> NULL
// 查看栈顶元素
printf("栈顶元素: %d\n", peek(stack)); // 输出: 30
// 弹栈操作
pop(&stack); // 弹出30
traverseStack(stack); // 输出: 20 -> 10 -> NULL
// 弹栈操作
pop(&stack); // 弹出20
traverseStack(stack); // 输出: 10 -> NULL
// 检查栈是否为空
if (isEmpty(stack)) {
printf("栈为空\n");
} else {
printf("栈不为空\n");
}
// 弹栈操作
pop(&stack); // 弹出10
traverseStack(stack); // 输出: NULL
// 最终检查栈是否为空
if (isEmpty(stack)) {
printf("栈为空\n");
} else {
printf("栈不为空\n");
}
return 0;
}
输出结果
压栈: 10
压栈: 20
压栈: 30
栈内容: 30 -> 20 -> 10 -> NULL
栈顶元素: 30
弹栈: 30
栈内容: 20 -> 10 -> NULL
弹栈: 20
栈内容: 10 -> NULL
栈不为空
弹栈: 10
栈内容: NULL
栈为空
栈的优缺点
优点
-
操作简单:栈的操作仅限于顶部,逻辑简单,易于实现。
-
高效的内存管理:特别是在函数调用中,栈用于管理局部变量和返回地址,效率高。
-
支持递归:栈结构天然支持递归调用,维护递归函数的调用状态。
缺点
-
访问限制:只能访问栈顶元素,无法随机访问栈中其他元素。
-
内存限制:使用链表实现栈时,每个节点需要额外的指针存储空间;使用数组实现时,可能需要预先定义栈的大小。
-
存储管理:需要手动管理内存(尤其是链表实现),易出现内存泄漏或悬挂指针等问题。
栈的变种与扩展
-
双栈(Two Stacks):在同一个数据结构中实现两个独立的栈,常用于解决某些算法问题,如队列的双栈实现。
-
动态栈(Dynamic Stack):使用链表或动态数组实现的栈,可以根据需要动态调整大小,避免固定大小的限制。
-
多级栈(Multi-level Stack):在复杂应用中使用多个栈来管理不同类型的数据或状态。
栈的应用场景
-
函数调用管理:编程语言的运行时使用栈来管理函数调用,包括参数传递、返回地址和局部变量。
-
表达式求值:中缀表达式转换为后缀表达式或直接计算后缀表达式时,使用栈进行操作。
-
撤销操作:文本编辑器或其他应用中的撤销功能通常使用栈来记录操作历史。
-
深度优先搜索(DFS):图或树的深度优先遍历常使用栈来实现节点的迭代访问。
-
语法解析:编译器在语法分析阶段使用栈来处理符号和产生式。