编程基础 - 队列 (Queue)
本文意指简明扼要的描述队列,并用C++从零到有的实现栈。
需要一定的数据结构与程序语言的基础,尤其是要了解什么是顺序表。
文章目录
1 队列的简介(Introduction of Queue)
-
定义:队列是一种只允许在一端插入,另一端删除的特殊线性表
- 允许插入的一端称为后端(Back/Rear)或队尾(Tail)
- 允许删除的一端称为前端(Front)或队头(Head)
- 插入操作称为进队或入队(Enqueue)
- 删除操作称为退队或出队(Dequeue)
-
限制:只允许在队头删除,队尾插入
-
运算规则(特点):先进先出(First-In-First-Out,FIFO)或后进后出(Last-In-Last-out,LILO)
2 队列的主要方法(Main Methods of Queue)
队列的主要操作包括:
-
初始化(Initialize)
-
入队(Enqueue):插入元素;
-
出队(Dequeue):如果队列中有元素,删除队头元素;
-
获取队头元素(GetFront):如果队列不为空,获取队头元素;
-
清空队列(Clear):如果队列不为空,则删除所有元素;
-
判断队列空(IsEmpty):栈空返回
true,否则返回false;
3 队列的实现(C++ Code)
虽然在各种高级语言中,队列已经都被实现了:
-
在C++中,使用栈需要加入
#include <queue>。 -
在C#中,使用栈需要加入
using System.Collection.Generic;。
但在这里为了更好的理解它,我们将用C++自己实现栈。
提示:以下代码编译器为VC++,属性__declspec(property...)代码在其它编译器可能不通用,如果用其它编译器可删除这一行,直接使用方法代替。
3.1 队列的抽象类(Abstract Class)
首先,我们新建一个头文件起名为Queues.h。它将包含如下内容:
-
一些需要的常量(主要是顺序结构需要):
- 队列的默认数组长度
- 对立的默认数组长度增长率
-
一个队列的模板抽象类:包含了队列的主要方法的虚函数。
-
一些需要的包含库(
#include)
我们同时为这些内容放入 命名空间Queues(之后所有代码都在它之中) 中:
#pragma once
#include <stdexcept> // Build-in exceptions
namespace Queues
{
constexpr int DEFAULT_MAX_COUNT = 255; // Max Count of Sequential Structure
constexpr int DEFAULT_INCREAMENT = 16; // Increament of Sequential Structure
template<typename T>
class AbstractQueue
{
public: // Constructor
virtual ~AbstractQueue()
{
};
public: // public Properties
inline virtual int GetCount() = 0;
__declspec(property(get = GetCount)) int count;
public: // public Methods
virtual void Enqueue(T item) = 0;
virtual T Dequeue() = 0;
virtual T Front() = 0;
virtual T Peek();
virtual void Clear() = 0;
virtual bool Empty() = 0;
};
template<typename T>
T AbstractQueue<T>::Peek()
{
return Front();
}
}
-
#include <stdexcept>:程序异常库,里面有许多常用的异常错误信息 -
DEFAULT_MAX_COUNT:数组队列的默认长度; -
DEFAULT_INCREAMENT:数组队列的默认增长长度; -
template<typename T> class AbstractQueue:对立的模板抽象类(T为元素的类型)int GetCount()与int count:是一个属性方法,获取队列内元素个数void Enqueue(T item):入队T Dequeue():出栈T Front():获取队头元素void Clear():清空队列bool Empty():队列是否为空
Tips
constexpr int可以替换成#define:
#define DEFAULT_MAX_COUNT 255#define DEFAULT_INCREAMENT 16
有了基类之后,我们来完成一些常见的队列形式。
3.2 数组型队列(Queue Using Array)
在数组存储方式中,数组是存储数据的主要类型。而我们在进行队列操作时,需要两个变量分别指向队头与队尾,从而数组型结构需要的变量:
-
元素数组:存储元素
- 数组初始化的长度
- 当数组满时,需要增长的长度
-
队头下标:指向队头元素(出队位置)
-
队尾下标:指向下一个插入位置
而在方法上,除了继承下来的方法,我们需要增加一些顺序结构的必要方法:
-
队尾到达数组最大值的动作:
- 当队列满时,扩充队列;
- 当队列不满时,整体移动元素到从数组起始位置开始
-
队列是否满
所以,我们建立一个新的头文件SequentialQueue.h来写顺序结构,并将顺序栈命名为SequentialQueue,
即,我们的最终结构为:
#pragma once
#include "Queues.h"
namespace Queues
{
template<typename T>
class SequentialQueue : virtual public AbstractQueue<T>
{
public: // Constructor
SequentialQueue();
virtual ~SequentialQueue() override;
protected: // protected Fields
T* m_Items; // 元素数组
int m_FrontIndex; // 队头下标
int m_RearIndex; // 队尾下标
int m_MaxCount; // 元素数组长度
int m_Increament; // 元素数组增长长度
public: // public Properties
inline virtual int GetCount() override;
inline virtual int GetMaxCount();
inline int GetIncreament();
inline void PutIncreament(int value);
__declspec(property(get = GetCount)) int count;
__declspec(property(get = GetMaxCount)) int maxCount;
__declspec(property(get = GetIncreament, put = PutIncreament)) int increament;
protected: // protected Methods
// 扩充队列。
// 如果队列满了,或余量 < 增长率,则每次扩充`m_Increament`的长度。
// 如果队列不满,且余量 >= 增长率,则将元素重置成从0开始
virtual void Overflow();
public: // public Methods
virtual void Enqueue(T item) override;
virtual T Dequeue() override;
virtual T Front() override;
virtual void Clear() override;
virtual bool Empty() override;
virtual bool Full(); // 队列是否满
};
}
3.2.1 初始化与销毁(Initialize and Destroy)
在构造函数,我们主要是进行数组的初始化:
template<typename T>
inline SequentialQueue<T>::SequentialQueue()
{
m_FrontIndex = 0;
m_RearIndex = 0;
m_MaxCount = DEFAULT_MAX_COUNT;
m_Increament = DEFAULT_INCREAMENT;
m_Items = new T[m_MaxCount];
if (m_Items == 0)
{
throw std::bad_alloc(); // 分配内存失败
}
}
类似的,在析构函数中,我们主要是对数组的销毁:
template<typename T>
inline SequentialQueue<T>::~SequentialQueue()
{
if (m_Items != 0)
{
delete[] m_Items;
}
m_FrontIndex = 0;
m_RearIndex = 0;
m_MaxCount = 0;
m_Increament = 0;
}
3.2.2 属性方法(Properties)
在属性中,有一个计算元素个数的方法GetCount()。
在数组型队列中,假设我们入队为abcdef的顺序:
| 步骤 | 入队 | 出队 | Front | Rear | 队列内元素 |
|---|---|---|---|---|---|
| Init | 0 | 0 | … | ||
| 1 | a | 0(a) | 1 | a… | |
| 2 | b | 0(a) | 2 | ab… | |
| 3 | cd | 0(a) | 4 | abcd… | |
| 4 | a | 1(b) | 4 | .bcd… | |
| 5 | bc | 3(d) | 4 | …d… | |
| 6 | ef | 3(d) | 6 | …def |
可以看到,在队列中元素个数等于队尾下标减去队头下标。
所以内部字段对外的属性方法:
template<typename T>
inline int SequentialQueue<T>::GetCount()
{
return m_RearIndex - m_FrontIndex;
}
template<typename T>
inline int SequentialQueue<T>::GetMaxCount()
{
return m_MaxCount;
}
template<typename T>
inline int SequentialQueue<T>::GetIncreament()
{
return m_Increament;
}
template<typename T>
inline void SequentialQueue<T>::PutIncreament(int value)
{
if (value < 1) // 对立的增长率不能小于1
{
value = 1;
}
m_Increament = value;
}
3.2.3 主要方法(Main Methods)
我们逐个添加,要注意每个方法的前置条件。
-
入队(Enqueue):要注意顺序,这里在初始化和入队都采用的是先入队后移下标(下同)。如果你需要先移下标后入队,还需更改初始化等方法。
template<typename T> void SequentialQueue<T>::Enqueue(T item) { if (m_RearIndex == m_MaxCount) // 如果队尾达到最大值 { Overflow(); // 整体移动或者扩充队列 } m_Items[m_RearIndex] = item; // 入队 m_RearIndex++; // 下标+1(后移) } -
出队(Dequeue):出队之后要判断是否为空,重置下标后可稍微有效的利用空间
template<typename T> T SequentialQueue<T>::Dequeue() { if (Empty()) // 如果队列空 { throw std::out_of_range("no element."); // 溢出错误 } T item = m_Items[m_FrontIndex]; // 取出元素 m_Items[m_FrontIndex] = 0; // 将此位置设置为空 m_FrontIndex++; // 队头位置+1(后移) if (Empty()) // 如果队列空 { m_FrontIndex = 0; // 重置队头下标 m_RearIndex = 0; // 重置队尾下标 } return item; // 返回取出的元素 } -
获取队头元素(Front):
template<typename T> T SequentialQueue<T>::Front() { if (Empty()) // 如果队列空 { throw std::out_of_range("no element."); // 溢出错误 } return m_Items[m_FrontIndex]; } -
清空队列(Clear):
template<typename T> void SequentialQueue<T>::Clear() { // 循环清空 for (int i = m_FrontIndex; i < m_RearIndex; i++) { m_Items[i] = 0; } m_FrontIndex = 0; // 重置队头 m_RearIndex = 0; // 重置队尾 } -
判断队列是否为空(Empty):
template<typename T> bool SequentialQueue<T>::Empty() { return count == 0; } -
判断队列是否为满(Full):
template<typename T> bool SequentialQueue<T>::Full() { return count == maxCount; } -
队尾溢出处理(Overflow):
template<typename T> void SequentialQueue<T>::Overflow() { if (Full() || m_FrontIndex < m_Increament) // 如果队列满了,或余量 < 增长率 { int newSize = m_MaxCount + m_Increament; // 新的长度 = 旧长度 + 增长率 T* items = new T[newSize]; // 用新的长度初始化新的元素数组 if (items == 0) { throw std::bad_alloc(); // 分配内存失败 } // 将旧数组中的元素复制到新数组 for (int i = 0; i < m_MaxCount; i++) { items[i] = m_Items[i]; } delete[] m_Items; // 删除旧数组 m_Items = items; // 将新数组赋值 m_MaxCount = newSize; // 更新长度 } else // 如果队列没有满,且余量 >= 增长率 { int length = count; // 保存当前数组长度 // 按从队头到队尾的顺序,整体移动到从0开始 for (int i = m_FrontIndex, j = 0; i < m_RearIndex; i++, j++) { m_Items[j] = m_Items[i]; m_Items[i] = 0; } m_FrontIndex = 0; // 重新设定队头下标 m_RearIndex = length; // 重新设定队尾下标 } }
3.3 循环队列(Circular Queue)
循环队列是为了处理数组队列的“假溢出”现象,它首尾相接形成一个环。
它可以用数组也可以用链表来表示。我们这里还是使用数组。
在前进过程中,当队头或队尾超过最大值时,它重新回到零位继续前进,即:
-
当队头超过数组最大空间时,队头重新回到0;
-
当队尾超过数组最大空间时,队尾重新回到0。
而在入队和出队时,它们前进的运算可以做判断或取模(%)。
它大部分内容和数组队列一样,我们只需修改一部分即可。
即,我们的最终结构为:
#pragma once
#include "SequentialQueue.h"
namespace Queues
{
template<typename T>
class CircularQueue : public SequentialQueue<T>
{
public: // public Properties
inline virtual int GetCount() override;
inline virtual int GetMaxCount() override;
__declspec(property(get = GetCount)) int count;
__declspec(property(get = GetMaxCount)) int maxCount;
protected: // protected Methods
virtual void Overflow() override; // 以this->m_Increament扩充循环队列。
public: // public Methods
virtual void Enqueue(T item) override;
virtual T Dequeue() override;
virtual void Clear() override;
};
}
Tips 关于队满条件
在数组队列中,我们的队满条件为count == maxCount,由于count与maxCount是我们封装的方法,所以只需要更改count与maxCount的方法就可以了,不用重写队满判断。
在循环队列中,队满条件也可以使用(m_RearIndex + 1) % m_MaxCount == m_FrontIndex,原理相同。
3.3.1 元素数量(Item Count)
循环队列中,由于队尾下标可以在队头下标前面,所以不能简单的使用m_RearIndex - m_FrontIndex,你要先判断其左右位置。
-
当队头小于等于队尾时,元素数量等于队尾下标减去队头下标;
-
当队头大于队尾时,元素数量等于最大容量减去队头下标加上队尾下标。
- 右侧元素 = 最大容量 - 队头下标(下标是从0开始,但容量是从1开始)
- 左侧元素 = 队尾下标(下标是从0开始,即是数量)
举例:
- 假设有最大容量5的队列
- 则元素位置为12345,对应下标为01234
- 如果队头指向元素4,队尾指向元素2,则:
右侧元素个数 = 最大容量5 - 元素位置4 + 1 = 最大容量5 - 队头下标3 = 2
左侧元素个数 = 元素位置2 - 1 = 队尾下标1 = 1
总数 = 2 + 1 = 3
代码即:
template<typename T>
inline int CircularQueue<T>::GetCount()
{
return (this->m_FrontIndex > this->m_RearIndex)
? (this->m_MaxCount - this->m_FrontIndex + this->m_RearIndex)
: (this->m_RearIndex - this->m_FrontIndex);
}
说完了元素数量,再来看看最大元素个数,这和数组队列的区别在于:队尾下标不能追到队头下标,即队头下标的前一个位置永远是空。这是由于如果最后一个位置入队了,那么队头就等于队尾,不要忘记,我们判断队列空就是用的队头等于队尾。
这也是我们需要重写的:
template<typename T>
inline int CircularQueue<T>::GetMaxCount()
{
return this->m_MaxCount - 1;
}
注意1:m_MaxCount指数组长度,maxCount指数组内元素最大数量,使用哪一个要分析清楚(对于外部使用来说,可能只关心“我可以存多少数据?”)。
注意2:如果你觉得名字一样看起来很乱,你可以:不重写GetMaxCount()方法,而是添加一个新的对外属性变量来表示队内元素最大数量,然后重写Full()方法((m_RearIndex + 1) % m_MaxCount == m_FrontIndex)。
3.3.2 主要方法(Main Methods)
循环队列中,我们不用特别关注重置下标的问题。
而在进行下标递增时,要额外注意达到最大容量时的操作。你可以使用条件判断使它归零,也可以使用取模来计算。
-
入队(Enqueue):
template<typename T> void CircularQueue<T>::Enqueue(T item) { if (this->Full()) // 如果队满 { Overflow(); // 扩充队列 } this->m_Items[this->m_RearIndex] = item; // 入队 this->m_RearIndex = (this->m_RearIndex + 1) % this->m_MaxCount; // 取模计算下标 } -
出队(Dequeue):
template<typename T> T CircularQueue<T>::Dequeue() { if (this->Empty()) // 如果队列空 { throw std::out_of_range("no element."); // 溢出错误 } T item = this->m_Items[this->m_FrontIndex]; // 取出元素 this->m_Items[this->m_FrontIndex] = 0; // 将此位置设置为空 this->m_FrontIndex = (this->m_FrontIndex + 1) % this->m_MaxCount; // 取模计算下标 return item; // 返回取出的元素 } -
清空队列(Clear):
template<typename T> void CircularQueue<T>::Clear() { while (!this->Empty()) { Dequeue(); } } -
队满溢出处理(Overflow):在队满处理时,你同样要注意队头与队尾的位置
template<typename T> void CircularQueue<T>::Overflow() { int newSize = this->m_MaxCount + this->m_Increament; // 新的长度 = 旧长度 + 增长率 T* items = new T[newSize]; // 用新的长度初始化新的元素数组 if (items == 0) { throw std::bad_alloc(); // 分配内存失败 } // 将旧数组中的元素复制到新数组 if (this->m_FrontIndex > this->m_RearIndex) { int newIndex = 0; // 先复制右侧数据 for (int i = this->m_FrontIndex; i < this->m_MaxCount; i++) { items[newIndex] = this->m_Items[i]; newIndex++; } // 再复制左侧数据 for (int i = 0; i < this->m_RearIndex; i++) { items[newIndex] = this->m_Items[i]; newIndex++; } this->m_FrontIndex = 0; // 重置队头下标 this->m_RearIndex = newIndex; // 重置队尾下标 } else // 在队头较小的情况下,队满只有一种情况,队头在0,队尾在this->m_MaxCount-1 { for (int i = 0; i < this->m_RearIndex; i++) { items[i] = this->m_Items[i]; } } delete[] this->m_Items; // 删除旧数组 this->m_Items = items; // 将新数组赋值 this->m_MaxCount = newSize; // 更新长度 }
3.4 链式队列(Linked Queue)
在链式结构中,链表用于存储数据。
它同样需要一个队头和队尾,我们采用两个指针,队头有头节点的方式。
-
插入时,在队尾,只更改队尾指针;
-
删除时,在队头,只更改队头指针。
我们建立一个新的头文件LinkedQueue.h来写链式结构,并将链式队列命名为LinkedQueue,
即,我们的链式队列最终为:
#pragma once
#include "Queues.h"
namespace Queues
{
template<typename T>
struct LinkedNode
{
T item;
struct LinkedNode<T>* next;
};
template<typename T>
class LinkedQueue : virtual public AbstractQueue<T>
{
public: // Constructor
LinkedQueue();
virtual ~LinkedQueue() override;
protected: // protected Fields
LinkedNode<T>* m_Front; // 队头
LinkedNode<T>* m_Rear; // 队尾
int m_Count;
public: // public Properties
inline virtual int GetCount() override;
__declspec(property(get = GetCount)) int count;
public: // public Methods
virtual void Enqueue(T item) override;
virtual T Dequeue() override;
virtual T Front() override;
virtual void Clear() override;
virtual bool Empty() override;
};
template<typename T>
inline int LinkedQueue<T>::GetCount()
{
return m_Count;
}
}
3.4.1 初始化与销毁(Initialize and Destroy)
初始化与销毁的主要对象是链表的头节点。
template<typename T>
inline LinkedQueue<T>::LinkedQueue()
{
m_Front = new LinkedNode<T>();
if (m_Front == 0)
{
throw std::bad_alloc(); // 内存分配失败
}
m_Front->item = 0;
m_Front->next = 0;
m_Rear = m_Front;
m_Count = 0;
}
template<typename T>
inline LinkedQueue<T>::~LinkedQueue()
{
Clear();
m_Rear = 0;
delete m_Front;
}
3.4.2 主要方法(Main Methods)
我们继续依次添加。
-
入队(Enqueue):
template<typename T> void LinkedQueue<T>::Enqueue(T item) { LinkedNode<T>* newRear = new LinkedNode<T>(); // 初始化新队尾 if (newRear == 0) { throw std::bad_alloc(); // 分配内存失败 } newRear->item = item; newRear->next = 0; m_Rear->next = newRear; //队尾插入 m_Rear = newRear; // 设置新队尾 m_Count++; // 数量+1 } -
出队(Dequeue):
template<typename T> T LinkedQueue<T>::Dequeue() { if (Empty()) // 如果队列空 { throw std::underflow_error("no element."); // 溢出错误 } LinkedNode<T>* oldFront = m_Front->next; // 取出当前队头,作为旧队头 m_Front->next = oldFront->next; // 设置新队头 oldFront->next = 0; T item = oldFront->item; // 取出队头数据 delete oldFront; // 删除旧队头 m_Count--; // 数量-1 if (Empty()) // 如果队列空 { m_Rear = m_Front; // 更新队尾 } return item; } -
获取队头元素(Front):
template<typename T> T LinkedQueue<T>::Front() { if (Empty()) // 如果队列空 { throw std::underflow_error("no element."); // 溢出错误 } return m_Front->next->item; } -
清空队列(Clear):
template<typename T> void LinkedQueue<T>::Clear() { LinkedNode<T>* node; while (!Empty()) { node = m_Front->next; // 取当前队头,作为旧队头 m_Front->next = node->next; // 设置新队头 node->next = 0; delete node; // 删除旧队头 } m_Rear = m_Front; m_Count = 0; } -
判断队列是否空(Empty):
template<typename T> bool LinkedQueue<T>::Empty() { return m_Front->next == 0; // 或者 count == 0; }
3.5 测试队列(Queue Testing)
到目前为止,我们完成了三种队列的编写,我们来稍微测试一下它们。
创建一个主函数main.cpp,内容如下:
#include <iostream>
#include "SequentialQueue.h"
#include "CircularQueue.h"
#include "LinkedQueue.h"
using namespace std;
int main()
{
Queues::SequentialQueue<char> sQueue;
Queues::CircularQueue<char> cQueue;
Queues::LinkedQueue<char> lQueue;
cout << "为每个队列添加[a, z]字母(Enqueue)" << endl;
for (char ch = 'a'; ch <= 'z'; ch++)
{
sQueue.Enqueue(ch);
cQueue.Enqueue(ch);
lQueue.Enqueue(ch);
}
cout << "数组队列数量:" << sQueue.count << " / " << sQueue.maxCount << endl;
cout << "循环队列数量:" << cQueue.count << " / " << cQueue.maxCount << endl;
cout << "链式队列数量:" << lQueue.count << endl;
cout << endl;
cout << "出队打印(Dequeue):" << endl;
// 打印结果应为:"aaa bbb .... zzz" (每10个字母换行)
int i = 0;
while (sQueue.count > 0)
{
cout << sQueue.Dequeue() << cQueue.Dequeue() << lQueue.Dequeue();
cout << ", ";
if (++i % 10 == 0)
{
cout << endl;
}
}
cout << endl;
cout << "数组队列数量:" << sQueue.count << " / " << sQueue.maxCount << endl;
cout << "循环队列数量:" << cQueue.count << " / " << cQueue.maxCount << endl;
cout << "链式队列数量:" << lQueue.count << endl;
cout << endl;
cout << "数组队列增长率16,先入26个,再退25个,当前front=25, rear=26" << endl;
cout << "再进入230个元素,此时队尾到达最大值,但余量是25。" << endl;
cout << "此时将整体移动数组队列位置(Overflow)" << endl;
for (int i = 0; i < 26; i++)
{
sQueue.Enqueue('a');
if (i != 0)
{
sQueue.Dequeue();
}
}
for (int i = 0; i < 230; i++)
{
sQueue.Enqueue('a');
}
cout << "数组队列数量:" << sQueue.count << " / " << sQueue.maxCount << endl;
cout << endl;
cout << "进入26个元素,此时将扩充数组队列(Overflow)" << endl;
for (int i = 0; i < 26; i++)
{
sQueue.Enqueue('a');
}
cout << "数组队列数量:" << sQueue.count << " / " << sQueue.maxCount << endl;
cout << endl;
cout << "清除数组队列(CLEAR)" << endl;
sQueue.Clear();
cout << "数组队列数量:" << sQueue.count << " / " << sQueue.maxCount << endl;
cout << endl;
system("pause");
return 0;
}
输出结果为:
为每个队列添加[a, z]字母(Enqueue)
数组队列数量:26 / 255
循环队列数量:26 / 254
链式队列数量:26
出队打印(Dequeue):
aaa, bbb, ccc, ddd, eee, fff, ggg, hhh, iii, jjj,
kkk, lll, mmm, nnn, ooo, ppp, qqq, rrr, sss, ttt,
uuu, vvv, www, xxx, yyy, zzz,
数组队列数量:0 / 255
循环队列数量:0 / 254
链式队列数量:0
数组队列增长率16,先入26个,再退25个,当前front=25, rear=26
再进入230个元素,此时队尾到达最大值,但余量是25。
此时将整体移动数组队列位置(Overflow)
数组队列数量:231 / 255
进入26个元素,此时将扩充数组队列(Overflow)
数组队列数量:257 / 271
清除数组队列(CLEAR)
数组队列数量:0 / 271
请按任意键继续. . .
4 队列的应用实例(Queue Examples)
实例使用内置的队列#include <queue>。
当然,你也可以使用上述完成的队列,也可以使用自己编写的队列。
内置队列的区别:
-
Enqueue名称为
push; -
Dequeue名称为
pop,且没有返回值,获取队头只能用front。
实例待补充,如果有补充将会在这里更新链接。
本文详细介绍了队列的基本概念,包括FIFO原则和主要操作,并通过C++展示了数组型、循环型和链式队列的实现,包括初始化、入队、出队等方法。此外,还讨论了队列在实际应用中的例子。
5万+

被折叠的 条评论
为什么被折叠?



