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; }
测试结果:
----------------------------------------------------------------------------------------------------------------------------
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)