《恋上数据结构第1季》队列、双端队列、循环队列、循环双端队列

简介: 《恋上数据结构第1季》队列、双端队列、循环队列、循环双端队列

我的《恋上数据结构》源码(第1季 + 第2季):https://github.com/szluyu99/Data_Structure_Note

队列 Queue

队列是一种特殊的线性表,只能在头尾两端进行操作;

  • 队尾(rear):只能从队尾添加元素,一般叫做 enQueue入队
  • 队头(front):只能从队头移除元素,一般叫做 deQueue出队
  • 先进先出的原则,First In First Out,FIFO

在这里插入图片描述

队列的接口设计

int size(); // 元素的数量
boolean isEmpty(); // 是否为空
void clear(); // 清空
void enQueue(E element); // 入队
E deQueue(); // 出队
E front(); // 获取队列的头元素

队列的内部实现是否可以直接利用以前学过的数据结构?

  • 动态数组、链表;
  • 优先使用双向链表,因为队列主要是往头尾操作元素;

队列源码

/**
 * 队列
 * @author yusael
 */
public class Queue <E>{
    private List<E> list = new LinkedList<>();
    
    /**
     * 入队
     */
    public void enQueue(E element){
        list.add(element);
    }
    /**
     * 出队
     */
    public E deQueue(){
        return list.remove(0);
    }
    /**
     * 元素的数量
     */
    public int size(){
        return list.size();
    }
    /**
     * 清空
     */
    public void clear(){
        list.clear();
    }
    /**
     * 队头元素
     */
    public E top(){
        return list.get(0);
    }
    /**
     * 是否为空
     */
    public boolean isEmpty(){
        return list.isEmpty();
    }
    
}

双端队列 Deque

双端队列是能在头尾两端添加、删除的队列;

  • 英文 deque 是 double ended queue 的简称;

双端队列接口设计

int size(); // 元素的数量
boolean isEmpty(); // 是否为空
void clear(); // 清空
void enQueueRear(E element); // 从队尾入队
E deQueueFront(); // 从队头出队
void enQueueFront(E element); // 从队头入队
E deQueueRear(); // 从队尾出队
E front(); // 获取队列的头元素
E rear(); // 获取队列的尾元素

双端队列源码

/**
 * 双端队列
 * @author yusael
 */
public class DeQueue <E> {
    // 双向链表实现双端队列
    private List<E> list = new LinkedList<>();
    /**
     * 元素的数量
     */
    public int size(){
        return list.size();
    }
    /**
     * 是否为空
     */
    public boolean isEmpty(){
        return list.isEmpty();
    }
    /**
     * 清空
     */
    public void clear(){
        list.clear();
    }
    /**
     * 从队尾入队
     */
    public void enQueueRear(E element){
        list.add(element);
    }
    /**
     * 从队头入队
     */
    public void enQueueFront(E element){
        list.add(0, element);
    }
    /**
     * 从队尾出队
     */
    public E deQueueRear(){
        return list.remove(list.size() - 1);
    }
    /**
     * 从队头出队
     */
    public E deQueueFront(){
        return list.remove(0);
    }
    /**
     * 获取队列的头元素
     */
    public E front(){
        return list.get(0);
    }
    /**
     * 获取队里的尾元素
     */
    public E rear(){
        return list.get(list.size() - 1);
    }
    
}

循环队列 Circle Queue

其实队列底层也可以使用动态数组实现,并且各项接口也可以优化到 O(1) 的时间复杂度;

  • 这个用数组实现并且优化之后的队列也叫做:循环队列

在这里插入图片描述

循环队列实现

/**
 * 循环队列
 * @author yusael
 */
@SuppressWarnings("unchecked")
public class CircleQueue<E> {
    private int front; // 队头指针
    private int size; // 元素数量
    // 利用动态扩容数组实现的循环队列
    private E elements[]; // 元素
    public static final int DEFAULT_CAPACITY = 10; // 初始容量

    public CircleQueue() {
        elements = (E[]) new Object[DEFAULT_CAPACITY];
    }

    /**
     * 元素的数量
     */
    public int size() {
        return size;
    }

    /**
     * 是否为空
     */
    public boolean isEmpty() {
        return size == 0;
    }

    /**
     * 清空
     */
    public void clear() {
        for (int i = 0; i < size; i++) {
            // elements[index(i)] = null;
            elements[(i + front) %elements.length] = null;
        }
        size = 0;
        front = 0;
    }

    /**
     * 从队头出队
     */
    public E deQueue() {
        E fronElement = elements[front];
        elements[front] = null;
        front = (front + 1) % elements.length;
        // front = index(1);
        size--;
        return fronElement;
    }

    /**
     * 从队尾入队
     */
    public void enQueue(E element) {
        // 扩容
        ensureCapacity(size + 1);
        elements[(front + size) % elements.length] = element;
        // elements[index(size)] = element;
        size++;
    }

    /**
     * 获取队列的头元素
     */
    public E front() {
        return elements[front];
    }

    // 扩容
    private void ensureCapacity(int capacity) {
        int oldCapacity = elements.length;
        if (oldCapacity >= capacity)
            return;
        // 新容量为旧容量的 1.5 倍
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        E[] newElements = (E[]) new Object[newCapacity];
        for (int i = 0; i < size; i++) { // 旧数组中元素移到新数组
            newElements[i] = elements[(i + front) % elements.length];
            // newElements[i] = elements[index(i)];
        }
        System.out.println("从" + oldCapacity + "扩容到" + newCapacity);
        elements = newElements;
        front = 0; // 重置front
    }

}

索引映射封装

可以发现循环队列中经常有 (front + size) % elements.length 的操作,那是因为如果 front 在队尾了,而又要往后移则会回到开头,该代码就是根据 front 的真实索引计算出在循环队列上的索引。

我们可以将这个封装为一个方法,实际上这个写法使用 % 运算符,性能十分低,后面会对此做优化。

// 将front真实索引转换为循环队列上的索引
private int index(int index) {
     return (front + index) % elements.length;
}

则循环队列中的其他方法可以修改为如下:

/**
 * 清空
 */
public void clear() {
    for (int i = 0; i < size; i++) {
        elements[index(i)] = null;
    }
    size = 0;
    front = 0;
}
/**
 * 从队头出队
 */
public E deQueue() {
    E fronElement = elements[front];
    elements[front] = null;
    front = index(1);
    size--;
    return fronElement;
}
/**
 * 从队尾入队
 */
public void enQueue(E element) {
    // 扩容
    ensureCapacity(size + 1);
    elements[index(size)] = element;
    size++;
}

循环队列 – %运算符优化

尽量避免使用 */%浮点数运算,效率低下;

原理:已知 n >= 0,m > 0:

  • n % m 等价于 n – (m > n ? 0 : m)

前提条件:n < 2m

由于我们已经封装了索引映射的方法,%运算符优化只需要修改 index 方法即可:

// 将真实索引转换为循环队列上的索引
private int index(int index) {
    // 10%8 = 2 10-8=2
    // 10%11 = 10 10
    index += front;
    return index - ((index >= elements.length) ? elements.length : 0);
}

完整源码

/**
 * 循环队列
 * @author yusael
 */
@SuppressWarnings("unchecked")
public class CircleQueue<E> {
    private int front; // 队头指针
    private int size; // 元素数量
    // 利用动态扩容数组实现的循环队列
    private E elements[]; // 元素
    public static final int DEFAULT_CAPACITY = 10; // 初始容量

    public CircleQueue() {
        elements = (E[]) new Object[DEFAULT_CAPACITY];
    }

    /**
     * 元素的数量
     */
    public int size() {
        return size;
    }

    /**
     * 是否为空
     */
    public boolean isEmpty() {
        return size == 0;
    }

    /**
     * 清空
     */
    public void clear() {
        // while(size >= 0){
        // elements[(front+size)%elements.length] = null;
        // size--;
        // }
        for (int i = 0; i < size; i++) {
            elements[index(i)] = null;
        }
        size = 0;
        front = 0;
    }

    /**
     * 从队头出队
     */
    public E deQueue() {
        E fronElement = elements[front];
        elements[front] = null;
        // front = (front + 1) %elements.length;
        front = index(1);
        size--;
        return fronElement;
    }

    /**
     * 从队尾入队
     */
    public void enQueue(E element) {
        // 扩容
        ensureCapacity(size + 1);
        // elements[(front + size) % elements.length] = element;
        elements[index(size)] = element;
        size++;
    }

    /**
     * 获取队列的头元素
     */
    public E front() {
        return elements[front];
    }

    // 将真实索引转换为循环队列上的索引
    private int index(int index) {
        // 10%8 = 2 10-8=2
        // 10%11 = 10 10
//        return (front + index) % elements.length;
        index += front;
        return index - ((index >= elements.length) ? elements.length : 0);
    }

    // 扩容
    private void ensureCapacity(int capacity) {
        int oldCapacity = elements.length;
        if (oldCapacity >= capacity)
            return;
        // 新容量为旧容量的 1.5 倍
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        E[] newElements = (E[]) new Object[newCapacity];
        for (int i = 0; i < size; i++) { // 旧数组中元素移到新数组
            //     newElements[i] = elements[(i + front) % elements.length];
            newElements[i] = elements[index(i)];
        }
        System.out.println("从" + oldCapacity + "扩容到" + newCapacity);
        elements = newElements;
        front = 0; // 重置front
    }

    @Override
    public String toString() {
        StringBuilder string = new StringBuilder();
        string.append("capcacity=").append(elements.length).append(" size=").append(size).append(" front=")
                .append(front).append(", [");
        for (int i = 0; i < elements.length; i++) {
            if (i != 0) {
                string.append(", ");
            }
            string.append(elements[i]);
        }
        string.append("]");
        return string.toString();
    }

}

循环队列测试

public static void main(String[] args) {
    CircleQueue<Integer> queue = new CircleQueue<Integer>();
    // 0 1 2 3 4 5 6 7 8 9
    for (int i = 0; i < 10; i++) {
        queue.enQueue(i);
    }
    // null null null null null 5 6 7 8 9
    for (int i = 0; i < 5; i++) {
        queue.deQueue();
    }
    // 15 16 17 18 19 f[5] 6 7 8 9
    for (int i = 15; i < 30; i++) {
        queue.enQueue(i);
    }
//        while (!queue.isEmpty()) {
//            System.out.println(queue.deQueue());
//        }
//        queue.clear();
    System.out.println(queue);
}
从10扩容到15
从15扩容到22
capcacity=22 size=20 front=0, [5, 6, 7, 8, 9, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, null, null]

循环双端队列 Circle Dequeue

循环双端队列:可以进行两端添加、删除操作循环队列

循环队列中用了 front 指针来表示队列的头部,双端循环队列是否需要再使用一个 rear 指针来表示队列的尾部?

  • 不需要,只要有了头指针便可以算出尾部

首先理解一下循环双端队列中索引封装映射

  • 传入的 index 是相对于 front 的索引,返回的是真实的索引:

比如要获得 头部指针 的前一位,则是 index(-1)(用于队头入队)
比如要获得 尾部指针,则是 index(size - 1)

private int index(int index) {
    index += front;
    if (index < 0) { // index 为负数
        return index + elements.length;
    }
    // index 为正数
    return index % elements.length;
}

循环双端队列实现

package com.mj.circle;

/**
 * 循环双端队列
 * @author yusael
 */
@SuppressWarnings("unchecked")
public class CircleDeque<E> {
    private int front; // 队头指针
    private int size; // 元素数量
    private E elements[]; // 元素
    public static final int DEFAULT_CAPACITY = 10; // 初始容量

    public CircleDeque() {
        elements = (E[]) new Object[DEFAULT_CAPACITY];
    }

    /**
     * 元素的数量
     */
    public int size() {
        return size;
    }

    /**
     * 是否为空
     */
    public boolean isEmpty() {
        return size == 0;
    }

    /**
     * 清空
     */
    public void clear() {
        for (int i = 0; i < size; i++) {
            elements[index(i)] = null;
        }
        front = 0;
        size = 0;
    }

    /**
     * 从队尾入队
     */
    public void enQueueRear(E element) {
        // 头 1 r(2) null null null f(6) 7 8 9 尾
        ensureCapacity(size + 1);

        elements[index(size)] = element;
        size++;
    }

    /**
     * 从队头入队
     */
    public void enQueueFront(E element) {
        ensureCapacity(size + 1);

        front = index(-1);
        elements[front] = element;
        size++;
    }

    /**
     * 从队尾出队
     */
    public E deQueueRear() {
        int rearIndex = index(size - 1);
        E rear = elements[rearIndex];
        elements[rearIndex] = null;
        size--;
        return rear;
    }

    /**
     * 从队头出队
     */
    // 头 1 r(2) null null f(5) 6 7 8 9 尾
    public E deQueueFront() {
        E frontElement = elements[front];
        elements[front] = null;
        front = index(1);
        size--;
        return frontElement;
    }

    /**
     * 获取队列的头元素
     */
    public E front() {
        return elements[front];
    }

    /**
     * 获取队列的尾元素
     */
    public E rear() {
        return elements[index(size - 1)];
    }

    // 索引封装映射
    private int index(int index) {
        index += front;
        if (index < 0) { // index 为负数
            return index + elements.length;
        }
        // index 为正数
        return index % elements.length;
    }

    // 数组扩容
    private void ensureCapacity(int capacity) {
        int oldCapacity = elements.length;
        if (oldCapacity >= capacity)
            return;

        int newCapacity = oldCapacity + (oldCapacity >> 1); // 扩容为1.5倍
        E newElements[] = (E[]) new Object[newCapacity];
        for (int i = 0; i < size; i++) {
            newElements[i] = elements[index(i)];
        }
        elements = newElements;
        front = 0; // 重置front
    }
}

循环双端队列 – %运算符优化

尽量避免使用 */%浮点数运算,效率低下;

原理:已知 n >= 0,m > 0:

  • n % m 等价于 n – (m > n ? 0 : m)

前提条件:n < 2m

由于我们已经封装了索引映射的方法,%运算符优化只需要修改 index 方法即可:

// 索引封装映射
private int index(int index) {
    index += front;
    if (index < 0) { // index 为负数
        return index + elements.length;
    }
    // index 为正数
    return index % elements.length;
}

完整源码

package com.mj.circle;

/**
 * 循环双端队列
 * @author yusael
 */
@SuppressWarnings("unchecked")
public class CircleDeque <E> {    
    private int front; // 队头指针
    private int size;  // 元素数量
    private E elements[]; // 元素
    public static final int DEFAULT_CAPACITY = 10; // 初始容量

    public CircleDeque() {
        elements = (E[]) new Object[DEFAULT_CAPACITY];
    }
    /**
     * 元素的数量
     */
    public int size(){
        return size;
    }
    /**
     * 是否为空
     */
    public boolean isEmpty(){
        return size == 0;
    }
    /**
     * 清空
     */
    public void clear(){
        for(int i = 0; i < size; i++){
            elements[index(i)] = null;
        }
        front = 0;
        size = 0;
    }
    /**
     * 从队尾入队
     */
    public void enQueueRear(E element){
        // 头 1 r(2) null null null f(6) 7 8 9  尾
         ensureCapacity(size + 1);
         
        elements[index(size)] = element;
        size++;
    }
    /**
     * 从队头入队
     */
    public void enQueueFront(E element){
        ensureCapacity(size + 1);
        
        /*if(front - 1 < 0){
            front += elements.length;
        }
        front = front - 1;
        elements[front-1] = element;*/
        
        front = index(-1);
        elements[front] = element;
        size++;
    }
    /**
     * 从队尾出队
     */
    public E deQueueRear(){
        E rearElement = elements[(front+size-1)%elements.length];
        elements[(front+size-1)%elements.length] = null;
        size--;
        return rearElement;
    }
    /**
     * 从队头出队
     */
    // 头 1 r(2) null null f(5) 6 7 8 9  尾
    public E deQueueFront() {
        E frontElement = elements[front];
        elements[front] = null;
        front = index(1);
        size--;
        return frontElement;
    }
    /**
     * 获取队列的头元素
     */
    public E front(){
        return elements[front];
    }
    /**
     * 获取队列的尾元素
     */
    public E rear(){
        return elements[index(size - 1)];
    }
    // 索引封装映射
    private int index(int index) {
        index += front;
        if (index < 0) {
            return index + elements.length;
        }
        return index - ((index >= elements.length) ? elements.length : 0);
    }
    // 数组扩容
    private void ensureCapacity(int capacity){
        int oldCapacity = elements.length;
        if(oldCapacity >= capacity) return;
        
        int newCapacity = oldCapacity + (oldCapacity >> 1); // 扩容为1.5倍
        E newElements[] = (E[]) new Object[newCapacity];
        for(int i = 0; i < size; i++){
            newElements[i] = elements[index(i)];
        }
        elements = newElements;
        front = 0; // 重置front
    }
    @Override
    public String toString() {
        StringBuilder string = new StringBuilder();
        string.append("capcacity=").append(elements.length)
        .append(" size=").append(size)
        .append(" front=").append(front)
        .append(", [");
        for (int i = 0; i < elements.length; i++) {
            if (i != 0) {
                string.append(", ");
            }
            
            string.append(elements[i]);
        }
        string.append("]");
        return string.toString();
    }
}

循环双端队列测试

public static void main(String[] args) {
    CircleDeque<Integer> queue = new CircleDeque<>();
    // 头5 4 3 2 1  100 101 102 103 104 105 106 8 7 6 尾
    
    // 头 8 7 6  5 4 3 2 1  100 101 102 103 104 105 106 107 108 109 null null 10 9 尾
    for (int i = 0; i < 10; i++) {
        queue.enQueueFront(i + 1);
        queue.enQueueRear(i + 100);
    }
    
    // 头 null 7 6  5 4 3 2 1  100 101 102 103 104 105 106 null null null null null null null 尾
    for (int i = 0; i < 3; i++) {
        queue.deQueueFront();
        queue.deQueueRear();
    }
    
    // 头 11 7 6  5 4 3 2 1  100 101 102 103 104 105 106 null null null null null null 12 尾
    queue.enQueueFront(11);
    queue.enQueueFront(12);
    System.out.println(queue);
//        while (!queue.isEmpty()) {
//            System.out.println(queue.deQueueFront());
//        }
}
capcacity=22 size=16 front=21, [11, 7, 6, 5, 4, 3, 2, 1, 100, 101, 102, 103, 104, 105, 106, null, null, null, null, null, null, 12]

练习

用栈实现队列

232_用栈实现队列:https://leetcode-cn.com/problems/implement-queue-using-stacks/
在这里插入图片描述
准备2个栈:inStack、outStack

  • 入队时,push 到 inStack 中
  • 出队时

如果 outStack 为空,将 inStack 所有元素逐一弹出,push 到 outStack,outStack 弹出栈顶元素
如果 outStack 不为空, outStack 弹出栈顶元素

/**
 * Your MyQueue object will be instantiated and called as such:
 * MyQueue obj = new MyQueue();
 * obj.push(x);
 * int param_2 = obj.pop();
 * int param_3 = obj.peek();
 * boolean param_4 = obj.empty();
 */
 
public class MyQueue {
    private Stack<Integer> inStack = new Stack<>();
    private Stack<Integer> outStack = new Stack<>();
    
   /** Initialize your data structure here. */
    public MyQueue() {
        
    }
    
    /** Push element x to the back of queue. */
    public void push(int x) {
        inStack.push(x);
    }
    
    /** Removes the element from in front of queue and returns that element. */
    public int pop() {
        if(outStack.isEmpty()){ // 右栈为空,则先全部放进右栈
            while(!inStack.isEmpty()){
                outStack.push(inStack.pop());
            }
        }
        return outStack.pop();
    }
    
    /** Get the front element. */
    public int peek() {
        if(outStack.isEmpty()){ // 右栈为空,则先全部放进右栈
            while(!inStack.isEmpty()){
                outStack.push(inStack.pop());
            }
        }
        return outStack.peek();
    }
    
    /** Returns whether the queue is empty. */
    public boolean empty() {
        return inStack.isEmpty() && outStack.isEmpty();
    }
}
相关文章
|
4天前
|
存储 Java
【数据结构】优先级队列(堆)从实现到应用详解
本文介绍了优先级队列的概念及其底层数据结构——堆。优先级队列根据元素的优先级而非插入顺序进行出队操作。JDK1.8中的`PriorityQueue`使用堆实现,堆分为大根堆和小根堆。大根堆中每个节点的值都不小于其子节点的值,小根堆则相反。文章详细讲解了如何通过数组模拟实现堆,并提供了创建、插入、删除以及获取堆顶元素的具体步骤。此外,还介绍了堆排序及解决Top K问题的应用,并展示了Java中`PriorityQueue`的基本用法和注意事项。
18 5
【数据结构】优先级队列(堆)从实现到应用详解
|
11天前
|
存储 C语言
数据结构基础详解(C语言): 栈与队列的详解附完整代码
栈是一种仅允许在一端进行插入和删除操作的线性表,常用于解决括号匹配、函数调用等问题。栈分为顺序栈和链栈,顺序栈使用数组存储,链栈基于单链表实现。栈的主要操作包括初始化、销毁、入栈、出栈等。栈的应用广泛,如表达式求值、递归等场景。栈的顺序存储结构由数组和栈顶指针构成,链栈则基于单链表的头插法实现。
|
13天前
|
Java
【数据结构】栈和队列的深度探索,从实现到应用详解
本文介绍了栈和队列这两种数据结构。栈是一种后进先出(LIFO)的数据结构,元素只能从栈顶进行插入和删除。栈的基本操作包括压栈、出栈、获取栈顶元素、判断是否为空及获取栈的大小。栈可以通过数组或链表实现,并可用于将递归转化为循环。队列则是一种先进先出(FIFO)的数据结构,元素只能从队尾插入,从队首移除。队列的基本操作包括入队、出队、获取队首元素、判断是否为空及获取队列大小。队列可通过双向链表或数组实现。此外,双端队列(Deque)支持两端插入和删除元素,提供了更丰富的操作。
14 0
【数据结构】栈和队列的深度探索,从实现到应用详解
|
28天前
|
机器学习/深度学习 消息中间件 缓存
栈与队列的实现
栈与队列的实现
37 0
|
1月前
|
测试技术
【初阶数据结构篇】队列的实现(赋源码)
首先队列和栈一样,不能进行遍历和随机访问,必须将队头出数据才能访问下一个,这样遍历求个数是不规范的。
|
1月前
|
算法
【数据结构与算法】优先级队列
【数据结构与算法】优先级队列
11 0
|
1月前
|
算法
【数据结构与算法】循环队列
【数据结构与算法】循环队列
16 0
|
9天前
|
存储 人工智能 C语言
数据结构基础详解(C语言): 栈的括号匹配(实战)与栈的表达式求值&&特殊矩阵的压缩存储
本文首先介绍了栈的应用之一——括号匹配,利用栈的特性实现左右括号的匹配检测。接着详细描述了南京理工大学的一道编程题,要求判断输入字符串中的括号是否正确匹配,并给出了完整的代码示例。此外,还探讨了栈在表达式求值中的应用,包括中缀、后缀和前缀表达式的转换与计算方法。最后,文章介绍了矩阵的压缩存储技术,涵盖对称矩阵、三角矩阵及稀疏矩阵的不同压缩存储策略,提高存储效率。
|
1月前
栈的几个经典应用,真的绝了
文章总结了栈的几个经典应用场景,包括使用两个栈来实现队列的功能以及利用栈进行对称匹配,并通过LeetCode上的题目示例展示了栈在实际问题中的应用。
栈的几个经典应用,真的绝了
|
17天前
|
Linux C++ Windows
栈对象返回的问题 RVO / NRVO
具名返回值优化((Name)Return Value Optimization,(N)RVO)是一种优化机制,在函数返回对象时,通过减少临时对象的构造、复制构造及析构调用次数来降低开销。在C++中,通过直接在返回位置构造对象并利用隐藏参数传递地址,可避免不必要的复制操作。然而,Windows和Linux上的RVO与NRVO实现有所不同,且接收栈对象的方式也会影响优化效果。