DS:单链表实现队列

                                          创作不易,友友们来个三连支持吧! 

一、队列的概念

队列:是只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表,队列具有先进先出FIFO(First In First Out)的特点。

入队列:进行插入操作的一端称为队尾

出队列:进行删除操作的一端称为队头

DS:单链表实现队列_第1张图片

二、单链表实现队列

DS:单链表实现队列_第2张图片

       队列可以用数组实现,也可以用链表实现,但是链表会稍微优势一点,因为涉及到出队列的时候是在队列头出的,如果是数组实现的话,需要把后面所有数据都往前挪一位,效率会相对低一点,所以以下博主会优先讲解单链表实现队列,数组实现队列会在下一篇博客中进行讲解。

2.1 相关结构体的创建

       因为使用单链表的方式去实现队列,所以我们应该构造一个节点指针结构体

typedef int QDatatype;//方便后面修改存储数据的数据类型
typedef struct QueueNode//队列结点的数据结构
{
	QDatatype data;//存储数据
	struct QueueNode* next;
}QNode;

       但是这远远不够,因为我们只是使用单链表的方式去实现队列,并不代表可以完全照抄单链表的模式,由于队列队头出数据和队尾入数据的特性,我们需要构造两个节点结构体指针,一个指向队列头,一个指向队列尾,这样我们可以再封装一个队列结构体。

typedef struct Queue
{
	QNode* phead;//指向队头,用于出队(头删)
	QNode* ptail;//指向队尾,用于入队(尾插)
	int size;//记录有效元素个数
}Queue;//创建一个队列相关结构体

2.1.1 为什么这里要构造两个结构体,构造一个不行吗?? 

写一个其实也是可以的,比如:

typedef int QDatatype;//方便后面修改存储数据的数据类型
typedef struct QueueNode//队列结点的数据结构
{
	QDatatype data;//存储数据
	struct QueueNode* next,*phead,*ptail;
    int size;
}QNode;

这种写法不仅在使用上有很大的劣势,而且不易和单链表区分开来,原因如下:

1、队列的管理更加严格,单链表的管理更加松散。

        单链表可以实现尾插、头插、指定位置插入、尾删、头删、指定位置删除……管理上很松散,而队列由于其一端进,一端出的特点,不能随意的去遍历,所以我们才会需要存储队列头和队列尾的结构体节点指针,方便我们进行入队和出队的操作,同时我们需要队列头和队列尾能对所有节点起到一个管理作用(即操作队列需要经过队列头或者队列尾的同意),严格管理,而构造两个结构体就能体现出这种作用,相当于用第二个结构体中的phead和ptail去管理第一个结构体中的next和data。在写代码的时候,可以体现出来

DS:单链表实现队列_第3张图片

      也就是说pq想指向链表的元素只能通过phead和ptail去指向,不能越级,一越级就会报错,总来来说就像是一级一级去管理链队,pq管理phead和ptail,而phead和ptail管理data和next 

2、方便参数的传递。

      调用函数会方便很多,比如不构造两个结构体的话,那么调用函数就需要传两个参数,即phead和ptail,但如果有一个结构体Queue将phead和ptail指针封装起来了,那么只要传一个参数就可以了,通过访问这个参数的成员就可以找到这两个指针了。

3、不需要使用二级指针了

      以往我们在单链表的实现中,使用的是二级指针,因为单链表中的phead就是结构体指针类型,而单链表的头删以及头插都需要改变phead,所以我们需要传的是该结构体指针的地址,也就是需要用二级指针来接受,但是在这里我们实现队列时又多封装了一个Queue的结构体,虽然我们有些时候也需要改变phead和ptail,但是这两个结构体指针都是Quene的成员,所以我们只需要取Quene结构体的地址即可,只需要一级指针就可以接受了!!

2、为什么我要在队列结构体里设置一个size,不设置可以吗??

    其实不设置size也是可以的,有些书上也没有设置size,我设置size也是考虑到2个原因:

1、栈有结构体成员top,而队列没有

栈中的top其实跟顺序表中的有效数据个数基本上差异不大,虽然名字是不一样的,但是通过top是可以得到栈的有效数据个数的,而队列是没有的。

2、队列管理较为严格,不能随便遍历

    首先,队列并不具备栈那样和size类似的成员,并且由于队列只能队头出列队尾入列,不能随意去遍历,如果要遍历需要边出队列才能边遍历,代价比较大,所以我们给了一个size,帮助我们在入队列和出队列的时候及时修改存储的size成员,这样可以方便我们获得队列中有效数据的个数

2.2 初始化队列

void QueueInit(Queue* pq)
{
	assert(pq);//判断传的是不是空指针
	pq->phead = pq->ptail = NULL;
	pq->size = 0;//因为队列不像栈一样,有一个top表示栈顶元素的下标
	//所以如果我们想知道这个队列的有效数据个数,就必须遍历队列
	//由于其先进先出的特性,我们默认只能访问到头元素和尾元素
	//所以必须访问一个头元素,就出队列一次,这样才能实现遍历
	//但是这样的代价太大了,为了方便,我们直接用size
}

2.3 队尾入队列

void QueuePush(Queue* pq, QDatatype x)
{
	assert(pq);
    //入队必须从队尾入!
	QNode* newnode = (QNode*)malloc(sizeof(QNode));//创建一个新节点
	if (newnode==NULL)//如果新节点申请失败,退出程序
	{
		perror("malloc fail");
	}
	//新节点创建成功,给新节点初始化一下
	newnode->data = x;
	newnode->next = NULL;
	//开始入队
	//如果直接尾插的话,由于会用到ptail->next,所以得考虑队列为空的情况
	if (pq->ptail== NULL)//如果为空,直接把让新节点成为phead和ptail
	{
		//按道理来说,如果ptail为空,phead也应该为空
		// 但是有可能会因为我们的误操作使得phead不为空,这个时候一般是我们写错的问题
		//所以使用assert来判断一下,有问题的话会及时返回错误信息
		assert(pq->phead == NULL);
		pq->phead = pq->ptail = newnode;
	}
	else//尾插
	{
		pq->ptail->next = newnode;
		pq->ptail = newnode;
	}
	pq->size++;
}

2.3.1 为什么不单独封装一个扩容函数?

       因为队列并不像链表一样,链表的头插、尾插、指定位置插入都需要创建新节点,如果设置一个扩容函数的话复用性很高,而队列只有在队尾入数据需要创建新节点,只会用到一次,所以没有必要去封装一个这样的函数。

2.3.2 如何思考边界情况

      进行尾插的时候,我们先不考虑边界情况去做一遍,比如上述代码的else部分就是实现尾插,而我们观察发现尾插的过程中需要用到ptail的成员next的指针,所以ptail必须不能是NULL,因此要分开讨论ptail为NULL的情况

2.3.3 为什么assert(pq->phead == NULL)

     因为我们考虑ptail为空的时候,不能用成员next,但是因为按道理来说一般情况下,ptail如果是NULL,phead肯定也是NULL,没必要单独搞这一出,但是这里也有可能我们前面操作有失误,phead不是NULL,这边的assert就是为了避免我们代码写错的情况,万一写错了,可以帮我们更快找出bug

2.4 队头出队列

void QueuePop(Queue* pq)
{
	assert(pq);
	//如果队列为空,没有删除的必要
	assert(!QueueEmpty(pq));
	//队列中的出队列相当于链表的头删
	//如果直接头删,那么如果队列只有一个有效数据的话,那么我们将phead的空间释放掉,但是没有将ptail给置空
	//这样会导致ptail成为一个野指针,所以我们需要考虑只有一个节点多个节点的情况
	if (pq->phead->next == NULL)//一个节点的情况,直接将这个节点释放并置空即可
	{
		free(pq->phead);
		pq->phead = pq->ptail = NULL;//置空,防止野指针
	}
	else//多个节点的情况,直接头删

	{
		QNode* next = pq->phead->next;//临时指针记住下一个节点
		free(pq->phead);
		pq->phead = next;//让下一个节点成为新的头
	}
	pq->size--;
}

 2.4.1 如何思考边界情况

        进行头删的时候,我们先不考虑边界情况去做一遍,比如上述代码的else部分就是实现头删,其实一开始发现不了什么问题,因为我们assert已经判断了,此时链表肯定不是空,而有一个节点的时候,phead的next指向NULL也很合理,不会有问题,但是我们要考虑到虽然我们释放了phead的空间,但是我们只给phead置NULL,而没有给ptail置NULL,这样ptail就变成一个野指针了!!

2.5 获取队列头部元素

QDatatype QueueFront(Queue* pq)
{
	assert(pq);
	assert(!QueueEmpty(pq));//队列如果为空,则不可能找得到队列头元素
	//队列不为空的时候,直接返回phead指向的数据
	return pq->phead->data;
}

2.6 获取队列队尾元素

QDatatype QueueBack(Queue* pq)
{
	assert(pq);
	assert(!QueueEmpty(pq));//队列如果为空,则不可能找得到队尾元素
	//队列不为空的时候,直接返回ptail指向的数据
	return pq->ptail->data;
}

2.7 获取队列中有效元素个数

int QueueSize(Queue* pq)
{
	assert(pq);
	return pq->size;
}

     在这个函数可以充分体现处我们构造一个size成员的好处,代码量很小,如果我们用size成员的话,那么需要遍历队列,会很麻烦。 

2.8 检测队列是否为空

bool QueueEmpty(Queue* pq)//链表为空的情况,可以根据容量,也可以根据ptail==NULL&&phead==NULL
{
	assert(pq);
	return pq->ptail == NULL && pq->phead == NULL;//也可以pq->size==0
}

     因为我们设置了一个size成员,其实用size成员也是可以的,通过判断是否等于0的返回值真假来决定是否是空,这边两张写法没啥区别,只要前面的没有出错就行。

2.9 销毁队列

void QueueDestory(Queue* pq)
{
	assert(pq);//判断传的是不是空指针
	//要逐个节点释放
	QNode* pcur = pq->phead;
	while (pcur)
	{
		QNode* next = pcur->next;
		free(pcur);
		pcur = next;
	}
	pq->phead = pq->ptail = NULL;
	pq->size = 0;
}

 和单链表差不多,逐个节点释放就行。

2.10 遍历打印队列

其实严格意义来说队列是不适合被打印的,因为打印需要遍历队列,而由于队列一端进一端出的特点,要遍历里面的元素就需要一边在队列头获取元素,一般在队列头出队列,知道队列为空才能访问完所有数据,而且这样做会导致队列的数据丢失,没有复用性,因此也不适合封装这个函数,这里只是告诉大家别忘记了队列访问的特点,不要跟单链表一样,队列的管理是很严格的。一般我们只会在main函数中测试一下

#include"Queue.h"
int main()
{
	Queue q;
	QueueInit(&q);//初始化
	QueuePush(&q, 1);//入队列
	QueuePush(&q, 2);//入队列
	QueuePush(&q, 3);//入队列
	QueuePush(&q, 4);//入队列
	while (!QueueEmpty(&q))//遍历打印队列
	{
		printf("%d ", QueueFront(&q));
		QueuePop(&q);
	}
}

三、链队列实现的全部代码

3.1 Queue.h

#pragma once
#include
#include
#include
#include
typedef int QDatatype;//方便后面修改存储数据的数据类型
typedef struct QueueNode//队列结点的数据结构
{
	QDatatype data;//存储数据
	struct QueueNode* next;
}QNode;

typedef struct Queue
{
	QNode* phead;//指向队头,用于出队(头删)
	QNode* ptail;//指向队尾,用于入队(尾插)
	int size;//记录有效元素个数
}Queue;//创建一个队列相关结构体
void QueueInit(Queue* pq);//队列的初始化
void QueuePush(Queue* pq, QDatatype x);//队列的入队(尾插)
void QueuePop(Queue* pq);//队列的出队(头删)
QDatatype QueueFront(Queue* pq);//获取队列头部元素
QDatatype QueueBack(Queue* pq);//获取队列尾部元素
int QueueSize(Queue* pq);//获取队列中有效元素个数
bool QueueEmpty(Queue* pq);//判断队列是否为空
void QueueDestory(Queue* pq);//队列的销毁

3.2 Queue.c

#include"Queue.h"

void QueueInit(Queue* pq)
{
	assert(pq);//判断传的是不是空指针
	pq->phead = pq->ptail = NULL;
	pq->size = 0;//因为队列不像栈一样,有一个top表示栈顶元素的下标
	//所以如果我们想知道这个队列的有效数据个数,就必须遍历队列
	//由于其先进先出的特性,我们默认只能访问到头元素和尾元素
	//所以必须访问一个头元素,就出队列一次,这样才能实现遍历
	//但是这样的代价太大了,为了方便,我们直接用size
}
void QueuePush(Queue* pq, QDatatype x)
{
	assert(pq);
    //入队必须从队尾入!
	QNode* newnode = (QNode*)malloc(sizeof(QNode));//创建一个新节点
	if (newnode==NULL)//如果新节点申请失败,退出程序
	{
		perror("malloc fail");
	}
	//新节点创建成功,给新节点初始化一下
	newnode->data = x;
	newnode->next = NULL;
	//开始入队
	//如果直接尾插的话,由于会用到ptail->next,所以得考虑队列为空的情况
	if (pq->ptail== NULL)//如果为空,直接把让新节点成为phead和ptail
	{
		//按道理来说,如果ptail为空,phead也应该为空
		// 但是有可能会因为我们的误操作使得phead不为空,这个时候一般是我们写错的问题
		//所以使用assert来判断一下,有问题的话会及时返回错误信息
		assert(pq->phead == NULL);
		pq->phead = pq->ptail = newnode;
	}
	else
	{
		pq->ptail->next = newnode;
		pq->ptail = newnode;
	}
	pq->size++;
}

void QueuePop(Queue* pq)
{
	assert(pq);
	//如果队列为空,没有删除的必要
	assert(!QueueEmpty(pq));
	//队列中的出队列相当于链表的头删
	//如果直接头删,那么如果队列只有一个有效数据的话,那么我们将phead的空间释放掉,但是没有将ptail给置空
	//这样会导致ptail成为一个野指针,所以我们需要考虑只有一个节点多个节点的情况
	if (pq->phead->next == NULL)//一个节点的情况,直接将这个节点释放并置空即可
	{
		free(pq->phead);
		pq->phead = pq->ptail = NULL;//置空,防止野指针
	}
	else//多个节点的情况,直接头删

	{
		QNode* next = pq->phead->next;//临时指针记住下一个节点
		free(pq->phead);
		pq->phead = next;//让下一个节点成为新的头
	}
	pq->size--;
}

QDatatype QueueFront(Queue* pq)
{
	assert(pq);
	assert(!QueueEmpty(pq));//队列如果为空,则不可能找得到队列头元素
	//队列不为空的时候,直接返回phead指向的数据
	return pq->phead->data;
}

QDatatype QueueBack(Queue* pq)
{
	assert(pq);
	assert(!QueueEmpty(pq));//队列如果为空,则不可能找得到队尾元素
	//队列不为空的时候,直接返回ptail指向的数据
	return pq->ptail->data;
}

int QueueSize(Queue* pq)
{
	assert(pq);
	return pq->size;
}

bool QueueEmpty(Queue* pq)//链表为空的情况,可以根据容量,也可以根据ptail==NULL&&phead==NULL
{
	assert(pq);
	return pq->ptail == NULL && pq->phead == NULL;
}

void QueueDestory(Queue* pq)
{
	assert(pq);//判断传的是不是空指针
	//要逐个节点释放
	QNode* pcur = pq->phead;
	while (pcur)
	{
		QNode* next = pcur->next;
		free(pcur);
		pcur = next;
	}
	pq->phead = pq->ptail = NULL;
	pq->size = 0;
}

3.3 test.c (测试)

#include"Queue.h"
int main()
{
	Queue q;
	QueueInit(&q);//初始化
	QueuePush(&q, 1);//入队列
	QueuePush(&q, 2);//入队列
	QueuePush(&q, 3);//入队列
	QueuePush(&q, 4);//入队列
	while (!QueueEmpty(&q))//遍历打印队列
	{
		printf("%d ", QueueFront(&q));
		QueuePop(&q);
	}
}

四、队列在实际生活中的作用举例——抽号机

     在生活中,我们经常需要排队,比如在饭堂打饭时,排在前面的因为来得早可以先打到饭,而排在后面的因为来得晚就得后打到饭,这符合我们追求公平的原则(先到先得),这本身也是维持秩序的一种方式。

      排队和队列是一样的原则,先到先得对应的就是队列的先进先出,但是哪怕大家知道这样的原则,但也有可能有人去钻空子,比如:

1、打饭的时候帮室友打饭或者帮室友(女朋友)占位置。

2、趁着人多的时候找一个地方偷偷插队

3、或者一些比较恶霸型的,可能直接插队也没人敢说

4、再或者,你并不是有意插队,只不过由于顾客太多,商家忘记了顺序,误把你排在了别人前面

所以商家为了控制这个局面,使用了抽号机

比如说,第一个同学来了,通过抽号机,拿到了1号,那么领取物品的时候,商家就按照这个序号的优先级来给予服务,避免了一些人钻空子的可能,同时为了避免有的人帮别人占位置,强制要求刷脸抽号。 

抽号机的本质就是队列,比如来了一个顾客,抽号机内部就入队列一次,服务完了这个顾客,抽号机内部就出队列一次,而如果我们还想让顾客知道自己前面有多少人,就需要有一个size来统计目前有效数据的个数。

当我们有多个窗口去服务的时候,我们也可以通过抽号机这个平台来叫号,避免现场混乱,比如说目前下一个轮到3号顾客了,那么A窗口一旦空出来,就会让3号顾客过去,而如果B窗口接着空出来了,就喊4号顾客过去,这使得本来混乱的排队场面变得有序了,顾客不需要排在拥挤的收货台前,抽完号码等着叫号就行了,如果你觉得你前面的人有点多,也可以自己转身就走。

DS:单链表实现队列_第4张图片

DS:单链表实现队列_第5张图片

你可能感兴趣的:(java,算法,数据结构)