ArrayList
当我们在代码中创建一个Array时,计算机实际上会在内存中开辟一块连续的地址,每个地址都可以直接访问,访问任意一个元素的时间复杂度都是相同的,也就是O(1)。但是如果我们要增加或者删除元素时,因为地址连续的缘故,就需要做比较多的事情了。
假设有个数组长度为10,现在要插入一个元素下标为4,那么则需要将原来index>=4的所有元素都向后移一位,将index=4的地址空间让出来给新增的元素,这样的操作导致时间复杂度上升为O(n),删除操作也同理。
源码分析
/**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return <tt>true</tt> (as specified by {@link Collection#add})
*/
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
这个add方法的作用是在数组的末端插入一个元素,ensureCapacityInternal(size + 1)保证数组的size有size+1这么大,然后在数组末尾添加上e这个元素,最后size++。
/**
* Inserts the specified element at the specified position in this
* list. Shifts the element currently at that position (if any) and
* any subsequent elements to the right (adds one to their indices).
*
* @param index index at which the specified element is to be inserted
* @param element element to be inserted
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public void add(int index, E element) {
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1); // Increments modCount!!
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
这个重载的add方法的作用就是在数组中间插入元素,首先会检查上下界,同时保证数组的大小,然后调用arrayCopy复制数组,这个方法的意思是,从elementData这个数组的下标index的元素开始,复制到目标数组elementData(第二个)的index+1的地方,总共复制size-index个元素。简单的来说就是将数组中下标从index开始的元素全部向后移动一个单位,将index的地址让出来。然后elementData[index] = element,将新元素插入到数组里,最后size++。
LinkedList
在插入和删除操作比较频繁的情况下,数组并不好用。而链表这个数组结构就是为了弥补数组的缺点。
LinkedList每个节点都有value和next,next指向下一个节点所在的地址,最后一个节点指向null,如果指向第一个节点,则这个链表就是循环链表。
在Java中的链表是双向链表,在源码中提供了一个prev来指向前一个节点。
假设我们需要在链表的第二个和第三个节点之间,插入一个新节点,只需要将第二个节点的next指向新插入的节点,然后将新插入的节点的next指向原来的第三个节点,即可完成插入操作,不需要操作其他节点,所以链表的插入效率非常高,时间复杂度为O(1),删除节点同理。
而正是因为链表的地址空间不连续,导致访问某个节点,只能一个个迭代过去,效率比较差,时间复杂度为O(n)。
SkipList
链表的优点是插入快而定位慢,但当链表内的元素都是按照一定的顺序排列的情况下,就可以使用一些方法来加速定位,再使用迭代器定位元素就显得很蠢,所以就出现了SkipList跳表,在一些热门项目里替代了平衡树,如Redis。
跳表的插入、删除、搜索的时间复杂度都是O(logn)。
优势:原理简单,方便实现,容易拓展,效率更高。
跳表的使用只能在链表的元素都是有序的情况下!!
跳表加速的方式
加速一维的数组结构经常采用的方式就是升维,如图
索引的元素并没有指向nxet元素,而是指向了距离更远的元素,这样划分出一个个区间来,定位元素的时候先通过高级索引来定位元素在哪个区间,然后逐步缩小区间,最后在原始链表中迭代定位元素,如果在原始链表中没找到则代表该元素不存在。
比如说要定位元素5,第一步在二级索引中,确定要定位的元素在1到7这两个元素之间,然后再通过一级索引,发现元素5在4和7之间,然后从元素4开始一个个查找,很快就能定位出元素。