初识LinkedList底层原理

转自 五月的仓颉: http://www.cnblogs.com/xrq730/p/5005347.html

LinkedList是通过链表实现的,链表是一种线性的存储结构,每个存储的数据都放在一个存储单元里面,每个存储单元除了存放有待存储的数据外,还存储下一个存储单元的地址(下一个存储单元的地址是必要的,有些存储结构还存放前一个存储单元的地址),每次查找数据的时候,通过当前存储单元中存储的下一个单元的地址,找到下一个单元

LinkedList是一种双向链表,双向链表有两种定义:

1> 链表中的存储单元能够向前或向后找到前一个存储单元或后一个存储单元

2> 链表的头结点的前一个存储单元是链表的尾节点,链表尾节点的后一个存储单元是链表的头节点

链表的每个存储单元定义

private static class Entry<E> {
	E element;
	Entry<E> next;
	Entry<E> previous;

	Entry(E element, Entry<E> next, Entry<E> previous) {
	    this.element = element;
	    this.next = next;
	    this.previous = previous;
	}
    }
E element 是真正存储数据的地方,previous是前一个存储单元的地址,next是后一个存储单元的地址


LinkedList的关注点

是否允许元素为空允许
是否有序有序
是否允许元素重复允许
是否线程安全非线程安全
添加元素
public static void main(String[] args)
{
    List<String> list = new LinkedList<String>();
    list.add("111");
    list.add("222");
}

首先创建一个LinkedList实例,看下源码

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
    private transient Entry<E> header = new Entry<E>(null, null, null);
    private transient int size = 0;

    /**
     * Constructs an empty list.
     */
    public LinkedList() {
        header.next = header.previous = header;
    }

new一个Entry对象,设置prev,next和element都是null,然后将prev,next指向header本身,如果header引用地址的字长为4个字节,假设是0x000000000,那么执行List<String> list = new LinkedList<String>()后表示:


接着看add("111")

 public boolean add(E e) {
	addBefore(e, header);
        return true;
    }
private Entry<E> addBefore(E e, Entry<E> entry) {
	Entry<E> newEntry = new Entry<E>(e, entry, entry.previous);
	newEntry.previous.next = newEntry;
	newEntry.next.previous = newEntry;
	size++;
	modCount++;
	return newEntry;
    }

addBefore 第一行翻译下就是

newEntry.element = e ;

newEntry.next = header;

newEntry.previous = header.previous, 这样链表中两个存储单元表示为


后面两步,将

newEntry.previous.next = newEntry; 就是将header的next地址设置为newEntry地址

newEntry.next.previous = newEntry;将header的previous地址设置为newEntry地址

这样。链表中的两个存储单元的结构就变为:


同样的,如果在add("222")

newEntry.element = e ;

newEntry.next = header;

newEntry.previous = header.previous, 新增的存储单元表示为


newEntry.previous.next = newEntry; 就是将111的next地址设置为newEntry(222)地址

newEntry.next.previous = newEntry;将header的previous地址设置为newEntry(222)地址

这样结果就是: 链表中的每一个节点都能向前后者向后找到下一个节点,这就是双向链表的特性

添加元素:只要创建一个节点并修改节点的previous和next引用指向的地址,就能添加元素

查询元素

 public E get(int index) {
        return entry(index).element;
    }
private Entry<E> entry(int index) {
        if (index < 0 || index >= size)
            throw new IndexOutOfBoundsException("Index: "+index+
                                                ", Size: "+size);
        Entry<E> e = header;
        if (index < (size >> 1)) {
            for (int i = 0; i <= index; i++)
                e = e.next;
        } else {
            for (int i = size; i > index; i--)
                e = e.previous;
        }
        return e;
    }

可以看出,按照索引查找节点是从头节点向前或者向后一个一个节点遍历过去,如果index<size/2就是向后遍历,否则就是向前遍历.

删除元素

和ArrayList类似,LinkedList也支持按照索引删除元素和按照元素删除元素,按照元素删除只会删除第一个匹配到的元素

 public E remove(int index) {
        return remove(entry(index));
    }
public boolean remove(Object o) {
        if (o==null) {
            for (Entry<E> e = header.next; e != header; e = e.next) {
                if (e.element==null) {
                    remove(e);
                    return true;
                }
            }
        } else {
            for (Entry<E> e = header.next; e != header; e = e.next) {
                if (o.equals(e.element)) {
                    remove(e);
                    return true;
                }
            }
        }
        return false;
    }

两种方法都会遍历找到这个节点,然后调用remove(e)方法

private E remove(Entry<E> e) {
	if (e == header)
	    throw new NoSuchElementException();

        E result = e.element;
	e.previous.next = e.next;
	e.next.previous = e.previous;
        e.next = e.previous = null;
        e.element = null;
	size--;
	modCount++;
        return result;
    }
e.previous.next = e.next; 将删除节点e的前一个节点的next引用指向e的下一个节点的地址

e.next.previous = e.previous;将删除节e的后一个节点的previous引用指定e的前一个节点的地址。

最后将e的 element,previous,next全部置null,让gc回收

插入元素

 public void add(int index, E element) {
        addBefore(element, (index==size ? header : entry(index)));
    }
 private Entry<E> addBefore(E e, Entry<E> entry) {
	Entry<E> newEntry = new Entry<E>(e, entry, entry.previous);
	newEntry.previous.next = newEntry;
	newEntry.next.previous = newEntry;
	size++;
	modCount++;
	return newEntry;
    }

和添加元素类似,只不过将header节点,换为 索引为index的节点,然后创建节点,并将previous和next引用修改

fast-fail

使用迭代器进行迭代的时候,同样存在fast-fail机制,在多线程环境下对LinkedList进行迭代,如果modCount!=exceptedModCount就是抛出异常,也就是多线程环境下操作LinkedList是不安全的。

final void checkForComodification() {
	    if (modCount != expectedModCount)
		throw new ConcurrentModificationException();
	}

ArrayList和LinkedList的比较

1. ArrayList顺序添加元素,(在数组容量足够的情况下)只需要将数组某个索引位置指向一个对象,LinkedList顺序添加元素则是有创建Entry的开销,并修改节点的previous和next引用指向的地址,这种情况下, ArrayList比LinkedList更快。

2. ArrayList删除元素,则是需要根据索引找到元素,然后进行数组的复制,向前移位的操作。

    LinkedList删除元素,找到元素需要从header节点开始遍历,找到这个节点后,需要对前一个和后一个节点进行previous和next维护,最后将这个节点置空. 这里那个集合性能表现更佳,存在争议

总结如下:

1> LinkedList做删除,插入时候,慢在节点的寻址,快在前后Entry引用维护

2> ArrayList则是快在元素的寻址,慢在数组的复制

如果待删除和插入的元素在非常靠前的位置,使用LinkedList效率会远远高于ArrayList,此时ArrayList需要操作数组的元素很多,如果是靠后的位置,由于LinkedList是双向链表,靠前和靠后效率都比较高,而ArrayList越是靠后,需要复制的数组长度越小,性能越高.

如果你十分确定你插入、删除的元素是在前半段,那么就使用LinkedList;如果你十分确定你删除、删除的元素在比较靠后的位置,那么可以考虑使用ArrayList。如果你不能确定你要做的插入、删除是在哪儿呢?那还是建议你使用LinkedList吧,因为一来LinkedList整体插入、删除的执行效率比较稳定,没有ArrayList这种越往后越快的情况;二来插入元素的时候,弄得不好ArrayList就要进行一次扩容,记住,ArrayList底层数组扩容是一个既消耗时间又消耗空间的操作

3. ArrayList查询元素速度很快,LinkedList则是需要从header节点,向前或向后遍历,ArrayList使用普通for循环遍历最快,LinkedList建议使用foreach遍历

ArrayList使用普通for循环快的原因

ArrayList的get方法只是从数组里面拿一个位置上的元素罢了。我们有结论,ArrayList的get方法的时间复杂度是O(1),O(1)的意思也就是说时间复杂度是一个常数,和数组的大小并没有关系,只要给定数组的位置,直接就能定位到数据。

LinkedList使用普通for循环慢的原因

LinkedList在get任何一个位置的数据的时候,都会把前面的数据走一遍。假如我有10个数据,那么将要查询1+2+3+4+5+5+4+3+2+1=30次数据,相比ArrayList,却只需要查询10次数据就行了,随着LinkedList的容量越大,差距会越拉越大。其实使用LinkedList到底要查询多少次数据,大家应该已经很明白了,来算一下:按照前一半算应该是(1 + 0.5N) * 0.5N / 2,后一半算上即乘以2,应该是(1 + 0.5N) * 0.5N = 0.25N2 + 0.5N,忽略低阶项和首项系数,得出结论,LinikedList遍历的时间复杂度为O(N2),N为LinkedList的容量


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值