Java数据结构与算法之队列(Queue)

一、前言

从上一篇内容中我们知道了栈最基本的操作是“出栈”和“入栈”,特点是“先进后出,后进先出”。而队列有很多相似的地方,最基本的操作是“入队”和“出队”,不过特点和栈恰恰相反,“先进先出,后进后出”。最形象的就是排队买票,排在最前的会先买到票,而排在后面的也就后买到票了。

上面的图是不是更新是一个水管子,队列的入栈和出栈就是像是水管中的水,最先进入水管子中的水,最先流出水管子。队列和栈一样也是中操作受限的线性表结构,同样,队列也是中抽象的数据结构,支持在队尾插入元素,在队头删除元素。用数组实现的队列叫作顺序队列,用链表实现的队列叫作链式队列。

二、队列的具体操作及简单实现

下面我们用数组实现对队列的基本操作:

声明队列的顶级接口:和栈的方法基本一致

/**
 * 队列的顶级接口
 * @author Administrator
 */
public interface Queue<T> {
	 /**
	  * 队列是否为空
	  * @return
	  */
	 boolean isEmpty();
	 /**
	  * 入队
	  * @param data
	  */
	 boolean enqueue(T data);
	 /**
	  * 出队删除data元素
	  * @param data
	  */
	 T dequeue();
	 /**
	  * 队列的大小
	  */
	 int size();
	 
}

三、队列的具体实现:

队列也是可以通过数组和链表实现,数组实现的是顺序队列,链表实现的是链式队列 。下面先用顺序队列来具体演示队列的这种数据结构。

首先实现顶级接口Queue,实现具体主要方法入队和出队操作,实现指定队列大小和默认队列大小两个构造方法:

//存放数据的数组
	private T []items;
	//数组的大小
	private int size;
	//队头下标
	private int head=0;
	//队尾下标
	private int tail=0;
	//队列默认大小
	private static final int DEFULE_LENGTH=10;
	/**
	 * 指定队列大小
	 * @param capactiyt
	 */
	public ArrayQueue(int capactiyt){
		items = (T[]) new Object[capactiyt];
		head=tail;
	}
	/**
	 * 默认队列大小为10
	 */
	public ArrayQueue(){
		items = (T[]) new Object[DEFULE_LENGTH];
		head=tail;
	}

具体操作:

在实现具体操作之前,先通过下图来了解下队列的入队和出队整个过程 

 

 

 实现代码如下:

        /**
	 * 入队操作
	 */
	@Override
	public boolean enqueue(T data) {
		//当队尾的下标等于size时表示队列已满不能入队
		if(tail==size){
			return false;
		}
		//将数据赋值到队尾的位置
		items[tail]=data;
		++tail;//队尾的位置+1
		size++;//队列大小+1
		return true;
	}
	/**
	 * 出队操作
	 */
	@Override
	public T dequeue() {
		//当对头和队尾为相同时表示队列为空,返回null
		if(head==tail){
			return null;
		}
		//取出队头数据返回
		T oldData = items[head];
		++head;//队头head向后移动
		size--;//队列的大小-1
		return oldData;
	}

在顺序表的实现过程中,可以发现,数组的实现的队列和数组存在一样的问题,在图E中,随着元素不停的入队和出队操作,head和tail都会持续的往后移动,当队尾tail移动到最后边时,及时数组还有空闲空间,队列也无法继续添加元素,也就是假溢出。此时数据搬移的思想就可以应用在此处,这样每次都只需要删除数组下标为0的数据,不过涉及整个队列的数据进行搬移,时间复杂度就会从原来的O(1)变成O(n)。这个问题该如何优化呢?

先来看下一种思路:

在出队时不做数据搬移,只在没有空闲空间时,在入队时,触发一次数据搬移的操作,这样出队函数我们保持不变,只需要修改下入队操作。具体代码如下:

        /**
	 * 入队操作
	 * 队满时触发数据搬移
	 */
	@Override
	public boolean enqueue(T data) {
		//当队尾的下标等于size时表示队列已满
		if(tail==size){
			//当tail==size && head==0表示整个队列已经满
			if(head==0){
				return false;
			}
			//数据搬移
			for (int i = head; i < tail; i++) {
				items[i-head]=items[i];
			}
			//搬移数据之后更新head和tail
			tail-=head;
			head=0;
		}
		//将数据赋值到队尾的位置
		items[tail]=data;
		++tail;//队尾的位置+1
		size++;//队列大小+1
		return true;
	}

从代码中,我们可以看出,虽然解决了假溢出的问题,但是空间浪费的问题依然存在,而且入队时在特殊场景下会变成O(n),这当然是我们不想看到的,此次出现了一种特殊的队列——循环队列。

四、循环队列

循环队列顾名思义是一个循环结构,可以有效利用存储空间,不过该如何判断队列已满,话不多说,先上图:

 

从图中可以看出,在队列已满时,再添加元素时,不是将队尾置位7,而是将其在环中后移一位,到下标为0的位置,然后依次入队。而如何判断队列已满。从图d中可以看出,当队列已满时(队尾+1)%size=head的规律。队头和队尾的计算方式为(队尾+1)%size=队尾,(队头+1)%size=队头,此时队尾是没有存储元素的,因此会浪费数据的一个存储空间。

代码实现:

        /**
	 * 入队操作
	 */
	@Override
	public boolean enqueue(T data) {
		//判断队列是否已满,已满队列返回false
		if((tail+1)%size==head){
			return false;
		}
		//将元素赋值到队尾
		items[tail]=data;
		//将队尾下标+1
		tail=(tail+1)%size;
		size++;//队列大小+1
		return true;
	}
	/**
	 * 出队操作
	 */
	@Override
	public T dequeue() {
		//如果队列为空,返回null
		if(head==tail){
			return null;
		}
		//取得要出队的数据
		T oldData = items[head];
		//队头下标后移一位
		head=(head+1)%size;
		size--;//队列大小减1
		return oldData;
	}

 从代码中看出,出队和入队的时间复杂度都为O(1),同时结合图例可以看出,循环队列有效解决了空间浪费的问题。

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值