实用指南:数据结构4.0(队列)顺序表实现保姆级教程
1 队列的基本概念
队列是一种特殊的线性数据结构,它遵循先进先出(First In First Out,FIFO)的原则
。这意味着最先进入队列的元素将最先被处理,而最后进入的元素将最后被处理。队列的这种特性类似于现实生活中的排队场景,如顾客在超市收银台前排队结账,新来的顾客需要排在队伍末尾,而正在接受服务的顾客总是从队伍前端离开。
队列支持两个基本操作:入队(enqueue)和出队(dequeue)。入队操作在队列的尾部添加新元素,而出队操作则从队列的头部移除元素
。除了这两个基本操作外,队列通常还提供一些辅助操作,如获取队首元素(getFront)、获取队列大小(getSize)和判断队列是否为空(isEmpty)等。
在计算机科学中,队列的应用非常广泛。它们常用于处理需要按顺序执行的任务,如操作系统中的进程调度、网络通信中的数据包传输、打印机作业排队以及广度优先搜索算法等
。队列确保了这些任务按照接收顺序依次处理,避免了混乱和不公平性。
队列可以通过不同的数据结构实现,主要包括顺序表(数组)实现和链表实现两种方式
。每种实现方式都有其优缺点,选择哪种实现取决于具体的应用场景和性能要求。顺序表实现的队列使用数组作为底层存储容器,通过两个指针(front和rear)来标记队列的头部和尾部位置。
2 顺序表实现队列的基础
使用顺序表(数组)实现队列是一种常见且高效的方法。顺序表为队列元素提供了连续的内存空间,这使得随机访问成为可能,同时提高了缓存利用率,从而提升了访问速度
。在顺序表实现中,我们需要维护两个关键指针:front和rear分别指示队列头部和尾部的位置。
在顺序队列的实现中,front指针指向队列中第一个元素的位置,而rear指针则指向队列中最后一个元素的下一个位置(即新元素将要插入的位置)
。这种指针设置方式是顺序表实现队列的常见做法,它使得入队和出队操作可以在常数时间内完成。
然而,简单的顺序表实现存在一个显著问题:假溢出(false overflow)
。当队列尾部到达数组末尾但数组前端仍有空闲空间时,会发生这种情况。为了解决这个问题,可以采用循环队列的概念,将数组视为一个环状结构。在循环队列中,当指针到达数组末尾时,会绕回到数组开头,从而充分利用数组空间。
另一种解决假溢出的方法是动态扩容。当队列已满时,可以分配一个更大的数组,将现有元素复制到新数组中,并释放原数组的内存
。这种方法增加了实现的复杂性,但提供了更大的灵活性。
以下表格展示了顺序表实现队列时不同指针定义方式的对比:
| 规定方式 | front指针含义 | rear指针含义 | 空队列条件 | 满队列条件 |
|---|---|---|---|---|
| 规定一 | 指向队头元素的前一个位置 | 指向队尾元素所在位置 | front == rear | (rear + 1) % capacity == front |
| 规定二 | 指向队头元素的位置 | 指向队尾元素的下一个位置 | front == rear | (rear + 1) % capacity == front |
表:顺序队列两种指针定义方式的对比
3 代码实现详解
3.1 队列类的定义
下面是一个基于顺序表的队列模板类的定义,它使用动态数组存储元素,并包含必要的成员变量和成员函数:
#include
#include
template
class Queue
{
private:
T* data; // 指向队列元素的指针
int rear; // 队尾指针,指向下一个入队元素的位置
int front; // 队头指针,指向队头元素的位置
int capacity; // 队列当前的最大容量
void resize(); // 扩容函数
public:
// 构造函数:初始化队列,默认容量为10
Queue() : data(new T[10]), front(0), rear(0), capacity(10) {};
// 析构函数:释放队列占用的内存
~Queue();
// 入队操作:向队列尾部添加元素
void enqueue(T element);
// 出队操作:从队列头部移除元素并返回
T dequeue();
// 获取队头元素:返回队头元素但不移除
T getFront() const;
// 获取队列中元素的数量
int getSize() const;
// 判断队列是否为空
bool isEmpty() const { return rear == front; }
};
这是一个模板类实现,使得队列可以存储任意类型的数据
。类的私有成员包括指向动态数组的指针data,表示队列尾部的rear索引,表示队列头部的front索引,以及当前数组的容量capacity。公有成员函数提供了队列的基本操作接口。
3.2 构造函数和析构函数
构造函数负责初始化队列对象,为动态数组分配内存并设置初始状态:
template
Queue::Queue() : data(new T[10]), front(0), rear(0), capacity(10) {}
构造函数使用初始化列表将data指针指向新分配的数组,初始容量设为10。front和rear指针都初始化为0,表示空队列。这种初始化方式确保了队列对象的初始状态是一致且正确的。
析构函数负责清理队列对象占用的资源,主要是释放动态数组的内存
template
Queue::~Queue()
{
delete[] data;
}
析构函数使用delete[]操作符释放为数组分配的内存,防止内存泄漏。这是RAII(Resource Acquisition Is Initialization)原则的简单应用,确保对象生命周期结束时自动释放资源。
3.3 扩容机制(resize函数)
当队列已满且需要添加新元素时,需要扩展队列的容量。resize()函数负责实现这一功能
template
void Queue::resize()
{
// 创建容量加倍的新数组
T* newdata = new T[capacity * 2];
// 将原数组中的元素复制到新数组
for (int i = 0; i < rear; i++)
{
newdata[i] = data[i];
}
// 释放原数组内存
delete[] data;
// 更新data指针指向新数组
data = newdata;
// 更新容量值
capacity *= 2;
}
这个函数首先创建一个容量为原数组两倍的新数组,然后将原数组中的所有元素复制到新数组中。接着,释放原数组的内存,并更新data指针指向新数组,最后更新容量值。
这种扩容策略虽然在某些情况下可能不是最高效的(因为需要复制所有元素),但它的均摊时间复杂度是O(1),这意味着平均每次操作的代价是常数时间的
。
3.4 入队操作(enqueue函数)
入队操作向队列的尾部添加一个新元素:
template
void Queue::enqueue(T element)
{
// 检查队列是否已满,若已满则先扩容
if (rear == capacity)
{
resize();
}
// 将新元素放入队尾,并更新rear指针
data[rear++] = element;
}
这个函数首先检查队列是否已满(即rear指针是否等于capacity)。如果已满,则调用resize()函数扩展容量。然后,将新元素存储在rear指针指向的位置,并将rear指针向后移动一位。
入队操作的时间复杂度在不需要扩容的情况下是O(1),在需要扩容的情况下是O(n)(因为需要复制所有元素)。但由于扩容不频繁,其均摊时间复杂度仍然是O(1)
。
3.5 出队操作(dequeue函数)
出队操作从队列的头部移除并返回一个元素:
template
T Queue::dequeue()
{
// 检查队列是否为空
if (rear == front)
{
throw std::underflow_error("Queue is empty");
}
// 返回队头元素,并更新front指针
return data[front++];
}
这个函数首先检查队列是否为空(即rear指针是否等于front指针)。如果为空,则抛出underflow_error异常。否则,返回front指针指向的元素,并将front指针向后移动一位。
出队操作的时间复杂度是O(1),因为它只需要简单地返回元素并移动指针,不需要移动其他元素
3.6 其他操作:获取队首、队列大小和判空
除了入队和出队操作外,队列还需要一些辅助操作:
// 获取队头元素但不移除
template
T Queue::getFront() const
{
// 检查队列是否为空
if (rear == front)
{
throw std::underflow_error("Queue is empty");
}
return data[front];
}
// 获取队列中元素的数量
template
int Queue::getSize() const
{
return rear - front;
}
// 判断队列是否为空
template
bool Queue::isEmpty() const
{
return rear == front;
}
getFront()函数返回队头元素但不移除它,如果队列为空则抛出异常。getSize()函数返回队列中元素的数量,通过计算rear和front指针的差值得到。isEmpty()函数检查队列是否为空,当rear等于front时返回true。
这些操作的时间复杂度都是O(1),因为它们只涉及简单的算术运算或比较操作
。
4 队列的应用场景
队列作为一种基本数据结构,在计算机科学和软件工程中有广泛的应用。以下是几个队列的典型应用场景:
操作系统中的进程调度:操作系统使用队列来管理等待CPU时间的进程。进程按照先来先服务的原则排队等待执行,确保了系统资源的公平分配
。网络通信中的数据包传输:网络路由器使用队列来管理等待转发的数据包。当网络拥塞时,数据包会在队列中排队,直到可以传输
。打印机作业管理:打印服务器使用队列来管理等待打印的文档。用户提交的打印任务被添加到打印队列中,打印机按照先进先出的顺序处理这些任务
。广度优先搜索算法:在图论和树结构中,广度优先搜索(BFS)算法使用队列来存储待访问的节点。算法从根节点开始,先访问所有相邻节点,再逐层扩展,确保按照距离顺序访问节点
。消息队列和异步处理:在分布式系统和企业应用中,消息队列用于在不同的系统组件之间异步传递消息。这种机制提高了系统的可扩展性和可靠性
。呼叫中心系统:呼叫中心使用队列来管理等待服务的客户呼叫。客户按照呼叫顺序排队,下一个可用的客服代表将为队列前端的客户服务
。
以下表格总结了队列在不同应用场景中的作用:
| 应用场景 | 队列的作用 | 队列类型 |
|---|---|---|
| 操作系统进程调度 | 管理等待CPU时间的进程 | 优先级队列 |
| 网络数据包传输 | 管理等待转发的数据包 | FIFO队列 |
| 打印机作业管理 | 管理等待打印的文档 | FIFO队列 |
| 广度优先搜索 | 存储待访问的节点 | FIFO队列 |
| 消息队列系统 | 异步传递消息 | 优先级队列/FIFO队列 |
| 呼叫中心系统 | 管理等待服务的客户呼叫 | 优先级队列/FIFO队列 |
表:队列在不同应用场景中的作用
5 顺序队列的优缺点分析
顺序表实现的队列有其明显的优点和缺点,这些特性决定了它在不同场景下的适用性
5.1 优点
高存储效率:顺序队列使用连续的内存空间存储元素,这意味着缓存利用率高,访问速度快。与链表实现相比,顺序表没有指针开销,每个元素占用的空间更小
。常数时间操作:在大多数情况下,顺序队列的入队和出队操作可以在常数时间O(1)内完成。虽然扩容操作需要线性时间,但由于其不频繁发生,均摊时间复杂度仍然是O(1)
。实现简单:顺序队列的实现相对简单,只需要一个数组和两个指针(front和rear)。这种简单性使得代码易于理解和维护
。随机访问能力:由于元素存储在连续的内存空间中,顺序队列支持随机访问,这意味着可以直接访问任何位置的元素(虽然队列操作通常不需要这种能力)
。
5.2 缺点
固定大小问题:顺序队列的初始大小是固定的,当队列满时需要扩容操作。扩容是一个昂贵的操作,需要分配新数组并复制所有元素
。假溢出问题:简单顺序队列实现(非循环队列)会遇到假溢出问题——即数组后端已满而前端还有空闲空间,但无法使用这些空间
。循环队列可以解决这个问题,但实现更复杂。内存浪费:顺序队列可能会浪费内存空间。一方面,扩容时通常会分配比实际需要更多的空间(如加倍扩容);另一方面,循环队列中总是至少有一个空间不能使用(用于区分空队列和满队列条件)
。不灵活的大小:与链表实现相比,顺序队列的大小调整不够灵活。链表实现的队列可以动态地增长和缩小,而顺序队列需要显式的扩容操作
。
5.3 与链式队列的比较
与链式队列相比,顺序队列在某些方面更有优势,在其他方面则不如链式队列
:
| 特性 | 顺序队列 | 链式队列 |
|---|---|---|
| 存储效率 | 较高(无指针开销) | 较低(有指针开销) |
| 访问速度 | 较快(缓存友好) | 较慢(缓存不友好) |
| 内存使用 | 可能浪费(由于扩容策略) | 更精确(按需分配) |
| 实现复杂度 | 简单 | 较复杂 |
| 扩容成本 | 高(需要复制所有元素) | 低(只需分配新节点) |
| 最大大小 | 受数组大小限制 | 只受内存限制 |
表:顺序队列与链式队列的特性对比
根据具体的应用场景和需求,开发人员需要在顺序队列和链式队列之间做出合适的选择。对于需要高性能和存储效率的场景,顺序队列通常是更好的选择;对于需要灵活大小和不确定最大元素数量的场景,链式队列可能更合适
。
6 总结
顺序表实现的队列是一种高效、简单且实用的数据结构,它遵循先进先出的原则,适用于多种计算机科学和软件工程场景。通过动态数组和两个指针(front和rear)的简单组合,顺序队列能够高效地处理入队和出队操作。
本文详细介绍了顺序队列的实现原理和代码细节,包括队列的初始化、入队操作、出队操作、扩容机制以及各种辅助操作。我们还探讨了顺序队列的优缺点,并与链式队列进行了比较分析,以帮助读者在实际应用中选择合适的队列实现方式。
顺序队列的主要优势在于其高存储效率和常数时间的均摊操作复杂度,但它也存在固定大小和潜在内存浪费的问题。对于大多数应用场景,顺序队列提供了一个优秀的性能与实现复杂度的平衡点。
队列作为一种基本数据结构,其应用范围远超出本文讨论的内容。从操作系统内核到分布式消息系统,从算法实现到用户界面事件处理,队列都发挥着重要作用。理解顺序队列的实现原理和特性,有助于软件开发者更好地利用这一工具解决实际问题。
希望该文章对你有所帮助,自己可以试着打一打加油:

7.0源代码及运行图片:
#include
#include
#include
#include
#include
using namespace std;
template
class Queue
{
private:
T* data;
int rear;
int front;
int capacity;
void resize();
public:
Queue() :data(new T[10]), front(0), rear(0), capacity(10) {};
~Queue();
void enqueue(T element);
T deQueue();
T getFront()const;
int getSize()const;
};
template
void Queue::resize()
{
T* newdata = new T[capacity * 2];
for (int i = 0; i < rear; i++)
{
newdata[i] = data[i];
}
delete data;
data = newdata;
capacity *= 2;
}
template
Queue::~Queue()
{
delete data;
}
template
void Queue::enqueue(T element)
{
if (rear == capacity)
{
resize();
}
data[rear++] = element;
}
template
T Queue::deQueue()
{
if (rear == front)
{
throw std::underflow_error("Queue is empty");
}
return data[front++];
}
template
int Queue::getSize()const
{
return rear - front;
}
template
T Queue::getFront()const
{
if (rear == front)
{
throw std::underflow_error("Queue is empty");
}
return data[front];
}
int main()
{
Queue q;
q.enqueue(3);
q.enqueue(4);
cout << q.getFront() << endl;
q.enqueue(5);
cout << q.getFront() << endl;
q.deQueue();
cout << q.getFront() << endl;
cout << q.getSize() << endl;
return 0;
}

浙公网安备 33010602011771号