1.队列的概念
队列:只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表,队列具有先进先出 FIFO(First In First Out)。 入队列:进行插入操作的一端称为队尾 。出队列:进行删除操作的一端称为队头。
2.队列的实现
队列是用数组实现呢还是用链表的结构实现?这里推荐用链表的结构,而且是单链表,因为数组出头部数据的时候效率会比较低,而单链表入数据的时候定义一个尾指针尾插,出数据的时候头删就可以了,非常方便。
typedef int QDataType;
typedef struct QueueNode
{
struct QueueNode* next;
QDataType data;
}QNode;
这样就定义好了一个队列的结点。原来定义好单链表的结点后我们仅需要定义此类型的指针变量指向头结点便可以帮我们实现我们需要的接口函数。现在队列中入队列时要尾插数据,因此除了头指针外还需要定义一个尾指针指向尾结点的位置,方便尾插。为了之后轻松的实现返回数据个数的接口,我们还需要定义一个变量来时刻记录。为了传参数数不传太多参数不好控制,多个变量再定义一个结构体,这样传参数就比较方便好控制。
typedef int QDataType;
typedef struct QueueNode
{
struct QueueNode* next;
QDataType data;
}QNode;
typedef struct Queue
{
QNode* head;
QNode* tail;
int size;
}Queue;
这样队列就定义好了。有时候会有疑惑说为什么要这样定义,能不能将它们合成一个结构?肯定是不可以的,因为一个结构就是存数据进行结点间的链接的,另一个就是存了指向该结构的头指针,尾指针,个数。一个局部,一个整体,不是一个东西。
2.1初始化
初始化需要断言吗?要,因为传的参数是结构体的指针,该指针为空后面所有项目就没有办法进行了。
void QueueInit(Queue* pq)
{
assert(pq);
pq->head = NULL;
pq->tail = NULL;
pq->size = 0;
}
2.2销毁
销毁就是遍历,每次销毁当前结点时保留下一个结点。结点销毁完后让指向结点的指针置空,防止野指针。
void QueueDestroy(Queue* pq)
{
assert(pq);
QNode* cur = pq->head;
while (cur)
{
QNode* next = cur->next;
free(cur);
cur = next;
}
pq->head = NULL;
pq->tail = NULL;
pq->size = 0;
}
2.3入队列
入队列新创建一个结点出来尾插,判断刚开始指向结点的指针为空和不为空时不同的尾插实现,最后不要忘了增加size个数。
void QueuePush(Queue* pq, QDataType x)
{
assert(pq);
QNode* newnode = (QNode*)malloc(sizeof(QNode));
if (newnode == NULL)
{
perror("malloc is fail");
return;
}
newnode->data = x;
newnode->next = NULL;
if (pq->head == NULL)
{
assert(pq->tail == NULL); //必须两个指针都要为空,这里暴力检查
pq->head = pq->tail = newnode;
}
else
{
pq->tail->next = newnode;
pq->tail = newnode;
}
pq->size++;
}
2.4出队列
出队列相当于头删,每次删当前结点时保留下一个结点,但队列中没数据时就不能出队列了。这里需要注意的是,当只有一个结点的时候要直接释放该结点,并把头指针和尾指针都置空,不这样做会让尾指针变成野指针。
void QueuePop(Queue* pq)
{
assert(pq);
assert(pq->head && pq->tail);
if (pq->head->next == NULL)
{
free(pq->head);
pq->head = pq->tail = NULL;
}
else
{
QNode* next = pq->head->next;
free(pq->head);
pq->head = next;
}
pq->size--;
}
2.5元素个数
这样就体现了前面定义size的好处,直接返回就可以了,而不是又O(N)的遍历。
int QueueSize(Queue* pq)
{
assert(pq);
return pq->size;
}
2.6判空
直接可以用size的情况来反应。
bool QueueEmpty(Queue* pq)
{
assert(pq);
return pq->size == 0;
}
2.7返回队头数据
在队列不为空的情况下直接返回。
QDataType QueueFront(Queue* pq)
{
assert(pq);
assert(!QueueEmpty(pq));
return pq->head->data;
}
2.8返回队尾数据
在队列不为空的情况下直接返回。
QDataType QueueBack(Queue* pq)
{
assert(pq);
assert(!QueueEmpty(pq));
return pq->tail->data;
}
3.完整代码
//Queue.h
typedef int QDataType;
typedef struct QueueNode
{
struct QueueNode* next;
QDataType data;
}QNode;
typedef struct Queue
{
QNode* head;
QNode* tail;
int size;
}Queue;
void QueueInit(Queue* pq);
void QueueDestroy(Queue* pq);
void QueuePush(Queue* pq, QDataType x);
void QueuePop(Queue* pq);
int QueueSize(Queue* pq);
bool QueueEmpty(Queue* pq);
QDataType QueueFront(Queue* pq);
QDataType QueueBack(Queue* pq);
//Queue.c
#include "Queue.h"
void QueueInit(Queue* pq)
{
assert(pq);
pq->head = NULL;
pq->tail = NULL;
pq->size = 0;
}
void QueueDestroy(Queue* pq)
{
assert(pq);
QNode* cur = pq->head;
while (cur)
{
QNode* next = cur->next;
free(cur);
cur = next;
}
pq->head = NULL;
pq->tail = NULL;
pq->size = 0;
}
void QueuePush(Queue* pq, QDataType x)
{
assert(pq);
QNode* newnode = (QNode*)malloc(sizeof(QNode));
if (newnode == NULL)
{
perror("malloc is fail");
return;
}
newnode->data = x;
newnode->next = NULL;
if (pq->head == NULL)
{
assert(pq->tail == NULL); //必须两个指针都要为空,这里暴力检查
pq->head = pq->tail = newnode;
}
else
{
pq->tail->next = newnode;
pq->tail = newnode;
}
pq->size++;
}
void QueuePop(Queue* pq)
{
assert(pq);
assert(pq->head && pq->tail);
if (pq->head->next == NULL)
{
free(pq->head);
pq->head = pq->tail = NULL;
}
else
{
QNode* next = pq->head->next;
free(pq->head);
pq->head = next;
}
pq->size--;
}
int QueueSize(Queue* pq)
{
assert(pq);
return pq->size;
}
bool QueueEmpty(Queue* pq)
{
assert(pq);
return pq->size == 0;
}
QDataType QueueFront(Queue* pq)
{
assert(pq);
assert(!QueueEmpty(pq));
return pq->head->data;
}
QDataType QueueBack(Queue* pq)
{
assert(pq);
assert(!QueueEmpty(pq));
return pq->tail->data;
}
//Test.c
#include "Queue.h"
void TestQueue()
{
Queue pq;
QueueInit(&pq);
QueuePush(&pq, 1);
QueuePush(&pq, 2);
QueuePush(&pq, 3);
QueuePush(&pq, 4);
QueuePush(&pq, 5);
while (!QueueEmpty(&pq))
{
printf("%d ", QueueFront(&pq));
QueuePop(&pq);
}
printf("\n");
QueueDestroy(&pq);
}
int main()
{
TestQueue();
return 0;
}
4.题目练习
1.4.1
用队列实现栈。
https://leetcode-cn.com/problems/implement-stack-using-queues/https://leetcode-cn.com/problems/implement-stack-using-queues/根据题目,用两个队列实现栈,比如现在在其中一个队列中入了1,2,3,4 正常情况下出队列出的是1,但栈的要求是后进先出,那如何才能出4呢?很容易想到将前三个数据导下来,这样就可以出4了。如果继续出数据那就继续和刚才一样导数据。如果此时继续入数据,就往不为空的队列中入数据。这样就可以大致总结思路:1.保持一个队列为空,一个队列存数据。2.实现出栈效果时把前面的数据导入空队列。
1.4.2
用栈实现队列也要像队列实现栈一样不断的导数据吗?比如在其中一个栈中入了1,2,3,4。对于队列来说,出数据的时候先出的是1,那在栈中怎么出1呢?
可以把左边栈中的数据导入到右边栈中,这样出数据的时候直接在右边栈中出栈顶的元素就可以了。那如果继续入数据呢?继续入就往左边的栈中入,如果右边的栈中有数据入右边的栈中会影响出栈的顺序。此时又要出数据呢?就还是先出左边的栈的数据,直到左边的栈的数据出完了再把右边的栈的数据导到左边继续从左边出。因此就有了大致思路:其中一个栈专门用来入数据,另一个栈专门用来出数据。
1.4.3
设计循环队列。
https://leetcode-cn.com/problems/design-circular-queue/https://leetcode-cn.com/problems/design-circular-queue/队列规定队尾入数据,队头出数据。设计一个循环队列,结合之前队列的实现,首先想到的就是用链表来实现。假设这个队列初始长度是4,定义Front和Rear指针分别指向队头和队尾。刚开始循环队列中没有存储数据队列为空时为下图样子。
每次在Rear的位置存入数据,再让Rear++,当队列存满数据时是下图样子。
当Front==Rear的时候链表是空还是满呢?这里有2种解决方案:1.多定义一个size变量记录个数,每次入队列就让size++,出队列就让size--,size个数等于4时代表满了,等于0时代表空。
2.多开一个空间,这样当Front==Rear时代表空,Rear的下一个如果是Front代表满。
队头出数据的时候就让Front++,队尾入数据的时候就从Rear指向的空间入,入完后让Rear++,总之Rear的next如果是Front就不能再插入了,Rear==Front就不能再出了。
用了这两种解决方法解决问题后,循环队列大部分情况实现都比较理想,但有一个很大的问题,就是链式存储不好取队尾的元素,因为它在Rear指针的前一个结点。如果实在是要找的话,就需要从Front开始遍历一遍循环队列,或者在一开始就另外再定义一个Prev指针,指向Rear指针的前一个节点,但这样非常麻烦。这时可以想到用数组形式存储有随机访问的能力,那用数组的方式存储是否会避免一些麻烦呢?
首先循环队列长度是固定的,所以不用考虑给数组扩容的问题。假设数组的初始长度是4。Front和Rear初始值都是0,刚开始循环队列中没有存储数据队列为空时为下图样子。
每次在下标是Rear的地方存入数据,再让Rear++,当Rear为3时,再次入完数据后Rear变成了4,这时只需要模初始长度4的值就可以返回下标为0的地方,当队列存满数据时是下图样子。
这里还是和之前一样涉及当Front==Rear的时候链表是空还是满的问题。可以选择加size变量不断记录,也可以选择多开一个空间,这里就以多开一个空间的方式来解决。
多开一个空间怎么判断链表什么时候满了呢?如果是以上两幅图中第二幅图的情况,Rear+1==
Front时就代表队列满了,那第一幅图怎么判断?因为是循环队列,第一幅图中我们想让Rear+1跳回到下标为0的第一个位置看看是不是等于Front的下标,可以数据中Rear+1就越界了,那怎么办?这里让(Rear+1)%(数组长度+1)就可以了。也就是判断 (rear + 1) % (k + 1) 是否等于front 。也可以发现在什么位置这样的方法都可以判断。
这样,当Front==Rear时代表空,(Rear + 1) % (K + 1) == Front时代表满。那以这样的方式实现后如何取队尾元素呢?
对于情况一来说返回Rear-1位置的元素就可以了,对于情况二来说,Rear-1就越界了,要想让Rear-1找到下标4,就让(Rear-1 + K+ 1)%(K+ 1)就可以了。这个方式不管在哪一种情况中都是适用的。比起链表的遍历来说相对容易。