欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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

程序员文章站 2022-06-06 20:57:59
...

    队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;