算法数据结构:队列

444 阅读12分钟

1、什么是队列

队列,可以把它想象成排队买票,先来的先买,后来的后买,后来的人只能站末尾,不允许插队。先进者先出,这就是典型的队列

我们知道,栈只支持两个基本操作:入栈push出栈pop。队列跟栈非常相似,支持的操作也很有限,最基本的操作也是两个:入队queue,放一个数据到队列尾部;出队dequeue,从队列头部取一个元素。

所以,队列跟栈一样,也是一种操作受限的线性表数据结构。

2、如何实现一个队列

队列跟栈一样,也是一种抽象的数据结构,具有先进先出的特性FIFO。它支持在队尾插入元素,在队头删除元素。队列既可以用数组来实现,也可以用链表来实现。用数组实现的队列叫作顺序队列,用链表实现的队列叫作链式队列

2.1、顺序队列

比起栈的数据组实现,队列的数组实现稍微有点复杂。对于栈来说,我们只需要一个栈顶指针就可以了。但队列需要两个指针:一个是head指针,指向队头;另一个是tail指针,指向队尾。

如图所示,当[a, b, c, d]依次入队之后,队列中的 head 指针指向 0 位置(head = 0),tail 指针指向 4 位置(tail = 4)。

当我们调用两次出队操作之后,队列中的 head 指针指向 2 位置(head = 2),tail 指针指向 4 位置(tail = 4

这个过程会有一个问题,随着不停地进行入队、出队操作,head 和 tail 都会持续往后移动。当 tail 移动到最右边,即使数组有空闲空间,也无法继续往队列中加数据了。

数组中使用的空间为:[head, tail],当 head = 0,tail 移动到最右边时,数组无可用空间;其他情况,数组都有空闲空间

如何解决当 tail 移动到最右边,head != 0 时的问题呢?我们常用的方法就是数据搬移,但是,每次进行出队操作都相当于删除数组下标为 0 的数据,要搬移整个队列中的数据,这样出队操作的时间复杂度就会从原来的O(1)变为O(n)

通常,我们会在出队时可以不用搬移数据。当 tail 已经在最右边时,我们只需在最新一次入队操作时,集中触发一次数据的搬移操作。具体实现如下:

/**
 * @description: 数组实现的队列
 * @author: erlang
 * @since: 2020-09-07 22:12
 */
public class ArrayQueue {
    // 数组:arr,数组大小:n
    private int[] arr;
    private int limit = 0;
    // head表示队头下标,tail表示队尾下标
    private int head = 0;
    private int tail = 0;

    // 申请一个大小为capacity的数组
    public ArrayQueue(int capacity) {
        arr = new int[capacity];
        limit = capacity;
    }

    public int dequeue() {
        // 如果head == tail 表示队列为空
        if (head == tail) {
            throw new RuntimeException("栈空了");
        }
        // 为了让其他语言的同学看的更加明确,把--操作放到单独一行来写了
        int ret = arr[head];
        ++head;
        return ret;
    }


    public boolean queue(int item) {
        // tail == limit 表示队列末尾没有空间了
        if (tail == limit) {
            // tail ==n && head==0,表示整个队列都占满了
            if (head == 0) {
                throw new RuntimeException("栈满了");
            }
            // 数据搬移
            for (int i = head; i < tail; ++i) {
                arr[i - head] = arr[i];
            }
            // 搬移完之后重新更新head和tail
            tail -= head;
            head = 0;
        }

        arr[tail] = item;
        ++tail;
        return true;
    }

    public static void main(String[] args) {
        ArrayQueue queue = new ArrayQueue(7);
        queue.queue(1);
        queue.queue(2);
        queue.queue(3);
        queue.queue(4);
        queue.queue(5);
        queue.queue(6);
        queue.queue(7);

        System.out.println(queue.dequeue());
        queue.queue(1);
        System.out.println(queue.dequeue());
        System.out.println(queue.dequeue());
        System.out.println(queue.dequeue());
        System.out.println(queue.dequeue());
        System.out.println(queue.dequeue());
        System.out.println(queue.dequeue());
        System.out.println(queue.dequeue());
        System.out.println(queue.dequeue());
        System.out.println(queue.dequeue());
        System.out.println(queue.dequeue());
    }
}

2.2、链式队列

基于链表的实现,我们同样需要连个指针:headtail。它们分别指向链表的第一个结点和最后一个结点。如图所示:入队时,tail.next = new Node(); tail = tail.next;出队时,head = head.next;

/**
 * @description: 链表实现队列
 * @author: erlang
 * @since: 2020-08-31 20:12
 */
public class LinkedListQueue<T> {

    public static class Node<T> {
        public T value;
        public Node<T> pre;
        public Node<T> next;

        public Node(T value) {
            this.value = value;
        }
    }

    public static class DoubleEndsQueue<T> {
        public Node<T> head;
        public Node<T> tail;

        public void addFromHead(T value) {
            Node<T> cur = new Node<>(value);
            if (head == null) {
                tail = cur;
            } else {
                cur.next = head;
                head.pre = cur;
            }
            head = cur;
        }

        public T popFromTail() {
            if (head == null) {
                return null;
            }
            Node<T> cur = tail;
            if (head == tail) {
                head = null;
                tail = null;
            } else {
                tail = tail.pre;
                cur.pre = null;
                tail.next = null;
            }
            return cur.value;
        }

        public boolean isEmpty() {
            return head == null;
        }
    }

    public DoubleEndsQueue<T> queue;
    public int count;
    public int limit;

    public LinkedListQueue(int limit) {
        queue = new DoubleEndsQueue<>();
        this.limit = limit;
        count = 0;
    }

    public void queue(T value) {
        if (limit == count) {
            throw new RuntimeException("栈满了");
        }
        count++;
        queue.addFromHead(value);
    }

    public T dequeue() {
        if (count == 0) {
            throw new RuntimeException("栈空了");
        }
        count--;
        return queue.popFromTail();
    }

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

    public static void main(String[] args) {
        LinkedListQueue<Integer> queue = new LinkedListQueue<>(7);
        queue.queue(1);
        queue.queue(2);
        queue.queue(3);
        queue.queue(4);
        queue.queue(5);
        queue.queue(6);
        queue.queue(7);
        //queue.push(8);

        System.out.println(queue.dequeue());
        System.out.println(queue.dequeue());
        System.out.println(queue.dequeue());
        System.out.println(queue.dequeue());
        System.out.println(queue.dequeue());
        System.out.println(queue.dequeue());
        System.out.println(queue.dequeue());
        System.out.println(queue.dequeue());

    }
}

2.3、循环队列

前面用数组实现队列的时候,在 tail 移动到最右边时,会有数据搬移操作,这样入队操作性能会受到影响。如何避免数据搬移操作?这就是循环队列的思想。

原本数组是有头有尾的,是一条直线,现在我们把收尾相连,形成一个环这就构成了我们所说的循环队列。循环队列,顾名思义,就是一个环状的可以周而复始。

通过这种方法,我们成功避免了数据搬移操作。具体代码如下:

 public static class RingArrayQueue {
        private final int[] arr;
        private int tail;
        private int head;
        private int size;
        private final int limit;

        public RingArrayQueue(int limit) {
            arr = new int[limit];
            tail = 0;
            size = 0;
            this.limit = limit;
        }

        public void queue(int value) {
            if (size == limit) {
                throw new RuntimeException("栈满了");
            }
            size++;
            arr[tail] = value;
            tail = nextIndex(tail);
        }

        public int dequeue() {
            if (size == 0) {
                throw new RuntimeException("栈空了");
            }
            size--;
            int ans = arr[head];
            head = nextIndex(head);
            return ans;
        }

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

        private int nextIndex(int index) {
            return index < limit - 1 ? index + 1 : 0;
        }

        public static void main(String[] args) {
            RingArrayQueue queue = new RingArrayQueue(7);
            queue.queue(1);
            queue.queue(2);
            queue.queue(3);
            queue.queue(4);
            queue.queue(5);
            queue.queue(6);
            queue.queue(7);

            System.out.println(queue.dequeue());
            System.out.println(queue.dequeue());
            System.out.println(queue.dequeue());
            System.out.println(queue.dequeue());
            System.out.println(queue.dequeue());
            System.out.println(queue.dequeue());
            System.out.println(queue.dequeue());
            System.out.println(queue.dequeue());
        }
    }

3、小结

队列最大的特点就是先进先出,主要的两个操作是入队和出队。跟栈一样,它既可以用数组来实现,也可以用链表来实现。用数组实现的叫顺序队列,用链表实现的叫链式队列。特是长得像环的循环队列。在数组实现队列的时候,会有数组搬移操作,要想解决数据搬移的问题,我们就需要像环一样的循环队列。