嗨嗨大家~我又来啦!今天为大家带来的是与队列相关的知识。我们马上进入知识的海洋~
目录
前言
一、队列
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);
//看队头元素是否为NULL
return pq->head == NULL;
}
判断队列是否为空,仅需要看队头元素是否为NULL。
2.4 入队
//入队
void QueuePush(Queue* pq, QDataType x)
{
//判空
assert(pq);
//创建新结点newnode
QNode* 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);
//看队头元素是否为NULL
return pq->head == NULL;
}
//入队
void QueuePush(Queue* pq, QDataType x)
{
//判空
assert(pq);
//创建新结点newnode
QNode* 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和q2
QueueDestory(&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);
}
本期的分享已经接近尾声,内容有些多,大家耐心些哈~重点还是要关注栈和队列的四道经典例题,它们能让我们更好的掌握核心知识。或许你在看的时候感觉有些难度,不要烦躁,更不要焦虑,没有谁能一蹴而就。你需要做的便是不断地重复、重复!!最能帮助你的那双手,就长在你的胳膊上。好啦,如果这篇文章对你们有帮助,记得留下三连加支持哈~你们的支持是我创作的最大动力!诸君加油,不负自己。那我们下期再会啦!