数据结构之队列

“后进先出”,“先进先出“ 我们经常会听见这两句。然而这两句话代表了两种不同的数据结构,前者我们已经熟悉,接下来我们就探究一下后者。

要点

在这里插入图片描述

一、队列(以数组队列为例)

概念:
队列是一种先进先出(FIFO)的数据结构。
特点:
队列内的数据,先入队的取元素时先出去(参考下生活实例图理解)。
与栈的区别(模型)

表面区别
1 相同之处 :规定好首端尾端后,都是把数据添加到尾端(没有数据时默认首也是尾端理解)
2 不同之处: 一个从首端取数据,一个从尾端取数据,取数据位置不同。

生活实例图:
在这里插入图片描述

队列的设计

通常用户(面向客户端程序员)是不关心我们具体实现的细节的,他们关心的时具有队列的功能,能够使用队列,所以本队列就以动态数组为基础。

特有api

在这里插入图片描述
构建队列接口

package queue;

/**
 * Create by SunnyDay on 2019/02/06
 * <p>
 * 队列接口(注意java 原生的Queue接口继承了Collection接口)
 */
public interface Queue<E> {
    E dequeue();
    E getFront();
    int getSize();
    void enqueue(E e);
    boolean isEmpty();
}

数组队列的实现(自定义)

我们遵循队列的概念先进先出很容易就设计出两个主要的api----入队、出队。
1 入队元素添加队尾
2 出队移除首个元素,也就是第一个元素
于是如下就可以很简单的设计出基础的数组队列。

package queue;

import array.UseGeneric;

/**
 * Create by SunnyDay on 2019/02/06
 */
public class ArrayQueue<E> implements Queue<E> {

    private UseGeneric<E> arrayQueue ;

    public ArrayQueue(int capacity){
     arrayQueue = new UseGeneric<>(capacity);
    }

    public ArrayQueue(){
        arrayQueue = new UseGeneric<>();
    }
    /**
     * 出队
     */
    @Override
    public E dequeue() {
        return arrayQueue.removeFirst();
    }

    /**
     * 入队
     */
    @Override
    public void enqueue(E e) {
      arrayQueue.addLast(e);
    }

    /**
     * 查看队的首个元素
     * */
    @Override
    public E getFront() {
        return arrayQueue.getFirst();
    }

    /**
     * 当前队的大小
     * */
    @Override
    public int getSize() {
        return arrayQueue.getSize();
    }

    /**
     * 是否为空
     * */
    @Override
    public boolean isEmpty() {
        return arrayQueue.isEmpty();
    }

    /**
     * 队列容量
     * */
    public int getCapacity(){
        return arrayQueue.getCapacity();
    }

    /**
     *
     * 队列  toString 特有
     * */
    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append("Queue: ");
        sb.append("front[");
        for (int i = 0; i < arrayQueue.getSize(); i++) {
            sb.append(arrayQueue.get(i));
            if (i!=arrayQueue.getSize()-1){
                sb.append(", ");
            }
        }
        sb.append("] :tail");
        return sb.toString();
    }
}
分析:
一顿操作猛如虎,三下五除二就把简单的数组队列设计出来了,然而我们带分析分析我们设计的队列。

1 首先看看入队还好说,每次只添加队尾,对于入队操作的时间复杂度不大。
2 再次看看出队操作,每次移除队首元素,那么问题来了,移除一下,后面的所有元素都要向前移动,如果数量小还可以,当数量很大时,移除多次,这个操作量还是巨大的。这时我们对于数组队列要采取优化措施,于是我们引入循环队列。

二 、循环队列

通过上面的分析我们就大概了解数组队列的弊端,于是我们就构建循环队列。首先看设计思路图解:

在这里插入图片描述
在这里插入图片描述

通过图解我们大概了解了循环队列的实现思路然而我们还需注意:
1 空队列时我们规定 front=tail都指向索引为0的元素位置
2 然而当front=tail时还可能队列为满所以我们在容量上设计,使tail=front+1时队列为满(还有弊端,到下面解决,先留个坑。)

于是我们不在使用我们以前封装好的数组由于此处添加了front,tail成员我们重新建立个类LoopQueue(循环队列)
package queue;

/**
 * Create by SunnyDay on 2019/02/06
 * 循环队列(降低出队的时间复杂度)
 */
public class LoopQueue<E> implements Queue<E> {
    private E[] data;
    private int front;
    private int tail;
    private int size;

    public LoopQueue(int capacity) {
        //  多出来的一个容量(不使用) 便于维护循环队列
        data = (E[]) new Object[capacity + 1];
        // 初始化  (不做系统会帮你做)
        front = 0;
        tail = 0;
        size = 0;
    }

    public LoopQueue() {
        //  多出来的一个容量(不使用) 便于维护循环队列
        this(10);
    }
    }

以上我们设计了循环队列类,还是使用动态数组为底层封装,front为首元素指针,tail为末尾元素后一位置指针。(队列空默认front=tail=0)

重要方法入队enqueue(E e)

思路:判断队是否列满,满了扩容。反之元素入队
分析:上述我们考虑利用前面出队的空间得出tail = front+1为队列满,但是我们发现:
在这里插入图片描述

在这里插入图片描述

最终结论:
  • front = (tail+1)%data.length 来判断是否队列满
  • 当前front = front数值%data.length
  • 当前tail = tail数值%data.length

ps:学过安卓的可以以imageview显示四张图片为例,设定一个按钮,每次点击按钮切换一张图片,四张按顺序循环切换。自己动手推导一遍。(没搞过得自己搞个简单数组循环重复输出数组内的数字一样)

于是:
/**
     * 循环队列重要方法:入队
     */
    @Override
    public void enqueue(E e) {
        // 判断队列是否满了,满了就扩容
        if (front == (tail + 1) % data.length) {
            // 扩大为原来的2倍
            resize(getCapacity() * 2);
        }
        data[tail] = e; // 队列中添加元素
        tail = (tail + 1) % data.length;// 重置tail的大小(%是为了防止越界)
        size++;
    }
/**
     * 重置 容量(扩容)
     */
    private void resize(int newCapacity) {
        // 一样的道理,多申请个空间
        E[] newData = (E[]) new Object[newCapacity + 1];
        // 循环旧的数组进行
        for (int i = 0; i < size; i++) {
            // 原来的front并不一定在索引为0的位置,而是存在front+i的关系
            newData[i] = data[(front + i) % data.length];//防止越界(大小0 到 size-1 )
        }
        data = newData;
        front = 0;
        tail = size;
    }

可以看出满时我们先扩容再入队:
扩容注意循环遍历元素的坑( newData[i] = data[(front + i) % data.length])
扩容后进行入队

有了入队出队就好理解了

 /**
     * 循环队列重要方法: 出队
     */
    @Override
    public E dequeue() {
        if (isEmpty()) {
            throw new IllegalArgumentException("can not dequeue ,loopQueue is empty");
        }
        E temp = data[front];
        data[front] = null;// 从队列移除
        front = (front + 1) % data.length;// 重置front大小
        size--;
        // 缩容处理(缩小大小不能为0)
        if (size == getCapacity() / 4 && getCapacity() / 2 != 0) {
            resize(getCapacity() / 2);
        }
        return temp;
    }

LoopQueue完整方法如下:

package queue;

/**
 * Create by SunnyDay on 2019/02/06
 * 循环队列(降低出队的时间复杂度)
 */
public class LoopQueue<E> implements Queue<E> {
    private E[] data;
    private int front;
    private int 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;
    }

    /**
     * 循环队列是否为空
     */
    public boolean isEmpty() {
        return front == tail;
    }

    /**
     * 队列大小
     */
    @Override
    public int getSize() {
        return size;
    }

    /**
     * 循环队列重要方法: 出队
     */
    @Override
    public E dequeue() {
        if (isEmpty()) {
            throw new IllegalArgumentException("can not dequeue ,loopQueue is empty");
        }
        E temp = data[front];
        data[front] = null;// 从队列移除
        front = (front + 1) % data.length;// 重置front大小
        size--;
        // 缩容处理(缩小大小不能为0)
        if (size == getCapacity() / 4 && getCapacity() / 2 != 0) {
            resize(getCapacity() / 2);
        }
        return temp;
    }

    /**
     * 循环队列重要方法:入队
     */
    @Override
    public void enqueue(E e) {
        // 判断队列是否满了,满了就扩容
        if (front == (tail + 1) % data.length) {
            // 扩大为原来的2倍
            resize(getCapacity() * 2);
        }
        data[tail] = e; // 队列中添加元素
        tail = (tail + 1) % data.length;// 重置tail的大小(%是为了防止越界)
        size++;
    }


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

    /**
     * 重置 容量(扩容)
     */
    private void resize(int newCapacity) {
        // 一样的道理,多申请个空间
        E[] newData = (E[]) new Object[newCapacity + 1];
        // 循环旧的数组进行
        for (int i = 0; i < size; i++) {
            // 原来的front并不一定在索引为0的位置,而是存在front+i的关系
            newData[i] = data[(front + i) % data.length];//防止越界(大小0 到 size-1 )
        }
        data = newData;
        front = 0;
        tail = size;
    }

    @Override
    public String toString() {
        //  System.out.println(Arrays.toString(data)); 默认会把没有的元素默认为0

        // 自定义 封装
        StringBuilder sb = new StringBuilder();
        sb.append(String.format("Queue size =  %d ,capacity = %d\n", size, getCapacity()));
        sb.append("front[");
        // 数字中的有效元素遍历
        for (int i = front; i != tail; i = (i + 1) % data.length) {
            sb.append(data[i]);
            // 不是最后一个元素时拼接
            if ((i + 1) % data.length != tail) {
                sb.append(",");
            }
        }
        sb.append("]tail");
        return sb.toString();
    }
    // TODO  toString 与 resize 使用了两种不同的遍历方式
}

优点(出队比较):

我自己列举了一些数据测试,普通队列耗时大约8秒,循环队列大约耗时0.06秒差距一目了然。

小结:

本文队列的设计主要难点为细节的掌握,循环队列的一些细节推导我们掌握了,就很容易理解了,其次理解后我们多动手搞几遍就熟悉了。

The end

细节决定成败 态度决定高度

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值