队列
常见的队列类型
首先,队列是一种先进先出的数据结构,就像我们日常生活的排队买东西,你开始排队的时间越早,买到东西的时间就越早。你开始排队的时间越晚,买到东西的时间就越晚。
队列可以由顺序表实现,也能由链表实现,前者叫做顺序队列,后者叫做链式队列。
顺序队列
要实现一个队列,需要两个指针,一个为头指针,指向队列头部,一个为尾指针,用来指向队列最后一个元素。
下面是一份C++实现队列的代码(来自leetcode)
#include <iostream>
class MyQueue {
private:
vector<int> data;
// a pointer to indicate the start position
int p_start;
public:
MyQueue() {p_start = 0;}
/** Insert an element into the queue. Return true if the operation is successful. */
bool enQueue(int x) {
data.push_back(x);
return true;
}
/** Delete an element from the queue. Return true if the operation is successful. */
bool deQueue() {
if (isEmpty()) {
return false;
}
p_start++;
return true;
};
/** Get the front item from the queue. */
int Front() {
return data[p_start];
};
/** Checks whether the queue is empty or not. */
bool isEmpty() {
return p_start >= data.size();
}
};
int main() {
MyQueue q;
q.enQueue(5);
q.enQueue(3);
if (!q.isEmpty()) {
cout << q.Front() << endl;
}
q.deQueue();
if (!q.isEmpty()) {
cout << q.Front() << endl;
}
q.deQueue();
if (!q.isEmpty()) {
cout << q.Front() << endl;
}
}
p_start用来指示队列的头部,入队时队列无变化,出队时p_start加1
MyQueue q;
q.enQueue(5);
q.enQueue(3);
q.deQueue();
然而,这样实现有一个缺点。试想这样一种情况
此时,队头指针前依旧有一个空位置可以容纳新的队列元素,但此时却无法入队,这样将对空间的浪费极大。要改善这一状况,我们可以每次出队时都将队列元素向前移动一位。其他函数不变,只改变出队函数,代码如下
bool deQueue() {
if (isEmpty()) {
return false;
}
for(int i = 0; i < data.size - 1; i++){
data[i] = data[i+1];
}
data.pop_back();
return true;
};
这样处理之后能够充分利用队列的每一个空间,但是队列出队的效率大大降低,由常数时间变为了O(n).
为了改变这一状况,我们可以使用一种批处理的方式,即不再每次出队都将所有元素后一一次,而是在入队的过程中做一个判断,若此时队列已满,则判断队头指针是否为0,若不为0则进行一次集体数据偏移,再将新的元素入队,若对头指针不为0,则入队失败。
bool enQueue(int x) {
if(data.size() == MAX_CAPACITY)
if(p_start != 0)
{
//数据偏移
for(int i = p_start; i < data.size(); i++){
data[i-p_start] = data[i];
}
//头指针变为0
p_start = 0;
//插入新的元素
data[data.size()-p_start] = x;
//减小data大小 利用vector的写法为了讲解原理,实际这样效率不高,不建议使用
//较好的办法是开辟一个固定的空间,使用两个指针来指示队列的头部、尾部
for(int i = 0; i < p_start - 1;i++)
data.pop_back();
}
else
return false;
return true;
}
这样大大减少了队列中数据偏移的次数,提高了队列出队、入队的效率
链式队列
链式队列是与顺序队列对应的队列,实现类似,不过多叙述。
循环队列
由于顺序队列需要数据偏移,导致顺序队列效率不高,尽管我们可以使用集体偏移的方法来缓解这一问题,但我们依旧希望有一种不需要偏移数据的方法,即循环队列。
循环队列又称为环形缓冲器,分别具有头指针和尾指针,头指针指向第一个元素,尾指针指向最后一个元素的下一位。入队时尾指针加1,出队时头指针加1。其不会浪费空间的最大奥妙便在于当队尾指针指向了队列最后一个元素时(即索引7 此时在顺序队列中表示队列已满),此时再入队,若队列还有多余空间(即之前队列有过出队操作,队头指针有过后移),队尾指针将指向队列的第一个位置(即索引0)
enque(13)
要实现循环队列并不是那么简单,最关键的两个点为判断清楚队列空和队列满的条件。
直观的来看
当队空时,我们有rear == font
当队满时,我们有rear+1 == font
然而,考虑下面这种情况
此时,同样表示队满,所以我们可以将队满条件修改为(rear+1)%8 == font
实现代码如下
class MyCircularQueue {
int* data;
int font;
int rear;
int capacity;
public:
MyCircularQueue(int k) {
//因为循环队列会浪费一个空间,所以需要长度为n的循环队列时需要开辟n+1的数组空间
data = new int[k+1];
font = 0;
rear = 0;
capacity = k+1;
}
bool enQueue(int value) {
if(isFull())
//若队列已满,则入队失败
return false;
else{
data[rear] = value;
rear = (rear+1)%capacity;
return true;
}
}
bool deQueue() {
if(isEmpty())
//若队列为空,则出队失败
return false;
else{
font = (font+1)%capacity;
return true;
}
}
int Front() {
if(isEmpty())
return -1;
return data[font];
}
int Rear() {
if(isEmpty())
return -1;
return data[(rear+capacity-1)%capacity];
}
bool isEmpty() {
return font == rear;
}
bool isFull() {
return (rear+1)%capacity == font;
}
};
开发时可能用到的队列
前面所讲为队列的基础知识,下面谈谈实际开发中经常用到的2种队列结构
阻塞队列
首先就算阻塞队列,阻塞队列在队列的基础上增加了阻塞操作,其可用于生产者-消费者模型,生产者往队列中增加数据,消费者往队列中取出队列。
阻塞操作即为:
当队列为空时,消费者不再往队列中取数据
当队列满时,生产者不再往队列中增加数据
同时,我们可以协调消费者和生产者个数来提高性能,比如多个消费者对应一个生产者
并发队列
前面说到阻塞队列可以协调消费者和生产者个数,如一个生产者对应于多个消费者,在这种情况下,会有多个线程操控一个队列,于是便引入了线程安全问题。
线程安全的队列叫做并发队列,实现并发队列最简单的方法就算再dequeue和enqueue操作中加锁,这样便实现了一个简单的线程安全的并发队列。
reference
极客时间数据结构与算法之美
leetcode队列