前言
队列,作为一种重要的数据结构,在计算机科学中扮演着重要的角色,它按照先进先出(FIFO)的原则进行操作。它可以用来解决许多实际问题,队列的应用范围广泛,从操作系统中的进程调度,到网络中的数据传输,再到算法中的搜索和排序,队列无处不在。那么接下来,让我们一同开始这段关于队列的探索之旅吧!
1.队列
1.1 队列的概念及结构
队列:
只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表,队列具有先进先出的特性。
- 入队列:进行插入操作的一端称为队尾
- 出队列:进行删除操作的一端称为队头
1.2 队列的实现
队列的特性是先进先出,这里就要考虑选择顺序表还是链表来实现队列。队列需要大量的头删尾插的操作,顺序表进行头删的效率太低,所以这里我们选用链式结构来实现。
1.2.1 队列的定义
typedef int Datatype; typedef struct QueueNode { struct QueueNode* next; Datatype data; }QueueNode;
和定义链表的节点一样。但是也有所不同,我们知道队列需要大量的头删和尾插,为了方便尾插和头删我们最好再创建两个指针,一个指向头,另一个指向尾。那这样岂不是需要二级指针解决吗?
这里我将会介绍一种新的方法来解决头删问题,前边我们已经知道对链表头进行操作方法有三种:
- 一种是使用二级指针来达到修改头节点的目的
- 另外一种是使用函数返回类型来达到修改的目的
- 此外我们还可以使用带头结点的链表来解决对链表头操作时需要特殊处理的情况
进行尾插操作时,需要传递头指针和尾指针,这样使用时函数的参数变多了,调用函数时也比较费劲,那这里我们不如直接再定义一个结构体来存放队列的头指针和尾指针,以及队列的长度。
typedef int Datatype; typedef struct QueueNode { struct QueueNode* next; Datatype data; }QueueNode; typedef struct Queue { QueueNode* head; QueueNode* tail; int size; }Que;
这样我们传参时就只需传第二个结构体就可以进行了。通过第二个结构体找到第一个结构体,然后进行一系列头删、尾插的操作,使用时实质和二级指针类似,它效果和带头节点的链表类似,只需要一级指针就可以解决。
虽然栈和队列的逻辑非常简单,但是它们的结构却变得更加复杂了,链表和顺序表是后续数据结构学习的基础,一定要灵活掌握。
1.2.2队列的初始化
void QueueInit(Que* pq) { assert(pq); pq->head = pq->tail = NULL; pq->size = 0; }
初始化就非常简单了,只需要将头指针和尾指针进行初始化置为NULL,长度置为0。这里我们也不需要创建新节点,由于我们就只要一个接口需要创建新节点(尾插),所以不需要封装成函数,对节点进行初始化。
我们在调用函数时也非常简单,将第二个结构体传过去:
Que q;//创建结构体变量 QueueInit(&q);
1.2.3 入队
void QueuePush(Que* pq, Datatype x) { assert(pq); QueueNode* newnode = (QueueNode*)malloc(sizeof(QueueNode)); if (newnode == NULL) { perror("malloc"); exit(-1); } newnode->data = x; newnode->next = NULL; if (pq->tail == NULL) { pq->head = pq->tail = newnode; } else { pq->tail->next = newnode; pq->tail = newnode; } pq->size++; }
在入队操作时,我们需要创建新的节点,这里注意:创建新节点是为队列的节点申请空间,定义的第一个结构体才是队列节点的类型,所以新建节点是的类型也应该与第一个结构体对于,链表没有节点时,这时需要将头指针和尾指针指向新节点,进行特殊处理。后续再次入队就可以直接将新节点链接到尾节点的后边。入队新节点的同时注意将size++;记录队列长度。
1.2.4 判空
入队之后就是出队,但考虑到后续多个接口中需要判空,这里就提前封装成一个函数
bool IsEmpty(Que* pq) { assert(pq); return (pq->head == NULL); }
函数类型为bool类型,该类型返回值只有true(1)和false(0)两种,所以最后直接返回条件:pq->head == NULL如果为真就为true,假就为false。
1.2.5 出队
void QueuePop(Que* pq) { assert(pq); assert(!IsEmpty(pq)); if (pq->head->next == NULL) { free(pq->head); pq->head = pq->tail = NULL; } else { QueueNode* next = pq->head->next; free(pq->head); pq->head = next; } pq->size--; }
出队需要先判断队列是否为NULL,然后将头指针向后移动(改变结构体指针),释放掉第一个节点,然后队列长度size--,但需要考虑一下特殊情况,就是删空的情况,如果只剩下一个节点就可以直接释放掉,然后将头指针和尾指针置为NULL。
1.2.6 队头队尾数据
//队头 Datatype QueueFront(Que* pq) { assert(pq); assert(!IsEmpty(pq)); return pq->head->data; } //队尾 Datatype QueueBlack(Que* pq) { assert(pq); assert(!IsEmpty(pq)); return pq->tail->data; }
这里没什么好说的,直接返回队列头指针和尾指针指向节点的数据。但注意都需要进行判空操作。
1.2.7 队列长度
int QueueSize(Que* pq) { assert(pq); return pq->size; }
直接返回队列的size即可。
1.2.8 队列销毁
void DestoryQueue(Que* pq) { assert(pq); QueueNode* cur = pq->head; while (cur) { QueueNode* next = cur->next; free(cur); cur = next; } pq->head = pq->tail = NULL; pq->size = 0; }
像链表一样,遍历队列一个一个的销毁节点,最后将指针置为NULL,队列长度size置为0。
在链表学习以来部分同学会不会有这样的疑惑:销毁空间是不是不允许部分销毁吗?那一个一个节点的销毁不就可以部分销毁?那是因为标准C规定的是,释放空间时,一个malloc申请的空间必须全部释放,不可以将一个malloc申请的空间部分释放,而链表是每次增加节点就申请一次空间(malloc一次),每个节点都是相对独立的空间,所以需要一个个的遍历逐个销毁。
总结
希望通过这篇博客,可以使你对队列有了更深入的了解,并能够应用这一概念解决实际问题。无论是在计算机科学中还是在日常生活中,队列都是一种重要的工具和思维方式,它能够帮助我们更好地组织和管理事务。最后,感谢阅读!