数据结构篇04、队列及其两种底层实现

632 阅读2分钟

我们知道队列这种数据结构是先进先出的特性;本章我们通过前两部分介绍的动态数组和链表分别实现队列这种数据结构;

java的util库中Queue接口的实现之一LinkedList就是一个链表,优先队列PriorityQueue底层实现是堆,也就是数组;

1、定义队列接口,规定队列需要实现的方法

队列接口如下所示,方法的作用见注释;

public interface Queue<E> {

    //获得元素个数
    int getSize();
    //判断是否为空
    boolean isEmpty();
    //元素入队列
    void enqueue(E e);
    //元素出队列
    E dequeue();
    //查看队首元素
    E getFront();
}

2、动态数组实现的队列

动态数组实现的队列如下所示,需要注意的点就是我们定义的队列是一个先进先出的数据结构;

其实功能的实现主要就是对我们第一部分实现的动态数组api的一层封装,这里我们以数组头为队列头,数组尾为队列尾;

enqueue入队方法就是调用array的addLast方法,往数组尾添加元素;

dequeue出队方法就是调用array的removeFirst方法,从数组头移除元素;

getFront查看队首元素就是调用array的getFirst方法,查找数组头元素;

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();
    }

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

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

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

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

    @Override
    public String toString(){

        StringBuilder res = new StringBuilder();
        res.append("Queue: ");
        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();
    }

    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);
            }
        }
    }
}

3、循环数组实现的队列

上一部分我们实现的队列其实是有很大的优化的,例如我们移除队首元素后,后面的元素都要往前移动一位,这样时间复杂度就是O(n)级别的,我们可以通过一个循环数组来实现队列,保证入队和出队的时间复杂度都为O(1);

循环数组实现的队列如下所示,这里我们不再使用封装好的动态数组,而是重新实现一遍循环数组的逻辑;

data表示存储元素的数组;

front和tail表示数组中数组头和数组尾的索引,front表示数组头元素的位置,tail表示数组尾马上添加元素的位置,我们通过维护这两个索引,就可以直接通过索引实现入队和出队,从而也就实现了O(1)的时间复杂度;

size表示队列中元素的个数;

其中两个构造方法,一个带参数构造方法可以传入队列的容量,另一个无参构造方法默认的容量为10;

getCapacity返回队列的容量,我们发现这里是data.length - 1,原因是循环数组有一个位置我们不能使用,就是当队列元素满了之后,应该把front索引前一个位置空出来;

isEmpty判断数组是否为空,直接通过front和tail索引是否相等即可,同样这里也解释了求容量时为什么要将front前一个位置空出来,如果不空出来,满了之后也会发生front==tail,就会发生混乱;

enqueue入队函数,首先判断是否需要扩容,然后往tail索引处添加元素e即可,接着维护一下tail索引和size元素个数,由于这里是循环数组,因此需要对data.length取模; dequeue出队函数,将front索引置空,然后维护一下front索引和size元素个数,最后判断是否需要缩容;

getFront查看队首元素,直接返回front索引处元素即可;

resize扩容缩容方法,新建数组,然后将原数组中的所有元素拷贝至新数组,最后将数组引用指向新数组,同时维护一下front和tail两个索引;

最后重写toString方法打印队列中所有元素,便于查看队列;

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

    private E[] data;
    private int front, tail;
    private int size;

    public LoopQueue(int capacity){
        data = (E[])new Object[capacity + 1];
        front = 0;
        tail = 0;
        size = 0;
    }

    public LoopQueue(){
        this(10);
    }

    public int getCapacity(){
        return data.length - 1;
    }

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

    @Override
    public int getSize(){
        return size;
    }

    @Override
    public void enqueue(E e){

        if((tail + 1) % data.length == front)
            resize(getCapacity() * 2);

        data[tail] = e;
        tail = (tail + 1) % data.length;
        size ++;
    }

    @Override
    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;
    }

    @Override
    public E getFront(){
        if(isEmpty())
            throw new IllegalArgumentException("Queue is empty.");
        return data[front];
    }

    private void resize(int newCapacity){

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

        data = newData;
        front = 0;
        tail = size;
    }

    @Override
    public String toString(){

        StringBuilder res = new StringBuilder();
        res.append(String.format("Queue: size = %d , capacity = %d\n", size, getCapacity()));
        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();
    }

    public static void main(String[] args){

        LoopQueue<Integer> queue = new LoopQueue<>(5);
        for(int i = 0 ; i < 10 ; i ++){
            queue.enqueue(i);
            System.out.println(queue);

            if(i % 3 == 2){
                queue.dequeue();
                System.out.println(queue);
            }
        }
    }
}

4、链表实现的队列

链表实现的队列如下所示,这里我们并没有使用第二部分实现的链表,而是重新封装了一个链表,我们第二部分实现的链表只维护了一个虚拟头节点,导致往链表尾部添加元素是O(n)时间复杂度的操作,我们重新封装的这个链表维护了head头节点和tail尾节点两个节点,可以保证从链表头取元素和往链表尾添加元素的操作都是O(1)时间复杂度的操作;

Node节点类,成员变量包括元素e和节点next,三个构造函数可按需使用,toString打印节点中元素的值;

head和tail两个节点分别指向链表头元素和链表尾元素,size表示队列元素个数;

enqueue入队操作分两种情况,当tail==null,也就是链表为空,此时直接将新添加的节点置为tail,然后让head=tail即可;当tail不为null,直接将tail的next指向新添加的元素,然后将tail指向tail.next,最后维护一下size;

dequeue出队操作,首先保存一下原头节点,然后将head指向head的next,如果head==null说明链表为空了,此时维护一下tail也置为null,最后维护一下size;

getFront查看队首元素直接返回head的成员变量e即可;

最后重写toString方法打印队列元素,便于查看队列;

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

    private class Node{
        public E e;
        public Node next;

        public Node(E e, Node next){
            this.e = e;
            this.next = next;
        }

        public Node(E e){
            this(e, null);
        }

        public Node(){
            this(null, null);
        }

        @Override
        public String toString(){
            return e.toString();
        }
    }

    private Node head, tail;
    private int size;

    public LinkedListQueue(){
        head = null;
        tail = null;
        size = 0;
    }

    @Override
    public int getSize(){
        return size;
    }

    @Override
    public boolean isEmpty(){
        return size == 0;
    }

    @Override
    public void enqueue(E e){
        if(tail == null){
            tail = new Node(e);
            head = tail;
        }
        else{
            tail.next = new Node(e);
            tail = tail.next;
        }
        size ++;
    }

    @Override
    public E dequeue(){
        if(isEmpty())
            throw new IllegalArgumentException("Cannot dequeue from an empty queue.");

        Node retNode = head;
        head = head.next;
        retNode.next = null;
        if(head == null)
            tail = null;
        size --;
        return retNode.e;
    }

    @Override
    public E getFront(){
        if(isEmpty())
            throw new IllegalArgumentException("Queue is empty.");
        return head.e;
    }

    @Override
    public String toString(){
        StringBuilder res = new StringBuilder();
        res.append("Queue: front ");

        Node cur = head;
        while(cur != null) {
            res.append(cur + "->");
            cur = cur.next;
        }
        res.append("NULL tail");
        return res.toString();
    }

    public static void main(String[] args){

        LinkedListQueue<Integer> queue = new LinkedListQueue<>();
        for(int i = 0 ; i < 10 ; i ++){
            queue.enqueue(i);
            System.out.println(queue);

            if(i % 3 == 2){
                queue.dequeue();
                System.out.println(queue);
            }
        }
    }
}