数据结构基础----队列

队列

我在这一章的前半部分向大家介绍了栈这种数据结构,那么我在这章的后部分,向大家介绍队列这种数据结构。

队列的概括

队列,本身也是一种线性数据结构,换句话说数据依旧是排成一排,这一点和栈一样。不过和栈不同的是,队列只能从一端添加元素,而从另一端取出元素v,通常。我们添加元素的一端称为队尾,而取出元素的一端称为队首**。事实上这一切都非常好理解,队列这个名字的由来也和我们生活中,排队那个队列是一致的。

我们简单的看一个如图演示:

假设这就是一个队列,可以想象一下,比如说

  1. 我们去银行办事情,相应的就有一个柜台,队列就是排队到这个柜台办业务
  2. 那么一旦来了一个新的人。在数据结构领域,其实就是来了一个新的元素,这个新的元素就应该从队尾的位置进入队列
  3. 下面再来一个新的元素,这个新的元素依然是从队尾的位置进入队列,以此类推……
  4. 但是现在要出队了,比如说,在柜台的服务人员已经办完了上一个业务,已经有空闲处理下一个任务啦,那么此时,就要队列中取出一个元素,取出的元素就应该是队首元素,这种情况下取出的元素就应该是1

通过分析可以看出,队列是一种先进先出的数据结构,也就是生活中的先到先得,这是和后进先出不一样,这个区别(从每端取出数据的区别)看似很小的。但是,在实际的应用中却大不相同。如果我们在生活中用栈这种方式来进行排队办理业务的话,相信没有人会愿意的。相应的这种先进先出,英文就是first in first out,缩写成FIFO

队列的实现

针对队列这种数据结构来说,基本上写了只涉及这5个操作,分别是

  1. 向队列中添加一个元素,也都是入队,通常叫做enqueue
  2. 从队列中取出一个元素,也叫做出队,通常叫做dequeue
  3. 看一下队首列的元素是谁,这个动作通常叫做getFront
  4. 看一下队列里一共有多少个元素getSize
  5. 判断一下队列是否为空isEmpty

我们实现的方式也是写一个接口叫做queue,我们依托不同的底层的数据结构可以实现这个接口。在队列的实现中,我将非常快速的和之前的栈一样,复用Array这个动态数组类来实现ArrayQueue

需要提一下,从用户的角度来看,只要实现上述操作就好,具体底层实现,用户并不关心,实际上,底层确实有多种实现方式。

我们准备在之前实现的动态数组基础上,来实现"队列"这种数据结构。 

先定义一个接口Interface,如下:

public interface Queue<E> {
    int getSize();  //查看队列中元素的个数
    boolean isEmpty(); //查看队列是否为空
    void enqueue(E e);  //入队
    E dequeue();  //出队
    E getFront();  //查看队首元素
}

ArrayQueue实现代码如下:

package com.lihe.leetcode;

public class ArrayQueue<E> implements Queue<E> {

	private Array<E> array;
	
    //构造函数
	public ArrayQueue(int capacity) {
		array = new Array<>(capacity);
	}
	
    //无参构造函数
	public ArrayQueue(){
		array = new Array<>();
	}
	
	@Override
	public int getSize() {
		return array.getSize();
	}

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

	@Override
	public void enqueue(E e) {
		array.addLast(e);
	}

	@Override
	public E dequeue() {
		return array.removeFirst();
	}

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

	public int getCapacity(){
		return array.getCapacity();
	}
	
	@Override
	public String toString(){
		StringBuilder ret = new StringBuilder();
		ret.append("Queue:");
		ret.append("Front [");
		for (int i = 0; i < array.getSize(); i++) {
			ret.append(array.get(i));
			if(i != array.getSize() - 1)
				ret.append(", ");
		}
		ret.append("] tail");
		return ret.toString();
	}
	
	public static void main(String[] args) {
		ArrayQueue<Integer> queue = new ArrayQueue<>();
		for (int i = 0; i < 10; i++) {
			queue.enqueue(i);
			System.out.println(queue);
			if(i % 3 == 2){
				queue.dequeue();
				System.out.println(queue);
			}
				
		}
	}
    /** 结果:
	 *  Queue:Front [0] tail
	 *  Queue:Front [0, 1] tail
	 *	Queue:Front [0, 1, 2] tail
	 *	Queue:Front [1, 2] tail
	 *	Queue:Front [1, 2, 3] tail
	 *	Queue:Front [1, 2, 3, 4] tail
	 *	Queue:Front [1, 2, 3, 4, 5] tail
	 *	Queue:Front [2, 3, 4, 5] tail
	 *	Queue:Front [2, 3, 4, 5, 6] tail
	 *	Queue:Front [2, 3, 4, 5, 6, 7] tail
	 *	Queue:Front [2, 3, 4, 5, 6, 7, 8] tail
	 *	Queue:Front [3, 4, 5, 6, 7, 8] tail
	 *	Queue:Front [3, 4, 5, 6, 7, 8, 9] tail
	 */
}

队列的复杂度分析

最后,对于我们实现的ArrayQueue数组队列,进行一下简单的复杂度分析,这个复杂度分析大部分和之前的栈一样。
添加元素(入队enqueue)的时候,每一次都是向数组的最后一个位置添加元素,在对进行均摊复杂度分析,最终是O(1)的复杂度,同时getSizeisEmptygetFront也都是O(1)的复杂度。不过,我们出队(dequeue)操作是一个O(n)复杂度的操作。这是因为在出队的时候,需要把整个数组的第一个元素(索引为0)拿出来,而数组中后面剩下的所有元素都要向前挪一个位置,这显然是O(n)的复杂度。

ArrayQueue<E>
·void enqueue(E)   O(1)    均摊
·E dequeue()       O(n)
·E getFront()      O(1)
·int getSize()     O(1)
·boolean isEmpty() O(1)

 循环队列

  • 数组队列的出队操作的复杂度是O(n),性能很差,解决方法就是使用循环队列(Loop Queue)
  • 正常情况:front指向队列第一个元素,tail指向队列中最后一个元素的后一个位置
  • 队列为空时,front和tail都指向第一个元素位置,不为空时,front != tail
  • 入队时:tail ++, 出队时: front ++
  • 循环队列的示意图如下:

入队出队过程:入队5个元素

  

出队两个元素(只需要front指向改变,不需要移动所有元素,复杂度为O(1)):

入队4个元素

注意:因为front == tail表示队列为空,故此时如果在添加一个元素会导致front == tail,然而此时表示队列已满。

因此,当tail + 1 == front 时表示队列为满。因为是循环队列故使用(tail + 1)% c == front(当front=0,tail=7时,使用(7+1)%8 ==0)。c代表capacity(容积)。

结论:循环队列使用%,类似钟表11点之后叫12点或者0点

  • 实现循环队列的业务逻辑,并进行测试:

  • package com.lihe.datastructure;
    
    public class LoopQueue<E> implements Queue<E>{
    
        private E[] data;  //队列
        private int front,tail; //队列队首队尾
        private int size;  //队列元素个数
    
        //有参构造函数
        public LoopQueue(int capacity) {
            //因为循环队列浪费一个空间,需要存储capacity个元素,故需要capacity+1的空间
            data = (E[])new Object[capacity + 1];
            front = 0;
            tail = 0;
            size = 0;
        }
    
        //无参构造函数
        public  LoopQueue() {
            this(10);//直接调用有参数的构造函数,然后传入一个默认值
        }
    
        //实际存储数据为容积-1,有意浪费一个空间
        //容积比长度少一
        public int getCapacity(){
            return data.length - 1;
        }
    
        @Override
        public int getSize() {
            return size;
        }
    
        @Override
        public boolean isEmpty() {
            return front == tail;
        }
    
        //入队
        @Override
        public void enqueue(E e) {
            if((tail + 1) % data.length == front){
                //扩容成所能存放个数的两倍,不是空间的两倍,因为浪费了一个空间
                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++) {
                //data中的元素不一定从0开始,与newData之间偏移量front,
                //因是循环队列防止越界,故用%data.length
                newData[i] = data[(front + i) % data.length];
            }
            data = newData;
            front = 0;
            tail = size;//元素个数不变
        }
    
        //出队
        @Override
        public E dequeue() {
            if(isEmpty()){
                throw new IllegalArgumentException("Cannot enqueue from an empty queue");
            }
            E ret = data[front];
            front = (front + 1) % data.length;
            size --;
            if(size == getCapacity() / 4 && getCapacity() / 2 != 0)
                resize(getCapacity() / 2);
            return ret;
        }
    
    
        @Override
        public E getFront() {
            if(isEmpty()){
                throw new IllegalArgumentException("Queue is empty ");
            }
            return data[front];
        }
    
        //override代表复用
        @Override
        public String toString() {
            StringBuilder res = new StringBuilder();
            res.append(String.format("Queue:size=%d,capacity=%d\n", size, getCapacity()));
            res.append("front [");
            //遍历循环队列,队首是front,队尾是tail-1,
            // 因为是循环故tail可能<front,故条件为i != tail不是i < tail,
            // 故递增为 i= (i + 1) % data.length
            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();
        }
    
      //测试
        public static void main(String[] args) {
            LoopQueue<Integer> queue = new LoopQueue<>();
            for (int i = 0; i < 10; i++) {
                queue.enqueue(i);
                System.out.println(queue);
                if(i % 3 == 2){
                    queue.dequeue();
                    System.out.println(queue);
                }
    
            }
        }
       
    }
    
    /**
     * 遍历循环队列中两种方法:
     * 法一:
     *   for (int i = 0; i < size; i++) {
     *             //data中的元素不一定从0开始,与newData之间偏移量front,
     *             //因是循环队列防止越界,故用%data.length
     *             newData[i] = data[(front + i) % data.length];
     *         }
     *
     * 法二:
     *   //遍历循环队列,队首是front,队尾是tail-1,
     *         // 因为是循环故tail可能<front,故条件为i != tail不是i < tail,
     *         // 故递增为 i= (i + 1) % data.length
     *         for (int i = front; i != tail; i= (i + 1) % data.length ) {
     *             res.append(data[i]);
     *             //不是最后一个元素的判断条件
     *             if ((i + 1) % data.length != tail) {
     *                 res.append(',');
     *             }
     *         }
     */
    
  • 输出结果:
  •  /**
         * 结果:
         *      Queue:size=1,capacity=10
         *      front [0] tail
         *      Queue:size=2,capacity=10
         *      front [0,1] tail
         *      Queue:size=3,capacity=10
         *      front [0,1,2] tail
         *      Queue:size=2,capacity=5
         *      front [1,2] tail
         *      Queue:size=3,capacity=5
         *      front [1,2,3] tail
         *      Queue:size=4,capacity=5
         *      front [1,2,3,4] tail
         *      Queue:size=5,capacity=5
         *      front [1,2,3,4,5] tail
         *      Queue:size=4,capacity=5
         *      front [2,3,4,5] tail
         *      Queue:size=5,capacity=5
         *      front [2,3,4,5,6] tail
         *      Queue:size=6,capacity=10
         *      front [2,3,4,5,6,7] tail
         *      Queue:size=7,capacity=10
         *      front [2,3,4,5,6,7,8] tail
         *      Queue:size=6,capacity=10
         *      front [3,4,5,6,7,8] tail
         *      Queue:size=7,capacity=10
         *      front [3,4,5,6,7,8,9] tail
         */

 循环队列的复杂度分析

  • LoopQueue<E>
    ·void enqueue(E)   O(1)    均摊
    ·E dequeue()   O(1)   均摊
    ·E getFront()  O(1)
    ·int getSize()   O(1)
    ·boolean isEmpty()   O(1)
    

使用简单算例测试ArrayQueue与LoopQueue的性能差异

package com.lihe.datastructure;

import java.util.Random;

public class Main {

    // 测试使用q运行opCount个enqueue和dequeue操作所需要的时间,单位:秒
    private static double testQueue(Queue<Integer> q, int opCount) {
        long startTime = System.nanoTime();//计时

        Random random = new Random();
        //多次入队
        for (int i = 0; i < opCount; i++) {
            q.enqueue(random.nextInt(Integer.MAX_VALUE));
        }
        //多次出对
        for (int i = 0; i < opCount; i++) {
            q.dequeue();
        }
        long endTime = System.nanoTime();//计时
        return (endTime - startTime) / 1000000000.0;//用时
    }


    public static void main(String[] args) {
        int opCount = 100000; //操作数量

        ArrayQueue<Integer> arrayQueue = new ArrayQueue<>();
        double time1 = testQueue(arrayQueue, opCount);
        System.out.println("ArrayQueue, time: " + time1 + " s");

        LoopQueue<Integer> loopQueue = new LoopQueue<>();
        double time2 = testQueue(loopQueue, opCount);
        System.out.println("LoopQueue, time: " + time2 + " s");

    }

}
  • 输出结果
  •  ArrayQueue, time: 5.891407568 s
     LoopQueue, time: 0.018795095 s
        

猜你喜欢

转载自blog.csdn.net/mingyuli/article/details/82385693