队列
队列(Queue)是一种线性数据结构,通常用来存储具有相同类型的元素。它按照先进先出(First-In-First-Out,FIFO)的原则进行操作。也就是说,最先进入队列的元素最先被处理,而最后进入队列的元素最后被处理。
队列可以看作是一种特殊的列表,只允许在表的一端(称为队尾)进行插入操作(称为入队),而在另一端(称为队头)进行删除操作(称为出队)。入队操作将元素添加到队尾,出队操作将队头的元素移除并返回。
与队尾和队头相对应的是队列的两个特殊指针或索引,分别称为“rear”和“front”。队尾指针指示队列中最后一个元素的位置,队头指针指示队列中第一个元素的位置。
队列的基本操作包括:
- 入队(Enqueue):将元素添加到队尾。
- 出队(Dequeue):将队头的元素删除并返回。
- 队列判空(isEmpty):判断队列是否为空。
- 队列大小(size):获取队列中元素的个数。
队列可以应用于许多实际场景,如任务调度、消息传递、缓冲区管理、广度优先搜索算法等。它能够按照特定顺序管理数据,并提供了一种合理的方式来处理在某一端插入、在另一端删除数据的需求。
C++ 实现
动态数组实现队列
数据结构图
代码
/**
* 数组实现队列
*/
#include <cstdlib>
#include <cstring>
#ifndef QUEUE_ARRAY_QUEUE_H
#define QUEUE_ARRAY_QUEUE_H
const int EXPAND_CAPACITY = 20; // 每次队列扩容大小
const int MAX_CAPACITY = INT_MAX; // 队列最大容量
template<typename T>
class Queue {
public:
void push(T e);
T back(); // 返回最后一个元素
T front();// 返回第一个元素
T pop(); // 弹出第一个元素,并返回其值
int size() const { return siz; } // 队列元素个数
bool empty() const { return siz == 0; } // 队列是否为空
// 清空队列
void clear(){
delete data;
data = (T *) malloc(sizeof(T) * 10);
head = 0;
tail = 0;
siz = 0;
}
Queue() {
data = (T *) malloc(sizeof(T) * 10);
}
~Queue() {
delete data;
}
private:
T *data; // 要存储的数据
int head = 0; // 第一个元素的索引
int tail = 0; // 最后一个元素索引+1
int siz; // 元素个数
int capacity = 10; // 队列容量
};
/**
* 从队列尾部插入元素
* 如果元素个数超出最大容量,取消插入操作
* 如果tail >= capacity 扩容
* 如果队列左边的无效元素个数>=右边的无效元素个数,队列向左移动到最左边
* @tparam T
* @param e
*/
template<typename T>
void Queue<T>::push(T e) {
if(siz >= MAX_CAPACITY) return;
if (tail >= capacity) {
capacity += EXPAND_CAPACITY;
if (capacity >= MAX_CAPACITY) {
capacity = MAX_CAPACITY;
}
T *t = (T *) malloc(sizeof(T) * capacity);
memcpy(t + head, data + head, sizeof(T) * siz);
delete data;
data = t;
}else if (head >= capacity - tail) {
T *t = (T *) malloc(sizeof(T) * capacity);
memcpy(t, data + head, sizeof(T) * siz);
delete data;
data = t;
head = 0;
tail = siz;
}
data[tail] = e;
tail++;
siz = tail - head;
}
template<typename T>
T Queue<T>::front() {
return data[head];
}
template<typename T>
T Queue<T>::back() {
return data[tail - 1];
}
template<typename T>
T Queue<T>::pop() {
if (!siz) return T();
head++;
siz = tail - head;
}
#endif //QUEUE_ARRAY_QUEUE_H
实现分析
上述代码中队列的入队和出队操作由两个函数实现:push和pop。
入队操作(push函数)的实现如下:
- 如果队列已满,即元素个数(siz)已经达到最大容量(MAX_CAPACITY),则取消插入操作,不执行任何操作。
- 如果尾部索引(tail)大于等于队列容量(capacity),则需要进行扩容操作。
- 先将容量(capacity)增加EXPAND_CAPACITY大小,并判断是否超过最大容量(MAX_CAPACITY),如果超过,则将容量置为最大容量。
- 然后,分配一个新的数组t,并将原始数据从头部索引(head)开始拷贝到新数组t的对应位置上。
- 释放旧数组data的内存,并将新数组t赋值给成员变量data。
- 如果左边的无效元素个数(head >= capacity - tail)大于等于右边的无效元素个数,表示队列左边的空间浪费更多,需要将队列向左移动到最左边。
- 首先,分配一个新的数组t,并将原始数据从头部索引(head)开始拷贝到新数组t的对应位置上。
- 释放旧数组data的内存,并将新数组t赋值给成员变量data。
- 将头部索引(head)置为0,尾部索引(tail)置为元素个数(siz)。
- 将要插入的元素e放入data数组的尾部索引(tail)处,然后将尾部索引增加1。
- 更新元素个数(siz),即为尾部索引(tail)减去头部索引(head)。
出队操作(pop函数)的实现如下:
- 首先,如果队列为空(siz为0),则返回一个默认构造的元素(T())。
- 将头部索引(head)增加1,表示弹出了一个元素。
- 更新元素个数(siz),即为尾部索引(tail)减去头部索引(head)。
动态数组实现缺点
-
动态扩容的开销:如果队列的元素个数超过当前容量,就需要进行动态扩容操作。扩容涉及到重新分配内存、复制数据等操作,开销较大。特别是在多次扩容后,可能存在大量的数据复制。
-
空间效率:由于采用数组存储元素,需要预先定义一个固定的容量。如果容量过大,会浪费内存空间;如果容量过小,可能会导致频繁的扩容操作。因此,空间利用效率可能不是最优的。
单链表实现队列
数据结构图
代码
/**
* 链表实现队列
*/
#ifndef QUEUE_LINK_QUEUE_H
#define QUEUE_LINK_QUEUE_H
template <typename T>
class Node{
public:
T val;
Node *next;
};
template<typename T>
class LinkQueue{
public:
void push(T t);
T back(){return tail->val;}; // 返回最后一个元素
T front(){return head->next->val;}// 返回第一个元素
T pop(){// 弹出第一个元素
if(siz == 0) return T();
Node<T> *hn = head->next;
head->next = hn->next;
T r = hn->val;
delete hn;
if(head->next == nullptr){
tail = head;
}
siz--;
return r;
};
int size() const { return siz; } // 队列元素个数
bool empty() const { return siz == 0; } // 队列是否为空
void clear(){
while(head != nullptr){
Node<T> *temp = this->head;
Node<T> *current = temp->next;
delete temp;
head = current;
}
// 初始化LinkQueue
head = tail = new Node<T>();
head = new Node<T>();
tail = new Node<T>();
tail->next = nullptr;
this->siz = 0;
}
LinkQueue() {
// 初始化LinkQueue
head = new Node<T>();
tail = new Node<T>();
tail->next = nullptr;
}
~LinkQueue() {
this->clear();
}
private:
Node<T> *head;
Node<T> *tail;
int siz = 0;
};
/**
* 从链表尾部添加元素
* @tparam T
* @param t 要添加的元素
*/
template<typename T>
void LinkQueue<T>::push(T t){
Node<T> *tn = new Node<T>();
tn->val = t;
tn->next = nullptr;
if(siz == 0){
head->next = tn;
tail = tn;
}else{
tail->next = tn;
tail = tn;
}
siz ++;
}
#endif //QUEUE_LINK_QUEUE_H
实现分析
这段代码是一个使用链表实现的队列类 LinkQueue
。下面对代码进行分析:
-
队列节点定义:
Node
类是一个模板类,具有一个val
成员用于存储值,一个next
指针指向下一个节点。 -
队列类定义:
- 成员变量:
head
和tail
分别是头节点和尾节点指针,用于表示队列的头部和尾部;siz
用于记录队列元素的个数。 - 成员函数:
push
函数:从链表尾部添加元素,创建一个新的节点,将值赋给新节点的val
成员,将新节点插入到队列的尾部并更新tail
指针,然后更新队列元素个数。back
函数:返回队列中最后一个元素的值。front
函数:返回队列中第一个元素的值。pop
函数:弹出第一个元素,首先检查队列是否为空,如果为空直接返回一个默认构造的T
类型对象,否则,通过操作头节点和头节点的下一个节点来删除并弹出第一个元素的值,并更新队列元素个数。size
函数:返回队列中元素的个数。empty
函数:判断队列是否为空。clear
函数:清空队列,通过循环释放头节点及其后续节点的内存,然后重新初始化队列。
- 成员变量:
-
构造函数和析构函数:
- 构造函数:初始化队列,创建头节点和尾节点,并将
tail
指针指向头节点。 - 析构函数:调用
clear
函数,释放队列的内存空间。
- 构造函数:初始化队列,创建头节点和尾节点,并将
链表实现队列的优缺点
优点:
-
动态扩容:链表实现的队列在插入元素时可以动态地分配内存,避免了数组实现的队列需要预先指定固定大小的问题。这使得链表实现的队列可以根据需求自动扩展,节省存储空间。
-
高效的插入和删除:链表实现的队列在头部和尾部插入、删除元素的操作上具有高效性。由于只需要调整节点之间的指针,而不需要移动元素,所以插入和删除操作的时间复杂度是 O(1)。
-
灵活性:链表实现的队列可以动态地添加和删除节点,可以处理任意数量的元素。
缺点:
-
非随机访问:链表实现的队列不能像数组一样通过索引直接访问队列中的元素,而是需要从头节点开始按照指针逐个访问。这导致了访问队列中间元素的效率较低,时间复杂度为 O(n),其中 n 是队列中元素的数量。
-
需要额外的空间:链表实现的队列除了存储元素值之外,还需要额外的指针来连接节点。这会占用一定的内存空间,尤其是在处理大量元素或者需要频繁插入和删除操作时可能会占用较多的内存空间。