# 队列简介

队列是一种线性结构,相比数组,队列对应的操作是数组的子集,只能从一端(队尾)添加元素从另一端(队首)取出元素。队列是一种先进先出(First in First Out简写FIFO)的数据结构。

用我们实际生活的场景来举例的话:就比如我们在车站排队买票,前面的乘客买完票后依次从队首离开,后面的需要买票的乘客依次从队尾排队进入买票。

# 队列的基本实现

对于队列来说我们主要实现以下方法:

方法名 含义
void enqueue(E) 入队(向队列中添加元素)
E dequeue() 出队(从队列中取出元素)
E getFront() 查看队首元素
E getSize() 查看队列中一共有多少元素
boolean isEmpty() 队列是否为空

这里我们使用之前实现的数组类来实现队列这种数据结构,由于调用的都是之前数组类中的方法这里不再赘述,直接给出具体实现:

public interface Queue<E> {
    int getSize();
    boolean isEmpty();
    void enqueue(E e);
    E dequeue();
    E getFront();
}

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();
    }
    @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 int getCapacity() {
        return array.getCapacity();
    }
}

测试我们实现的方法:

public class Main {
    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);
            }
        }
    }
} 

# 复杂度分析

方法名 时间复杂度 备注
void enqueue(E e) O(1) 均摊时间复杂度
E dequeue() O(n)
int getSize() O(1)
boolean isEmpty() O(1)
E getFront() O(1)

出队操作的时间复杂度是O(n)级别的,这是因为上述队列我们是通过数组实现的,当我们出队的时候也就是 把数组中的第一个元素拿出的时候,数组中其余的元素都要向前挪一位,所以出队的时间复杂度是O(n)的。

# 循环队列

上面我们通过数组实现的队列在出队的时候时间复杂度是O(n)级别的,有没有办法让队列出对的时候时间复杂度也是O(1)呢?

前面我们已经说过,导致出队时间复杂度是O(n)级别的原因是因为把数组中的第一个元素拿出后,数组中其余的元素都要向前挪一位。那我们可不可以不让其他元素往前移呢?

我们可以使用一个变量来记录下队首记为front队尾记为tail,当队首元素出队时只需要让front向前移动一位指向新的队首即可。

当front==tail时表明队列是空的,当有一个元素入队,只需要tail往后挪一位即可,依次类推。当队首元素出队时,只需要front向后挪动即可,依次类推,这时出队的时间复杂度就变成了O(1)。当将tail移动到队尾的时候此时再有元素入队,由于队首有空出的位置,可以将tail移动到队首的位置进行元素添加,直到(tail+1)%capacity == front的时候表示对列满了,此时需要扩容(注:capacity表示数组的长度)。

入队操作

public void enqueue(E e) {
    // 判断队列是否满了,满了的话进行扩容
    if ((tail + 1) % data.length == front)
        resize(getCapacity() * 2);
    // 向队尾添加元素
    data[tail] = e;
    // 将tail向后移一位,因为是循环队列需要对data.length取余,构成循环
    tail = (tail + 1) % data.length;
    size++;
}

扩容操作

private void resize(int newCapacity) {
    // newCapacity + 1 有意识的浪费一个空间
    E[] newData = (E[]) new Object[newCapacity + 1];

    for (int i = 0; i < size; i++)
        // newData[i]对应的应该是data[i+front],对data.length取余是为了让队列循环起来
        newData[i] = data[(i + front) % data.length];

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

如图简单举例说下扩容操作,当i的取值是[0, 1]时newData[i]对应的是data[i+front]位置的元素,当 i= 2时newData[i]的取值对应的是data[(i+front) % 4]的值,由于data是一个循环队列newData[i]对应的值应该是data[(i+front) % 4]位置的值。

出队操作

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

    // 获取队首元素
    E ret = data[front];
    // 将队首位置的元素置空,便于垃圾回收
    data[front] = null;
    
    // 移动front,因为是循环队列,所以需要对data.length取余
    front = (front + 1) % data.length;
  
    size--;

    // 如果队列中的元素出队了容积的四分之一,这时队列应该进行缩容,注意:缩容的值不能是零
    if (size == getCapacity() / 4 && getCapacity() / 2 != 0)
        resize(getCapacity() / 2);

    return ret;
}

完整代码:

public class LoopQueue<E> implements Queue<E> {
    private E[] data;
    private int front, tail, size;

    public LoopQueue(int capacity) {
        // capacity + 1是为了有意识的浪费一个空间,防止队列满时tail == front
        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("Cannot dequeue from an empty queue.");

        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 [");
        // 循环队列中第一个元素在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<>();

        for (int i = 0; i < 10; i++) {
            queue.enqueue(i);

            System.out.println(queue);

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

# 复杂度分析

方法名 时间复杂度 备注
void enqueue(E e) O(1) 均摊时间复杂度
E dequeue() O(1) 均摊时间复杂度
int getSize() O(1)
boolean isEmpty() O(1)
E getFront() O(1)

# 数组队列和循环队列的比较

上面我们费了半天劲终于实现了循环队列,那它比咱们之前实现队列的方法性能真的有提高吗?下面我们来编码测试下:

import java.util.Random;

public class Main {
    // 测试使用q运行opCount个enqueueu和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");
    }
}

下图为运行上述测试代码结果截图:

可以看到使用循环队列从性能上确实得到了不少的提升。

使用size不浪费空间

浪费一个空间不使用size

# 双端队列

可以在队列的两端添加元素,也可以在队列的两端删除元素。

# 练习

前面我们所实现的栈和队列都是通过动态数组来实现的,假设没有动态数组这种数据结构,现在有一个封装好的栈数据结构,我们怎么使用栈这种数据结构实现队列呢?反过来,给了一个封装好的队列这种数据结构,我们怎么使用这种数据结构实现栈呢?

# 第一题

请你仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通队列的全部四种操作(push、top、pop 和 empty)。

实现 MyStack 类:

  • void push(int x) 将元素 x 压入栈顶。
  • int pop() 移除并返回栈顶元素。
  • int top() 返回栈顶元素。
  • boolean empty() 如果栈是空的,返回 true ;否则,返回 false 。

注意:

  • 你只能使用队列的基本操作 —— 也就是 push to back、peek/pop from front、size 和 is empty 这些操作。
  • 你所使用的语言也许不支持队列。 你可以使用 list (列表)或者 deque(双端队列)来模拟一个队列 , 只要是标准的队列操作即可。

示例:

输入:
["MyStack", "push", "push", "top", "pop", "empty"]
[[], [1], [2], [], [], []]
输出:
[null, null, null, 2, 2, false]

解释:
MyStack myStack = new MyStack();
myStack.push(1);
myStack.push(2);
myStack.top(); // 返回 2
myStack.pop(); // 返回 2
myStack.empty(); // 返回 False

提示:

  • 1 <= x <= 9
  • 最多调用100 次 push、pop、top 和 empty
  • 每次调用 pop 和 top 都保证栈不为空

来源:力扣(LeetCode) 链接:https://leetcode-cn.com/problems/implement-stack-using-queues 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

# 第二题

请你仅使用两个栈实现先入先出队列。队列应当支持一般队列的支持的所有操作(push、pop、peek、empty):

实现 MyQueue 类:

  • void push(int x) 将元素 x 推到队列的末尾
  • int pop() 从队列的开头移除并返回元素
  • int peek() 返回队列开头的元素
  • boolean empty() 如果队列为空,返回 true ;否则,返回 false

说明:

  • 你只能使用标准的栈操作 —— 也就是只有 push to top, peek/pop from top, size, 和 is empty 操作是合法的。
  • 你所使用的语言也许不支持栈。你可以使用 list 或者 deque(双端队列)来模拟一个栈,只要是标准的栈操作即可。

示例:

输入:
["MyQueue", "push", "push", "peek", "pop", "empty"]
[[], [1], [2], [], [], []]
输出:
[null, null, null, 1, 1, false]

解释:
MyQueue myQueue = new MyQueue();
myQueue.push(1); // queue is: [1]
myQueue.push(2); // queue is: [1, 2] (leftmost is front of the queue)
myQueue.peek(); // return 1
myQueue.pop(); // return 1, queue is [2]
myQueue.empty(); // return false

提示:

  • 1 <= x <= 9
  • 最多调用 100 次 push、pop、peek 和 empty
  • 假设所有操作都是有效的 (例如,一个空的队列不会调用 pop 或者 peek 操作)

来源:力扣(LeetCode) 链接:https://leetcode-cn.com/problems/implement-queue-using-stacks 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。