[toc]
参考:
前情提要:
1. 定义
队列(Queue):具有一定操作约束的线性表,线性结构的两种常见应用之一,是一种可以实现”先进先出”的存储结构,类似于人排队买票。
- 插入和删除操作:只能在一端(front)插入,而在另一端(rear)删除
- 数据插入:入队列(AddQ)
- 数据删除:出队列(DeleteQ)
- 先进先出:FIFO
2. 分类
- 静态队列:用数组实现 静态队列通常都必须是循环队列
- 链式队列:用链表实现 (比较简单)
3. 应用
所有和时间有关的事件都有队列的影子。
4. 学习循环队列
需弄清楚的 7 个问题
静态队列通常都必须是循环队列,为了减少内存浪费。
循环队列的讲解:
- 静态队列为什么必须是循环队列
- 循环队列需要几个参数来确定
- 循环队列各个参数的含义
- 循环队列入队伪算法讲解
- 循环队列出队伪算法讲解
- 如何判断循环队列是否为空
- 如何判断循环队列是否已满
静态队列为什么必须是循环队列:
如图,现在如果一个数组里面存了四个元素,一开始 队头 front 就指向第一个有效元素,而 队尾 real 指向最后一个元素的下一个元素。
如果进行出队操作,先进先出,则从队头 front 开始先出,假如出队到数组下标为 2 的元素位置,此位置变成了队头 front,那么数组下标为 0/1 的元素内存被释放。出队后的数组元素内存无法重新入队,因为入队是从队尾 rear 开始,不能从队头 front 之前进行入队,而且数组中入队操作达到数组最大下标后,就无法入队操作。同理出队到数组最大下标,也无法出队操作,数组卡死,所以对静态队列必须使用循环队列来提高数组的利用率。
如果要删除元素(出队),front 只能加(往数组最大下标方向增加);如果要增加元素(入队),rear 只能加(往数组最大下标方向增加)。(按照一般数组的方法)。rear 指向当前队列的下一个位置。 队尾 rear 类似于链表创建(尾插法)和链表遍历算法的演示
中 PNODE create_list(void)
此函数中的PNODE pTail 指向尾节点
也就是说:
循环队列需要几个参数来确定
需要 2 个参数来确定:front 和 rear
循环队列各个参数的含义:
2 个参数在不同场合有不同的含义
- 队列初始化
front 和 rear 的值相等,均为 0
- 队列非空
front 代表的是队列的第一个元素
rear 代表的是队列的最后一个有效元素的下一个元素
- 队列空
front 和 rear 的值相等,但不一定为 0
循环队列入队伪算法讲解:
准备工作:
- 入队前要先判断 rear 的位置,因为 rear 有可能正指向数组最后一个元素,所以 rear + 1 就越界了。
- 如何知道 rear 是否快越界了:rear + 1 后对整个数组长度求余数(%),余数为 0,则 rear + 1 处于最后一个数组元素位置,即将越界。
两步完成:
- 将值存入rear 所代表的位置
- 错误的写法:rear = rear + 1;
正确的写法是:rear = (rear+1) % 数组的长度
循环队列出队伪算法讲解:
同入队的伪算法一样:
front =(front + 1)% 数组的长度
如何判断循环队列是否为空:
front == rear。
即如果 front 与 rear 的值相等,则该队列就一定为空。
如何判断循环队列是否已满:
已知:上图中 front 的值和 rear 的值没有规律,即可以大(3>1),小(0<4),等(f=r)。
刚开始 f=r 一定为空,那么之后经过一个循环后又 r=f 了,但此时 r=f 为满,即不能通过 r=f 这个条件来判断其既是空又是满。所以必须通过 f和r 的其他关系来判断其为满。
所以有两种方式:
- 多增加一个标识是否满的参数(但每次对队列进行操作这个标志参数都要更新,浪费系统资源)
- 少用一个元素 【通常用此种方式】
如果 front和rear 的值相差 1,且 front>rear,则证明队列已满。
用 C 语言伪算法表示为:
if ((rear+1)%数组长度==front)
已满
else
未满
5. 代码示例
5.1 静态队列(循环队列)
关于 Init(QUEUE* pQ)
的内存分配方式:有 2 种(不过没什么区别感觉)
malloc
pQ->pBase = (int *)malloc(sizeof(int)* MaxSize);
|
static
static int staticArray[MaxSize]; pQ->pBase = staticArray;
|
5.1.1 C——郝斌老师
#include <stdio.h> #include <stdlib.h> #define MaxSize 20 typedef struct Queue{ int* pBase; int front; int rear; }QUEUE;
void Init(QUEUE* pQ); bool Enter(QUEUE* pQ, int val); bool Out(QUEUE* pQ, int* pVal); bool Is_Full(QUEUE* pQ); bool Is_Empty(QUEUE* pQ); void Traverse(QUEUE* pQ);
int main(void) { QUEUE Q; Init(&Q); Enter(&Q, 1); Enter(&Q, 2); Enter(&Q, 3); Enter(&Q, 4); Enter(&Q, 5); Traverse(&Q); int pVal; if ( Out(&Q, &pVal) ) printf("出队成功!\n出队的元素是:%d\n", pVal); Traverse(&Q); return 0; }
void Init(QUEUE* pQ){
pQ->pBase = (int *)malloc(sizeof(int) * MaxSize); if ( NULL == pQ->pBase ){ printf("动态内存分配失败!\n"); exit(-1); } else { pQ->front = 0; pQ->rear = 0; } }
bool Enter(QUEUE* pQ, int val){ if ( Is_Full(pQ) ){ return false; } else { pQ->pBase[pQ->rear] = val; pQ->rear = (pQ->rear + 1) % MaxSize; return true; } }
bool Out(QUEUE* pQ, int* pVal){ if ( Is_Empty(pQ) ){ return false; } else { *pVal = pQ->pBase[pQ->front]; pQ->front = (pQ->front + 1) % MaxSize; return true; } }
bool Is_Full(QUEUE* pQ){ if ( (pQ->rear + 1) % MaxSize == pQ->front ) return true; else return false; }
bool Is_Empty(QUEUE* pQ){ if ( (pQ->front == pQ->rear) ) return true; else return false; }
void Traverse(QUEUE* pQ){ int i = pQ->front; while ( i != pQ->rear ){ printf("%d ", pQ->pBase[i]); i = (i + 1) % MaxSize; } printf("\n"); }
|
5.1.2 C——浙大数据结构
(浙大版)数据结构(四)队列
队列的顺序存储结构通常由一个一维数组和一个记录队列头元素位置的变量 front 以及一个记录队列尾元素位置的变量 rear 组成,其中 front 指向整个队列的头一个元素的再前一个,rear 指向的是整个队列的最后一个元素,从 rear 入队,从 front 出队,且仅使用 n-1 个数组空间。
#include <stdio.h> #include <stdlib.h> #define MaxSize 100 typedef int ElementType; typedef struct QNode{ ElementType Data[MaxSize]; int front; int rear; }*Queue;
Queue createQueue(); void addQ(Queue Q, ElementType e); int isFull(Queue Q); ElementType deleteQ(Queue Q); int isEmpty(Queue Q);
int main(void) { Queue Q; Q = createQueue(); addQ(Q, 1); printf("1入队\n"); addQ(Q, 2); printf("2入队\n"); addQ(Q, 3); printf("3入队\n"); printf("%d出队\n", deleteQ(Q)); printf("%d出队\n", deleteQ(Q)); return 0; }
Queue createQueue(){ Queue Q; Q = (Queue)malloc(sizeof(struct QNode)); if ( NULL == Q ){ fprintf(stderr, "内存分配失败\n"); exit(EXIT_FAILURE); } Q->front = -1; Q->rear = -1;
return Q; }
int isFull(Queue Q){ return ((Q->rear + 1) % MaxSize == Q->front); }
void addQ(Queue Q, ElementType e){ if ( isFull(Q) ){ printf("队列已满"); return; } else { Q->rear = (Q->rear + 1) % MaxSize; Q->Data[Q->rear] = e; } }
int isEmpty(Queue Q){ return ((Q->rear == Q->front)); }
ElementType deleteQ(Queue Q){ if ( isEmpty(Q) ){ printf("队列为空"); return 0; } else { Q->front = (Q->front + 1) % MaxSize; return Q->Data[Q->front]; } }
|
5.1.3 C++——王道
5.1.3.1 C++ 引用
主要区别是使用了 C++ 的引用,而不是传递指针。 引用是 C++ 中的特性,允许在函数中修改传递的参数,而无需显式地使用指针
定义:
在C++中,引用是一个别名,它提供了对变量的另一个名称。引用在C++中是通过使用 & 符号进行声明和定义的。引用必须在初始化时被绑定到一个对象,而且一旦绑定,它将一直引用该对象。
基本语法:
type &refName = originalVariable;
|
其中,type 是原始变量的类型,refName 是引用的名称,originalVariable 是原始变量。引用的声明和初始化必须同时进行。
示例:
#include <iostream> using namespace std; int main() { int a = 42; int &b = a;
cout << "a: " << a << endl; cout << "b: " << b << endl;
b = 99; cout << "a: " << a << endl; cout << "b: " << b << endl; return 0; }
|
其中:b 是 a 的引用,它们实际上指向同一个内存地址。因此,对 b 的修改也会影响到 a。
引用有一些重要的特性和规则:
- 引用必须在声明时初始化,而且一旦初始化,就无法改变引用的目标。
- 引用不占用额外的内存空间,它只是目标变量的另一个名字。
- 引用通常用于函数参数,允许在函数中直接操作原始变量,而不是其副本。
- 引用用于提供对对象的更直观、更自然的访问。
引用在C++中广泛用于函数传递、返回引用值、操作符重载等场景。
引用和指针的异同:
引用(Reference)和指针(Pointer)是 C++ 中两种用于引用和操作变量的机制,它们有相似之处,也有一些重要的区别。以下是引用和指针的异同点:
相同点:
- 别名机制:
引用和指针都提供了对变量的别名机制,可以通过它们来访问和操作变量。
- 用于传递参数:
引用和指针都可以用于函数参数,使得函数能够直接操作原始变量,而不是对变量的副本进行操作。
- 用于修改变量值:
通过引用或指针,可以修改原始变量的值,而不仅仅是传递它们的副本。
不同点:
- 语法和操作:
- 引用:
- 引用在声明时使用 & 符号,例如 int &ref = variable;。
- 引用一旦初始化,就不能再引用其他变量,一直引用同一个对象。
- 引用不需要使用解引用操作符(*)来访问目标对象。
- 引用不能为 NULL,引用必须在初始化时指向一个有效的对象。
- 指针:
- 指针在声明时使用 * 符号,例如 int *ptr = &variable;。
- 指针可以在任何时候重新指向其他对象。
- 使用解引用操作符(*)来访问目标对象。
- 指针可以为 NULL,即指向空地址,表示不指向任何有效的对象。
- 内存占用:
- 引用:
- 引用在内存中不占用额外的空间,它只是目标变量的别名。
- 指针:
- 空引用和空指针:
- 引用:
- 指针:
- 指针可以为 NULL,表示不指向任何有效的对象,称为空指针。
- 操作符重载:
- 引用不支持操作符重载。
- 指针支持操作符重载,可以通过指针实现一些底层的操作。
- 适用场景:
- 访问对象成员:
- 引用:
- 引用使用
.
点运算符访问对象的成员,因为引用本质上是一个别名,就是绑定到某个对象上的别名,它可以像对象本身一样访问成员。
- 指针:
- 当有一个指向结构体或类的指针时,使用
->
箭头运算符来访问对象的成员。
在选择使用引用还是指针时,取决于具体的场景和需求:通常情况下,引用更适用于作为函数参数传递,而指针更适用于需要动态分配内存、在运行时改变指向等场景。
#include <stdio.h> #include <stdlib.h>
#define MaxSize 100 typedef int ElemType; typedef struct{ ElemType data[MaxSize]; int front, rear; }SqQueue;
void initQueue(SqQueue &Q); bool isEmpty(SqQueue Q); bool isFull(SqQueue Q); bool enQueue(SqQueue &Q, ElemType e); bool outQueue(SqQueue &Q, ElemType &e); bool getHead(SqQueue &Q, ElemType &e);
void printQueue(SqQueue Q); void testQueue();
int main(void) { testQueue(); return 0; }
void initQueue(SqQueue &Q){ Q.rear = Q.front = 0; }
bool isEmpty(SqQueue Q){ if ( Q.rear == Q.front ){ return true; } else { return false; } }
bool isFull(SqQueue Q){ if ( (Q.rear + 1) % MaxSize == Q.front ){ return true; } else { return false; } }
bool enQueue(SqQueue &Q, ElemType e){ if ( isFull(Q) ){ return false; } Q.data[Q.rear] = e; Q.rear = (Q.rear + 1) % MaxSize; return true; }
bool outQueue(SqQueue &Q, ElemType &e){ if ( isEmpty(Q) ){ return false; } e = Q.data[Q.front]; Q.front = (Q.front + 1) % MaxSize; return true; }
bool getHead(SqQueue &Q, ElemType &e){ if ( isEmpty(Q) ){ return false; } e = Q.data[Q.front]; return true; }
void printQueue(SqQueue Q){ printf("开始打印队列\n"); while ( Q.front != Q.rear ){ printf("%d ", Q.data[Q.front]); Q.front = (Q.front + 1) % MaxSize; } printf("结束打印\n"); }
void testQueue(){ printf("开始测试\n"); SqQueue Q; initQueue(Q); if ( enQueue(Q, 1) ){ printf("入队成功!\n"); } else { printf("入队失败!\n"); } if ( enQueue(Q, 2) ){ printf("入队成功!\n"); } else { printf("入队失败!\n"); } if ( enQueue(Q, 3) ){ printf("入队成功!\n"); } else { printf("入队失败!\n"); } printQueue(Q); int e; if ( outQueue(Q, e) ){ printf("出队成功,弹出的元素是:%d\n", e); } else { printf("出队失败!"); } if ( getHead(Q, e) ){ printf("获取队头成功!队头元素是:%d\n", e); } else { printf("获取队头失败!\n"); } if ( isEmpty(Q) ){ printf("队空\n"); } else { printf("队非空\n"); } printf("结束测试\n"); }
|
5.2 链式队列
5.2.0 头结点有无与否
在链式队列中,是否使用头结点主要影响链表的头指针和尾指针的处理方式。
带头结点的链式队列:
- 头结点存在: 链表中会额外添加一个头结点,头结点的 Next 指针指向队列的头元素,而尾指针指向队列的尾元素。这样,头结点的存在使得链表中的每个节点都有一个前驱节点。
- 操作方便: 在插入和删除操作时,无需特殊处理头节点的情况,简化了代码逻辑。
- 判空方便: 判断队列是否为空可以通过检查头结点的 Next 指针是否为空来实现。
不带头结点的链式队列:
- 头结点不存在: 链表的头指针直接指向队列的头元素,而尾指针指向队列的尾元素。链表中的第一个节点即为队列的头元素。
- 操作需特殊处理: 在插入和删除操作时,需要特殊处理头节点的情况,因为头节点和其他节点的处理方式不同。
- 判空需特殊处理: 判断队列是否为空通常需要检查头指针和尾指针是否相等,因为没有头结点来帮助判断。
选择是否使用头结点主要取决于具体的设计需求和实现偏好。
带头结点的链表在某些情况下(适用于对插入和删除操作的简洁性有较高要求的情况)对边界情况的处理上更加方便,可以使代码更加简洁;而不带头结点的链表(适用于对内存占用比较敏感的情况,且可以接受插入和删除操作的代码逻辑稍微复杂一些)更接近实际队列的逻辑。
5.2.1 C++——郝斌老师
5.2.1.1 Clear();
错误写法:
void Clear(){ while( !this->isEmpty() ) { int val; this->outQueue(val); } }
|
正确写法:
void Clear(){ int val; Out_Queue(val); }
|
不需要使用循环:在每次调用 Out_Queue 之后,函数内部已经包含了循环的逻辑。只要队列不为空,就会一直执行出队操作。因此,在 Clear_Queue 中只需调用一次 Out_Queue 即可达到清空队列的目的。
5.2.1.2 ~Queue();
错误写法:
~Queue(){ this->Clear(); delete this->pHead; }
|
正确写法:
~Queue(){ Clear(); delete pHead; }
|
在析构函数中不需要显式调用 this->Clear(),因为析构函数会在对象销毁时自动调用。
在C++中,类的析构函数(Destructor)是一个特殊的成员函数,它在对象被销毁时自动调用。析构函数的作用是释放对象占用的资源,执行清理工作。
在示例代码中,析构函数的主要目的是清理队列,即释放队列中节点所占用的内存。已经定义了 Clear 函数来完成这个清理工作,所以在析构函数中调用 Clear 是合理的。
然而,根据C++的析构函数自动调用机制,如果析构函数中没有显式调用 Clear,对象销毁时也会自动调用 Clear,因为 Clear 包含了释放队列中节点内存的逻辑。
所以,可以不在析构函数中显式调用 Clear,而是依赖析构函数自动调用它。这样的做法更加简洁,因为析构函数的主要职责是清理资源,而 Clear 已经负责了这部分工作。
带头结点。
#include <iostream> using namespace std;
typedef struct Node{ int data; struct Node *pNext; }NODE, *PNODE;
class Queue{ private: PNODE pHead, pTail; public: Queue() : pHead(new NODE), pTail(pHead){ this->pHead->pNext = nullptr; } ~Queue(){ Clear(); delete pHead; } void Traverse() const{ if ( isEmpty() ){ cout << "队列为空,无法遍历!" << endl; return; } PNODE pTemp = this->pHead->pNext; while ( pTemp != nullptr ){ cout << pTemp->data << " "; pTemp = pTemp->pNext; } cout << endl; } bool isEmpty() const{ return (this->pHead == this->pTail); } void enQueue(int val){ PNODE pNew = new NODE; pNew->data = val; pNew->pNext = nullptr; this->pTail->pNext = pNew; this->pTail = pNew; } bool outQueue(int &val){ if ( isEmpty() ){ cout << "队列为空,无法出队!" << endl; return false; } PNODE pTemp = this->pHead->pNext; val = pTemp->data; this->pHead->pNext = pTemp->pNext; delete pTemp; pTemp = nullptr; if (nullptr == this->pHead->pNext){ this->pTail = this->pHead; } return true; } void Clear(){ int val; outQueue(val); } };
int main(void) { Queue Q; for ( int i = 0; i < 5; i++ ){ Q.enQueue(i + 1); } Q.Traverse(); int val; if ( Q.outQueue(val) ){ cout << "出队成功,出队的元素是: " << val << endl; } else cout << "出队失败!" << endl; Q.Traverse(); Q.Clear(); return 0; }
|
5.2.2 C——浙大数据结构
CSDN——数据结构(四)队列
不带头结点。
队列的链式存储结构也可以用一个单链表实现。插入和删除操作分别在链表的两头进行,front 在链表头,rear 在链表尾,从 rear 入队,从 front 出队。
#include <stdio.h> #include <stdlib.h> typedef int ElemType;
struct Node{ ElemType data; struct Node *Next; };
typedef struct QNode{ struct Node *rear; struct Node *front; }*Queue;
Queue createQueue(); void addQueue(Queue Q, ElemType e); ElemType deleteQueue(Queue Q); int isEmpty(Queue Q);
int main(void) { Queue Q; Q = createQueue(); printf("入队 5\n"); addQueue(Q,5); printf("入队 4\n"); addQueue(Q,4); printf("入队 3\n"); addQueue(Q,3); printf("出队 %d\n", deleteQueue(Q)); printf("出队 %d\n", deleteQueue(Q)); printf("出队 %d\n", deleteQueue(Q)); printf("%d\n", deleteQueue(Q)); return 0; }
Queue createQueue(){ Queue Q; Q = (Queue)malloc(sizeof(struct QNode)); Q->front = NULL; Q->rear = NULL; return Q; }
int isEmpty(Queue Q){ return (Q->front == NULL ); }
void addQueue(Queue Q, ElemType e){ struct Node *p; p = (struct Node*)malloc(sizeof(struct Node)); p->data = e; p->Next = NULL; if ( NULL == Q->front ){ Q->rear = Q->front = p; } else { Q->rear->Next = p; Q->rear = p; } }
ElemType deleteQueue(Queue Q){ if ( isEmpty(Q) ){ printf("队列空 "); return 0; } struct Node *p = Q->front; ElemType x = p->data; if ( Q->rear == p ){ Q->rear = Q->front = NULL; } else { Q->front = p->Next; } free(p); return x; }
|
5.2.3 C++——王道
带头结点。
#include <stdio.h> #include <stdlib.h>
typedef struct LinkNode{ int data; struct LinkNode *Next; }LinkNode;
typedef struct{ LinkNode *front, *rear; }LinkQueue;
void initQueue(LinkQueue &Q); bool enQueue(LinkQueue &Q, int x); bool deQueue(LinkQueue &Q, int &x); bool getHead(LinkQueue Q, int &x); bool isEmpty(LinkQueue Q); void clearQueue(LinkQueue &Q);
void initQueue(LinkQueue &Q){ Q.front = Q.rear = (LinkNode*)malloc(sizeof(LinkNode)); if ( Q.front == nullptr ){ fprintf(stderr, "内存分配失败\n"); exit(EXIT_FAILURE); } Q.front->Next = nullptr; }
bool enQueue(LinkQueue &Q, int x){ LinkNode *s = (LinkNode *)malloc(sizeof(LinkNode)); if ( s == nullptr ){ fprintf(stderr, "内存分配失败\n"); exit(EXIT_FAILURE); } s->data = x; s->Next = nullptr; Q.rear->Next = s; Q.rear = s; return true; }
bool deQueue(LinkQueue &Q, int &x){ if ( isEmpty(Q) ){ return false; } LinkNode *p = Q.front->Next; x = p->data; Q.front->Next = p->Next; if ( Q.rear == p ){ Q.rear = Q.front; } free(p); return true; }
bool getHead(LinkQueue Q, int &x){ if ( isEmpty(Q) ){ return false; } x = Q.front->Next->data; return true; }
bool isEmpty(LinkQueue Q){ return (Q.front == Q.rear); }
void clearQueue(LinkQueue &Q){ int x; while ( deQueue(Q, x) ){ } free(Q.front); }
void printQueue(LinkQueue Q){ printf("开始打印\n"); LinkNode *temp = Q.front->Next; while ( temp != Q.rear ){ printf("%d ", temp->data); temp = temp->Next; } printf("\n打印完成\n"); }
void testLinkQueue(){ printf("开始测试!\n"); LinkQueue Q; initQueue(Q); if ( enQueue(Q, 1) ){ printf("入队成功 1\n"); } else { printf("入队失败\n"); } if ( enQueue(Q, 2) ){ printf("入队成功 2\n"); } else { printf("入队失败\n"); } if ( enQueue(Q, 3) ){ printf("入队成功 3\n"); } else { printf("入队失败\n"); } printQueue(Q); int x; if ( deQueue(Q, x) ){ printf("出队成功,弹出的元素是 %d\n", x); } else { printf("出队失败\n"); } if ( getHead(Q, x) ){ printf("获取队头成功,队头元素是 %d\n", x); } else { printf("获取队头元素失败\n"); } if ( isEmpty(Q) ){ printf("队空\n"); } else { printf("队非空\n"); } clearQueue(Q); printf("测试结束!\n"); }
int main(void) { testLinkQueue(); return 0; }
|