[java] LinkedList inserts faster than ArrayList

Posted by Peggy on Sun, 20 Feb 2022 14:25:14 +0100

1. General

Reprint: Manual of facial classics Chapter 8 "LinkedList inserts faster than ArrayList? Are you sure?"

2. Data structure

Linked + List = linked list + list = linked list


LinkedList is implemented based on linked list. Data nodes are interspersed by two-way chains next and prev. Therefore, when inserting data, you do not need to expand the array like the ArrayList described in the previous chapter.

However, it cannot be said that all insertions are efficient. For example, in the middle area, he also needs to traverse the elements to find the insertion position

4, Source code analysis

#1. Initialization

Unlike ArrayList, LinkedList initialization does not need to create an array because it is a linked list structure. Moreover, it is not passed to the constructor to initialize the input parameters of many spaces. For example, this is not allowed, as follows;


However, the constructor provides the same way as ArrayList to initialize the input parameters, as shown in the following four ways;

@Test
public void test_init() {
    // Initialization mode; Ordinary way
    LinkedList<String> list01 = new LinkedList<String>();
    list01.add("a");
    list01.add("b");
    list01.add("c");
    System.out.println(list01);
    
    // Initialization mode; Arrays.asList
    LinkedList<String> list02 = new LinkedList<String>(Arrays.asList("a", "b", "c"));
    System.out.println(list02);
    
    // Initialization mode; Inner class
    LinkedList<String> list03 = new LinkedList<String>()\\{
        {add("a");add("b");add("c");}
    \\};
    System.out.println(list03);
    
    // Initialization mode; Collections.nCopies
    LinkedList<Integer> list04 = new LinkedList<Integer>(Collections.nCopies(10, 0));
    System.out.println(list04);
}

// test result

[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
 

These methods can be used to initialize operations, which can be selected as needed.

2. Insert

LinkedList has many insertion methods. The interface in the List provides add by default. You can also specify the insertion location. However, the LinkedList also provides header addFirst and footer addLast.

About inserting this part, we will talk about why; Sometimes LinkedList insertion is more time-consuming, and sometimes ArrayList insertion is better.

2.1 connector

First, let's take a look at a data structure comparison chart. Review the insertion of ArrayList and the insertion of LinkedList, as follows;


Looking at the picture above, we can analyze several points;

When inserting the ArrayList header, you need to pass the array elements through arrays The array elements are shifted by copyof. If the capacity is insufficient, it needs to be expanded.
When the LinkedList head is inserted, the expansion and displacement problems do not need to be considered. The element can be directly positioned in the first place and the contact chain can be linked.

Source code

Here we compare the source code of LinkedList header insertion, as follows;

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

First, the first node will always be recorded, which is very convenient for header insertion.
When inserting, a new node element will be created, new node < > (null, e, f), and then the new header element will be assigned to first.
Then judge whether the f node exists. If it does not exist, take the head insertion node as the last node. If it exists, use the prev link of the previous chain of the f node.
Finally, record the size, number of elements and modCount. modCount is used for verification during traversal. modCount= expectedModCount

#2.1.2 verification

ArrayList, LinkeList, header source code verification

@Test
public void test_ArrayList_addFirst() {
    ArrayList<Integer> list = new ArrayList<Integer>();
    long startTime = System.currentTimeMillis();
    for (int i = 0; i < 10000000; i++) {
        // add(i) is tail interpolation
        list.add(0, i);
    }
    System.out.println("Time consuming:" + (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("Time consuming:" + (System.currentTimeMillis() - startTime));
}
 

Comparison results:


Here, we respectively verify that the data volume of 100000, 1 million and 10 million is a time-consuming situation during head insertion.
As in our data structure comparison diagram, ArrayList needs to do a lot of displacement and copy operations, and the advantages of LinkedList are reflected. It takes only time to instantiate an object.

2.2 tail insertion

First, let's take a look at a data structure comparison chart. Review the insertion of ArrayList and the insertion of LinkedList, as follows;


Looking at the picture above, we can analyze several points;

When tailoring ArrayList, there is no need for data displacement. What is more time-consuming is that when data is expanded, it needs to be copied and migrated.
In LinkedList tail interpolation, compared with header interpolation, the time-consuming point will be on the instantiation of the object.

2.2.1 source code

Here we compare the source code of LinkedList tail insertion, as follows;

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

Compared with header code, there is little difference, except that first is replaced by last
The time-consuming point is only to create nodes. Node < E >

#2.2.2 verification

ArrayList, LinkeList, tail insertion source code verification

@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("Time consuming:" + (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("Time consuming:" + (System.currentTimeMillis() - startTime));
}

Comparison results:


Here we respectively verify that the data volume of 100000, 1 million and 10 million is a time-consuming situation during tail interpolation.
As in our data structure comparison diagram, ArrayList does not need to make displacement copies, which is less time-consuming, while LinkedList needs to create a large number of objects. Therefore, the effect of ArrayList tail interpolation here is better.

2.3 intermediate plug

First, let's take a look at a data structure comparison chart. Review the insertion of ArrayList and the insertion of LinkedList, as follows;

Looking at the picture above, we can analyze several points;

First of all, we know that the positioning time complexity of ArrayList is O(1). The time-consuming point is data migration and capacity expansion when the capacity is insufficient.
LinkedList is inserted in the middle. The actual insertion of linked list data will not take much time, but the time complexity of the elements it locates is O(n), so this part and the instantiation of elements are relatively time-consuming.

#2.3.1 source code

Here's the source code inserted at the specified location of LinkedList;

Insert using the add (position, element) method:

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

Location 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. This part of the code determines whether the element is located in the left half interval or the right half interval. It is performing circular search.

Execute insert:

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

The insertion process of finding the specified position is relatively simple, which is not different from the head insertion and tail insertion.
It can be seen from the whole process that the time-consuming points in the insertion will be traversed to find the insertion position.

2.3.2 verification

ArrayList, LinkeList, insert source code verification in the middle

@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("Time consuming:" + (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("Time consuming:" + (System.currentTimeMillis() - startTime));
}

Comparison results:


Here, we respectively verify that the data volume of 100000, 1 million and 10 million is a time-consuming situation when inserting in the middle.
It can be seen that when the Linkedlist is inserted in the middle, traversing to find the location is still very time-consuming. Therefore, in different cases, you need to select different List sets for business.

3. Delete

After talking about so many insertion operations, the deleted knowledge points are well understood. Unlike ArrayList, there is no need to copy elements to delete. LinkedList is to find the location of elements and connect the front and back chains of elements. Basically as shown in the figure below;


Determine the element x to be deleted and replace the links before and after.
If you delete the first and last elements, it will be easier to operate, which is why insertion and deletion are fast. However, if the middle position is deleted, you need to traverse to find the corresponding position.

3.1 delete operation method

Serial numbermethoddescribe
1list.remove();Consistent with removeFirst()
2list.remove(1);To delete the location element node with Idx=1, you need to traverse the location
3list.remove("a");To delete the node with element = "a", you need to traverse and locate
4list.removeFirst();Delete first node
5list.removeLast();Delete end node
6list.removeAll(Arrays.asList("a", "b"));Delete in batches according to the collection, and the bottom layer is Iterator deletion

Source code:

@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"));
}
 

#Source code

The source code of deletion operation is almost the same. It is divided into the chain breaking operation of nodes when deleting head and tail nodes and other nodes. Here we take an example to delete the source code in other places for learning, as follows;

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

This part is element positioning and unlink(x) unlinking. There is no difficulty in finding the corresponding element in a loop.
unlink(x) unlinking

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

This part of the source code mainly has the following knowledge points;

Obtain the information of the node to be deleted; Element item, next node of element, prev node of element.
If the previous node is empty, the next node of the element to be deleted is assigned to the first node; otherwise, the next node of the node to be deleted is assigned to the child node of the previous node of the node to be deleted.
Similarly, for the next node of the node to be deleted, perform the same operation in step 2.
Finally, set the deleted node to null and deduct the number of size and modeCount.
#4. Traversal
Next, the traversal of ArrayList and LinkedList is common, basically including five ways.

Here, we first initialize the set of 10 million data to be traversed;

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

#4.1 ordinary for loop

@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("Time consuming:" + (System.currentTimeMillis() - startTime));
}
 

#4.2 enhanced for loop

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

#4.3 Iterator traversal

@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("Time consuming:" + (System.currentTimeMillis() - startTime))
}
 

#4.4 forEach cycle

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

#4.5 stream

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

So, who is the slowest of the above five traversal methods? Learn and analyze according to our source code. Welcome to leave your answer in the comment area!

5, Summary

If you can't use ArrayList and ArrayList, you can't use ArrayList very well. However, if you can be sure that you will have a large number of insert, delete and get operations at the top of the collection, you can use LinkedList because it has corresponding methods; addFirst, addLast, removeFirst, removeLast, getFirst, getLast. The time complexity of these operations is O(1), which is very efficient.

The linked list structure of LinkedList does not necessarily save space than ArrayList. Firstly, the memory it occupies is not continuous. Secondly, it also needs a large number of instantiated objects to create nodes. Although it does not necessarily save space, the linked list structure is also a very excellent data structure, which can play a very excellent role in your program design. For example, the visual link tracking diagram needs the linked list structure, and each node needs to spin once for the serial business.

The essence of the program is often the design of data structure, which can provide very efficient changes for your program development. Maybe you can't use it yet, but in case you need to build it one day 🚀 Where's the rocket?

Topics: Java data structure linked list