时间复杂度:衡量算法流程中发生了多少次常数时间操作,
额外空间复杂度:衡量算法流程中必须开辟的空间。
1. LinkedList
基于链表实现。
add:
/**
* 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) {
checkPositionIndex(index);
if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}
请注意这个node(index)的代码:
/**
* Returns the (non-null) Node at the specified element index.
*/
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;
}
}
这里进行了2分遍历查找相应坐标下的index节点!当插入坐标小于2分之size,从左边开始遍历,大于从右边开始遍历寻找节点,所以这部分的时间复杂度实为:O(N)
当index == size时,说明在往集合的最后一个节点进行新增操作:
/**
* Links e as last element.
*/
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
获得最后一个节点l,然后new Node<>(l, e, null),new了一个节点,指定该节点的上一个节点为l,值为e,即add的值,该节点的下一个节点此时为null。当创建完成后,最后一个节点指针指向新建的节点。再判断最后一个节点l是否为空。
如果为空,说明此时集合为空,无值,头部指针再执行新建的节点,newNode作为集合内的第一个节点,头指针和尾指针都指向它。
如果不为空,则l节点的next指向newNode,l作为newNode创建之前的尾节点。此是l和新的尾节点newNode的双端指向已构建完毕。
节点创建完毕后,size++即长度++,而modCount记录的是此集合在结构上被修改的次数。 结构修改是那些改变列表大小的修改。
可以发现节点插入的过程中只有指针指向发生了和长度计量发生了变化,所以add头部的时间复杂度为O(1),且并没有创建额外的集合空间,所以空间复杂度为O(1)。
当index != size时,说明在往集合的中间或者最开始的部分进行新增:
/**
* Inserts element e before non-null Node succ.
*/
void linkBefore(E e, Node<E> succ) {
// assert succ != null;
final Node<E> pred = succ.prev;
final Node<E> newNode = new Node<>(pred, e, succ);
succ.prev = newNode;
if (pred == null)
first = newNode;
else
pred.next = newNode;
size++;
modCount++;
}
succ是原集合中该下标对应的节点,现需要在该下标中插入一个新值。
pred获取原节点的上一个节点,创建一个新的节点newNode,指定上一个节点为pred,传入值e,指定下一个节点为succ,同时指定succ的上一个节点为newNode。
如果原节点的上一个节点为空说明,succ为头节点,而newNode即在头节点插入,first头部节点指针指向newNode。
如果原节点的上一个节点不为空说明,succ为中间节点,同时让pred的next指针发生变化由succ变为newNode,此时双端节点构建完成。
该过程的节点新增同样只有节点指针和长度发生了变化。但是,这部分需要找到相应index下标的Node节点,及上述调用的node(index)方法进行了遍历,所以时间复杂度为O(N),无额外空间,空间复杂度为O(1)。
remove:
public E remove(int index) {
checkElementIndex(index);
return unlink(node(index));
}
E unlink(Node<E> x) {
// assert x != null;
final E element = x.item;
final Node<E> next = x.next;
final Node<E> prev = x.prev;
if (prev == null) {
first = next;
} else {
prev.next = next;
x.prev = null;
}
if (next == null) {
last = prev;
} else {
next.prev = prev;
x.next = null;
}
x.item = null;
size--;
modCount++;
return element;
}
remove的过程也无特殊变化,同样也是节点的指向变化而已。
获取需要删除的下表对应的节点,获取该节点的上一个节点为prev,下一个节点为next。
判断上一个节点prev是否为空,为空就说明它是头节点,不为空时,让删除节点的上一个节点prev的下一个节点指向删除节点的下一个节点next!同时让x需要删除的节点的上一个节点指针指向null。
判断下一个节点next是否为空,为空就说明它是尾节点,不为空时,让删除节点的下一个节点next的上一个节点指向删除节点的上一个节点prev!即跳过x(需要删除的节点),同时让x的下一个节点指向null。
让x的值变为null,长度–,修改次数++。
删除节点x此时变为“孤儿”,没人指向它,它指向的是null。根据JDK1.8用的默认GC为 ParallelGC即PS + PO,其判断是否为垃圾的算法为根可达算法,即是否与内存有直接或间接的”链接“,由于x此时无指向,经过一次GC后判断其为“垃圾”,最终被回收掉了。
可以发现reomve的过程同样也是指针的指向变化,无额外操作。但同样调用了遍历方法node(index)找到相应下标,此时的时间复杂度:O(N),空间复杂度O(1)。
节点在集合的排列方式大致如下:

0为头节点,2为尾节点。之前有种说法是头节点的上一个节点指针指向尾节点,尾节点的下一个节点指针指向头节点。不过我这次在JDK1.8关于LinkedList的双端链表它的源码中并未发现这种结构。有兴趣可以交流下。正文继续。
2. ArrayList
基于数组实现。
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++;
}
ArrayList的新增操作的算法体现在 System.arraycopy(elementData, index, elementData, index + 1,size - index); elementData为它维护的一个Object[]数组。
在Systen中,arraycopy是一个native方法即调用的是本地方法栈中的c语言编写的方法
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);
src - 源数组。
srcPos – 源数组中的起始位置。
dest - 目标数组。
destPos – 目标数据中的起始位置。
length – 要复制的数组元素的数量。
根据注释其实可以判断其作用,即将elementData数组从index的位置作为起始位置,size-index作为结束位置,插入到一个新的elementData数组的index+1的位置,从而覆盖这个新数组在index + 1位置后的数。index相当于空出来的一个位置,原值已经后移了一位,方法执行结束后再 elementData[index] = element;传值,完成指定下标的add操作。
图解如下:

这里使用了两个数组进行操作,且截取了一个数组的起始位置和结束位置,这里根据数组的特性是使用了偏移量截取到了一个[index,index+1]范围数组,虽然看不到具体代码,但截取的数组必然要遍历插入到新数组,所以可以判断它的时间复杂度为O(N)。而空间复杂度,因为使用的仍是有限的空间,所以空间复杂度为O(1)。
public E remove(int index) {
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
这里删除依然同理,需要删除的元素下标为index,截取了原数组范围为[index+1,size-1-index]范围的数组,即从需要删除的下标+1到最后一个坐标。然后遍历插入到数组,从index位置开始!根据数组的特性也就是说,原index位置的元素被index+1位置的元素覆盖掉了,后面的部分元素左移了一个位置,这样就实现了ArrayList的删除操作。与LinkedList不同的是:LinkedList利用GC回收掉节点。ArrayList覆盖元素。
以此可以得出remove的时间复杂度为:O(N),空间复杂度为O(1)。
3. 总结
根据Index进行增加和删除时,LinkedList和ArrayList的时间复杂度和空间复杂度:
| 时间复杂度 | 空间复杂度 | |
|---|---|---|
| LinkList | 尾部add为:O(1),其余情况都为:O(N) | O(1) |
| ArrayList | O(N) | O(1) |
4. 后言
前途似海,来日方长。
本文详细解析了LinkedList和ArrayList在不同场景下的增删操作时间与空间复杂度,并通过具体实现对比了两者之间的性能差异。

3337

被折叠的 条评论
为什么被折叠?



