《恋上数据结构第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();
    }
}
相关文章
|
19天前
|
C语言
【数据结构】栈和队列(c语言实现)(附源码)
本文介绍了栈和队列两种数据结构。栈是一种只能在一端进行插入和删除操作的线性表,遵循“先进后出”原则;队列则在一端插入、另一端删除,遵循“先进先出”原则。文章详细讲解了栈和队列的结构定义、方法声明及实现,并提供了完整的代码示例。栈和队列在实际应用中非常广泛,如二叉树的层序遍历和快速排序的非递归实现等。
98 9
|
22天前
|
算法 安全 NoSQL
2024重生之回溯数据结构与算法系列学习之栈和队列精题汇总(10)【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丢脸好嘛?】
数据结构王道第3章之IKUN和I原达人之数据结构与算法系列学习栈与队列精题详解、数据结构、C++、排序算法、java、动态规划你个小黑子;这都学不会;能不能不要给我家鸽鸽丢脸啊~除了会黑我家鸽鸽还会干嘛?!!!
|
1月前
初步认识栈和队列
初步认识栈和队列
60 10
|
1月前
|
存储 算法 定位技术
数据结构与算法学习二、稀疏数组与队列,数组模拟队列,模拟环形队列
这篇文章主要介绍了稀疏数组和队列的概念、应用实例以及如何使用数组模拟队列和环形队列的实现方法。
21 0
数据结构与算法学习二、稀疏数组与队列,数组模拟队列,模拟环形队列
|
1月前
【数据结构】-- 栈和队列
【数据结构】-- 栈和队列
17 0
|
1月前
探索数据结构:队列的的实现与应用
探索数据结构:队列的的实现与应用
|
1月前
|
存储 C语言
栈和队列题目练习
栈和队列题目练习
18 0
|
10天前
|
存储 算法
非递归实现后序遍历时,如何避免栈溢出?
后序遍历的递归实现和非递归实现各有优缺点,在实际应用中需要根据具体的问题需求、二叉树的特点以及性能和空间的限制等因素来选择合适的实现方式。
19 1
|
13天前
|
存储 算法 Java
数据结构的栈
栈作为一种简单而高效的数据结构,在计算机科学和软件开发中有着广泛的应用。通过合理地使用栈,可以有效地解决许多与数据存储和操作相关的问题。
|
16天前
|
存储 JavaScript 前端开发
执行上下文和执行栈
执行上下文是JavaScript运行代码时的环境,每个执行上下文都有自己的变量对象、作用域链和this值。执行栈用于管理函数调用,每当调用一个函数,就会在栈中添加一个新的执行上下文。

热门文章

最新文章