目录
0.前言
栈与队列都是线性表,本文讲详细介绍其功能及特点。
1.栈
1.1 栈的简单介绍
栈是一种后进先出的数据结构。如图:
栈底是封闭的,所以只能从栈顶出去,也就是说谁在最顶上谁就能先出去。顺序表与链表都可以实现栈。相较而言,顺序表更为合适。可以试想栈数据的插入和删除分别用链表和顺序表作底层时,哪个操作方便。
1.2 顺序表实现栈
1.2.1 基本结构
// 支持动态增长的栈
typedef int STDataType;
typedef struct Stack
{
STDataType* a; // 栈底
int top; // 栈顶
int capacity; // 容量
}Stack;
top指向栈顶,其实有两种:
- 指向栈顶元素。
- 指向栈顶元素的下一个位置。
要说明这个的原因是因为希望大家在使用栈时尽量使用栈配套的函数,因为你不知道他到底是哪种实现方式,这样下标就无法确定。
本文将实现第一种。
1.2.2 基本函数
接下来,我会逐一介绍下面的函数,利用这些函数可以实现对栈的操作。
// 初始化栈
void StackInit(Stack* ps);
// 入栈
void StackPush(Stack* ps, STDataType data);
// 出栈
void StackPop(Stack* ps);
// 获取栈顶元素
STDataType StackTop(Stack* ps);
// 获取栈中有效元素个数
int StackSize(Stack* ps);
// 检测栈是否为空,如果为空返回非零结果,如果不为空返回0
int StackEmpty(Stack* ps);
// 销毁栈
void StackDestroy(Stack* ps);
1.2.2.1 初始化函数
typedef int STDataType;
void StackInit(Stack* ps)
{
assert(ps);
STDataType* tmp = (STDataType*)malloc(sizeof(STDataType) * 4);
//判空
if (!tmp)
{
perror("create fail!");
exit(-1);
}
//初始化
ps->a = tmp;
ps->capacity = 4;
ps->top = -1;
}
参考一下顺序表的初始化。
1.2.2.2 入栈函数
top是从-1开始的,所以要先++top然后再插入数据。
由于是顺序表实现的,无可避免需要检查是否需要扩容。
void StackPush(Stack* ps, STDataType data)
{
assert(ps);
//检查是否需要扩容
if (ps->top + 1 == ps->capacity)
{
STDataType* tmp = (STDataType*)realloc(ps->a, ps->capacity * 2 * sizeof(STDataType));
if (!tmp)
{
perror("create fail!");
exit(-1);
}
//扩容
ps->a = tmp;
ps->capacity *= 2;
}
//插入数据,++top
ps->a[++ps->top] = data;
}
入栈是最后一行代码,其余的代码是检查是扩容。
1.2.2.3 出栈函数
因为仅对栈顶元素进行操作,所以基本只要top变化就行,后面的数据再入栈时可以覆盖掉原本的pop掉的数据。
void StackPop(Stack* ps)
{
assert(ps);
//top不可以越界
assert(StackEmpty(ps));
//出栈(删除)
ps->top--;
}
1.2.2.4 取栈顶元素函数
STDataType StackTop(Stack* ps)
{
assert(ps);
assert(StackEmpty(ps));
return ps->a[ps->top];
}
假如设计的栈top从0开始,那么这里将是返回顺序表中下标为top - 1的元素。所以说要用栈配套的函数。
1.2.2.5 栈size函数
返回栈有多少个元素。
int StackSize(Stack* ps)
{
assert(ps);
return ps->top + 1;
}
1.2.2.6 栈判空函数
可以使用布尔类型。
int StackEmpty(Stack* ps)
{
assert(ps);
return ps->top == -1;
}
1.2.2.7 栈销毁函数
void StackDestroy(Stack* ps)
{
assert(ps);
free(ps->a);
ps->a = NULL;
ps->capacity = 0;
ps->top = -1;
}
以上便是栈的所有基本操作函数,使用栈时,可以通过top函数与出入栈的控制来达到匹配效果。具体如何运用还得需要看场景。
2.队列
那么接下来就是队列的介绍了。
2.1 队列的简单介绍
队列也是一种线性表,与栈很相似,但是数据的出入方式不同,队列是先进先出的方式。具体结构如下图:(rear == tail)
队列的两端是敞开的,如上图就是右边进左边出,一般只有rear指针在移动。
队列的话因为要常常用到头删,所以选择链表来实现这个结构。
2.2 单链表实现队列
2.2.1 基本结构
// 链式结构:表示队列
typedef struct QListNode
{
struct QListNode* next;
QDataType data;
}QNode;
// 队列的结构
typedef struct Queue
{
QNode* head;
QNode* tail;
int size;
}Queue;
QNode是Queue的里层结构,可以这么理解,队列的结构有两层,
第一层,叫外层,你需要知道队列的头尾,也就是head和tail。
第二层,也就是里层,你需要知道队列里每个元素如何连接起来,于是基础结构是单链表,需要知道next和data。
于是通过第一层把第二层联系起来,就能串起队列。
2.2.1.1 队列初始化
void QueueInit(Queue* q)
{
assert(q);
q->head = NULL;
q->tail = NULL;
q->size = 0;
}
2.2.1.2 队列push()函数
在队尾插入一个元素。
void QueuePush(Queue* q, QDataType data)
{
assert(q);
//建立结点,作为队列存储数据的基本单位
QNode* newnode = (QNode*)malloc(sizeof(QNode));
if (newnode == NULL)
{
perror("malloc fail");
exit(-1);
}
//存数据
newnode->data = data;
newnode->next = NULL;
//判是否为首次插入
if (q->tail == NULL)
{
q->head = newnode;
q->tail = newnode;
}
else //连接结点
{
q->tail->next = newnode;
q->tail = newnode;
}
q->size++;
}
2.2.1.3 队列pop()函数
将队尾元素弹出。
void QueuePop(Queue* q)
{
assert(q);
//空队列不可弹出
assert(!QueueEmpty(q));
//找到队尾的前一个结点作为新队尾
//只有一个结点的情况单独处理
Queue* del = q->head;
if (q->head == q->tail)
{
free(q->tail);
q->head = q->tail = NULL;
}
else
{
q->head = q->head->next;
free(del);
}
q->size--;
}
2.2.1.3 取队头元素
QDataType QueueFront(Queue* q)
{
assert(q);
assert(!QueueEmpty(q));
return q->head->data;
}
2.2.1.4 取队尾元素
QDataType QueueBack(Queue* q)
{
assert(q);
assert(!QueueEmpty(q));
return q->tail->data;
}
2.2.1.5 队列size函数
int QueueSize(Queue* q)
{
assert(q);
return q->size;
}
2.2.1.6 队列判空函数
int QueueEmpty(Queue* q)
{
assert(q);
return q->tail == NULL;
}
2.2.1.7 队列销毁函数
void QueueDestroy(Queue* q)
{
assert(q);
//从队头开始
QNode* cur = q->head;
//销毁
while (cur)
{
QNode* del = cur;
cur = cur->next;
free(del);
}
//指针置空
q->head = q->tail = NULL;
}
3. 结语
到这里,栈和队列的基本结构和基本操作函数已经介绍完成,也是鸽了挺久的一篇文章了,栈与队列可以解决很多特别的问题,比如匹配,迷宫等。希望大家看完后能对栈与队列这两个数据结构更加了解,为以后的学习打好基础。