数据结构之队列(设计并实现一个自己的队列:循环数组+扩容策略)

来源:互联网 发布:淘宝火拼下架 编辑:程序博客网 时间:2024/06/06 17:31

1、队列介绍

队列是一种由有次序关系的数据项构成的数据结构,只能在一端(称为队尾)插入数据项,而从另一端(称为队头)移除数据项。

位于队列中队头位置的数据项称为首数据项。

2、队列实现分析

实现队列有2种方式,第一种方式是用链表来实现,第二种方式是用数组来实现。

首先我们来思考用哪种方式实现队列更高效:

队列最常用的操作:在队尾插入数据项,在队头移除数据项。

如果用链表实现,只需要定义成员变量队尾和队头,便可轻易的实现队列操作,而且也不用考虑扩容问题。


如果用数组实现,在队尾插入数据项时,可能会容量不足,需要进行扩容操作。

在队头删除数据项时,需要将后面的数据项向前移动一位,非常影响效率,当然肯定有解决方案,后面会详细介绍。


进行对比后,链表实现相对于简单、有优势。原因如下:

①插入数据项时,不用考虑扩容问题

②删除数据项时,后面的数据项不需要向前移动

然后链表实现也有缺点:

①过多的引用,处理复杂


Queue接口的实现类有链表LinkedList,也有数组实现类ArrayDeque、PriorityQueue,不推荐使用PriorityQueue。

这里暂时先用数组实现队列的数据结构。

3、队列设计

首先解决删除数据项时,需要将后面数据项向前移动的问题。

我们可以改进使用两个变量来记录队头(front)和队尾(rear)下标。这样的话每次删除队头操作时,数组后面的数据项就不用向前移动一位,只需要将队头(front)下标向后移一位。便可提升很大的效率。

当然,这样设计的话,移除数据项后,前面的空闲资源得不到利用,很浪费数组的资源空间。

我们继续改进,解决空间问题,我们可以使用循环数组,当插入数据超过最大下标时,我们将队尾(rear)下标置为0,充分利用数组空间资源。

最后就是扩容问题了,当数组被填满时,我们需要对数组进行扩容,我选择的是将原空间翻倍再加上一。

4、队列实现

①:新建类

public class IQueue<E>{}
当然要使用泛型来限制队列的数据类型。

②:定义成员变量

private static final int DEFAULT_INITIAL_CAPACITY = 11;private transient Object[] queue;private int size;private int front;private int rear;
DEFAULT_INITIAL_CAPACITY:用来记录默认队列容量大小。

queue:用于存储队列的数据项,使用transient关键字来关闭序列化,提高代码效率。

size:队列大小

front:队头数据项的下标

rear:队尾数据项的下标

③:构造方法

public IQueue() {// TODO Auto-generated constructor stubthis(DEFAULT_INITIAL_CAPACITY);}public IQueue(int initialCapacity) {// TODO Auto-generated constructor stubif (initialCapacity < 1)            throw new IllegalArgumentException();size = 0;queue = new Object[initialCapacity];}
初始化队列数据项数组和size。

④:add()方法,添加数据项到队尾

public synchronized void add(E e){if(size == queue.length){ensureCapacity(size*2+1);}if(size == 0){front = 0;rear = 0;}else{rear = nextIndex(rear);}queue[rear] = e;size++;}
首先当然是判断是否还有数组空间可以使用,如果空间不足时,需要进行扩容。

然后判断是否是第一次插入数据,第一次插入数据需要初始化队头下标front和队尾下标rear。如果不是第一次插入,需要将rear向后移动一位。

既然我们使用的循环数组,当然不能直接rear++,而是使用了nextIndex()方法。

public synchronized int nextIndex(int i){return ++i==queue.length?0:i;}
如果下标加一后,超过了数组最大下标值,则赋值为0。

⑤:remove()方法,移除队头数据项并返回

public synchronized E remove(){E answer;answer = (E) queue[front];queue[front] = null;front = nextIndex(front);size--;return answer;}
首先获取队头的值,用于返回。

然后将其队头数据项赋值为null,帮助垃圾回收。

然后只需要将front向后移一位,使用同上方法(nextIndex()),便成功的完成了删除操作。

⑥:ensureCapacity()方法,扩容

public synchronized void ensureCapacity(int minCapacity){if(queue.length < minCapacity){if(size == 0){queue = new Object[minCapacity];}else{Object[] newQueue = new Object[minCapacity];if(front <= rear){System.arraycopy(queue, front, newQueue, 0, size);}else{int h = queue.length-front;int q = rear+1;System.arraycopy(queue, front, newQueue, 0, h);System.arraycopy(queue, 0, newQueue, h, q);}front = 0;rear = size - 1;queue = newQueue;}}}
首先既然方法名叫做“确保容量”,则需要判断当前数组容量是否小于了最小需求容量(minCapacity),如果小于了最小需求容量,则说明需要扩容。

然后判断当前队列长度是否为0,如果为0,则只需要申请空间就可以了,不需要复制数据。

如果队列长度不为0,则需要复制数据。由于使用了循环数组,复制数据又出现了两种情况:

数据只使用了一段(front<=rear)

数据分为了两段(front>rear)
当然也不复杂,分为了两段后多复制一次到后面就行了。

最后都需要进行同样的操作,重新初始化front和rear,并将新队列赋值给queue。当然size是没有变化的,则不需要修改。

⑦:其他方法

public int size() {return size;}public boolean isEmpty() {return size == 0;}
这就不用解释了吧。

最近比较赶时间,后面再对文章进行改进,希望对大家有帮助。


原创粉丝点击