本文讨论使用c语言实现栈。
什么是栈?
栈是一种后进先出的线性表。它只允许在表的一端进行插入和删除操作。在栈中的元素要满足先入栈的后出栈,而后出栈的元素先出栈。
什么意思?我们就把栈想象成一个杯子,现在要往杯子里面放物体;随着放入物体的增多,下面的物体要想拿出就必须等上面的物体拿出杯子后才能取出。

所以,栈是一种只能在一端进行插入和删除操作的线性数据结构。而能操作的这一端我们称为栈顶 (Top),另一端则为栈底(Bottom)。
在栈的实现中,常常有以下功能需要实现:
- 初始化
- 入栈(压栈):往栈中放入新元素。
- 出栈:从栈中取出元素。
- 读取栈顶元素
- 判断栈的空与满
栈的实现
栈的实现有两种方式,为顺序栈和链式栈;就是用顺序表或链表来实现。我们依次来看。
一、使用顺序表实现栈
使用顺序表,即我们需要创建一个数组。为了更适合动态变化,我们需要对数组进行动态扩容;并且为了标记栈的栈顶,我们需要一个top始终指向栈顶,这是栈实现的核心。
来看代码:
cpp
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
typedef int StackType;
typedef struct Stack
{
StackType* data;
int top;//栈顶指针,始终指向最顶端的元素
int capacity;//数组容量,判断扩容
};
下面我们来实现功能。
1、初始化
初始化时,我们需要定义数组的初始大小,因此除了需要传入指针外,我们再传入一个初始的数组大小。
这里还需要注意的是,一般我们会将top赋值为-1,表示"始终指向栈顶元素的下标"。因此,初始化时栈没有元素,我们就先给一个-1。代码如下:
cpp
void InitStack(Stack* p, int n)
{
assert(p);
p->data = (StackType*)malloc(sizeof(StackType) * n);
if (p->data == NULL)
{
perror("malloc failed!");
return;
}
p->capacity = n;
p->top = -1;//栈顶,赋值-1表示为空栈
}
2、销毁
malloc出的内存都要记得销毁!
cpp
void DestroyStack(Stack* p)
{
assert(p);
free(p->data);
p->data = NULL;
p->capacity = 0;
p->top = -1;
}
3、入栈
直接看代码:
cpp
void StackPush(Stack* p, StackType x)
{
assert(p);
if (p->top + 1 == p->capacity)
{
StackType* tmp = (StackType*)realloc(p->data, sizeof(StackType) * p->capacity * 2);
if (tmp == NULL)
{
perror("realloc failed");
return;
}
p->data = tmp;
p->capacity *= 2;
}
p->data[++p->top] = x;
}
最后赋值的时候要先让top+1再赋值。
4、出栈
也是直接上代码:
cpp
void StackPop(Stack* p)
{
assert(p);
if (p->top == -1)
{
printf("出栈失败!当前栈为空!\n");
return;
}
p->top--;
}
这里直接使用top--来实现删除,实际上在内存中该元素还是存在,只是对栈来说不存在了,在下一次入栈时会将该部分覆盖掉。因此我们直接使用top--就能实现删除。
5、返回栈顶元素
这个函数就很简单了。因为p->top对应的就是栈顶元素的下标。
cpp
StackType StackTop(Stack* p)
{
assert(p);
assert(p->top != -1);//保证非空
return p->data[p->top];
}
6、测试
目前我们栈的功能已经实现完整了。下面我们写一个测试函数用于在main中使用:
cpp
void Test()
{
Stack s;
InitStack(&s, 3); // 初始化容量为3
printf("入栈\n");
StackPush(&s, 10);
StackPush(&s, 20);
StackPush(&s, 30);
// 测试自动扩容
StackPush(&s, 40);
printf("栈顶元素: %d\n", StackTop(&s)); // 应该是40
printf("出栈\n");
StackPop(&s);
printf("栈顶元素: %d\n", StackTop(&s)); // 应该是30
StackPop(&s);
printf("栈顶元素: %d\n", StackTop(&s)); // 应该是20
StackPush(&s, 50);
printf("栈顶元素: %d\n", StackTop(&s)); // 应该是50
// 出栈所有元素
while (s.top != -1)
{
printf("出栈元素: %d\n", StackTop(&s));
StackPop(&s);
}
DestroyStack(&s);
printf("栈已销毁\n");
}
二、使用链表实现栈
链表栈会永远将栈顶放在头节点上便于O(1)操作。
1、头文件
结构体和我们写过的链表一样。
cpp
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
typedef int LSType;
typedef struct Node
{
LSType data;
struct Node* next;
}Node;
typedef struct LStack
{
Node* _top;
};
void InitStack(LStack* p);
void DestroyStack(LStack* p);
void StackPush(LStack* p, LSType x);
void StackPop(LStack* p);
LSType StackTop(LStack* p);
2、源文件
明白了栈的原理,链表的实现也很简单。直接上代码:
cpp
#include "ListStack.h"
void InitStack(LStack* p)
{
assert(p);
p->_top = NULL;
}
void StackPush(LStack* p, LSType x)
{
assert(p);
Node* newnode = BuyNewNode(x);
newnode->next = p->_top;
p->_top = newnode;
}
Node* BuyNewNode(LSType x)
{
Node* newnode = (Node*)malloc(sizeof(Node));
if (newnode == NULL)
{
perror("malloc falied!");
return NULL;
}
newnode->data = x;
newnode->next = NULL;
return newnode;
}
void StackPop(LStack* p)
{
assert(p);
if (p->_top == NULL)
{
printf("该栈为空!\n");
return;
}
Node* next = p->_top->next;
free(p->_top);
p->_top = next;
}
LSType StackTop(LStack* p)
{
assert(p);
assert(p->_top);
return p->_top->data;
}
void DestroyStack(LStack* p)
{
assert(p);
Node* cur = p->_top;
while (cur)
{
Node* tmp = cur;
cur = cur->next;//必须在赋值之前
free(tmp);
}
p->_top = NULL;
}
测试文件
最后给出测试文件:
cpp
#include "Stack.h"
#include "ListStack.h"
void Test1()
{
Stack s;
InitStack(&s, 3); // 初始化容量为3
printf("入栈\n");
StackPush(&s, 10);
StackPush(&s, 20);
StackPush(&s, 30);
// 测试自动扩容
StackPush(&s, 40);
printf("栈顶元素: %d\n", StackTop(&s)); // 应该是40
printf("出栈\n");
StackPop(&s);
printf("栈顶元素: %d\n", StackTop(&s)); // 应该是30
StackPop(&s);
printf("栈顶元素: %d\n", StackTop(&s)); // 应该是20
StackPush(&s, 50);
printf("栈顶元素: %d\n", StackTop(&s)); // 应该是50
// 出栈所有元素
while (s.top != -1)
{
printf("出栈元素: %d\n", StackTop(&s));
StackPop(&s);
}
DestroyStack(&s);
printf("栈已销毁\n");
}
void Test2()
{
LStack s;
InitStack(&s);
StackPush(&s, 10);
StackPush(&s, 20);
StackPush(&s, 30);
printf("栈顶: %d\n", StackTop(&s)); // 30
StackPop(&s);
printf("栈顶: %d\n", StackTop(&s)); // 20
StackPop(&s);
StackPop(&s);
StackPop(&s); // 栈为空提示
DestroyStack(&s);
printf("链表栈已销毁\n");
}
int main()
{
Test1();
Test2();
return 0;
}
总结
通过上面的代码,我们已经掌握了使用顺序表和链表来实现栈,最后我们来对比总结一下两个方法。
顺序栈适用于元素数量已知或增长可控的场景,对访问速度要求高的场景。
链表栈使用于元素数量不确定,有频繁出入栈操作,且对内存使用灵活性要求高的场景。