手写由动态数组实现的简易列队(循环队列)及原理分析

好多人都觉得为什么要自己写这样的数据结构,变成里面不是有吗?为什么要去写,有这个疑问,其实这个疑问这我的脑海中也存在了很长一段时间,本人是学习java编程的,直接看java的集合框架不行吗?这个时候如果你的水平到了还好。如果没有,你会发现你根本就理解不了编程语言里面数据结构,看了就忘掉了,也理解不了,学习了半个月编程里面的集合发现学不会,还要抱怨怎么可以这样,看了半个月都没有看懂,于是就放弃了。如果让我来分析缘由,那就是市面上已经发布的编程语言里面的数据结构(集合框架)都是王者水平(巅峰王者)的人写的,这样厉害的人写出的东西,你一个没有啥基础的编程人员(按照游戏段位排序:青铜),几天几个月就整的明明白白,如果这样都能会,只有一种可能,你是天才,不是普通人。
所以基于这样的分析,我们学习就是要先从没有进数据结构的大门到变得更加优秀(进军白银段位以致更高的水平),所以简易版的练习过程是在必行,脚踏实地,方是走过漫漫长路的捷径之路,于此,便联系了一些数据结构的简易版,今天分析由动态数组设计的循环队列。

循环队列

循环思维分析

在这里插入图片描述

首先队列是从队首出队,队尾进队,如图,队首为front,队尾为tail,当队列中一个元素没有的时候,front和tail都为0,当加入可a,front不变,tail+1,之后再加入b、c、d,tail又+3为4,设定capacity容器容量为8,当数组满时进行resize扩容(待优化)。

在这里插入图片描述
由于要设计循环队列,需要考虑三种情况:
1、队列为空时,fronttail都为0;
2、队列没有出队的情况下,与正常队列一样;
3、队列有出队情况,front下标进行了偏移,如上图所示,当然满了的时候不能是front
tail。要不无法判断队列为空和为满的情况,于此,在队列设计之初就将capacity大小+1,浪费一个空间,当然tail所在的位置正是这个为空的位置上,当队列满时,tail+1=front(当然这个条件有局限性,以为设计的数组并不是环状的数据结构),所以为了使tail+1能够转到front的位置,进行了取模设计,即(tail+1)%capacity=front,比如上图所示的情况,当tail下标到1时,为队列已满的情况,条件是(1+1)%8=2成立。
循环思维分析就到这里,接下来进行源码分析。

循环队列源码分析

public class LoopQueue<E> implements QueueDemo<E> {
    private E[] data;
    private int front, tail;
    private int size;
    public LoopQueue(int capacity) {
        data = (E[]) new Object[capacity + 1];
        front = 0;
        tail = 0;
        size = 0;
    }
    public LoopQueue() {
        this(10);
    }
    @Override
    public int getSize() {
        return size;
    }
    @Override
    public int getCapacity() {
        return data.length - 1;
    }
    @Override
    public boolean isEmpty() {
        return front == tail;
    }
    @Override
    public String toString() {
        StringBuilder res = new StringBuilder();
        res.append(String.format("Queue:size=%d , capacity= %d\n", size, getCapacity()));
        res.append("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();
    }

}

属性front(队首)、tail(队尾),data数组,size记录队列大小;这里的注意点就是数组在创建的时间要比原有的大小+1,一个空的位置,正是tail所在的位置。当然类中重写的toString有些复杂,下面会对取余思维进行在分析。

数组扩容resize方法分析

源码如下:

 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;
    }

resize方法:
1、创建新的数组,大小为newCapacity + 1,依然要留出一个空的位置;
2、将原来的数组的元素进行转移到新数组,这个时候就数组的下标就要进行取模获取下标了,而新数组按照正常的位置放置元素,接下来分析旧数组获取下标的思维图:
在这里插入图片描述
正常情况下,front没有出队过元素,front=0,那么newData[0] = data[(0 + 0) % 8];新数组的下标位置对应旧数组的下标位置都为0;但是,如果队列出队过,并且之前没有进行resize操作,那么front这个时候不在是0,比如现在是front=2,这个时候循环下标的话,从头front=2开始到转过一圈0的位置,以front=2, data[(i + front) % data.length],i=7(为for循环结束)时,
data[(7 + 2) % 8]=data[9%8]=data[1],正好转到tail的位置,将tail此时的空值以及从front到tail之间的所有值都放入了newData[]。
3、将newData指向data,front头进行赋值为0,从0开始,tail赋值为size(应该是0到size-1,size为空位置)

入队代码分析

代码如下:

  @Override
    public void enqueue(E e) {
        if((tail+1)%data.length==front)
            resize(2*getCapacity());
        data[tail]=e;
        tail=(tail+1)%data.length;
        size++;
    }

1、首先上面已经分析了循环队列为满的情况,(tail+1)%data.length==front,所以在队列进队前进行校验,如果条件成立,就进行扩容操作,扩容的大小是原来的真实能够存入元素大小的二倍(这里设计循环数组的时候有一个位置是空闲的),所以getCapacity()方法的实现data.length 要减1。扩容后的真正大小是2*data.length-1 。
getCapacity方法:

 @Override
    public int getCapacity() {
        return data.length - 1;
    }

2、队尾进行入队操作data[tail]=e
3、对tail的下标进行管理,如果front=0的话,tail+1即可,但是如果front不在是0了,比如为front=2的情况,就需要考虑循环的问题,所以在tail+1的基础上进行取余,即 tail=(tail+1)%data.length;赋值为tail
4、入队完元素需要size+1(size++)

出队源码分析

代码如下:

  @Override
    public E dequeue() {
        if (isEmpty())
            throw new IllegalArgumentException("Cannot dequeue from an empty queue");
        E datum = data[front];
        data[front] = null;
        front=(front+1)%data.length;
        size--;

        if (size == getCapacity() / 4 && getCapacity() / 2 != 0)
            resize(getCapacity() / 2);
        return datum;
    }

出队分析:
1、首先需要判断队列是否为空
2、出队从队首出,所以有E datum = data[front]
3、元素已经出队了,元素所在的引用设置为null
4、当出队以后,front为位置发生变化,即front+1,由于循环设计,所以到头以后还需要在转回去,需要取余,即 front=(front+1)%data.length;
5、去出元素,size-1(size–)
6、对于出队进行动态缩容,当一直出队,真实存放的元素数量为容量大小的四分之一时,且容量的二分之一不能为0时,进行缩容,大小为原来的二分之一
7、将出队的元素返回

查看队列头的元素

代码如下:

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

1、判断是否为空
2、将front为位置的元素返回

写博不易,关注博客了解最新原创内容

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值