作者:低调
作者宣言:写好每一篇博客
文章目录
前言
今天我又来更新新的数据结构了,继顺序表和链表的讲解后,我们终于迎来栈和队列,这是属于线性表的最后两种结构了,今天我们为什么把这两种结构放到一起讲,因为他们有相似之处,可以相互借鉴的去学习,并且他们有了链表和顺序表作为基础,理解起来更容易,更透彻。那我们接下来就开始讲解栈和队列的相关知识点。
以下是本篇文章正文内容,下面案例可供参考
一、栈的表示和实现
1.1栈的概念及结构
栈:一种特殊的线性表,其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端称为栈顶,另一端称为栈底。栈中的数据元素遵守后进先出LIFO(Last InFirst Out)的原则。
1.让我们了解以下两个概念:
压栈:栈的插入操作叫做进栈/压栈/入栈,入数据在栈顶。
出栈:栈的删除操作叫做出栈。出数据也在栈顶。
2.大家接下来看图来了解一下:
1.2栈的实现
我们一般实现栈用数组或者链表,但我们今天所讲的是用数组来实现栈,因为数组的数据结构相对来说比较简单,我们以尾做栈顶,在尾上进行入栈时付出的代价较小。
这是数组实现:
缺点:唯一的缺陷就是空间不够需要扩容
如果使用链表,也可以完成实现栈的目的,但我们建议使用双链表,若使用单链表,我们进行出栈的时候需要保存上一个的地址,时间复杂度就不够优越,但我们入栈可以采用头插的方式,以第一个元素做为栈顶,那样就可以解决问题,虽然双向连边可以解决单链表出现的问题,但他的结构他复杂,相比较于数组,我们最终使用数组来实现栈。
这是单链表实现:
注:这个图大家看看就行了,自己去画图在理解一下,相信大家看了我前面链表部分的讲解后,理解链表的原理应该不成问题。
思考:入栈的顺序和出栈的顺序是否相等?
二、栈的接口功能实现
我们上面讲了,栈需要用数组来实现,但为了防止栈里面的空间不足,所以我们采取动态的数组,我们之前学习了顺序表,其实栈的创建和顺序虚表一样,看代码:
typedef int STDataType;
struct Stack//创建一个栈
{
STDataType* a;//用数组来存储数据
int top;//记录栈里的数据
int capacity;//容量
};
typedef struct Stack ST;
这一看不就是顺序表的创建嘛。
接下来我们看看需要实现那些接口:
这里在顺序表那一篇博客中详细介绍过,大家可以点开链接去看看link
// 初始化栈
void StackInit(Stack* ps);
// 入栈
void StackPush(Stack* ps, STDataType data);
// 出栈
void StackPop(Stack* ps);
// 获取栈顶元素
STDataType StackTop(Stack* ps);
// 获取栈中有效元素个数
int StackSize(Stack* ps);
// 检测栈是否为空,如果为空返回true结果,如果不为空返回false
bool StackEmpty(Stack* ps);
// 销毁栈
void StackDestroy(Stack* ps);
2.1初始化栈
void StackInit(ST* ps)
{
assert(ps);
ps->a = (STDataType*)malloc(sizeof(STDataType)*4);
if (ps->a == NULL)
{
printf("malloc fail\n");
exit(-1);
}
ps->top = 0;//为0的话,top指向的是栈顶的下一个元素,如果为-1;就指向栈顶元素
ps->capacity = 4;
}
因为使用数组实现的所以初始化和顺序表的几乎一模一样。
2.2入栈
void StackPush(ST* ps, STDataType x)
{
assert(ps);
if (ps->top == ps->capacity)
{
STDataType* tmp = (STDataType*)realloc(ps->a, ps->capacity * 2 * sizeof(STDataType));
if (tmp == NULL)
{
printf("realloc fail\n");
exit(-1);
}
else
{
ps->a = tmp;
ps->capacity *= 2;
}
}
ps->a[ps->top] = x;
ps->top++;
}
这里相当于顺序表的尾插,我们没有把扩容包装成一个函数,因为只有在入栈时才会涉及到扩容的情况。
2.3出栈
void StackPop(ST* ps)
{
assert(ps);
assert(ps->top > 0);//断言栈里是否有元素
ps->top--;
}
出一个数,栈里面的数据就少一个。
2.4获取栈顶的数字
STDataType StackTop(ST* ps)//返回栈顶的元素
{
assert(ps);
assert(ps->top > 0);//只有栈里面的有数据,才能返回栈顶的数据
return ps->a[ps->top - 1];
}
一般跟出栈配合使用,返回一个栈顶数据,相当于出了栈里数据,数据就少一个。
2.5获取栈中有效元素个数
int StackSize(ST* ps)//返回栈里有多少个元素
{
assert(ps);
return ps->top;
}
就是计算栈里有多少个数据,top不就是记录有效数据的个数的嘛,所以返回top的值就行了。
2.6检测栈是否为空
bool StackEmpty(ST* ps)
{
assert(ps);
return ps->top == 0;
}
这里我们使用一个bool类型,他的结果就两种,真假,return
ps->top==0,意思就是等于0为真返回true,否则返回false
2.7销毁栈
void StackDestory(ST* ps)
{
assert(ps);
free(ps->a);
ps->a = NULL;
ps->capacity = ps->top = 0;
}
相信大家看到这里,应该对栈有所了解了,并且栈和顺序表特别相似,而且比顺序表简单,熟练掌握顺序表,那实现栈应该也得心应手。
三、栈的运行结果展示
为了看的更加准确,我们写一个打印函数:
void StackPrint(ST* ps)
{
assert(ps);
int i = 0;
for (i = 0; i < ps->top; i++)
{
printf("%d ", ps->a[i]);
}
printf("\n");
}
注:这个函数建议先放到自己定义的头文件里面声明一下。
#include"Stack.h"
void StackTest()
{
ST st;
StackInit(&st);
StackPush(&st, 1);
StackPush(&st, 2);
StackPush(&st, 3);
StackPush(&st, 4);
StackPush(&st, 5);
StackPush(&st, 6);
StackPush(&st, 7);
StackPush(&st, 8);
printf("入栈后的数据的是:");
StackPrint(&st);
StackPop(&st);
printf("出栈后的剩余的数据是:");
StackPrint(&st);
int size = StackSize(&st);
printf("size=%d\n", size);
STDataType Top = StackTop(&st);
printf("栈顶的元素=%d\n", Top);
printf("出栈后的数为:");
while (!StackEmpty(&st))//打印出栈的元素
{
printf("%d ", StackTop(&st));//先打印栈顶
StackPop(&st);//在删除栈顶
}
StackDestory(&st);
}
int main()
{
StackTest();
return 0;
}
大家可以自己下来看着这篇博客先自己敲一遍,加深理解,相信学过顺序表的你对于栈的理解应该是非常容易的,不懂的可以评论,博主会尽快给你解答的。
接下来我们来讲解队列的表示和实现:
四、队列的表示和实现
4.1队列的概念及结构
队列:只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表,队列具有先进先出FIFO(First In First Out) 入队列:进行插入操作的一端称为队尾 出队列:进行删除操作的一端称为队头
4.2队列的实现
队列也可以数组和链表的结构实现,使用链表的结构实现更优一些,因为如果使用数组的结构,出队列在数组头上出数据,效率会比较低。
注:队列也可以使用数组和链表,但看上面的结构,如果是出队列,用数组来实现的话,第一个元素出以后,后面的数字都要往前面移一位,性能不够优化,有的人会说我们以最后一个元素做为对头呢,那我们在进行入队列的时候就相当于头插,也会挪动后面的数据,相比较而言,链表结构就解决了这个问题,所以我们今天用链表的结构来实现队列,因为双向链表结构复杂,综合考虑使用单链表。
思考:入队列的顺序和出队列的顺序是否相等?
五、队列的接口功能实现
我们使用单链表的结构去实现队列,让我们直接看代码看怎么创建一个队列:
typedef int QDataType;
typedef struct QueueNode//用单链表来实现队列
{
struct QueueNode* next;
QDataType data;
}QNode;
typedef struct Queue//创建一个队列
{
QNode* head;//队头
QNode* tail;//队尾
}Queue;
我们先创建一个单链表,再创建一个队列来记录队头和队尾。与单链表稍微有点不同。注意队列中的指针是定义的单链表结构类型
让我们来看看队列需要实现哪些接口吧
// 初始化队列
void QueueInit(Queue* q);
// 队尾入队列
void QueuePush(Queue* q, QDataType data);
// 队头出队列
void QueuePop(Queue* q);
// 获取队列头部元素
QDataType QueueFront(Queue* q);
// 获取队列队尾元素
QDataType QueueBack(Queue* q);
// 获取队列中有效元素个数
int QueueSize(Queue* q);
// 检测队列是否为空,如果为空返回true,如果非空返回false
int QueueEmpty(Queue* q);
// 销毁队列
void QueueDestroy(Queue* q);
5.1初始化队列
void QueueInit(Queue* pq)//初始化队列
{
assert(pq);
pq->head = pq->tail = NULL;
}
因为队列里没有数据,所以队头队尾先为空。
5.2队尾入队列
入队列可以理解为单链表的尾插
void QueuePush(Queue* pq, QDataType x)//入队列//尾插
{
assert(pq);
QNode* newNode = (QNode*)malloc(sizeof(QNode));
if (newNode == NULL)
{
printf("开辟失败\n");
exit(-1);
}
newNode->data = x;
newNode->next = NULL;
if (pq->head == NULL)
{
pq->head = pq->tail = newNode;
}
else
{
pq->tail->next = newNode;
pq->tail = pq->tail->next;
}
这里不像单链表一样把开辟新节点包装成一个函数,因为开辟新节点指挥子啊入队列这个函数中才会用到,大家也可以通过这个图很直观看到,在结合代码去理解一下。
5.3队头出队列
void QueuePop(Queue* pq)//出队列//头删
{
assert(pq);
assert(pq->head);
if (pq->head->next == NULL)//一个结点,防止tail是野指针
{
free(pq->head);
pq->head = pq->tail = NULL;
}
else
{
QNode* next = pq->head->next;
free(pq->head);
pq->head= next;
}
}
队列中只有一个元素的时候,进行出队列的时候需要将head和tail置为NULL;防止野指针,一个以上结点的时候要注意记录队头的下一个,防止把出队列后,找不到下一个作为队头。
5.4获取队列头部元素
QDataType QueueFront(Queue* pq)//返回队头元素
{
assert(pq);
assert(pq->head);
return pq->head->data;
}
head就是指向头部的,返回里面的元素就行了。
5.5获取队列尾部元素
QDataType QueueBack(Queue* pq)//返回队尾元素
{
assert(pq);
assert(pq->head);
return pq->tail->data;
}
tail就是指向尾部的,返回里面的元素即可。
5.6获取队列中有效元素个数
int QueueSize(Queue* pq)//计算队列元素个数
{
assert(pq);
int size = 0;
QNode* cur = pq->head;
while (cur)
{
++size;
cur = cur->next;
}
return size;
}
注:不能直接对head进行引用,应该创建一个新的指针变量cur,然后在进行层序遍历,计算出有效个数。
5.7检测队列是否为空
bool QueueEmpty(Queue* pq)//判断队列是否为空
{
assert(pq);
return pq->head == NULL;
}
用了一个布尔类型。pq->head代表队头,他为空代表队列为空,放回true,否则返回false。
5.8销毁队列
void QueueDestory(Queue* pq)//销毁队列
{
assert(pq);
QNode* cur = pq->head;
while (cur)
{
QNode* next = cur->next;
free(cur);
cur = next;
}
pq->head = pq->tail = NULL;
}
我们要创建一个新的指针变量保存队头,在循环里面有创建一个变量保存要销毁的下一个,避免找不到他的地址。
六、队列的运行结果展示
为了使结果更加准确我们实现一个打印函数:
void QueuePrint(Queue* pq)
{
assert(pq);
QNode* cur = pq->head;
while (cur)
{
printf("%d ", cur->data);
cur = cur->next;
}
printf("\n");
}
#include "Queue.h"
void QueueTest()
{
Queue q;
QueueInit(&q);
QueuePush(&q, 1);
QueuePush(&q, 2);
QueuePush(&q, 3);
QueuePush(&q, 4);
QueuePush(&q, 5);
QueuePush(&q, 6);
printf("入队列后的数据:");
QueuePrint(&q);
QueuePop(&q);
QueuePop(&q);
printf("出队列后剩余的数据:");
QueuePrint(&q);
int size=QueueSize(&q);
printf("size=%d\n", size);
printf("出队列的数据:");
while (!QueueEmpty(&q))
{
printf("%d ", QueueFront(&q));
QueuePop(&q);
}
QueueDestory(&q);
}
int main()
{
QueueTest();
return 0;
}
相信大家看到这里对队列又有了更深的了解,实现的接口也比链表要少,希望大家自己下去在去实现一遍队列,这样更能熟能生巧。
这里我给大家放几道栈和队列的选择题:
1.循环队列的存储空间为 Q(1:100) ,初始状态front=rear=100 。经过一系列正常的入队与退队操作后, front=rear=99 ,则循环队列中的元素个数为( )
A 100
B 2
C 99
D 0
2.下列与队列应用的是()
A 函数的递归调用
B 数组元素的引用
C 多重循环的执行
D 先到先服务的作业调度
3.一个栈的初始状态为空。现将元素1、2、3、4、5、A、B、C、D、E依次入栈,然后再依次出栈,则
元素出栈的顺序是( )。
A 12345ABCDE
B EDCBA54321
C ABCDE12345
D 54321EDCBA
4.若进栈序列为 1,2,3,4 ,进栈过程中可以出栈,则下列不可能的一个出栈序列是()
A 1,4,3,2
B 2,3,4,1
C 3,1,4,2
D 3,4,2,1
答案:
1.D
2.D
3.B
4.C
七、总结
到这我们线性表的主要内容就说完了,说白了就两种类型,数组和链式结构,这写结构帮助我们更好的存放数据,所以我们要牢牢掌握这些基本的数据结构,如果大家有什么不懂的或者有什么疑问,请在评论区讲出来,博主会尽快给大家回复解答的,还请大家多多支持,我会持续更新优质的博文,供大家去阅读学习的。