Java之集合(二)ArrayDeque
转载请注明源出处:http://www.cnblogs.com/lighten/p/7283928.html
1.前言
上章讲解了Java中的集合接口和相关实现抽象类,本章开始介绍一些具体的实现类,第一个介绍的就是继承自抽象类AbstractCollection的实现类ArrayDeque,其同时实现了Deque接口。Queue的结构是一个单端的队列,从一端进另一端出,Deque是一个双端队列。而ArrayDeque是一个使用循环数组实现的双端队列了。双端队列可以实现单端队列的先入先出的方式,也可以实现栈结构的先入后出的方式,使用比较灵活,看具体需求。ArrayDeque是线程非安全的,所以如果需要实现线程安全,就需要自己处理了。
2.实现讲解
2.1 接口
Deque接口在上章没有讲解,先从这里开始说明。该接口继承了Queue接口,而且由于其是双端队列,所以自然而然有如下接口:
这些接口看名称也很容易理解,操作首端和尾端对应的add、offer、remove、poll、element、peek这些方法。除了上面的这些接口,还定义了两个接口:push、pop。这两个接口用于当双端队列作为栈的形式使用时的入栈、出栈接口。剩下的一个接口就是descendingIterator接口了,其提供了一个倒序的迭代器。
2.2 ArrayDeque
循环数组的双端队列实现起来非常简单,里面的数据结构就只有下图:
一个数组,一个指向头的下标,一个指向尾的下标。就这么一个简单的结构实现了双端队列。值得注意的是数组的大小必须是2n,为何这么做网上说是由于内存的分配是以2的幂指数个页帧为单位进行的,标准的内存分配大小是4K,用2的幂指数为大小分配空间有利于内存管理。可能是由于这个问题,而我更想讲解的是其是如何保证输入的值是2n的。看下面的代码:
这一段代码乍一看感觉有些莫名其妙,但是正是由于这段代码保证了其分配的数组大小是2n。这段代码所计算的是:输入一个数字X计算X<Y=2n,使这个等式成立时n取最小的时候Y的值,Y就是分配的数组大小了。所以不管你输入多少(超过Java整数范围除外),最终会满足这个公式使得最终分配的结果是2n。上段代码所实现的功能知道了,但是其这么实现的具体原理是什么呢?如下:
1.要明确2n使用二进制的表现形式如下:0...010...0,中间有一个1,其它的都是0。
2.根据1的形式,计算使输入任意的X,等式成立的Y。X的二进制形式为????????,是一个未知数,这样如何求得Y呢?方法很简单,找到X最高位为1的位置:那么X就是0..001???,这种形式了。那么所求的Y就是0..010...0,其值就是比X最高位为1再高一位为1,其它位为0的值。
3.X的最高为1的那一位是未知的,如何求更高一位为1的Y呢?直接求是没有办法的,但是可以通过将X最高位为1后面所有位都变成1,再加1进位的方式办到。就是0..001???变成0.001..1,使用这个+1就会变成所要的Y:0.010...0了。
4.如何保证X最高位为1后面都是1呢?这个就是上面位运算所实现的内容了。假设X是0..01???,左移一位就是0.001??,做或运算就变成了0..011??,是不是很巧妙,出现了两位为1的就移动2位,获得四位为1的值,这样移动到16的时候就涵盖了32位整数的所有范围了。这个时候+1可能发生整数溢出,所以再左移一位保证在整数范围内。
以8位整数为例,假设输入的是8:00001000,运算过程如下:
这样就算出来了结果。循环列表扩容代码如下:
一个循环数组扩容时,先把队列头右边的放入扩容后的数组,再把队列头左边的内容放入数组后面。
关于循环数组的操作,其实了解队列的应该也很清楚,就是从头端入就是element[head-1]=Object,从尾端入就是element[tail+1]=Object。出也是对应的,然后就是对head和tail的值进行修改而已,其它的都不难理解,看代码就能清楚了。这里有一个问题就是数组越界的问题,-1或+1都会超过数组的长度,按照循环列表应该定位到数组的尾部或者头部,这里代码中借助了数组长度是2n,解决了这个问题。就是:得到的值 & (elements.length - 1),这样就保证了在数组范围内,且是正确的位置。这个很好理解,不做介绍。
2.3 操作图
这就是整个添加和扩容的流程了。移除的方法是一样的,就不再画图描述了。