数据结构(三)——基于数组的队列和循环队列

    队Queue也是一个线性的存储结构,原则是先入先出(FIFO)区别于栈的先进后出。就类似与排队买票,先进入队列的就先买票出列;入队在一端操作(队尾),出队只能在另一端操作(队首);

    一个队列的基本操作就是入队,出队,获取队列大小,判断是否为空等等;这篇博客就是自己实现一个基于数组的队列和循环队列。

    根据上面的分析,创建一个Queue接口,提供入队,出队,判空等操作:

public interface Queue<E> {             //使用泛型使得队列可以存储各种类型的元素
	void enqueue(E e);		//入队(从队尾操作)
	E dequeue();			//出队,返回出队的元素(从队首操作)
	public int getSize();	        //获取队列中元素的个数
	boolean isEmpty();		//判断队列是否为空
	E front();			//获取队首的元素
}

下面,基于数组实现一个队列,来看看具体的方法怎么实现:

为了实现队列的可扩容,我们用于实现的数组是博客“数据结构(一)——封装动态数组”中的动态数组;具体的原理可以查看之前的那篇博客,这里只贴代码:

package com.itheima.array;

/**
 * @author GuoBaoYu
 *	使用泛型,可以存放任意类型的数据;
 */
public class Array<E> {
	private E[] data;   //内部是静态数组
	private int size;
	
	public Array(int capacity) {
		data =(E[]) new Object[capacity];
		size = 0;
	}
	
	/**
	 * 默认构造一个容量为10的数组
	 */
	public Array() {
		this(10);
	}
	
	public int getCapacity(){
		return data.length;
	}
	
	public boolean isEmpty(){
		return size==0 ;
	}
	
	public int getSize(){
		return size;
	}
	
	public void addLast(E element){
		add(size, element);
	}
	
	public void addFirst(E element){
		add(0,element);
	}
	
	//动态数组进行自动扩容和减少容量
	private void resize(int newCapacity){
		E[] newData = (E[]) new Object[newCapacity];
		for(int i = 0; i < size;i++){
			newData[i] = data[i];
		}
		data = newData;
	}
	
	/**
	 * @param index
	 * @param element
	 * 向指定的位置插入元素
	 */
	public void add(int index,E element){
		if(size == getCapacity()){
//			throw new IllegalArgumentException("AddLast is failed. Array is full.");
//		之前如果数组空间满了,抛出异常;现在进行扩容
			int newCapacity = getCapacity()*2;
			resize(newCapacity);
		}
		
		if(index < 0 || index > size){
			throw new IllegalArgumentException("Argument index is illegal.index is required index >=0 and index <= size. ");
		}
		
		for(int i = size-1; i>=index; i--){
			data[i+1] = data[i];
		}
		data[index] = element;
		size++;
	}

	@Override
	public String toString(){
		StringBuilder builder = new StringBuilder();
		builder.append(String.format("size = %d,capacity = %d\t", size,getCapacity()));
		builder.append("[");
		for(int i = 0; i < size; i++){
			builder.append(data[i]);
			if(i<size-1){
				builder.append(",");
			}
		}
		builder.append("]");
		return builder.toString();
	}
	
	public E get(int index){
		if(index >=size || index <0){
			throw new IllegalArgumentException("Argument index is illegal.");
		}
		return data[index];
	}
	
	public void set(int index, E element){
		if(index >=size || index <0){
			throw new IllegalArgumentException("Argument index is illegal.");
		}
		data[index] = element;
	}
	
	public boolean contains(E element){
		for(int i =0; i <size;i++){
			if(data[i].equals(element)){
				return true;
			}
		}
		return false;
	}
	
	public E getFirst(){
		return get(0);
	}
	
	public E getLast(){
		return get(size-1);
	}
	
	public int find(E element){
		for(int i=0 ; i < size; i++){
			if(data[i].equals(element)){
				return i;
			}
		}
		return -1;
	}
	
	//删除index位置的元素,并返回删除的元素的值
	public E remove(int index){
		if(index < 0 || index >= size){
			throw new IllegalArgumentException("Remove failed.index is an illegal argument.");
		}  
		E element = data[index];
		for(int i = index+1; i < size; i++){
			data[i-1] = data[i];
		}
		size--;
		
		if(size <= getCapacity() /4 && data.length/2 !=0){
			resize(getCapacity()/2);
		}
		return element;
	}
	
	//删除第一个元素
	public E  removeFirst(){
		return remove(0);
	}
	//删除最后一个元素
	public E removeLast(){
		return remove(size-1);
	}
	
	/**
	 * @param element
	 * @return
	 * 删除指定的数据
	 */
	public boolean removeElement(E element){
		int find = find(element);
		if(find!=-1){
			remove(find);
			return true;
		}
		return false;
	}
}

基于这个动态数组Array,实现一个队列:

package com.itheima.queue;

import com.itheima.array.Array;

public class ArrayQueue<E> implements Queue<E>{
	private Array<E> array;
	
	public ArrayQueue() {
		array = new Array<E>(10);
	}
	
	//允许用户指定队列的容量
	public ArrayQueue(int capacity){   		  
		array = new Array<E>(capacity);
	}
	
	public int getCapacity(){
		return array.getCapacity();
	}
	
	//入队从数组的尾部操作
	public void enqueue(E e) {
		array.addLast(e);     //入队时队列是否满,是否触发扩容操作在array.addLast()方法中考虑,所以这里直接调用;
	}

	//出队从数组的首部操作
	public E dequeue() {
		return array.removeFirst();
		//出队时队列是否为空,是否触发缩容操作在array.removeFirst()方法中考虑,所以这里直接调用;
	}

	public int getSize() {
		return array.getSize();
	}

	public boolean isEmpty() {
		return array.isEmpty();
	}

	public E getFront() {
		return array.getFirst();
	}

	@Override
	public String toString() {
		StringBuilder res = new StringBuilder();
		res.append("ArrayQueue:");
		res.append("front:[");
		
		for(int i = 0; i < array.getSize();i++){
			res.append(array.get(i));
			if(i < array.getSize()-1){
				res.append(",");
			}
		}
		res.append("] tail");
		return res.toString();
	}
	
	
}

一个简单的测试:


上面的这种实现方式可以实现队列的动态扩容。但是依旧存在一定的缺陷:基于数组实现的队列,在进行出队操作时是调用了数组的removeFirst()方法,移除数组下标为0的元素,后面的每个元素向前移动一个位置,再进行size--,时间复杂度为o(n):过程如下:(第三步的size还要进行size--,标在4的位置,图截错了..这里声明一下)



为了解决这一点,在数组队列的基础上改进循环队列:基本思路是,在进行入队和出队操作(主要为了出队操作,不需要移动数组中元素的位置),只需要维护队列(数组)的front(头)和tail(尾)的值,出队变成了O(1)的操作;


使用这种方式的另一个好处是构成了循环队列:什么是循环呢?

当出现下面这种存储情况时:


看起来不能再入队,tail不能再进行tail++了,因为tail指示的是队尾的后面第一个空的位置;但是实际上并不是不能再入队,而是可以把数组看成一个环,数组的前面还有空间,tail变为(tail+1)%8=0,tail指示在下标为0的空间!所以tail真正的操作应该是(tail+1)%数组长度;



以上就是循环队列的基本思想。下面是循环队列的实现,是基于普通静态数组的:

package com.itheima.queue;

import com.itheima.array.Array;

/**
 * @author GuoBaoYu
 *
 * @param <E>
 * 循环队列
 */
public class LoopQueue<E> implements Queue<E> {
	private E[] data;
	private int front,tail;
	private int size;   //队列的元素的个数
	
	public LoopQueue(int capacity) {
		//用户想要存放capacity个数据,根据之前的原理分析,为了防止判空和判满冲突,需要预留一个空间!
		data = (E[]) new Object[capacity+1];
		front = 0;
		tail = 0;
		size = 0;
	}
	
	public LoopQueue() {
		this(10);
	}
	
	public int getCapacity(){
		return data.length-1;
	}
	
	/* (non-Javadoc)
	 * @see com.itheima.queue.Queue#enqueue(java.lang.Object)
	 * 入队:
	 * 	1.如果队满,调用resize()方法进行扩容;
	 * 	2.把数据放入tail的位置
	 * 	3.维护tail和size的值
	 */
	public void enqueue(E e) {
		if( (tail+1)% data.length == front){  		//数组判满;进行%data.length是为了让数组循环起来

			//TODO 队列满了以后进行扩容
			resize(getCapacity() *2);
		}
		data[tail] = e;
		tail = (tail+1)%data.length;
		size++;
	}

	private void resize(int newCapacity) {
		E[] newData = (E[]) new Object[newCapacity + 1];
		for(int i= 0; i < size;i++){
			newData[i] = data[(front+i)% data.length];
		}
		data = newData;
		front = 0;
		tail = size;
	}

	public E dequeue() {
		if(isEmpty()){
			throw new IllegalArgumentException("Cannot dequeue from an empty queue.");
		}
		E ret = data[front];
		data[front] = null;
		front = (front+1)%data.length;
		size--;
		
		if(size==getCapacity()/4 && getCapacity()/2!=0){
			resize(getCapacity()/2);
		}
		return ret;
	}

	public int getSize() {
		return size;
	}

	public boolean isEmpty() {
		return front == tail;
	}


	public E front() {
		if(isEmpty()){
			throw new IllegalArgumentException("No elements in this queue.");
		}
		return data[front];
	}

	@Override
	public String toString() {
		StringBuilder res = new StringBuilder();
		res.append("LoopQueue:");
		res.append("front:[");
		
		for(int i = front; i != tail;i = (i+1)%data.length){
			res.append(data[i]);
			if((i +1)% data.length != tail){
				res.append(",");
			}
		}
		res.append("] tail");
		return res.toString();
	}
	
	
}

一个简单的测试:


可以看到,出队的结果和使用removeFirst()达到的效果是一样的;

代码中值得注意的地方在于自动调整容量的resize()方法的实现和遍历队列的toString()方法:


在进行入队时,先根据之前分析的条件,(tail+1)%data.length==front判断是否队满,如果队满进行扩容。扩容的思路是将队列数组的大小调整为原来的2倍,当然也可以是1.5倍,看个人了;当需要扩容时,数组中的元素可能是这样存放的:


要想调整为容量为16的数组,我们完全没有必要还按这个顺序存放数据,可以像下面这样:


相当于在扩容的同时对数据进行了整理;还是创建一个两倍大小+1的数组,新数组的0位置对应原来的front,1对应front+1......

位置都存在front的偏移,所以有了resize()代码中:把data[(front+i)%data.length]赋值给newData[i]


最后更改data的指向,维护新数组的front和tail的值;如此实现扩容!当然resize()方法只是传递了一个新的容量作为参数;在enqueue()方法中传递一个大的新容量作为扩容;当出队很多数据后可以传递一个小的容量,进行缩容。

在覆盖toString()方法时,和之前的从下标为0开始遍历不同,变为从front开始遍历,直到遍历到最后一个元素,也需要注意一下。

以上就是一个循环队列的实现,把出队操作的时间复杂度降为了O(1)。

下面通过测试比较一下两种队列的效率:


可以看到对10000个数据的出队,LoopQueue循环队列的效率远高于ArrayQueue;


猜你喜欢

转载自blog.csdn.net/baoyu_g/article/details/80945666