引言
栈(Stack)是一种基础数据结构,广泛应用于计算机科学中的各个领域。栈遵循后进先出(LIFO,Last In First Out)原则,即最后进入栈的数据会最先被取出。在C语言编程中,栈不仅是操作系统和编译器的重要组成部分,也是解决许多编程问题的有力工具。
在这篇博客中,我们将详细介绍栈的基本概念、栈的代码实现、栈的优缺点、栈的应用场景,以及栈的实现逻辑。
栈的基本概念
栈是一种线性数据结构,它的特点是“后进先出”(LIFO),即在栈中的数据只能从栈顶插入和删除。栈的基本操作有两个:
压栈(Push):将一个元素添加到栈顶。
弹栈(Pop):移除栈顶元素并返回它。
栈的其他常见操作包括:
查看栈顶元素(Top):查看栈顶元素,但不移除它。
栈空检查(isEmpty):检查栈是否为空。
栈满检查(isFull):检查栈是否已满(数组实现时使用)。
栈的实现可以基于数组或链表。栈的操作是受限的,只能在栈顶进行,因此它属于受限访问的数据结构。
接下来我用一张图来帮助大家理解
栈的实现
基于数组实现栈
定义一个栈的数据类型
typedef int DataType;
typedef struct Stack
{
DataType* a;//数组指针
int top;//栈顶
int capacity;//栈容量
}ST;
栈的初始化和销毁
void STInit(ST* pst)//栈的初始化
{
assert(pst);
pst->a = NULL;//栈顶指向第一个元素
//pst->a = 0;//栈顶指向第一个元素下一个元素
pst->top = 0;
pst->capacity= 0;
}
void STDestory(ST* pst)//栈的销毁:先释放,后置空
{
assert(pst);
free(pst);
pst->a = NULL;
pst->capacity = pst->top =0;
}
入栈和出栈
void STPush(ST* pst, DataType x)//入栈
{
assert(pst);
//考虑扩容
if (pst->top == pst->capacity)
{
int Newcapacity = pst->capacity==0?4:2* pst->capacity;//需要扩多大的空间:运用三目操作符
DataType* temp = (DataType*)realloc(pst->a, Newcapacity * sizeof(DataType));
if (temp == NULL)
{
perror("realloc fail!");
return;
}
pst->a = temp;
pst->capacity = Newcapacity;
}
pst->a[pst->top] = x;
pst->top++;
}
void STPop(ST* pst)//出栈
{
assert(pst);
assert(pst->top > 0);
pst->top--;
}
判空,栈的大小,取栈顶元素
bool STEmpty(ST* pst)//判断栈是否为空
{
assert(pst);
return pst->top == 0;
}
size_t STSize(ST* pst)//取栈的大小
{
return pst->top;
}
DataType STTop(ST* pst)//取栈顶元素
{
return pst->a[pst->top - 1];
}
int main()
{
ST s;
STInit(&s);//对栈进行初始化
STPush(&s, 1);//插入第一个元素1
STPush(&s, 2);//插入第二个元素2
STPush(&s, 3);//插入第二个元素3
STPush(&s, 4);//插入第二个元素4
for (int i = 0; i < 4; i++)
{
printf("%d ", s.a[i]);//先打印一下
}
printf("\n打印栈顶元素:%d ", STTop(&s));//打印栈顶元素
printf("\n栈的大小:%d ", STSize(&s));//栈的大小
STPop(&s);//将栈顶元素也就是4给pop掉
printf("\n打印栈顶元素:%d ", STTop(&s));//打印栈顶元素
printf("\n栈的大小:%d ", STSize(&s));//栈的大小
return 0;
}
基于链表实现
创建节点和栈结构体
// 定义链表节点
typedef struct Node {
int data;
struct Node* next;
} Node;
// 栈结构体
typedef struct {
Node* top; // 栈顶指针
} Stack;
初始化和判空
// 初始化栈
void initStack(Stack* stack) {
stack->top = NULL; // 栈初始化为空
}
// 判断栈是否为空
int isEmpty(Stack* stack) {
return stack->top == NULL;
}
压栈,弹栈,获取栈顶元素
// 压栈操作
void push(Stack* stack, int value) {
Node* newNode = (Node*)malloc(sizeof(Node)); // 创建新节点
if (!newNode) {
printf("Memory allocation failed!\n");
return;
}
newNode->data = value;
newNode->next = stack->top; // 新节点指向当前栈顶
stack->top = newNode; // 更新栈顶为新节点
printf("Pushed %d\n", value);
}
// 弹栈操作
int pop(Stack* stack) {
if (isEmpty(stack)) {
printf("Stack underflow!\n");
return -1;
}
Node* temp = stack->top; // 保存栈顶节点
int poppedValue = temp->data; // 获取栈顶元素
stack->top = stack->top->next; // 更新栈顶为下一个节点
free(temp); // 释放原栈顶节点
return poppedValue;
}
// 获取栈顶元素
int peek(Stack* stack) {
if (isEmpty(stack)) {
printf("Stack is empty!\n");
return -1;
}
return stack->top->data;
}
int main() {
Stack stack;
initStack(&stack); // 初始化栈
push(&stack, 10); // 压入元素
push(&stack, 20);
push(&stack, 30);
printf("Top element is %d\n", peek(&stack)); // 获取栈顶元素
printf("Popped element: %d\n", pop(&stack)); // 弹出元素
printf("Popped element: %d\n", pop(&stack));
return 0;
}
栈的优缺点
优点
操作简单:栈的基本操作只有压栈和弹栈,非常简单且易于实现。
高效性:栈的插入和删除操作时间复杂度为O(1),非常高效。
动态扩展(链表实现):链表实现的栈可以动态扩展,不会受到预设大小的限制,适用于元素数量不确定的场合。
缺点
空间限制(数组实现):数组实现的栈需要预设最大容量,一旦超出容量会导致栈溢出。
访问限制:栈只允许从栈顶插入和删除数据,无法直接访问栈中的其他元素,不适合用于需要随机访问的数据存储场景。
栈的应用场景
栈在许多算法和系统中都有着重要的应用,以下是栈的几个典型应用场景:
函数调用管理:
在程序执行时,每当调用一个函数,系统会将该函数的局部变量、返回地址等信息压入栈中,函数执行完毕后,这些信息会从栈中弹出,恢复到调用函数的状态。这种机制被称为调用栈。
括号匹配:
在编译器中,栈被用于检查表达式中的括号是否匹配。通过将每个左括号压栈,当遇到右括号时弹栈,检查是否匹配。
表达式求值:
栈广泛应用于数学表达式的求值,特别是后缀表达式(逆波兰表示法)。通过栈操作,可以将操作数和运算符按正确顺序求值。
深度优先搜索(DFS):
栈在图的遍历算法中常被用于实现深度优先搜索(DFS),它用于存储当前路径的节点。
总结
栈是一种非常重要的基础数据结构,它的“后进先出”特性使得它在许多场景下都非常有用。在C语言中,栈可以通过数组或链表来实现,具体选择哪种方式取决于实际需求。栈的主要优点是操作简单、高效,但也有一些缺点,如空间的预设限制和只能从栈顶访问数据。
掌握栈的实现与应用,能够帮助程序员在实际编程中解决很多复杂问题。希望这篇文章能够帮助你更好地理解栈的概念和应用,如果你有任何问题,欢迎在评论区提问!