之所以萌发出写这类博客的想法,源自于前不久看到的一个有关于数据结构与算法的教程,写的实在太好。可惜的是,这类教程是基于C语言来进行讲解,对于Java开发者来说可能就会有一种”屠龙之技,学而无用”的错觉。所以本系列将针对Java来讲一讲数据结构的相关知识。先附上数据结构教程,对数据结构还不是十分了解的朋友可以先学习下。
本系列主要讲解我们常用的数据存储类(例如:List、Set、Map等)它们的内部实现原理,全篇围绕一下3个方面展开:
1.内部存储结构
2.基本操作及原理
3.优缺点及应用场景
这是“java与数据结构”的第一篇,本篇我们来谈谈List集合。
List
List实际是一个接口,它申明了一个有序集合并提供了add()、remove()、set()、get() 等方法对集合里的元素进行增删改查等操作。List的实现类有很多,我们来看看常见的几个实现类:ArrayList、LinkedList、Vector、Stack。
类图结构如下:
ArrayList
内部存储结构:顺序表(数组)
transient Object[] elementData;
private int size;
方法 | 描述 | 时间复杂度 |
---|---|---|
boolean add(E e) | 添加一个元素到集合的末尾 | O(1) |
void add(int index, E element) | 插入元素到指定位置 | O(n) |
E remove(int index) | 删除指定位置的元素 | O(n) |
get(int index) | 获取指定位置的元素 | O(1) |
ArrayList内部是通过Object[]数组来存放元素的,每次向ArrayList添加元素的时候,都会先确定数组容量的大小是否满足,如果不满足,则进行容量的扩充,每次扩充的容量为原来的1.5倍。由于数组是一个连续的存储空间,所以对元素的访问数组下标直接访问,时间复杂度为O(1)。但是对于元素的插入、删除操作可能会牵涉到大量元素的整体移动,所以时间复杂度为O(n)。
LinkedList
内部存储结构:双向链表
transient Node<E> first; //头节点
transient Node<E> last; //尾节点
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
方法 | 描述 | 时间复杂度 |
---|---|---|
boolean add(E e) | 添加一个元素到集合的末尾 | O(1) |
void add(int index, E element) | 插入元素到指定位置 | O(1) |
E remove(int index) | 删除指定位置的元素 | O(1) |
get(int index) | 获取指定位置的元素 | O(n) |
LinkedList的内部实现是双向链表,每个节点都是一个Node对象,prev指向当前元素的上一个元素,next指向当前元素的下一个元素,item就是数据域。使用双向链表的好处是:
- 内存空间都是动态申请的,不需要提前申请
- 在进行插入和删除操作时,只需要改变相应节点的指向,无需大量移动元素,所以时间复杂度为O(1)
- 单向链表只能通过“从前往后”的查找,而双向链表增加“从后往前”的查找功能,在访问元素上效率更高。
Node<E> node(int index) {
// assert isElementIndex(index);
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
Vector
内部存储结构:顺序表(数组)
protected Object[] elementData;
protected int elementCount;
方法 | 描述 | 时间复杂度 |
---|---|---|
boolean add(E e) | 添加一个元素到集合的末尾 | O(1) |
void add(int index, E element) | 插入元素到指定位置 | O(n) |
E remove(int index) | 删除指定位置的元素 | O(n) |
get(int index) | 获取指定位置的元素 | O(1) |
Vector和ArrayList的内部实现大致相同,它们不同的地方有以下几点:
- Vector是线程安全的,ArrayList是线程不安全的
- 容量扩充上,Vector默认是扩充到原来的2倍,ArrayList默认扩充到1.5倍
如果不考虑线程安全问题,那么ArrayList显然比Vector效率更高。
Stack
内部存储结构:顺序栈
方法 | 描述 | 时间复杂度 |
---|---|---|
E push(E item) | 元素入栈 | O(1) |
E pop() | 元素出栈 | O(1) |
E peek() | 获取栈顶元素 | O(1) |
Stack是Vector的子类,因此,Stack的栈功能是通过数组实现的,存储过程如下:
总结
以上介绍了List集合几个常用的实现类的数据结构,下面对这几个类的时间复杂度和使用场景做一个总结:
类 | 访问 | 插入\删除 | 线程安全 | 使用场景 |
---|---|---|---|---|
ArrayList | 快 | 较慢 | 不安全 | 访问元素较多,插入删除较少,不考虑线程安全 |
LinkedList | 慢 | 快 | 不安全 | 访问元素较少,插入删除较多,不考虑线程安全 |
Vector | 较快 | 慢 | 安全 | 访问元素较多,插入删除较少,考虑线程安全 |
Stack | 较快 | 慢 | 安全 | 适用于栈结构存储的数据 |