C++数据结构与算法(八) 队列及队列的应用
队列和栈一样,是一种特殊的线性表,队列的删除和插入操作在队列两端进行,所以队列是一个FIFO的结构。
实现用数组描述的双端队列数据结构
定义:队列是一种特殊的线性表,队列的删除和插入操作在队列两端进行,插入端为队尾,删除元素的那一端称为队首。
队列的ADT如下:
#ifndef QUEUE_ABC_H
#define QUEUE_ABC_H
// 定义抽象类
template<typename T>
class queueABC
{
public:
// virtual ~queueABC();
virtual bool empty() const=0; // 纯虚函数 只读
virtual int size() const=0; // 返回队列中元素的个数
virtual T& front() = 0; // 返回队首元素
virtual T& back() = 0; // 返回队尾元素
virtual void pop() = 0; // 删除队首的元素
virtual void push(T x) = 0; // 队尾插入元素
};
#endif
数组描述方法:
方案一:
queuefront: 队首元素所在的位置
queueback: 队尾元素所在的位置
插入元素时,queueback+1, 将新元素插入,时间按复杂度
删除元素时,数组中的元素整体向左移动一位,时间按复杂度
方案2:
插入元素时,如果数组左端有空位,数组中的元素整体向左移动一位,然后再插入。如果没有空位,queueback+1,插入元素,最坏情况下,时间复杂度
删除元素时,queuefront+1,时间按复杂度
上述两个方案,如果删除的效率高,插入的效率就会低。插入的效率高,删除的效率就会低。
解决方案:
将队列的两端环接,在数组长度不变的情况下,插入和删除操作的时间复杂度均为
把数组视为一个环,而不是一条直线。
队列中位置arrayLength的下一个位置是0
location(i) = (location(队首元素位置)+i)%arrayLength
queuefront沿逆时针方向,指向队列首元素的下一个位置,queueback指向队列的最后一个位置。
当且仅当queuefront=queueback=0时,队列为空。但是当队列插满的时候,也有queuefront=queueback,导致无法判断队列是满还是空,所以约定队列不能插满。插入操作时先检查插入是否会使队列变满,如果是,则要增加数组长度。
类queue的实现:
queue.h文件:
#ifndef QUEUE_H
#define QUEUE_H
#include <iostream>
#include "E:\back_up\code\c_plus_code\dequeue\external_file\queueABC.h" // 包含ABC文件
#include "E:\back_up\code\c_plus_code\dequeue\external_file\queueemptyEx.h" // 包含异常类文件
using namespace std;
template<typename T>
class queue : public queueABC<T>
{
private:
int arrayLength; // 数组的长度
int queueSize; // 队列中元素的个数
int queueFront; // 队首元素所在的位置
int queueBack; // 队尾元素所在的位置
T* element;
void ensureArrayLength(); // 进行数组扩容
public:
queue(int arrayLength=10); // 构造函数
~queue(); // 析构函数
queue(const queue& q); // 拷贝构造函数
// ADT
bool empty() const;
int size() const;
T& front();
T& back();
void pop();
void push(T x);
void display_queue() const; // 打印输出队列中的元素
};
template<typename T>
queue<T>::queue(int arrayLength)
{
this->arrayLength = arrayLength;
this->queueSize = 0;
this->queueFront = 0;
this->queueBack = 0;
element = new T[arrayLength];
}
template<typename T>
queue<T>::~queue()
{
delete [] element;
}
template<typename T>
queue<T>::queue(const queue& q)
{
arrayLength = q.arrayLength;
queueSize = q.queueSize;
queueFront = q.queueFront;
queueBack = q.queueBack;
element = new T[arrayLength];
for(int i=0; i<queueSize; i++)
{
element[i] = q.element[i];
}
}
template<typename T>
bool queue<T>::empty() const
{
return queueSize==0;
}
template<typename T>
int queue<T>::size() const
{
return queueSize;
}
template<typename T>
T& queue<T>::front()
{
return element[(queueFront+1)%arrayLength]; // queueFront是队首元素的前一个位置,+1是表示队首元素的位置
}
template<typename T>
T& queue<T>::back()
{
return element[queueBack%arrayLength]; // queueback队尾元素的位置
}
template<typename T>
void queue<T>::ensureArrayLength()
{
// 如果需要,增加数组长度
if((queueBack+1)%arrayLength==queueFront) // 环形的数组
{
T* old;
old = element;
delete element;
// arrayLength = arrayLength*2; // 增加分配的内存长度
element = new T[2*arrayLength];
// 环形数组重新布局
// 先将数组复制过来
for(int i=0; i<queueSize; i++)
{
if(i!=(queueFront)%arrayLength) // 队列中这个位置没有值,下一个位置才是队列的首元素
{
element[i] = old[i];
}
else
{
continue;
}
}
delete old;
// 重新布局 // 分为三种情况:1.queuefront=0; queuefront==arrayLength-1; else;
int pre_start = queueFront%arrayLength; // 队列首元素的前一个位置
if(pre_start==0)
{
// 移动所有的数组元素
for(int i=0; i<queueSize; i++)
{
element[2*arrayLength-1-i] = element[queueBack%arrayLength-i];
}
queueFront = queueFront + arrayLength;
queueBack = queueBack + arrayLength;
}
else if(pre_start==arrayLength-1) // arrayLength还是原来的值
{
// 不用移动数组元素,之更改front
queueFront = queueFront + arrayLength;
}
else
{
// 移动第二段的元素:
int element_to_move = arrayLength-1-(queueFront%arrayLength); // 计算第二段需要移动的元素的数量
for(int i=0; i<element_to_move; i++)
{
element[2*arrayLength-1-i] = element[queueBack%arrayLength-i];
}
queueFront = queueFront + arrayLength;
}
arrayLength = arrayLength*2; // 更新arrayLength
}
// 添加一个动态减少内存的函数
}
// push()操作:
template<typename T>
void queue<T>::push(T x)
{
ensureArrayLength(); // 先保证数组长度满足条件
queueSize++;
queueBack = (queueBack+1)%arrayLength;
element[queueBack] = x;
}
//pop()操作
template<typename T>
void queue<T>::pop()
{
if(empty())
throw queueEmptyException(0);
queueFront++;
queueSize--;
// element[queueFront].~T;
}
template<typename T>
void queue<T>::display_queue() const
{
//int tmp = queueFront;
// int start_pos = (tmp+1)%arrayLength;
if(empty())
{
cout << "The queue is empty!" << endl;
return;
}
for(int i=0; i<queueSize; i++)
{
cout << element[(queueFront+1+i)%arrayLength] << " ";
}
cout << endl;
}
#endif
在队列进行pop()操作的时候,需要判断队列是否为空,如果为空,需要抛出异常,这一功能由queue_empty_exception实现:
// 自定义异常类
#ifndef QUEUE_EMPTY_EXCEPTION
#define QUEUE_EMPTY_EXCEPTION
#include <stdexcept>
#include <iostream>
using namespace std;
class queueEmptyException : public runtime_error
{
private:
int queueSize;
public:
queueEmptyException(int queueSize) : runtime_error("The queue empty!")
{
this->queueSize = queueSize;
}
void display_error_info()
{
cout << "The queue size is " << queueSize << endl;
}
};
#endif
测试代码:
main.cpp
#include <iostream>
#include "E:\back_up\code\c_plus_code\dequeue\external_file\queue.h"
using namespace std;
int main(int argc, char *argv[])
{
cout<<"Hello C-Free!"<<endl;
queue<int> q1;
try // 测试异常类
{
q1.pop();
}
catch(queueEmptyException& ex)
{
cout << ex.what() << endl;
ex.display_error_info();
}
for(int i=0; i<15; i++)
{
q1.push(i+1);
}
q1.push(22);
q1.push(34);
q1.pop();
q1.pop();
q1.pop();
while(!q1.empty())
{
cout << q1.front() << " ";
q1.pop();
}
cout << endl;
for(int i=0; i<7; i++)
{
q1.push(i*2);
}
q1.push(100);
q1.pop();
q1.display_queue();
return 0;
}
运行结果:
函数ensureArrayLength的共工作方式:
在队列中元素的数量超过数组的长度时,需要为队列分配一个更大的数组,这里采取数组长度加倍的操作,需要将原来的数组中的内容分配到新的数组中。再复制完数组后,需要对原来队列的queueFront和queueback参数进行修改,因为队列的数组是按照环形的方式来处理的,queueFront和queueback参数能够推算出队列中的元素在数组中的实际位置(即索引值)。
在环形数组展开之后,会出现三种元素分布的方式:
(a).queueFront位于数组的第一个位置
(b) queueFront位于数组的最后一个位置
(c) queueFront位于数组的其他位置
在三种不同的操作下,需要将就数组复制到新数组的操作不同,且queueFront和queueback参数的变化也不相同,具体见代码所示。
template<typename T>
void queue<T>::ensureArrayLength()
{
// 如果需要,增加数组长度
if((queueBack+1)%arrayLength==queueFront) // 环形的数组
{
T* old;
old = element;
delete element;
// arrayLength = arrayLength*2; // 增加分配的内存长度
element = new T[2*arrayLength];
// 环形数组重新布局
// 先将数组复制过来
for(int i=0; i<queueSize; i++)
{
if(i!=(queueFront)%arrayLength) // 队列中这个位置没有值,下一个位置才是队列的首元素
{
element[i] = old[i];
}
else
{
continue;
}
}
delete old;
// 重新布局 // 分为三种情况:1.queuefront=0; queuefront==arrayLength-1; else;
int pre_start = queueFront%arrayLength; // 队列首元素的前一个位置
if(pre_start==0)
{
// 移动所有的数组元素
for(int i=0; i<queueSize; i++)
{
element[2*arrayLength-1-i] = element[queueBack%arrayLength-i];
}
queueFront = queueFront + arrayLength;
queueBack = queueBack + arrayLength;
}
else if(pre_start==arrayLength-1) // arrayLength还是原来的值
{
// 不用移动数组元素,之更改front
queueFront = queueFront + arrayLength;
}
else
{
// 移动第二段的元素:
int element_to_move = arrayLength-1-(queueFront%arrayLength); // 计算第二段需要移动的元素的数量
for(int i=0; i<element_to_move; i++)
{
element[2*arrayLength-1-i] = element[queueBack%arrayLength-i];
}
queueFront = queueFront + arrayLength;
}
arrayLength = arrayLength*2; // 更新arrayLength
}
// 添加一个动态减少内存的函数
}
队列的应用:
在文章栈的应用中,使用栈结构实现了列车车厢重排序问题:https://blog.csdn.net/zj1131190425/article/details/88086003
现在,列车车厢重排序问题,这次的缓冲轨道为队列,一个FIFO的结构,如下图所示:
代码实现:
#include <iostream>
#include "E:\back_up\code\c_plus_code\dequeue\external_file\queue.h"
using namespace std;
int cache_track_num = 3; // 缓冲轨道的数量
//queue<int>* H = new queue<int>[cache_track_num];
queue<int> H[3];
queue<int> in_rail;
queue<int> out_rail;
void showRailState() // 显示缓冲轨道及出入轨道的车厢序号
{
cout << "=================================" << endl;
cout << "In rail state:";
in_rail.display_queue();
cout << "H1 state: ";
H[0].display_queue();
cout << "H2 state: ";
H[1].display_queue();
cout << "H3 state: ";
H[2].display_queue();
cout << "Out rail state: ";
out_rail.display_queue();
}
// 缓冲轨道上的处理
void cache_rail_process(int& cnt_number, int n1=10) // 参数cnt_number为需要在缓冲车厢查找的车厢编号
{
if(cnt_number<=n1)
{
for(int i=0; i<cache_track_num; i++)
{
if(!H[i].empty() && H[i].front()==cnt_number)
{
out_rail.push(H[i].front());
H[i].pop();
cache_rail_process(++cnt_number);
}
}
}
}
void rail(int train_number[], int n) // 参数:车厢初始顺序,车箱数
{
for(int i=0; i<n; i++) // 初始化入轨道
{
in_rail.push(train_number[i]);
}
int train_number_cnt = 1; // 当前需要寻找的车厢编号
while(!in_rail.empty())
{
showRailState();
int tmp_number = in_rail.front();
in_rail.pop();
if(tmp_number == train_number_cnt) // 如果出队列的车厢是刚好要寻找的车厢:
{
out_rail.push(tmp_number); // 直接放入出轨道
train_number_cnt++;
// 现在需要在缓冲轨道上选找是否有满足条件的车厢,通过递归的方式
cache_rail_process(train_number_cnt, n);
}
else // 放入缓冲轨道
{
// 如果缓冲均为空轨道
if(H[0].empty() && H[1].empty() && H[2].empty())
{
H[0].push(tmp_number);
}
else // 有非空的缓冲轨道,优先寻找非空的可放置的缓冲轨道
{
int trace_flag[cache_track_num];
int non_empty_placable[] = {0, 0, 0}; // 非空且可放置的位置
for(int k=0; k<cache_track_num; k++)
{
if(H[k].empty())
{
trace_flag[k] = 0;
}
else
{
trace_flag[k] = tmp_number - H[k].back(); // 队列尾部的元素要小于需要放置的元素
if(trace_flag[k]>0)
{
non_empty_placable[k] = 1;
}
}
}
int non_placable_cnt = 0;
for(int k=0; k<cache_track_num; k++)
{
if(non_empty_placable[k]==1)
{
non_placable_cnt++;
}
}
if(non_placable_cnt==0) // 非空可放置的位置为0.放入空位
{
for(int k=0; k<cache_track_num; k++)
{
if(trace_flag[k]==0)
{
H[k].push(tmp_number);
break; // 放一次
}
}
}
else if(non_placable_cnt==1) // 一个非空可防止的位置
{
for(int k=0; k<cache_track_num; k++)
{
if(non_empty_placable[k]==1) // 当前位置,唯一满足条件的
{
H[k].push(tmp_number);
break;
}
}
}
else // 有多个非空可放置的位置
{
int cmp = 100;
int cmp_index = 0;
for(int k=0; k<cache_track_num; k++)
{
if(trace_flag[k]>0 && trace_flag[k]<cmp)
{
cmp = trace_flag[k];
cmp_index = k;
}
}
H[cmp_index].push(tmp_number);
}
}
}
//showRailState();
}
showRailState();
}
int main(int argc, char *argv[])
{
int carNumber[] = {5,8,1,7,4,2,9,6,3};
int nn = 9;
rail(carNumber, nn);
return 0;
}
测试结果:
----------------------------------------------------------------------------------------------------------------------------