栈与队列Part2--队列的定义、顺序、循环、链队列及其部分操作具体实现(C语言描述)

本文介绍了队列的基本概念,包括队列的定义和性质,以及队列的顺序存储结构(顺序队列、循环队列)和链式存储结构(链队列)。讨论了不同存储结构下的入队、出队操作及其时间复杂度,分析了循环队列解决顺序队列空间问题的方法,并对比了循环队列和链队列在时间和空间性能上的差异。最后总结了栈和队列作为特殊线性表的特点。

此文章仅作为自己学习过程中的记录和总结,同时会有意地去用英文来做笔记,一些术语的英译不太准确,内容如有错漏也请多指教,谢谢!


一、队列(Queue)的定义:

  • 队列(Queue) : 是只允许在一端进行插入操作,在另一端进行删除操作的特殊的线性表。

队列是一种先进先出(FIFO)的线性表。允许插入的一段称为队尾(an),允许删除的一端称为队头(a1)。
队列在程序设计中用的很频繁。例如,我们用键盘进行各种输入到显示器上的输出;客服系统;操作系统。

同样地,作为一种特殊的线性表,队列也有顺序、链式两种存储结构。

在这里插入图片描述


二、队列的抽象数据类型:

队列也有类似线性表的各种操作特性。但操作上会有些区别,特别是插入与删除操作。

Data: 性质同线性表。数据元素具有相同的类型,相邻元素具有前驱和后驱的关系。

[ADT of queue: ]

Operations :
	InitQueue(*Q) : Set up an empty queue. //初始化操作,建立一个空队列Q。 
	DestroyQueue(*Q) : Destroy the queue if it exists. //若队列存在,则销毁它。 
	ClearQueue(*Q) : Empty the queue. //将队列清空。
	QueueEmpty(Q) : (Bool Type) Judge whether the queue is empty. //若队列为空,则返回true,否则返回false。
	GetHead(Q, *e) : If the queue exists and not empty, give the top element value to "e" and return "e". //若队列存在且非空,用e返回S的队头元素。
	EnQueue(*Q, e) : If the queue exists, push new element "e" into queue to be the top. //若队列存在,插入新元素e到栈S中并成为队尾元素。 
	DeQueue(*Q, *e) : Delete the top element in stack, give its value to "e", and return "e". //删除队列S中的队头元素,并用e返回其值。
	QueueLength(Q) : Return the amount of elements in the queue. //返回队列中元素个数。

end ADT

三、队列的顺序存储结构(Sequential Queue):

基于对队头的标定,关于这种存储结构有两种假设方法:

  1. 假设一个队列有n个元素,首先我们需要建立一个大于n的数组,并且将队列的所有元素存储在数组的前n个单元,数组下标为0的一端即是队头。易分析得,此时的入队列操作其实就是在队尾追加一个元素,不需要移动任何元素,T(n)= O(1)。但是由于队列的特性,当进行出队列操作时,所有元素都需要向前移动,以保证队头不为空,T(n) = O(n)。
  2. 为了消除第一种方法的低效,我们可以规定队头不一定要在下标为0的位置。为了避免当队列只有一个元素时,队头和队尾重合使得处理变得麻烦,此时我们引入两个指针:front指针指向队头元素;rear指针指向队尾元素的下一个位置。此时当 front == rear 时,此队列是空队列而不是只有一个元素。但是这种方法会存在“假溢出”的问题。即,实际上队列前端处是存在空闲位置的,但是rear指针往后加会产生数组越界的错误,无法访问到有效且空闲的位置。

由此可见,队列的顺序存储结构是存在很多缺陷的,于是我们又探寻新的更优的方法。

在这里插入图片描述


四、循环队列(Circular Queue):

  • 循环队列(Circular Queue) : 队列头尾相接的顺序存储结构。

引入循环队列,我们可以解决队列顺序存储结构的”假溢出“问题。但是新的问题又来了:空队列时有 front == rear,而队列满时也是 front == rear。

为了解决这一问题,我们有两个办法:

  1. 设置一个标志变量flag,当 front == rear 且 flag == 0 时为队列空;而当 front == rear 且 flag ==1 时为队列满。
  2. 当队列空时,条件就是front == rear,当队列满时我们修改其条件,保留一个元素空间,也就是说当队列满时,数组中还有一个空闲单元。此时假设队列的最大尺寸为QueueSize,那么队列满的条件为:(rear + 1) % QueueSize == front。(取模 % 的目的就是为了整合rear和front大小为一个问题。)并且,通用的计算队列长度公式为:(QueueSize + rear - front) % QueueSize。

在这里插入图片描述
接下来是循环队列的顺序存储结构的结构代码:

[The structure code of a sequential circular queue.] //循环队列的顺序存储结构代码
typedef struct
{
	QElemType data[MAXSIZE];
	int front; //front指针指向队头元素。
	int rear; //rear指针指向队尾元素的下一个位置。
}SqQueue;

五、循环队列部分操作具体实现:

  • "InitQueue"(循环队列的初始化):
[Initialize a circular queue.] //循环队列的初始化代码
//RESULT: Initialize an empty queue.
Status InitQueue (SqQueue *Q)
{
	Q->front = 0;
	Q->rear = 0;
	
	return OK;
}
  • "QueueLength"(求循环队列的长度):
[Get the length of a circular queue.] //求循环队列的长度
//RESULT: Return the amount of the elements in queue "Q" (the length of it).
int QueueLength(SqQueue Q)
[
	return (MAXSIZE + Q.rear - Q.front) % MAXSIZE;
]
  • "EnQueue"(循环队列实现入队列操作):T(n)=O(1)
[Achieve the operation of getting a new element into the queue.] //循环队列实现入队列操作
//RESULT: If the queue isn't full, insert element "e" into "Q" as the new end-element.
Status EnQueue(SqQueue* Q, QElemType e)
{
	if ((Q->rear + 1) % MAXSIZE == Q->front) //队列已满的情况。
	{
		return ERROR;
	}
	Q->data[Q->rear] = e;
	Q->rear = (Q->rear + 1) % MAXSIZE; //rear指针向后移一个位置,若到最后则转到数组头部。

	return OK;
}
T(n)=O(1)
  • "DeQueue"(循环队列实现出队列操作):T(n)=O(1)
 [P117: Achieve the operation of moving an element away from the queue.] //循环队列实现出队列操作
//RESULT: If the queue isn't empty, delete the first element in "Q" and return its value using "e".
Status DeQueue(SqQueue* Q, QElemType* e)
{
	if (Q->rear == Q->front)
	{
		return ERROR;
	}
	*e = Q->data[Q->front];
	Q->front = (Q->front + 1) % MAXSIZE;

	return OK;
}
T(n)=O(1)

在这里插入图片描述
综上:可以发现单单是顺序存储,若不是循环队列则算法的时间性能是不高的; 但循环队列又面临着数组可能会溢出的问题。


六、队列的链式存储结构(linked queue):

  • 链队列(linked queue):只能尾进头出的特殊的单链表。

为了操作上的方便,我们将队头指针front指向链队列的头结点,而队尾指针rear指向终端结点。空队列时,front和rear都指向头结点。

在这里插入图片描述

接下来是链队列的结构代码:

//链队列的结构代码:
[The structure code of a linked queue.] //The structure code of the node. 结点结构
typedef struct QNode
{
	QElemType data;
	struct QNode* next;
}QNode, *QueuePtr;

typedef struct
{
	QueuePtr front, rear; //队列的链表结构
}LinkQueue;

七、链队列部分操作具体实现:

  • "EnQueue"(循环队列实现入队列操作):T(n)=O(1)

本质上,就是在链表尾部插入新结点。

[Achieve the operation of getting a new element into the queue.] //链队列实现入队列操作
//RESULT: If the queue isn't full, insert element "e" into "Q" as the new end-element.
Status EnQueue(LinkQueue* Q, QElemType e)
{
	QueuePtr newnode = (QueuePtr)malloc(sizeof(QNode));
	if (!newnode)
	{
		exit(OVERFLOW);
	}
	newnode->data = e;
	newnode->next = NULL;
	Q->rear->next = newnode; //将新结点与原先的队尾结点连接在一起。
	Q->rear = newnode; //让新结点成为新的队尾结点。

	return OK;
}
T(n)=O(1)
  • "DeQueue"(循环队列实现出队列操作):T(n)=O(1)

其实就是头结点的后继结点出队,将头结点的后继改为它后面的结点。若链表除了头结点外只剩一个元素,则需将rear指向头结点。

[Achieve the operation of moving an element away from the queue.] //链队列实现出队列操作
//RESULT: If the queue isn't empty, delete the first element in "Q" and return its value using "e".
Status DeQueue(LinkQueue* Q, QElemType* e)
{
	QueuePtr temp;
	if (Q->front == Q->rear)
	{
		return ERROR;
	}
	temp = Q->front->next;
	*e = temp->data;
	Q->front->next = temp->next;
	if (Q->rear == temp) //若队头是队尾,则删除后将rear指向头结点。
	{
		Q->rear = Q->front;
	}
	free(temp);

	return OK;
}
T(n)=O(1)

在这里插入图片描述

总结:

  • 时间性能:循环队列与链队列的基本操作都是常数时间,即O(1)。不过循环队列是事先申请好空间,使用期间不释放;而对于链队列,每次申请和释放结点也会存在一些时间开销,如果入队出队频繁,则两者还是有细微差异。
  • 空间性能:循环队列必须有一个固定的长度,因此就有了存储元素个数和空间浪费的问题;而链队列则不存在这个问题,尽管它需要一个指针域,会产生一些空间上的开销,但也可以接受。因此空间性能方面,链队列更加灵活。

总的来说:
在可以确定队列长度最大值的情况下,建议使用循环队列;
而如果无法预估队列的长度,则用链队列。


八、总结回顾:

栈和队列都是特殊的线性表,只不过对插入和删除操作做了限制。

  • 栈(stack)是限定仅在表尾进行插入和删除操作的线性表。
  • 队列(queue)是限定只允许在一端进行插入操作,在另一端进行删除操作的线性表。

它们均可以用线性表的顺序存储结构来实现,但都存在着顺序存储结构的一些弊端,但也有对应的技巧来解决这个问题:

  • 对于栈来说,如果是两个相同数据类型的栈,则可以用数组的两端作栈底的方法来让两个栈共享数据,这样就可以最大化地利用数组的空间。
  • 对于队列来说,为了避免数组插入和删除时需要移动数据,于是就引入了循环队列,使得队头和队尾可以在数组中循环变化。解决了移动数据的时间损耗,使得本来插入和删除是O(n)的时间复杂度变成了O(1)。

当然,它们也都可以通过链式存储结构来实现。
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值