Java集合
引言
在实现某个方法时,选择不同的数据结构,代码的简洁性与时间效率会有所不同,根据需要,选择合适的数据结构解决问题。比如要搜索很大数据量但是有序的数据(数组),或者在序列中间插入一个或删除一个元素(链表),或者需要建立键与值的关系(map),等等。以上一些数据结构在java中如何实现的?
接口的概念
在介绍接口之前,先给出一个java的集合框架图
简单说明一下:
短虚线表示的矩形框:接口(8个,下面的两个不属于集合范畴,不算)。
长虚线表示的矩形框:抽象类(5个),对接口的部分实现,方便类库的实现者。接口的方法很多,都实现太麻烦了,而抽象类中实现了接口的部分方法,只要在此基础上扩展就可以了。
实线表示的矩形框:实现类(10个,Collections和Arrays不算,这两个后面会提到)
抽象类可以不必掌握,对于使用者而言(实现者可以看看),所以可以把上面的框架图简化一下
对比上图说明一下:
抽象类去掉了,我们不考虑
标有legacy的也去掉了,遗留问题后面再讲
SortedSet,SortedMap,WeakHashMap去掉了
增加了LinkedHashSet,LinkedHashMap
这5个实现类后面会讲到
现在基本框架清晰了,开始讲接口。
接口与实现分离
java集合类将接口与实现分离,这是一种设计模式,接口模式或桥接模式,这里不细讲,感兴趣可以看看大话设计模式。举个简单例子,常见的队列(queue),在java中如何实现的呢?
interface Queue<E>//standard library
{
void add(Element e);
E remove();
int size();
}
接口只是定义了队列,并没有讲队列如何实现。在java集合中队列有两种实现方式,就是前面的框架图中的ArrayDeque和LinkedList,为什么会是两种呢?
在这里引入一些数据结构的知识,帮助理解队列的实现。
队列作为一种线性表,存储方式(即实现方式)有两种,顺序存储和链式存储。先来看顺序存储。
假设队列中有n个元素,则顺序存储需要建立一个大于n的数组,把n个元素放在前n个位置,后面的空间方便添加元素。此时队头指向下标为0的元素。
入队:就是在队尾加一个元素,O(1)
出队:移除队头元素,并将后面的元素前移,O(n)
这就好比现实中排队买票,前面的人买好走了,后面的人自然要补上。但我们希望提高性能,如果不限制前n个位置存储元素(队头不固定在下标0),这样就不用移动后面的元素
这样的话就需要两个指针:front,rear
比如,长度为5数组,初始队列为空,front和rear指向下标0。然后a1,a2,a3,a4依次入队
出队a1,a2。然后入队a5
发现问题了,此时rear指向什么地方?
此时队列元素少于5个,但队尾已被占用,如果继续入队新元素,则会产生数组越界的错误,但前面0,1位置都是空的,这就是所谓的“假溢出”
如果你坐公交车,发现后排座位已满,但前面还有座位,你会怎么办?下车,然后说公交车满了,等下一班车吗?显然没有人会这么傻。
以上例子就是为了说明,数组方式实现队列,显然缺陷太多,所以数据结构里面提供了一种循环队列的方式。
前面的问题可以这样解决
此时入队a6,a7
这里有个问题:队列空,front==rear。现在队列满,front==rear
解决:
方法一:设置一个flag,
当front==rear,flag==0时,为空
当front==rear,flag==1时,为满
方法二:保留一个空间
这个时候,我们认为队列已经满了。
左边图:(rear+1)%QueueSize==front
右边图:(rear+1)==front
总结:(rear+1)%QueueSize==front时,队列满
队列的实际长度
左边图:rear>fornt,长度为rear-fornt
右边图:rear<fornt,长度为(QueueSize-front)+(rear-0)=rear-fornt+QueueSize
总结:长度为:(rear-fornt+QueueSize)%QueueSize
至于队列的链表实现,不再讲了,就是单链表,想看的同学可以去看看大话数据结构。
总之队列的实现无外乎就是以上讲的两种:循环数组和链表
class CircleArrayQueue<E> implements Queue<E>//只是举个例子
{
CircleArrayQueue(int capacity){
...}
public void add(Element e){
...}
public E remove(){
...}
public int size(){
.