LinkedList 插入和 ArrayList

分别是:10万、100万、1000万的数据在两种集合下面不同位置的插入效果!

  1. ArrayList 中间插入快。
  2. LinkedList 头插、尾插快。

一、数据结构

Linked + List = 链表 + 列表 = LinkedList = 链表列表

LinkedList,是基于链表实现,由双向链条next、prev,把数据节点穿插起来,所以在插入数据时,是不需要ArrayList那样扩容数组。

二、源码分析

1.初始化

与ArrayList不同,LinkedList初始化不需要创建数组,因为它是一个链表结构,而且没有传给构造函数初始化多少个空间的入参,如下:

但是,构造函数一样提供了和ArrayList一些相同的方法,来初始化入参,入下:

@Test
public void test_init() {
    LinkedList<String> list01 = new LinkedList<String>();
    list01.add("a");
    list01.add("b");
    list01.add("c");
    System.out.println(list01);

    LinkedList<String> list02 = new LinkedList<String>(Arrays.asList("a", "b", "c"));
    System.out.println(list02);

    LinkedList<String> list03 = new LinkedList<String>(){
        {add("a");add("b");add("c");}
    };
    System.out.println(list03);

    LinkedList<Integer> list04 = new LinkedList<Integer>(Collections.nCopies(10, 0));
    System.out.println(list04);
}
[a, b, c] 
[a, b, c] 
[a, b, c] 
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]  
Process finished with exit code 0

2.插入

LinkedList的插入方法比较多,List中接口中默认提供的是add,也可以指定位置插入。

但在LinkedList中还提供了头插addFirst和尾插addLast

2.1头插

ArrayList的插入和LinkedList插入对比,如下:

1.ArrayList头插时,需要把数组元素通过Arrays.copyOf的方式把数组元素移位,如果容量不足需要扩容。
2.LinkedList头插时,不需要考虑移位、以及扩容的问题,直接把元素定位到首位,接点链条就可以了。

2.1.1 源码

对照LinkedList源码

  1. first,首节点会一直被记录,这样就非常方便头插。
  2. 插入时候会创建新的节点元素,new Node<>(null, e,f),紧接着把新的头元素赋值给first。
  3. 之后判断f节点是否存在,不存在则把头节点作为最后一个节点,存在则用f节点的上一个链条prev链接。
  4. 最后记录size大小、和元素数量modCount。modCount用在便利时做校验,modCount!=expectedModCount.
private void linkFirst(E e) {
    final Node<E> f = first;
    final Node<E> newNode = new Node<>(null, e, f);
    first = newNode;
    if (f == null)
        last = newNode;
    else
        f.prev = newNode;
    size++;
    modCount++;
}

2.1.2验证

ArrayList、LinkeList,头插源码验证

@Test
public void test_ArrayList_addFirst() {
    ArrayList<Integer> list = new ArrayList<Integer>();
    long startTime = System.currentTimeMillis();
    for (int i = 0; i < 10000000; i++) {
        list.add(0, i);
    }
    System.out.println("耗时:" + (System.currentTimeMillis() - startTime));
}
@Test
public void test_LinkedList_addFirst() {
    LinkedList<Integer> list = new LinkedList<Integer>();
    long startTime = System.currentTimeMillis();
    for (int i = 0; i < 10000000; i++) {
        list.addFirst(i);
    }
    System.out.println("耗时:" + (System.currentTimeMillis() - startTime));
}

比对结果:

10万、100万、1000万的数据量,在头插时的消耗情况。
1.ArrayList需要做大量的位移和复制操作,而LinkedList的优势就体现出来了,耗时只是实例化的一个对象。

2.2尾插

ArrayList的插入与LinkedList插入对比

1.ArrayList尾插时,是不需要数据位移,比较耗时的是数据扩容时候,需要数据拷贝迁移。
2.LinkedList尾插时,与头插相比耗时点会在对象的实例化上面

2.2.1源码

对照一下LinkedList的尾插源码,如下:

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++;
}

1.头插代码相比几乎没有什么区别,只是first换成last
2.耗时点知识在创建节点上,Node<E>

2.2.2验证

ArrayList、LinkedList,尾插源码验证

@Test
public void test_ArrayList_addLast() {
    ArrayList<Integer> list = new ArrayList<Integer>();
    long startTime = System.currentTimeMillis();
    for (int i = 0; i < 10000000; i++) {
        list.add(i);
    }
    System.out.println("耗时:" + (System.currentTimeMillis() - startTime));
}
@Test
public void test_LinkedList_addLast() {
    LinkedList<Integer> list = new LinkedList<Integer>();
    long startTime = System.currentTimeMillis();
    for (int i = 0; i < 1000000; i++) {
        list.addLast(i);
    }
    System.out.println("耗时:" + (System.currentTimeMillis() - startTime));
}

比对结果:

10万、100万、1000万的数据量,在尾插时的一个耗时情况。
1.ArrayList不需要做位移拷贝也就不会特别耗时,而LinedList则需要创建大量的对象,所以这边ArrayList尾插效果更好

2.3中间插入

结构比对图,ArrayList和LinkedList 插入做下对比,如下:

1.ArrayList中间插入,首先我们知道它的定位时间复杂度是O(1),比较耗时的点在于数据迁移和容量不足的时候扩容。
2.LinkedList中间插入,链表的数据实际插入时候并不会怎么耗时,但是它定位的元素的时间复杂度但是O(n),所以这部分比较耗时

2.3.1源码

看LinkedList指定位置的插入源码

使用add(位置、元素)方法插入

public void add(int index, E element) {
    checkPositionIndex(index);
    if (index == size)
        linkLast(element);
    else
        linkBefore(element, node(index));
}

位置定位node(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;
    }
}

size >> 1,这部分代码判断元素位置在左半区间,还是右半区间,在进行循环查找

执行插入:

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++;
}

1.找到指定位置插入的过程就比较简单了,与头插、尾插,相插不大。
2.整个过程可以看到,插入中比较耗时的点会在便利寻找插入的位置上。

2.3.2验证

ArrayList、LinkeList,中间插入源码验证

@Test
public void test_ArrayList_addCenter() {
    ArrayList<Integer> list = new ArrayList<Integer>();
    long startTime = System.currentTimeMillis();
    for (int i = 0; i < 10000000; i++) {
        list.add(list.size() >> 1, i);
    }
    System.out.println("耗时:" + (System.currentTimeMillis() - startTime));
}
@Test
public void test_LinkedList_addCenter() {
    LinkedList<Integer> list = new LinkedList<Integer>();
    long startTime = System.currentTimeMillis();
    for (int i = 0; i < 1000000; i++) {
        list.add(list.size() >> 1, i);
    }
    System.out.println("耗时:" + (System.currentTimeMillis() - startTime));
}

比对结果:

10万、100万、1000万的数据量,在中间插入的一个耗时情况。
1.LinkedList在中间插入时,遍历寻找位置还是非常耗时,所以不同的情况下,需要选择不同的List集合做不同的业务

3.删除

与ArrayList不同,删除不需要拷贝元素,LinkedList是找到元素位置。

1.确定出要删除的元素X,把前后的链接进行替换。
2.如果是删除首尾元素,操作起来会更加容易,这也就是为什么说插入和删除块,但中间位置删除,需要遍历找到对应的位置

3.1删除操作方法

序列 方法 描述
1 list.remove() 与removeFirst()一致
2 list.remove(1) 删除Idx=1的位置元素节点,需要遍历定位
3 list.remove('a') 删除元素=‘a'的节点,需要遍历定位
4 list.removeFirst(); 删除首位节点
5 list.removeLast(); 删除结尾节点
6 list.removeAll(Arrays.asList('a','b')) 按照集合批量删除,底层是Iterator

源码:

@Test
public void test_remove() {
    LinkedList<String> list = new LinkedList<String>();
    list.add("a");
    list.add("b");
    list.add("c");
    list.remove();
    list.remove(1);
    list.remove("a");
    list.removeFirst();
    list.removeLast();
    list.removeAll(Arrays.asList("a", "b"));
}

3.2源码

分为删除首尾节点与其他节点的时候,对节点的解链操作。

list.remove('a');

public boolean remove(Object o) {
    if (o == null) {
        for (Node<E> x = first; x != null; x = x.next) {
            if (x.item == null) {
                unlink(x);
                return true;
            }
        }
    } else {
        for (Node<E> x = first; x != null; x = x.next) {
            if (o.equals(x.item)) {
                unlink(x);
                return true;
            }
        }
    }
    return false;
}

1.这一部分是元素定位,和unlink(x)解链.循环查找对应的元素。

unlink(x)解链

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;
}

1.获取待删除节点的信息;元素item、元素下一个节点next、元素上一个节点prev。
2.如果上一个节点为空,则把待删除元素的下一个节点赋值给首节点,否则把待删除节点的下一个节点,赋值给删除节点的上一个节点的子节点。
3.同样待删除节点的下一个节点next,也执行2步骤同样操作。
4.最后是把删除节点设置为null,并扣减size和modeCount数量

4.遍历

ArrayList与LinkedList的遍历都是通用的,基本包括5种方式。

int xx = 0;
@Before
public void init() {
    for (int i = 0; i < 10000000; i++) {
        list.add(i);
    }
}

4.1普通for循环

@Test
public void test_LinkedList_for0() {
    long startTime = System.currentTimeMillis();
    for (int i = 0; i < list.size(); i++) {
        xx += list.get(i);
    }
    System.out.println("耗时:" + (System.currentTimeMillis() - startTime));
}

4.2增强for循环

@Test
public void test_LinkedList_for1() {
    long startTime = System.currentTimeMillis();
    for (Integer itr : list) {
        xx += itr;
    }
    System.out.println("耗时:" + (System.currentTimeMillis() - startTime));
}

4.3Iterator遍历

@Test
public void test_LinkedList_Iterator() {
    long startTime = System.currentTimeMillis();
    Iterator<Integer> iterator = list.iterator();
    while (iterator.hasNext()) {
        Integer next = iterator.next();
        xx += next;
    }
    System.out.println("耗时:" + (System.currentTimeMillis() - startTime))
}

4.4forEach循环

@Test
public void test_LinkedList_forEach() {
    long startTime = System.currentTimeMillis();
    list.forEach(integer -> {
        xx += integer;
    });
    System.out.println("耗时:" + (System.currentTimeMillis() - startTime));
}

4.5stream(流)

@Test
public void test_LinkedList_stream() {
    long startTime = System.currentTimeMillis();
    list.stream().forEach(integer -> {
        xx += integer;
    });
    System.out.println("耗时:" + (System.currentTimeMillis() - startTime));
}

1.ArrayList与LinkedList都有自己的使用场景,如果在集合首位有大量的插入、删除以及获取操作那么可以使用LinkedList,因为它有相应的方法:addFirst、addLast、removeFirst、removeLast、getFirst、getLast、这些操作的时间复杂度都是O(1),很高效。
2.LinkedList的链表结构不一定比ArrayList节约空间,首先它占的内存不是连续的,其次它需要大量的实例化对象创造节点,虽然不一定节省空间,但是链表结构也是很好的数据结构,它能在你程序设计中起到很优秀的作用,列入可视化链路追踪图,就是需要链表结构,并需要每一个节点自旋一次,用于串联业务

猜你喜欢

转载自blog.csdn.net/u013946285/article/details/112979287