JDK容器与并发—数据结构
若想提高编程水平,一种方式就是看优秀框架的源码,JDK的源码就是一个很好例子,顺便也熟悉一下经常用到的类。在了解过程中,可以了解其框架设计方式,为什么要这样设计。先看看list 的UML类图:
基础数据结构
数组
对于n个元素的数组可如下表示:
数组在初始化时需要明确指定长度,一旦成功完成后,该数组所占用的内存空间就固定下来,一般是连续分配的内存空间。例如对于数组array,可以通过array[i]来访问或设置数组元素值(其中i为索引,对于长度为n的数组,第一个索引值为0,最后一个为n-1)。
优点:随机访问效率高,时间复杂度为O(1);缺点:长度固定,不能动态增加、删除元素。
单链表
对于n个节点的单链表可以如下表示:
其类可如下表示:
class Node<E> {
E item;
Node<E> next;
Node(E element, Node<E> next) {
this.item = element;
this.next = next;
}
}
每一个Node包括两个属性:数据值及指向下一个Node的next引用,一般为了方便存取第一个和最后一个节点,会维护两个Node引用:first引用(指向第一个节点)、last引用(指向最后一个节点)。通过操作first、last引用,链表很方便地在首尾增加、删除节点,修改及访问首尾节点;当然也可以在中间增加、删除、修改、访问节点,但是都需要先遍历找到索引节点位置。单链表中的节点很可能不是连续分配的内存空间,它们只需要通过next指针联系在一起就可以了。
优点:可动态增加、删除节点;缺点:随机访问get(index)时间复杂度为O(n),效率低。当然,如果有维护first、last引用,则访问首尾节点的为O(1)。与数组的优缺点基本上是相反的。
常用数据结构
实际上,数组、单链表是最底层的数据结构,基本上所有的数据结构都是在数组、单链表基础上进行构造的,通过增加新的逻辑,来实现特定功能。下面一一介绍:
双向链表
对于n个节点的双向链表可如下表示:
实际上双向链表与单链表的区别体现在多增加了一个指向前一个节点的prev引用,所以相对于单链表,双向链表能进行双向遍历,当然这也增加了维护两个指针的成本。
队列
可以把队列看成是一种基于数组或链表实现的逻辑上的数据结构,只不过是增加了自己的逻辑而已。按照元素出入队方式主要分FIFO、优先级队列、LIFO(Stack)、传输队列、双端队列。按照并发性质可分为:非线程安全队列、线程安全队列,其中线程安全队列又可分为阻塞队列、lock-free、wait-free队列。后续再详细介绍。
树
树是基于链表实现的数据结构,主要是增加了自己的性质。二叉树是我们用的最多的,其可表示如下:
每个节点在链表性质的基础上定义了三个引用:指向父节点的parent引用、指向左子树的left引用、指向右子树的right引用。最顶层的节点为根节点,其parent引用为null。
Node表示:
class Node {
Object value;
Entry<K,V> left;
Entry<K,V> right;
Entry<K,V> parent;
}
跳表
跳表也是基于链表,通过增加自己的性质、逻辑来实现的,在JDK ConcurrentSkipListMap的实现中采用的数据结构就是跳表,可以看看它是怎么用跳表的,先看一张整体的跳表图:
在看看wiki上的一张跳表图:
ConcurrentSkipListMap中跳表的实现有三种节点类型Node、Index、HeadIndex,相对于单链表,跳表查找节点很快是在于:通过HeadIndex维护索引层次,通过Index从最上层开始往下查找元素,一步步缩小查找范围,到了最底层Node单链表层,就只需要比较很少的元素就可以找到待查找的元素节点。
class Node<K,V> {
K key;
Object value;
Node<K,V> next;
}
class Index<K,V> {
Node<K,V> node;
Index<K,V> down;
Index<K,V> right;
}
class HeadIndex<K,V> extends Index<K,V> {
int level;
HeadIndex(Node<K,V> node, Index<K,V> down, Index<K,V> right, int level) {
super(node, down, right);
this.level = level;
}
}
HashMap
HashMap是基于数组、单链表实现的:
Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
int hash;
}
其数据结构为Entry<K,V>[],数组中的每一个元素又是Entry<K,V>类型的一种单链表。HashMap在增加或查找元素时,分两步:
1)对其key的hashcode再hash得到一个hash值,将该hash值作为table数组元素的索引找到table[hash],即bucket;
2)遍历bucket找到元素。
HashMap第一步是先初步确定元素的位置,第二步是最终确定元素的位置。
HashMap的一种极端的结构就是每一个bucket都只有一个元素,这样等效于数组;另一种极端结构就是table数组长度为1,这样等效于单链表。这两种极端结构都不是HashMap的最初设计目标,HashMap是吸取数组、链表的优点进行构造的:第一步是利用了数组的高效随机访问,第二步是利用了链表的动态增加、删除节点。
Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
int hash;
}
更多推荐
所有评论(0)