挑战408——数据结构(11)——线性结构(2)

我们提到过一个叫栈(stack)的数据结构,它是一种符合最后一个进栈的元素一定会是第一个出栈的元素(LIFO),这种机制在计算机编程中是非常有用的,因为它反映了我们编程中函数调用的机制。然而在现实生活中却显得限制太多而且无用。

队列的概念

在人类社会中,我们的公平的集体概念赋予了先发制人的优先权,如“先到先得”所表达的那样。在编程中,这种策略的通常称为是“先到先得” ,缩写为FIFO。
使用FIFO规则存储项目的数据结构称为队列。 队列中的基本操作(类似于栈的push和pop操作)被称为入队(enqueue)和出队(dequeue)。 在队列的末尾添加一个新元素,称为它的尾部(tail)。出队操作将删除队列开头的元素,这个元素称为它的头(head)。这两者的差别用图表可以很容易的看出来,就像下面一样:

  • 在stack中,进程必须从这个数据结构的尾部添加或者删除元素,就像这样:
    在这里插入图片描述
  • 在queue中,进程通过下面的方式来添加或者删除元素,就像这样:
    在这里插入图片描述

Queue类的结构看起来非常像它对应的Stack结构,主要的操作也就是dequeue还有enqueue两种了,其他的操作与stack完全一样。

基于数组实现的队列

对比之前的Stack,我们这次也使用数组的方式来实现队列这种数据结构,并且分析其算法复杂度。
栈和队列的接口也非常相似。两个接口的public部分的唯一变化是定义类的行为的两种方法的名称。 来自Stack类的push方法现在称为入队(enqueue),pop方法现在称为出队(dequeue)。这些方法的行为也不同。
由于队列中的动作不再局限于队列的一端,所以我们需要两个索引来跟踪队列中的头和尾位置。 因此必须有的私有实例变量看如下:

ValueType *array;
int capacity;
int head;  //标记队头
int tail;//标记队尾

在该表示中,head成员保存下一个将要出队列的元素的索引(或者称为下标),tail成员下一个将要入队的元素的索引。 在一个空的队列中,很明显,tail应该为0,以指示数组中的初始位置,但head成员呢? 为了方便起见,通常的策略是将head设置为0。当以这种方式定义队列时,使head和tail成员相等即表示队列为空。即队空等价于

head == tail;

由此,我们很容易得出,我们初始化一个空队列(也就是我们的构造函数)应该是这样的:

template <typename ValueType>
Queue<ValueType>::Queue(){
    head = tail = 0;
}

通常在编程中,我们通过绘制图表可以来确保在转向实现之前,准确了解队列的运行方式。

  1. 假设一开始的队列为空队列,那么内部的结构应该是这样的:
    在这里插入图片描述
  2. 假设现在有五个元素加入队列,以字母A到E表示。这些客户按顺序排队,如下图所示:
    在这里插入图片描述
  3. head域中的值0指示队列中的第一个客户存储在数组的位置0; tail的值5表示下一个元素进入队列将被放置在位置5,很好。此刻假设在队列开始时交替地为元素执行出队列,然后添加一个新的元素到最后。例如,A离开队列,F此时到达低劣,这是情况如下图所示:
    在这里插入图片描述
  4. 持续运行,直到元素J到达,此时情况如下图所示:
    在这里插入图片描述
  5. 此时,队列中只有五个元素,但我们已经占用了所有可用空间。也就是说tail指向超出数组的末尾。但是另一方面,你现在在数组的开头有未使用的空间。因此,如果你代替增加的尾部,使其表示不存在的位置10,除了扩展队列的容量,我们还可以从数列的末端“环绕”回到位置0,如下所示:
    在这里插入图片描述
  6. 现在需要考虑的唯一剩余问题是如何检查队列是否已满。测试完整队列比你想象的更复杂。因此我们要了解可能出现问题的原因,假设有三名元素随后到达。假设要到达的元素是L,M和N,显然都在K的后面,情况如下所示:
    在这里插入图片描述
  7. 倘若此时再来个元素O,按照入队的规则,情况应该是这样:
    在这里插入图片描述
  8. 显然这是不合理的,因为按照之前的规定,head == tail的情况是队列已经空了,但是显然此时队列是满了

此时按原来的逻辑我们是没有办法从队列结构本身的内容中得出这个条件中的哪一个是空的或全满的,因为两种种情况下的数据值看起来都一样。虽然你可以通过对空队列采用不同的定义并编写一些特殊情况代码来解决此问题,但最简单的方法是将队列中的元素数量限制为小于容量的数量,并在每次扩展数组限制它到达尾部
允许队列中的元素从数组结尾开始循环,则活动元素始终从head索引向上延伸到tail索引之前的位置,因为数组的首末端就像是连接在一起,通常把这个表示称为一个环形缓冲区(ring buffer):
在这里插入图片描述
值得注意的是,我们需要查看它们是否从数组结尾开始循环。代码使用%运算符自动计算正确的索引。使用余数将计算结果减少到小的周期性整数范围的技术是称为模数运算.

基于数组的队列的实现

头文件部分与stack的基本一致,唯一的不一样就是元素的添加跟删除不一致:

queue.h
/*
 *这部分文件实现我们之前所使用的queue类
 *它主要的原理为 后进后出(LILO)
 */

 #ifndef _Queue_h
 #define _Queue_h

 /*
  *类型: Queue<ValueType>
  *此类建立一个称为队列的线性结构,其中仅从一端添加和删除值。
  *这个规定产生了一个(LILO)的行为,它是队列的定义特征。 
  *基本堆栈操作是dequeue·(把元素从队列中移除)和enqueue(把元素添加到队列中)。
  */
 template <typename ValueType>
 class Queue{
    public:
        /*
         *构造函数:Queue
         *用法:Queue <ValueType> queue
         *-----------------------------
         *初始化一个空队列
         */
        Queue();
        //析构函数
        ~Queue();
        /*
         *方法:size()
         *用法:int n = queue.size();
         *--------------------------
         *返回队列中元素的个数
         */
        int size();
        /*
         *方法:isEmpty()
         *用法:queue.isEmpty();
         *--------------------------
         *判断队列中元素是否为空 
         */ 
        bool isEmpty();
        /*
         *方法:clear()
         *用法:queue.clear();
         *--------------------------
         *清空队列中的所有元素 
         */ 
        void clear();
        /*
         *方法:enqueue()
         *用法:stack.enqueue();
         *--------------------------
         *向队尾插入一个元素 
         */ 
        void enqueue(ValueType);
        /*
         *方法:pop()
         *用法:queue.dequeue();
         *--------------------------
         *移除队头的一个元素,并返回其值,如果队空 则返回一个错误 
         */
        ValueType dequeue(); 
        /*
         *方法:peek()
         *用法:queue.peek();
         *--------------------------
         *返回队头的值,但是不移除,peek 偷看的意思,如果队空 则返回一个错误
         */ 
         ValueType peek(); 

         #include "queuepriv.h" //私有成员部分


 };

 #include "queueimpl.cpp" //将实现文件包含进来,因为此时用的模板

#endif
queuepriv.h文件
private:
	static const int INITIAL_CAPACITY = 10;
	/* Instance variables */
	ValueType *array; /* A dynamic array of the elements */
	int capacity; /* The allocated size of the array */
	int head; /* The index of the head of the queue */
	int tail; /* The index of the tail of the queue */
			  /* Private method prototypes */
	void expandCapacity();
	/* Make it illegal to copy queues */
	Queue(const Queue & value) { }
	const Queue & operator=(const Queue & rhs) { return *this; }
queueimpl.cpp文件
#ifdef _Queue_h
#include "error.h"

/*
*构造函数,这个函数要为队列的元素分配存储空间,并为所有的对象进行初始化元素赋值
*/
template <typename ValueType>
Queue<ValueType>::Queue() {
	capacity = INITIAL_CAPACITY;
	array = new ValueType[capacity];
	head = 0;
	tail = 0;
}

/*
*析构函数:释放我们之前分配的内存
*/
template <typename ValueType>
Queue<ValueType>::~Queue() {
	delete[]array;
}

/*
*返回队列中的元素个数,
*/
template<typename ValueType>
Queue<ValueType>::size() {
	return (tail + capacity - head) % capacity;
}

/*
*判断队列是否为空,这个判断标准,无论是使用指针还是我们的数组
*判断标准都是tail == head,请注意,这种解释意味着不能允许队列完全填写容量,
*并且必须始终留下一个未使用的空间。
*/
template <typename ValueType>
bool Queue<ValueType>::isEmpty() {
	return tail == head;
}

/*
*清空队列,清除方法不需要考虑环形缓冲区(即循环队列)中存在的任何现有数据的位置,
*并且可以简单地重置头部和尾部索引。
*/
template <typename ValueType>
void Queue<ValueType>::clear() {
	head = tail = 0;
}

/*
*入队列,此方法必须首先检查以确定元素是否有足够的空间供其插入,并在必要时
*扩展数组存储空间。否则不可能区分队列为空的情况与队列完全充满时的情况,
*当该大小小于容量时,该实现会扩展队列
*/
template <typename ValueType>
void Queue<ValueType>::enqueue(ValueType element) {
	if (size = capacity - 1) { expandCapacity(); }
	array[tail] = element;
	tail = (tail + 1) % capacity;
}

/*
*出队列,这个方法必须检查我们的队列是否为空,如果是,那就报错
*/
template <typename ValueType>
ValueType Queue<ValueType>::dequeue() {
	if (isEmpty())
	{
		error("dequeue: Attempting to dequeue an empty queue");
	}
	ValueType result = array[head];
	head = (head + 1) % capacity;
	return result;
}

/*
*查看队头元素
*/
template <typename ValueType>
ValueType Queue<ValueType>::peek() {
	if (isEmpty())
	{
		error("peek: Attempting to peek at an empty queue");
	}
	return array[head];
}

/*
*扩展容量,只要空间不足,这个私有方法将动态数组的容量加倍。
*为此,它必须分配一个新数组,将所有元素从旧数组复制到新的数组,并释放旧存储。
*请注意,此实现还将所有元素都移回数组的开头。
*/
template <typename ValueType>
void expandCapacity() {
	ValueType *oldArray = array;
	int count = Size();
	capacity = capacity * 2;
	array = new ValueType[capacity];
	for (int i = 0; i <= count; i++)
	{
		array[i] = oldArray[(head + i) % capacity];
	}
	head = 0;
	tail = count;
	delete[]oldArray;
}
#endif
重点代码分析
  • 返回队列中元素的个数
template<typename ValueType>
Queue<ValueType>::size(){
    return (tail + capacity - head) % capacity;
}

我们来看看为什么会有这个算法,为什么不是直接队尾减去队头?假设一开始的队列如下’:
在这里插入图片描述
此时tail - head = 6 - 1 =5,是正常的。但是当tail的数值小于 head呢?就像下图:
在这里插入图片描述
此时,tail - head = 4 - 5 = -1,但是队列数值是不可能为负数的,只是因为队列多了一次循环。所以size = tail + capacity - head.也就是 4 + 10 - 5 = 9.当循环进行多次的时候,就可用模数运算得出正确的个数。

  • 入队操作;
/*
*入队列,此方法必须首先检查以确定元素是否有足够的空间供其插入,并在必要时
*扩展数组存储空间。否则不可能区分队列为空的情况与队列完全充满时的情况,
*当该大小小于容量时,该实现会扩展队列
*/
template <typename ValueType>
void Queue<ValueType>::enqueue(ValueType element) {
	if (size = capacity - 1) { expandCapacity(); }
	array[tail] = element;
	tail = (tail + 1) % capacity;
}

第一步,判断队列中元素的个数是否是容量数 - 1,是的话就进行容量扩展,这一步就是为了避免出现 tail = head 的时候,无法判断是队满还是对空。接下来的操作就是跟Stack一样,只不过放的位置不一样,且tail的计算要模多一次容量值。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值