LinkedList is really slow to find, add and delete quickly? Refresh your knowledge!

Test Results

Not much nonsense, first report the test results. The author inserts and finds the time consumed by 100,000 elements at the head, tail, and middle three positions of ArrayList and LinkedList to perform comparative tests. The following are the test results

WeChat screenshot_20201009160035.png

Test conclusion

  • The search performance of ArrayList is absolutely first-class, no matter which position of the element is queried

  • In addition to the better performance of ArrayList insertion at the end (the lower the position, the better the performance), the performance of other positions is not satisfactory

  • LinkedList finds and inserts performance at the beginning and the end are great, but if you operate in the middle position, the performance is far worse, and it is not at an order of magnitude from ArrayList.

Source code analysis

We regard ArrayList and LinkedList in Java as an implementation of sequential table and doubly linked list respectively, so before the source code analysis, let us briefly review the key concepts of sequential table and doubly linked list in the data structure

  • Sequence table: You need to apply for contiguous memory space to save the elements, and you can directly find the logical position of the element through the physical location in the memory. Inserting or deleting an element in the middle of the sequence table requires moving all the elements after the element forward or backward.

  • Doubly linked list: There is no need to apply for continuous memory space to store the elements, and the predecessor and successor elements need to be found through the head and tail pointer of the element (when searching for an element, you need to traverse the entire linked list from the beginning or the end until the target element is found). Inserting or deleting an element in a doubly linked list does not need to move the element, just change the head and tail pointer of the related element.

So we subconsciously think: ArrayList lookup is fast, adding and deleting slow. LinkedList is slow to look up and quick to add and delete. But is this really the case? We together look.

test program

The test program code is basically not nutritious, so I won't post it here, but it is necessary to post the running results of the program to facilitate analysis one by one.

operation result

Inserting 100000 elements at the end of ArrayList takes time: 26ms Inserting 100000 elements at the end of LinkedList takes time: 28ms Inserting 100000 elements at the end of ArrayList takes time: 859ms Inserting 100000 elements at the head of LinkedList takes time: 15ms Inserting 100000 elements in the middle of ArrayList takes time :1848ms Inserting 100000 elements in the middle of LinkedList Time-consuming: 15981ms Time-consuming to read 100,000 elements at the head of ArrayList: 7ms Time-consuming to read 100,000 elements at the head of LinkedList: 11ms Time-consuming to read 100,000 elements at the end of ArrayList: 12ms Reading at the end of LinkedList Time-consuming to fetch 100,000 elements: 9ms Time-consuming to read 100,000 elements in the middle of ArrayList: 13ms Time-consuming to read 100,000 elements in the middle of LinkedList: 34928ms

Insert at the end of ArrayList

Source code

add(E e) method

public boolean add(E e) {// Check whether expansion is needed 
       ensureCapacityInternal(size + 1); // Increments modCount!!
       // Add elements directly at the end
       elementData[size++] = e;
       return true;
   }

It can be seen that inserting the tail of the ArrayList can be directly inserted without additional operations.

Insert at the end of LinkedList

Source code

The head and tail nodes are defined in LinkedList

/**
    * Pointer to first node.
    */
   transient Node<E> first;    /**
    * Pointer to last node.
    */
   transient Node<E> last;

add(E e) method, which calls the linkLast(E e) method

public boolean add(E e) {
       linkLast(e);
       return true;
   }

The linkLast(E e) method, it can be seen that when inserting at the end, there is no need to traverse the entire linked list from the beginning, because the end node has been saved in advance, so you can insert the element directly after the end node

/**
    * Links e as last element.
    */
   void linkLast(E e) {        // 先把原来的尾结点保存下来
       final Node<E> l = last;        // 创建一个新的结点,其头结点指向last
       final Node<E> newNode = new Node<>(l, e, null);        // 尾结点置为newNode
       last = newNode;
       if (l == null)
           first = newNode;
       else            // 修改原先的尾结点的尾结点,使其指向新的尾结点
           l.next = newNode;
       size++;
       modCount++;
   }

总结

对于尾部插入而言,ArrayList与LinkedList的性能几乎是一致的

ArrayList头部插入

源码

add(int index, E element)方法,可以看到通过调用系统的数组复制方法来实现了元素的移动。所以,插入的位置越靠前,需要移动的元素就会越多

public void add(int index, E element) {
       rangeCheckForAdd(index);

       ensureCapacityInternal(size + 1);  // Increments modCount!!
       // 把原来数组中的index位置开始的元素全部复制到index+1开始的位置(其实就是index后面的元素向后移动一位)
       System.arraycopy(elementData, index, elementData, index + 1,
                        size - index);        // 插入元素
       elementData[index] = element;
       size++;
   }

LinkedList头部插入

源码

add(int index, E element)方法,该方法先判断是否是在尾部插入,如果是调用linkLast()方法,否则调用linkBefore(),那么是否真的就是需要重头开始遍历呢?我们一起来看看

public void add(int index, E element) {
       checkPositionIndex(index);

       if (index == size)
           linkLast(element);
       else
           linkBefore(element, node(index));
   }

在头尾以外的位置插入元素当然得找出这个位置在哪里,这里面的node()方法就是关键所在,这个函数的作用就是根据索引查找元素,但是它会先判断index的位置,如果index比size的一半(size >> 1,右移运算,相当于除以2)要小,就从头开始遍历。否则,从尾部开始遍历。从而可以知道,对于LinkedList来说,操作的元素的位置越往中间靠拢,效率就越低

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

这个函数的工作就只是负责把元素插入到相应的位置而已,关键的工作在node()方法中已经完成了

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

to sum up

  • For LinkedList, the time complexity of head insertion and tail insertion are both O(1)

  • But for ArrayList, each insertion of the head needs to move size-1 elements, the efficiency can be imagined

  • But if they are inserted in the middle position, ArrayList is nearly 10 times faster than LinkedList.

ArrayList, LinkedList search

  • This is nothing to say. For ArrayList, no matter where it is, the element is located directly by index, and the time complexity is O(1)

  • For LinkedList search, the core method is the node() method mentioned above, so the head and tail search speed is extremely fast, and the closer to the middle, the lower the efficiency.

At last

Thank you all for seeing here, the article has deficiencies, welcome to point out; if you think it is well written, then give me a thumbs up.



Guess you like

Origin blog.51cto.com/14849432/2540858