优雅,实在是太优雅了
能把复杂的东西简单化就是功底。
我为何有如此感慨,了解ArrayDeque的实现你就知道,今天我们要讲的是以栈为思想而实现的ArrayDeque,我们都知道栈是先进后出,和队列相反,如下图,先往数组中依次放入,1,2,3,5,那么在取出时依次取出5,3,2,1
在数组存储时,采取倒放方式,然后定义索引head则可轻易获取当前要出队或要存储的位置,如,默认数组长度为16,先存储1,则会进行计算得到15,head=15,此时数组如下:
[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,1]
再存储个2,经过计算head=14,那么此时数组存储如下
[null,null,null,null,null,null,null,null,null,null,null,null,null,null,2,1]
在取出时则可通过head的值取出数组数据,直接就是先进后出
你可能看出来存储最重要的就是动态的计算出每次小标,如扩容后的呀,删除后的呀。。等等
那么先来准备工作,定义如下全局变量
// 存储数据对象
transient Object[] elements;
// 头部索引
transient private int head;
// 尾部索引,双端队列
transient private int tail;
public ArrayDeque() {
elements = new Object[16];
}
1.入栈
进来看代码你会发现,很简单把,就几行代码,主要进来计算要存储的下标,计算完毕把当前下标赋值给head(留下次使用,取出数据也可使用),并把当前元素赋值到当前下标元素里,如果正常的话我说的这句化是三行代码,人家一行搞定,都不多废一行代码,哈哈
计算公式是通过与运算,&数组长度-1
第一个数据存储时head为0,0-1=-1,-1的二进制为11111111,16-1=15,15的二进制为 00001111
& 11111111
00001111
= 00001111
根据与运算规则只有两个数都得1才为1,所以-1&15就等于15
第一个数据存储在索引15的位置上,并把head置为15
14 00001110
15 00001111
= 00001110 =14
第二个数据存储时则为15-1=14,16-1=15,14&15=14
第二个数据存储在索引14的位置上,并把head置为14
以此类推,全部存储完毕head就等于0,此时对比head和tail,tail也等于0,需要扩容
// 相当于ArrayDeque的push、addFirst、offerFirst
@Override
public void push(E e) {
// 这样就可以从后存入了
//head=0-1 ,-1的二进制为:11111111
//16-1 =15 ,15的二进制为:00001111
// &:00001111 计算完还是15,此时head=15
// 14 14/2=7余0,7/2=3余1,3/2=1余1 1/2=0余1 1110
// 14的二进制和15的二进制比还是14,利用这样的去计算下标
elements[head = (head - 1) & (elements.length - 1)] = e;
System.out.println("push.idx head:" + head);
// head计算完就为15了,再存储一个就为14,直到容器都存储完毕head和tail相等,需要扩容
if (head == tail) {
doubleCapacity();
}
}
扩容主要是要阔到原数据的2倍,、关注怎么拷贝数据,怎么处理head和tail的指针索引,
咱们举例假如16长度数组塞满,要扩容进入此方法的情况,请看如下片段对着代码边看边想
p=0;n=16;r=16; newCapacity (32)=16*2
临时开辟32个空间给a数组,第一部分的拷贝,注意:从原数组(elements)第0(p=0)个位置拷贝到a数组中第0个位置,拷贝数量为16(r=16);
第二部分的拷贝:从原数组(elements)第0个位置拷贝到数组a中第16(r=16)个位置,拷贝数量为0(p=0);
看到这里你会想,第二部分的拷贝,没有意义把,
假如第二次扩容时,head=16,tail=16,证明32个数组元素都被占满,此时各参数为:
p=16;n=32;r=16;newcapacity=64
第一部分拷贝为:从原数组(elements)第16(p=16)个位置开始拷贝到a数组中的第0个位置,拷贝数量为16(r=16),这样就实现了后进入的数组放入前半部分
第二部分拷贝为:从原数组(elements)第0个位置开始拷贝到a数组中第16(r=16)位置开始,拷贝数量为16(p=16),这样就实现了先进入放入数组的后半部分
然后把head置为0,这样如果此时取出,就可以取出最后时间插入的数据
tail置为32,这样后面在进行扩容,需要把从32(64长度)之后的位置都占满,此时head就等于32,继续扩容
private void doubleCapacity() {
assert head == tail;
int p = head;
int n = elements.length;
int r = n - p;
int newCapacity = n << 1;
if (newCapacity < 0) {
throw new IllegalStateException("Sorry, deque too big");
}
Object[] a = new Object[newCapacity];
// 参数:1.原数组
// 参数:2.原数组开始位置
// 参数:3.拷贝的目标数组
// 参数:4.目标开始位置
// 参数:5.拷贝数量
// 先拷贝后半部分放入临时空间的前半部分
System.arraycopy(elements, p, a, 0, r);
// 再拷贝前半部分放入临时空间的后半部分,这样就能保证后进先出
System.arraycopy(elements, 0, a, r, p);
elements = a;
head = 0;
tail = n;
}
2.出栈
出栈也是相当简单,还是那句话,能简单的代码绝对不复杂,前面我们入栈的时候,最新数据的位置已经用head标记过了,所以我们取head得值就可。
我们取出head位置数组值以后,我们要把当前取出得值置为空,然后还得计算把head标记为下一个最新的值,这样再次入栈,出栈,就会在正确的位置
所以取出0的位置数据head=0+1&16-1,最后等于1,这样再次取出时则从1取,取完2&15还是等于2,以此类推。。。
// 相当于ArrayDeque的pollFirst
@Override
public E pop() {
int h = head;
E result = (E) elements[h];
if (result == null) {
return null;
}
elements[h] = null;
// 00000001
// 00000111 最后还是1 1000 0001
head = (h + 1) & (elements.length - 1);
return result;
}
全部代码
/**
* @Author df
* @Date 2022/12/1 15:15
* @Version 1.0
* java中使用stack需要使用ArrayDeque,原stack则很粗糙,java推荐使用Deque栈数据结构
*/
public class ArrayDeque<E> implements Deque<E> {
transient Object[] elements;
transient private int head;
transient private int tail;
public ArrayDeque() {
// 为了测试,改成长度为2,这样到2就扩容了
elements = new Object[2];
}
// 相当于ArrayDeque的push、addFirst、offerFirst
@Override
public void push(E e) {
// 这样就可以从后存入了
//head=0-1 ,-1的二进制为:11111111
//16-1 =15 ,15的二进制为:00001111
// &:00001111 计算完还是15,此时head=15
// 14 14/2=7余0,7/2=3余1,3/2=1余1 1/2=0余1 1110
// 14的二进制和15的二进制比还是14,利用这样的去计算下标
elements[head = (head - 1) & (elements.length - 1)] = e;
System.out.println("push.idx head:" + head);
// head计算完就为15了,再存储一个就为14,直到容器都存储完毕head和tail相等,需要扩容
if (head == tail) {
doubleCapacity();
}
}
private void doubleCapacity() {
assert head == tail;
int p = head;
int n = elements.length;
int r = n - p;
int newCapacity = n << 1;
if (newCapacity < 0) {
throw new IllegalStateException("Sorry, deque too big");
}
Object[] a = new Object[newCapacity];
// 先拷贝后半部分放入临时空间的前半部分
System.arraycopy(elements, p, a, 0, r);
// 再拷贝前半部分放入临时空间的后半部分,这样就能保证后进先出
System.arraycopy(elements, 0, a, r, p);
elements = a;
head = 0;
tail = n;
}
// 相当于ArrayDeque的pollFirst
@Override
public E pop() {
int h = head;
E result = (E) elements[h];
if (result == null) {
return null;
}
elements[h] = null;
// 00000001
// 00000111 最后还是1 1000 0001
// 00000010 2
// 00000111 最后还是2
head = (h + 1) & (elements.length - 1);
return result;
}
public void addLast(E e) {
if (e == null)
throw new NullPointerException();
elements[tail] = e;
if ( (tail = (tail + 1) & (elements.length - 1)) == head)
doubleCapacity();
}
@Override
public boolean isEmpty() {
return head == tail;
}
public static void main(String[] args) {
ArrayDeque arrayDeque = new ArrayDeque();
arrayDeque.push(1);
arrayDeque.push(2);
arrayDeque.push(3);
arrayDeque.push(5);
arrayDeque.pop();
}
}