嗨嗨大家~我又来啦!今天为大家带来的是与队列相关的知识。我们马上进入知识的海洋~
目录
前言
一、队列
1 队列的概念
2 队列的实现
2.1 队列的定义
2.2 队列的初始化
2.3 队列的判空
2.4 入队
2.5 出队
2.6 取队头元素
2.7 取队尾元素
2.8 取有效元素个数(队列大小)
2.9 队列的销毁
二、源代码
三、栈与队列的经典题
1 有效的括号
2 用队列实现栈
3 用栈实现队列
4 设计循环队列
前言
说起队列,不妨来想象一下,在我们的生活中总是存在一种现象:排队。它就可以看作是队列,比如:我们在排队打饭的时候,先排队的人先打饭,打完饭后便出队了,也就是说最先排队的人最先出队,最后排队的人最后出队。有了此依据,我们来对队列作出详细讲解:
一、队列
首先根据下图直观的了解队列的相关内容:
1 队列的概念
队列是一种特殊的线性表,特性是先进先出,即First In First Out(FIFO)。最先加入的元素最先取出,最后加入的元素最后取出。队列有头部和尾部,队列头部称为队头(首),队列尾部称为队尾,队列内的元素从队头到队尾的顺序符合加入队列的顺序。它只允许在一端进行插入(入队),在另一端删除的线性表(出队)。文字描述未免过于死板,为了更好的帮助大家理解,附以下图解:
左边为队头,右边为队尾。将数字 1 到 7 依次入队之后,此时队首元素是 1 ,队尾元素是 7 ,第一个出队的元素是 1 。
2 队列的实现
队列一般需要这样几个功能:
- 初始化队列
- 判断队列是否为空
- 入队
- 出队
- 取队头元素
- 取队尾元素
- 取有效元素个数(大小)
- 队列的销毁
2.1 队列的定义
//定义
typedef int QDataType;//链式队列结点
typedef struct QueueNode
{struct QueueNode* next;QDataType data;
}QNode;//链式队列
typedef struct Queue
{QNode* head;//队列的头QNode* tail;//队列的尾
}Queue;
2.2 队列的初始化
//初始化
void QueueInit(Queue* pq)
{//判空assert(pq);//不带哨兵位(即不带头结点)pq->head = pq->tail = NULL;
}
这里实现的是不带头结点的链式队列,初始时 front 和 rear 都指向NULL。
2.3 队列的判空
//判空
bool QueueEmpty(Queue* pq)
{//判空assert(pq);//看队头元素是否为NULLreturn pq->head == NULL;
}
判断队列是否为空,仅需要看队头元素是否为NULL。
2.4 入队
//入队
void QueuePush(Queue* pq, QDataType x)
{//判空assert(pq);//创建新结点newnodeQNode* newnode = (QNode*)malloc(sizeof(QNode));//判空if (newnode == NULL){perror("malloc fail\n");exit(-1);}newnode->data = x;newnode->next = NULL;//链表为空if (pq->tail == NULL){//在空队列中插入第一个元素//修改队头队尾指针pq->head = pq->tail = newnode;}else{//链表不为空pq->tail->next = newnode;//新结点插入到tail结点之后pq->tail = newnode;//修改tail指针}
}
在入队之前,需要调用 malloc 函数开辟一个新结点 newnode。对于不带头结点的情况,第一个元素入队时要特殊处理。由于一开始这两个指针都是指向NULL的,因此插入第一个元素时对这两个指针都要进行修改。
2.5 出队
//出队
void QueuePop(Queue* pq)
{//判空assert(pq);//判断队列是否为空assert(!QueueEmpty(pq));//只含一个结点if (pq->head->next == NULL){free(pq->head);//释放最后一个结点pq->head = pq->tail = NULL;//将队头与队尾指针都置空}else//含多个结点{QNode* next = pq->head->next;//next为队头结点的下一个结点free(pq->head);//释放队头结点pq->head = next;//修改头指针}
}
在出队之前,首先需要判断队列是否为空,若队列为空,则无法进行出队操作。当只剩下最后一个元素未出队时需特殊处理。首先需要调用 free函数释放首元素,然后将队头指针与队尾指针都置为 NULL。当还剩多个元素时,首先找到队头结点的下一个结点,然后调用 free函数释放掉队头结点,最后将 队头指针head 指向 next,更新队头元素。
2.6 取队头元素
//取队头元素
QDataType QueueFront(Queue* pq)
{//判空assert(pq);//判断队列是否为空assert(!QueueEmpty(pq));//取队头元素return pq->head->data;
}
在取队头元素之前,首选需要调用函数 QueueEmpty(pq) 判断队列是否为空,若为空,则无法取队头元素。若队列不为空,则取队头元素。
2.7 取队尾元素
//取队尾元素
QDataType QueueBack(Queue* pq)
{assert(pq);//判断队列是否为空assert(!QueueEmpty(pq));//取队尾元素return pq->tail->data;
}
在取队尾元素之前,首选需要调用函数 QueueEmpty(pq) 判断队列是否为空,若为空,则无法取队尾元素。若队列不为空,则取队尾元素。
2.8 取有效元素个数(队列大小)
//取有效元素个数(队列大小)
int QueueSize(Queue* pq)
{//判空assert(pq);//设置指针变量cur,用于遍历队列QNode* cur = pq->head;int size = 0;//遍历队列while (cur){++size;cur = cur->next;}return size;
}
在求队列的元素个数时,首先设置指针变量 cur,并使其指向队头元素,然后设置一个变量 size,用于统计元素个数。让指针变量 cur进入 while循环遍历整个队列,每遍历一个元素, size就自增1,直到 cur走到队尾,则跳出循环,并返回 size大小。
2.9 队列的销毁
//销毁
void QueueDestory(Queue* pq)
{//判空assert(pq);//设置指针变量cur,用于遍历队列QNode* cur = pq->head;//遍历队列while (cur){QNode* next = cur->next;free(cur);cur = next;}//将队头指针,队尾指针均置为空pq->head = pq->tail = NULL;
}
首先设置指针变量 cur,并使其指向队头元素,然后让指针变量 cur进入 while循环遍历整个队列,每遍历一个元素,就将其前一个元素释放掉,直到 cur走到队尾,则跳出循环。在销毁整个链表之后要将队头指针与队尾指针均置为 NULL。
二、源代码
Queue.h#pragma once#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
#include<stdbool.h>typedef int QDataType;//链式队列结点
typedef struct QueueNode
{struct QueueNode* next;QDataType data;
}QNode;//链式队列
typedef struct Queue
{QNode* head;//队列的头QNode* tail;//队列的尾
}Queue;//初始化
void QueueInit(Queue* pq);//判空
bool QueueEmpty(Queue* pq);//入队
void QueuePush(Queue* pq, QDataType x);//出队
void QueuePop(Queue* pq);//取队头元素
QDataType QueueFront(Queue* pq);//取队尾元素
QDataType QueueBack(Queue* pq);//元素个数(队列大小)
int QueueSize(Queue* pq);//销毁
void QueueDestory(Queue* pq);
Queue.c#include"Queue.h"//初始化
void QueueInit(Queue* pq)
{//判空assert(pq);//不带哨兵位(即不带头结点)pq->head = pq->tail = NULL;
}//判空
bool QueueEmpty(Queue* pq)
{//判空assert(pq);//看队头元素是否为NULLreturn pq->head == NULL;
}//入队
void QueuePush(Queue* pq, QDataType x)
{//判空assert(pq);//创建新结点newnodeQNode* newnode = (QNode*)malloc(sizeof(QNode));//判空if (newnode == NULL){perror("malloc fail\n");exit(-1);}newnode->data = x;newnode->next = NULL;//链表为空if (pq->tail == NULL){//在空队列中插入第一个元素//修改队头队尾指针pq->head = pq->tail = newnode;}else{//链表不为空pq->tail->next = newnode;//新结点插入到tail结点之后pq->tail = newnode;//修改tail指针}
}//出队
void QueuePop(Queue* pq)
{//判空assert(pq);//判断队列是否为空assert(!QueueEmpty(pq));//只含一个结点if (pq->head->next == NULL){free(pq->head);//释放最后一个结点pq->head = pq->tail = NULL;//将队头与队尾指针都置空}else//含多个结点{QNode* next = pq->head->next;//next为队头结点的下一个结点free(pq->head);//释放队头结点pq->head = next;//修改头指针}
}//取队头元素
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;
}//取有效元素个数(队列大小)
int QueueSize(Queue* pq)
{//判空assert(pq);//设置指针变量cur,用于遍历队列QNode* cur = pq->head;int size = 0;//遍历队列while (cur){++size;cur = cur->next;}return size;
}//销毁
void QueueDestory(Queue* pq)
{//判空assert(pq);//设置指针变量cur,用于遍历队列QNode* cur = pq->head;//遍历队列while (cur){QNode* next = cur->next;free(cur);cur = next;}//将队头指针,队尾指针均置为空pq->head = pq->tail = NULL;
}
test.c#include"Queue.h"void TestQueue()
{Queue q;//初始化QueueInit(&q);//入队QueuePush(&q, 1);QueuePush(&q, 2);QueuePush(&q, 3);QueuePush(&q, 4);QueuePush(&q, 5);QueuePush(&q, 6);QueuePush(&q, 7);//出队while (!QueueEmpty(&q)){//取对头元素printf("%d ",QueueFront(&q));//出队QueuePop(&q);}printf("\n");//销毁QueueDestory(&q);
}int main()
{test();return 0;
}
三、栈与队列的经典题
1 有效的括号
题目描述:
给定一个只包括'(',')','{','}','[',']'的字符串s,判断字符串是否有效
有效字符串需满足:
- 左括号必须用相同类型的右括号闭合
- 左括号必须以正确的顺序闭合
分析:
我们可以把左括号依次压入栈中,越往后被压入的左括号越先被弹出栈进行匹配。每出现一个右括号,就”消耗”一个左括号进行匹配检查,这个过程对应出栈操作。扫描一连串括号的过程中若发现下列情况都说明括号序列不合法,终止操作。
注意:
- 弹出栈的左括号与刚刚遇到要检查的右括号不匹配;
- 扫描到右括号时发现栈空了(右括号单身);
- 处理完所有括号后,栈非空(右括号单身)。
代码实现:
bool isValid(char * s)
{Stack st;StackInit(&st);while(*s){//左括号入栈,右括号找最近的左括号匹配if(*s == '[' || *s == '(' || *s == '{'){StackPush(&st, *s);s++;}else{if(StackEmpty(&st))//只有右括号的情况{StackDestroy(&st);//销毁return false;}char top = StackTop(&st);//不匹配的情况if ( (top == '[' && *s != ']') || (top == '(' && *s != ')')|| (top == '{' && *s != '}') ) {StackDestroy(&st);return false;}else //匹配的情况{StackPop(&st);s++;}}}//如果最后栈内为空才说明是匹配的(防止最后栈内还剩下左括号的情况)bool ret = StackEmpty(&st);StackDestroy(&st);return ret;//特别注意:在return之前需要先把栈销毁掉
}
2 用队列实现栈
题目描述:
请你仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的全部四种操作(push,top,pop和empty)。
实现MyStack类:
- void push(int x):将元素x压入栈顶
- void pop():移除并返回栈顶元素
- int top():返回栈顶元素
- boolean empty():如果栈是空的,返回true;否则,返回false
分析:
队列 queue1保存原始输入数据,队列 queue2作为临时队列缓存数据。当进行 stack_pop操作时,先将 queue1里除最后一个元素外全部出队,并将出队的数据保存在临时队列queue2里,然后保存 queue1的最后一个元素,最后再将 queue2里的全部元素出队,且出队的元素重新放进 queue1里,返回保存的 queue1最后的元素。
代码实现:
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
#include<stdbool.h>typedef int QDataType;//链式队列结点
typedef struct QueueNode
{struct QueueNode* next;QDataType data;
}QNode;//链式队列
typedef struct Queue
{QNode* head;//队列的头QNode* tail;//队列的尾
}Queue;//初始化
void QueueInit(Queue* pq);//销毁
void QueueDestory(Queue* pq);//入队
void QueuePush(Queue* pq, QDataType x);//出队
void QueuePop(Queue* pq);//取队头元素
QDataType QueueFront(Queue* pq);//取队尾元素
QDataType QueueBack(Queue* pq);//判空
bool QueueEmpty(Queue* pq);//元素个数
int QueueSize(Queue* pq);/**********用队列实现栈**********///构造一个包含两个队列的栈
typedef struct
{Queue q1;Queue q2;
}MyStack;//初始化栈
MyStack* myStackCreate()
{//调用malloc为栈开辟内存空间MyStack* obj = (MyStack*)malloc(sizeof(MyStack));//初始化两个队列QueueInit(&obj->q1);QueueInit(&obj->q2);return obj;
}//入栈
void myStackPush(MyStack* obj, int x)
{//判空assert(obj);//往不为空的队列插入元素,若两个队列均为空,插入其中一个即可if (!QueueEmpty(&obj->q1)){QueuePush(&obj->q1,x);}else{QueuePush(&obj->q2,x);}//出栈
int myStackPop(MyStack* obj)
{//判空assert(obj);//假设q1队列为空,q2队列不为空Queue* emptyQ = &obj->q1;Queue* nonEmptyQ = &obj->q2;//若q1队列不为空,则将q2队列设为空,q1队列设为非空if (!QueueEmpty(&obj->q1)){emptyQ = &obj->q2;nonEmptyQ = &obj->q1;}//把非空队列的数据导入到空队列,也就是将非空队列的前n-1个数据导入至另一个空队列while (QueueSize(nonEmptyQ) > 1){QueuePush(emptyQ,QueueFront(nonEmptyQ));//取非空队列对头的数据插入到空队列中去QueuePop(nonEmptyQ);//出队}int top = QueueFront(nonEmptyQ);//取非空队列的队头元素QueuePop(nonEmptyQ);//出队return top;
}//取栈顶元素
int myStackTop(MyStack* obj)
{//判空assert(obj);//若q1队列不为空,则取q1队尾元素if (!QueueEmpty(&obj->q1)){return QueueBack(&obj->q1);}else{//若q2队列不为空,则取q2队尾元素return QueueBack(&obj->q2);}
}//判断栈是否为空
bool myStackEmpty(MyStack* obj)
{//判空assert(obj);//只有当两个队列均为空时,才表示栈为空return QueueEmpty(&obj->q1) && QueueEmpty(&obj->q2);
}//销毁栈
void myStackFree(MyStack* obj)
{//判空assert(obj);//销毁队列q1和q2QueueDestory(&obj->q1);QueueDestory(&obj->q2);//释放栈free(obj);
}
3 用栈实现队列
题目描述:
请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(push,pop,peek,empty)。
实现MyQueue类:
- void push(int x):将元素x推到队列的末尾
- int pop():从队列的开头移除并返回元素
- int peek():返回队列开头的元素
- boolean empty():如果队列为空,返回true;否则,返回false
分析:
用两个栈实现一个队列,设置其中一个栈 pushst专门入数据,另一个栈 popst专门出数据。若要入队,则进栈 pushst;若要出队,首先看 popst栈是否为空,如果为空,就先把栈 pushst的数据转移过来,然后出队,如果不为空,则直接出栈 popst的数据。
代码实现:
typedef struct
{Stack pushST;Stack popST;
} MyQueue;MyQueue* myQueueCreate()
{MyQueue* q = (MyQueue*)malloc(sizeof(MyQueue));StackInit(&q->pushST);StackInit(&q->popST);return q;
}void myQueuePush(MyQueue* obj, int x)
{//不管栈内有没有数据,只要是入队操作就向Push栈入数据即可StackPush(&obj->pushST, x);
}//获取队头数据
int myQueuePeek(MyQueue* obj)
{//如果pop栈为空,先把push栈数据导入pop栈if(StackEmpty(&obj->popST)){while(!StackEmpty(&obj->pushST)){StackPush(&obj->popST, StackTop(&obj->pushST));StackPop(&obj->pushST);}}return StackTop(&obj->popST);
}//出队
int myQueuePop(MyQueue* obj)
{//如果pop栈为空,先把push栈数据导入pop栈/*if(StackEmpty(&obj->popST)){while(!StackEmpty(&obj->pushST)){StackPush(&obj->popST, StackTop(&obj->pushST));StackPop(&obj->pushST);}}*///复用int top = myQueuePeek(obj);//易错点:不能写&obj->popST,因为该传入队列的指针StackPop(&obj->popST);return top;
}bool myQueueEmpty(MyQueue* obj)
{//push栈和pop栈同时为空,队列才为空return StackEmpty(&obj->pushST) && StackEmpty(&obj->popST);
}void myQueueFree(MyQueue* obj)
{StackDestroy(&obj->pushST);StackDestroy(&obj->popST);free(obj);
}
4 设计循环队列
题目描述:
设计你的循环队列实现。循环队列是一种线性数据结构,其操作表现基于FIFO(先进先出)原则并且队尾被连接在队首之后以形成一个循环。它也被称为"环形缓冲器"。
循环队列的一个好处是我们可以利用这个队列之前用过的空间。在一个普通队列里,一旦一个队列满了,我们就不能插入下一个元素,即使在队列前面仍有空间。但是使用循环队列,我们能使用这些空间去存储新的值。
你的实现应该支持如下操作:
- MyCircularQueue(k):构造器,设置队列长度为k
- Front:从队首获取元素。如果队列为空,返回-1
- Rear:获取队尾元素。如果队列为空,返回-1
- enQueue(value):向循环队列插入一个元素,如果成功插入则返回真
- deQueue():从循环队列中删除一个元素。如果成功删除则返回真
- isEmpty():检查循环队列是否为空
- isFull():检查循环队列是否已满
分析:
- 采用数组或者链表都可以,但是数组缓存利用率更高,所以这里主要采用数组的方式。
- 用模运算将存储空间在逻辑上变成“环状”。当发现rear指针要指向MaxSize时,不应该让它指向MaxSize而是应该让它指向数组下标为0的位置。
- 队列判空:Q.rear==Q.front;队列判满:队尾指针的下一个位置是对头,即(Q.rear+1)%MaxSize==Q.front
代码实现:
//循环队列的定义
typedef struct
{int* a;//动态开辟数组int k;//当前有效元素个数int head;//队头int tail;//队尾
}MyCircularQueue;//循环队列的初始化
MyCircularQueue* myCircularQueueCreate(int k)
{//为队列开辟一块动态内存空间MyCircularQueue* obj = (MyCircularQueue*)malloc(sizeof(MyCircularQueue));//为数组开辟一个包含(k+1)个元素的内存空间obj->a = (int*)malloc(sizeof(int) * (k + 1));//队头,队尾起始都指向数组下标为0的位置obj->head = obj->tail = 0;//当前有效元素个数设置为k个obj->k = k;return obj;
}//判断队列是否为空
bool myCircularQueueIsEmpty(MyCircularQueue* obj)
{//判空assert(obj);return obj->head == obj->tail;
}//判断队列是否已满
bool myCircularQueueIsFull(MyCircularQueue* obj)
{//判空assert(obj);int next = obj->tail + 1;//当tail指向数组最后一个下标的下一个位置时,则将tail指向数组下标为0的位置if (next == obj->k + 1){next = 0;}//队尾指针的下一个位置是对头,则表示队列已满return next == obj->head;
}//入队
bool myCircularQueueEnQueue(MyCircularQueue* obj, int value)
{//判空assert(obj);//检查队列是否已满if (myCircularQueueIsFull(obj)){return false;}//未满则将value插入队尾obj->a[obj->tail] = value;obj->tail++;//队尾指针后移//当tail指向数组最后一个下标的下一个位置时,则将tail指向数组下标为0的位置if (obj->tail == obj->k + 1){obj->tail = 0;}//obj->tail%=(k+1);return true;
}//出队
bool myCircularQueueDeQueue(MyCircularQueue* obj)
{//判空assert(obj);//检查队列是否为空if (MyCircularQueueIsEmpty(obj)){return false;}//若不空,则队头指针后移++obj->head;//当head指向数组最后一个下标的下一个位置时,则将head指向数组下标为0的位置if (obj->head == obj->k + 1){obj->head = 0;}return true;
}//取队头元素
int myCircularQueueFront(MyCircularQueue* obj)
{//判空assert(obj);//检查队列是否为空if (myCircularQueueIsEmpty(obj)){return -1;}//若不为空,则取队头元素return obj->a[obj->head]};//取队尾元素
int myCircularQueueRear(MyCircularQueue* obj)
{//判空assert(obj);//检查队列是否为空if (myCircularQueueIsEmpty(obj)){return -1;}//因为tail指向队尾元素的下一个位置,所以要取tail前一位置的下标int pre = obj->tail - 1;//若tail在数组起始位置,则前一位置的下标为数组的末尾位置if (obj->tail == 0){pre = obj->k;}//int pre = obj->tail - 1 + obj->k + 1;//pre %= (obj->k+1);//取队尾元素return obj->a[pre];
}//销毁
void myCircularQueueFree(MyCircularQueue* obj)
{//判空assert(obj);//释放动态开辟的数组free(obj->a);//释放队列free(obj);
}
本期的分享已经接近尾声,内容有些多,大家耐心些哈~重点还是要关注栈和队列的四道经典例题,它们能让我们更好的掌握核心知识。或许你在看的时候感觉有些难度,不要烦躁,更不要焦虑,没有谁能一蹴而就。你需要做的便是不断地重复、重复!!最能帮助你的那双手,就长在你的胳膊上。好啦,如果这篇文章对你们有帮助,记得留下三连加支持哈~你们的支持是我创作的最大动力!诸君加油,不负自己。那我们下期再会啦!