【数据结构】-如何掌握栈和队列并熟练的实现他的多个接口

作者:低调
作者宣言:写好每一篇博客


前言

今天我又来更新新的数据结构了,继顺序表和链表的讲解后,我们终于迎来栈和队列,这是属于线性表的最后两种结构了,今天我们为什么把这两种结构放到一起讲,因为他们有相似之处,可以相互借鉴的去学习,并且他们有了链表和顺序表作为基础,理解起来更容易,更透彻。那我们接下来就开始讲解栈和队列的相关知识点。


以下是本篇文章正文内容,下面案例可供参考

一、栈的表示和实现

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

七、总结

到这我们线性表的主要内容就说完了,说白了就两种类型,数组和链式结构,这写结构帮助我们更好的存放数据,所以我们要牢牢掌握这些基本的数据结构,如果大家有什么不懂的或者有什么疑问,请在评论区讲出来,博主会尽快给大家回复解答的,还请大家多多支持,我会持续更新优质的博文,供大家去阅读学习的。

  • 7
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 8
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

橘柚!

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值