优雅,实在是太优雅了
能把复杂的东西简单化就是功底。
我为何有如此感慨,了解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];}
进来看代码你会发现,很简单把,就几行代码,主要进来计算要存储的下标,计算完毕把当前下标赋值给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@Overridepublic 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;}
出栈也是相当简单,还是那句话,能简单的代码绝对不复杂,前面我们入栈的时候,最新数据的位置已经用head标记过了,所以我们取head得值就可。
我们取出head位置数组值以后,我们要把当前取出得值置为空,然后还得计算把head标记为下一个最新的值,这样再次入栈,出栈,就会在正确的位置
所以取出0的位置数据head=0+1&16-1,最后等于1,这样再次取出时则从1取,取完2&15还是等于2,以此类推。。。
// 相当于ArrayDeque的pollFirst@Overridepublic E pop() {int h = head;E result = (E) elements[h];if (result == null) {return null;}elements[h] = null;// 00000001// 00000111 最后还是1 1000 0001head = (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 implements Deque {transient Object[] elements;transient private int head;transient private int tail;public ArrayDeque() {// 为了测试,改成长度为2,这样到2就扩容了elements = new Object[2];}// 相当于ArrayDeque的push、addFirst、offerFirst@Overridepublic 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@Overridepublic 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 最后还是2head = (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();}@Overridepublic 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();}
}