[JDK source code] LinkedList source code analysis

LinkedList source code analysis

1. Introduction

  • Through the inheritance system, we can see that LinkedList not only implements the List interface, but also implements the Queue and Deque interfaces, so it can be used as a List, a double ended Queue, and of course, a stack.
  • It can be seen from the inheritance system that LinkedList implements Cloneable and Serializable interfaces, indicating that it can be cloned or serialized! Similarly, when LinkedList is cloned, it is the same as ArrayList. Both are shallow copies.

2. Main attributes

// Number of elements
transient int size = 0;

// List head node
transient Node<E> first;

// End of list node
transient Node<E> last;

The attribute is very simple. It defines the number of elements, size and the beginning and end nodes of the linked list. transient guarantees that it will not be serialized.

3. Main internal categories

Typical double linked list structure:

private static class Node<E> {
    E item; // Elements stored by the current node
    Node<E> next; // precursor
    Node<E> prev; // Rear drive

    Node(Node<E> prev, E element, Node<E> next) {
        this.item = element;
        this.next = next;
        this.prev = prev;
    }
}

4. Main construction methods

//Nonparametric structure
public LinkedList() {
}

//Parametric structure
public LinkedList(Collection<? extends E> c) {
    this();
    addAll(c);
}

5. Add elements

As a double ended queue, there are two main ways to add elements: one is to add elements at the end of the queue, and the other is to add elements at the head of the queue. These two forms are mainly realized in the LinkedList through the following two methods.

Queue header add

public void addFirst(E e) {
    linkFirst(e);
}
// Add element to queue header
private void linkFirst(E e) {
    // First node
    final Node<E> f = first;
    // Create a new node
    final Node<E> newNode = new Node<>(null, e, f);
    // Make the new node the first node
    first = newNode;
    // Determine whether it is the first element added
    // If so, set last as the new node
    //Otherwise, set the prev pointer of the original first node to the new node
    if (f == null)
        last = newNode;
    else
        f.prev = newNode;
    // Number of elements plus one
    size++;
    // The number of modifications plus one indicates that this is a set that supports fail fast
    modCount++;
}

Add at the end of queue

public void addLast(E e) {
    linkLast(e);
}
// Add element from end of queue
void linkLast(E e) {
    // Queue tail node
    final Node<E> l = last;
    // Create a new node. The prev of the new node is the tail node
    final Node<E> newNode = new Node<>(l, e, null);
    // Make the new node the new tail node
    last = newNode;
    // Determine whether it is the first element added
    // If so, set first as the new node
    // Otherwise, set the next pointer of the original node to the new node
    if (l == null)
        first = newNode;
    else
        l.next = newNode;
    //Number of elements plus one
    size++;
    // Number of modifications plus one
    modCount++;
}

As an unbounded queue, adding elements always succeeds:

public boolean offerFirst(E e) {
    addFirst(e);
    return true;
}

public boolean offerLast(E e) {
    addLast(e);
    return true;
}

As a List, you need to support adding elements in the middle, mainly through the following method.

Add at specified location

//Adds an element at the specified index location
public void add(int index, E element) {
    //Check for out of bounds
    checkPositionIndex(index);
    //If index is a position after the end of the queue node
    //Add the new node directly after the tail node
    //Otherwise, the linkBefore() method is called to add nodes in the middle
    if (index == size)
        linkLast(element);
    else
        linkBefore(element, node(index));
}
//Find the node corresponding to the index location
Node<E> node(int index) {
    //Because it is a double linked list, it determines whether to traverse from the front or from the back according to whether the index is in the first half or the second half
    //In this way, index can traverse half the elements less
    if (index < (size >> 1)) {
        //If it's in the first half
        //Previous traversal
        Node<E> x = first;
        for (int i = 0; i < index; i++)
            x = x.next;
        return x;
    } else {
        //In the second half if
        //Just traverse from the back
        Node<E> x = last;
        for (int i = size - 1; i > index; i--)
            x = x.prev;
        return x;
    }
}

// Add element before node succ
void linkBefore(E e, Node<E> succ) {
    // Find the front node of the node to be added, that is, the front node of succ
    final Node<E> pred = succ.prev;
    // Create a new node between its predecessor and successor
    final Node<E> newNode = new Node<>(pred, e, succ);
    // Modify the leading pointer of the subsequent node to point to the new node
    succ.prev = newNode;
    // Judge whether the front node is empty
    //If it is empty, it indicates that it is the first added element. Modify the first pointer
    //Copy and modify the nxet of the front node as a new node
    if (pred == null)
        first = newNode;
    else
        pred.next = newNode;
    //Number of elements + 1
    size++;
    //Modification times + 1
    modCount++;
}

The method of adding elements in the middle of LinkedList is also very simple. A typical double linked list adds elements in the middle.

summary

The three ways to add elements are roughly as shown in the following figure:

  • Adding elements at the beginning and end of the queue is very efficient, and the time complexity is O(1).
  • Adding elements in the middle is inefficient. First, find the node at the insertion position, and then modify the pointers of the front and rear nodes. The time complexity is O(n).

6. Delete element

As a double ended queue, there are two ways to delete elements, one is to delete elements at the beginning of the queue, and the other is to delete elements at the end of the queue.

As a List, it also supports intermediate deletion of elements, so there are three methods to delete elements, as follows:

Delete header node

//An exception is thrown if no element is remove d
public E removeFirst() {
    final Node<E> f = first;
    if (f == null)
        throw new NoSuchElementException();
    return unlinkFirst(f);
}

//poll returns null if there is no element
public E pollFirst() {
    final Node<E> f = first;
    return (f == null) ? null : unlinkFirst(f);
}

//Delete first node
private E unlinkFirst(Node<E> f) {
    //Element value of the first node
    final E element = f.item;
    //next pointer of the first node
    final Node<E> next = f.next;
    //Set the value of the first node to null to assist the GC
    f.item = null;
    f.next = null; // help GC
    //Take the next of the first node as the new first node
    first = next;
    //If only one element is deleted, set last to null
    //Otherwise, set the leading pointer of next to null
    if (next == null)
        last = null;
    else
        next.prev = null;
    //Number of elements minus one
    size--;
    //Number of modifications plus one
    modCount++;
    //Returns the deleted element
    return element;
}

Delete tail node

//An exception is thrown if no element is remove d
public E removeLast() {
    final Node<E> l = last;
    if (l == null)
        throw new NoSuchElementException();
    return unlinkLast(l);
}

//poll returns null if there is no element
public E pollLast() {
    final Node<E> l = last;
    return (l == null) ? null : unlinkLast(l);
}

//Delete tail node
private E unlinkLast(Node<E> l) {
    //Element value of tail node
    final E element = l.item;
    //Leading pointer of tail node
    final Node<E> prev = l.prev;
    //Clear the contents of the tail node to assist the GC
    l.item = null;
    l.prev = null; // help GC
    //Let the front node be called the new tail node
    last = prev;
    //If there is only one element, delete and set first to null
    //Otherwise, set the next of the preceding node to null
    if (prev == null)
        first = null;
    else
        prev.next = null;
    //Number of elements minus one
    size--;
    //Number of modifications plus one
    modCount++;
    //Returns the deleted element
    return element;
}

Delete intermediate specified node

//Delete intermediate node
public E remove(int index) {
    //Check for out of bounds
    checkElementIndex(index);
    //Delete the node at the specified index location
    return unlink(node(index));
}

//Delete specified node
E unlink(Node<E> x) {
    // Element value of node x
    final E element = x.item;
    // Front node of x
    final Node<E> next = x.next;
    // Post node of x
    final Node<E> prev = x.prev;
    //If the front node is empty
    //The description is the first node. Let first point to the post node of x
    //Otherwise, modify the post node whose next of the front node is x
    if (prev == null) {
        first = next;
    } else {
        prev.next = next;
        x.prev = null;
    }
    //If the post node is empty
    //The description is the tail node. Let last point to the front node of x
    //Otherwise, modify the front node whose prev is x
    if (next == null) {
        last = prev;
    } else {
        next.prev = prev;
        x.next = null;
    }
    //Clear the element value of x to assist GC
    x.item = null;
    //Number of elements minus one
    size--;
    //Number of modifications plus one
    modCount++;
    //Returns the deleted element
    return element;
}

summary

The three methods of deleting elements are typical double linked list methods. The general process is shown in the following figure:

  • It is efficient to delete elements at the beginning and end of the queue, and the time complexity is O(1).
  • It is inefficient to delete elements in the middle. First, find the node at the deletion position, and then modify the front and rear pointers. The time complexity is O(n).

7. Removal method

Clear all elements

public void clear() {
    //In order to make GC recycle the placed elements faster, you need to empty the reference relationship between node s.
    for (Node<E> x = first; x != null; ) {
        Node<E> next = x.next;
        x.item = null;
        x.next = null;
        x.prev = null;
        x = next;
    }
    first = last = null;
    //Set the number of elements to 0
    size = 0;
    //Number of modifications plus one
    modCount++;
}

8. As a stack

LinkedList is a double ended queue, which can be used as a stack.

//Element stack
public void push(E e) {
    // The head element of the queue is the top element of the stack
    addFirst(e);
}

//Delete the stack top element and return
public E pop() {
    // The head element of the queue is the top element of the stack
    return removeFirst();
}

The feature of the stack is LIFO, so it is easy to use as a stack. Adding and deleting elements only operate on the first node of the queue.

9. Summary

  1. LinkedList is a List implemented as a double linked List
  2. LinkedList is also a double ended queue, which has the characteristics of queue, double ended queue and stack
  3. LinkedList is very efficient in adding and deleting elements at the beginning and end of the queue, and the time complexity is O(1)
  4. LinkedList is inefficient to add and delete elements in the middle, and the time complexity is O(n)
  5. LinkedList does not support random access, so it is inefficient to access non queue elements
  6. LinkedList is functionally equal to ArrayList + ArrayDeque

Keywords: Java JDK Interview linked list Container

Added by beboni on Sun, 02 Jan 2022 05:48:45 +0200