好多人都觉得为什么要自己写这样的数据结构,变成里面不是有吗?为什么要去写,有这个疑问,其实这个疑问这我的脑海中也存在了很长一段时间,本人是学习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下标进行了偏移,如上图所示,当然满了的时候不能是fronttail。要不无法判断队列为空和为满的情况,于此,在队列设计之初就将capacity大小+1,浪费一个空间,当然tail所在的位置正是这个为空的位置上,当队列满时,tail+1=front(当然这个条件有局限性,以为设计的数组并不是环状的数据结构),所以为了使tail+1能够转到front的位置,进行了取模设计,即(tail+1)%capacity=front,比如上图所示的情况,当tail下标到1时,为队列已满的情况,条件是(1+1)%8=2成立。
循环思维分析就到这里,接下来进行源码分析。
public class LoopQueue implements QueueDemo {
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有些复杂,下面会对取余思维进行在分析。
源码如下:
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为位置的元素返回
写博不易,关注博客了解最新原创内容