Implementation principle of LinkedList

Keywords: Java list

1, Overview

Let's take a look at this comment in the source code. Let's try to extract some information from it:

/**
 * Doubly-linked list implementation of the {@code List} and {@code Deque}
 * interfaces.  Implements all optional list operations, and permits all
 * elements (including {@code null}).
 *
 * <p>All of the operations perform as could be expected for a doubly-linked
 * list.  Operations that index into the list will traverse the list from
 * the beginning or the end, whichever is closer to the specified index.
 *
 * <p><strong>Note that this implementation is not synchronized.</strong>
 * If multiple threads access a linked list concurrently, and at least
 * one of the threads modifies the list structurally, it <i>must</i> be
 * synchronized externally.  (A structural modification is any operation
 * that adds or deletes one or more elements; merely setting the value of
 * an element is not a structural modification.)  This is typically
 * accomplished by synchronizing on some object that naturally
 * encapsulates the list.
 */
  • From this comment, we can know that LinkedList is implemented through a two-way linked list, which allows the insertion of all elements, including null. At the same time, it is asynchronous.

[the external chain image transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-7rlinlzx-163608740213) (images / 2. Implementation principle of LinkedList / image-20211101103951991.png)]

In addition to the data field, each node of the two-way linked list also has a front pointer and a rear pointer, which point to the predecessor node and the successor node respectively (if there is a predecessor / successor). In addition, the two-way linked list also has a first pointer to the head node and a last pointer to the tail node.

2, Attributes

// Number of nodes in the linked list
transient int size = 0;

// Pointer to header node
transient Node<E> first;

// Pointer to tail node
transient Node<E> last;

3, Method

Node is a static internal class defined in LinkedList. It represents the structure of each node in the linked list, including a data field item, a post pointer next and a pre pointer prev.

1. Node structure

private static class Node<E> {
    E item;
    Node<E> next;
    Node<E> prev;

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

2. Add element

For the linked list data structure, the operation of adding elements is nothing more than inserting elements at the header / footer, or inserting elements at a specified position. Because the LinkedList has a head pointer and a tail pointer, it only takes O(1) to insert elements into the header or tail of the table, while it needs to traverse the linked list before inserting elements at the specified position, so the complexity is O(n).

  • The process of adding elements to the header is as follows:

[the external chain image transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-tjybgnyo-163608740214) (images / 2. Implementation principle of LinkedList / image-20211101104252005.png)]

When inserting a node into the header, it is obvious that the precursor of the current node must be null, and the subsequent node is the node pointed to by the first pointer. Of course, it is necessary to modify the first pointer to point to the new header node. In addition, the original head node becomes the second node, so you need to modify the precursor pointer of the original head node to point to the header node

private void linkFirst(E e) {
    final Node<E> f = first;
    // The predecessor of the current node points to null, and the successor pointer to the original header node
    final Node<E> newNode = new Node<>(null, e, f);
    // The header pointer points to the new header node
    first = newNode;
    // If the original node has a head, update the precursor pointer of the original node; otherwise, update the tail pointer
    if (f == null)
        last = newNode;
    else
        f.prev = newNode;
    size++;
    modCount++;
}
  • Adding elements at the end of the table is similar to adding elements at the header, as shown in the figure:

[the external chain image transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-fmxtahke-163608740216) (images / 2. Implementation principle of LinkedList / image-20211101104808228.png)]

When inserting a node into the footer, it is obvious that the successor of the current node must be null, and the predecessor node is the node pointed to by the last pointer, and then modify the last pointer to point to the new footer node. In addition, the successor pointer of the original tail node should be modified to point to the new tail node

void linkLast(E e) {
    final Node<E> l = last;
    // The predecessor of the current node points to the tail node, and the successor points to null
    final Node<E> newNode = new Node<>(l, e, null);
    // The tail pointer points to the new tail node
    last = newNode;
    // If there is a tail node, the subsequent pointer of the original node is updated; otherwise, the header pointer is updated
    if (l == null)
        first = newNode;
    else
        l.next = newNode;
    size++;
    modCount++;
}
  • Finally, insert before specifying the node, as shown in the figure:

[the external chain picture transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the picture and upload it directly (img-7x23h6cx-163608740218) (images / 2. Implementation principle of LinkedList / image-20211101104943855.png)]

When a node is inserted before the specified node, the successor of the current node is the specified node, and the predecessor node is the predecessor node of the specified node. In addition, it is also necessary to modify the successor of the predecessor node as the current node and the predecessor of the successor node as the current node

void linkBefore(E e, Node<E> succ) {
    // assert succ != null;
    // Specifies the precursor of the node
    final Node<E> pred = succ.prev;
    // The precursor of the current node is the precursor of the pointing node, and the successor is the specified node
    final Node<E> newNode = new Node<>(pred, e, succ);
    // Updates the precursor of the specified node to the current node
    succ.prev = newNode;
    // Update the successor of the predecessor node
    if (pred == null)
        first = newNode;
    else
        pred.next = newNode;
    size++;
    modCount++;
}

3. Delete element

The deletion operation is similar to the addition operation. For example, the process of deleting a specified node is shown in the following figure. The successor of the predecessor node of the current node needs to be modified to the successor of the current node, and the predecessor of the successor node of the current node needs to be modified to the predecessor of the current node (is it easy?):

[the external link image transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-ovfd7uvu-163608740221) (images / 2. Implementation principle of LinkedList / image-20211101105140691.png)]

Deleting the head node and the tail node is very similar to deleting the specified node. I won't introduce them one by one. The source code is as follows:

// Delete the header node and return the value of the header element
private E unlinkFirst(Node<E> f) {
    // assert f == first && f != null;
    final E element = f.item;
    final Node<E> next = f.next;
    f.item = null;
    f.next = null; // help GC
    first = next; // The header pointer points to the next node
    if (next == null)
        last = null;
    else
        next.prev = null;// The precursor of the new header node is null
    size--;
    modCount++;
    return element;
}


// Delete the footer node and return the value of the footer element
private E unlinkLast(Node<E> l) {
    // assert l == last && l != null;
    final E element = l.item;
    final Node<E> prev = l.prev;
    l.item = null;
    l.prev = null; // help GC
    last = prev;// The tail pointer points to the previous node
    if (prev == null)
        first = null;
    else
        prev.next = null;// The successor of the new tail node is null
    size--;
    modCount++;
    return element;
}

// Deletes the specified node and returns the value of the specified element
E unlink(Node<E> x) {
    // assert x != null;
    final E element = x.item;
    final Node<E> next = x.next;// Successor of current node
    final Node<E> prev = x.prev;// Precursor of current node

    if (prev == null) {
        first = next;
    } else {
        prev.next = next;// Update the successor of the predecessor node to the successor of the current node
        x.prev = null;
    }

    if (next == null) {
        last = prev;
    } else {
        next.prev = prev;// Update the predecessor of the subsequent node to the predecessor of the current node
        x.next = null;
    }

    x.item = null;
    size--;
    modCount++;
    return element;
}

4. Get element

// Get header element
public E getFirst() {
    final Node<E> f = first;
    if (f == null)
        throw new NoSuchElementException();
    return f.item;
}
// Get footer element
public E getLast() {
    final Node<E> l = last;
    if (l == null)
        throw new NoSuchElementException();
    return l.item;
}

// Gets the element of the specified subscript
Node<E> node(int index) {
    // assert isElementIndex(index);

    // According to whether the subscript exceeds half the length of the linked list, you can choose whether to traverse from the head or from the tail
    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;
    }
}

5. Common methods

The addition and deletion of linked lists were introduced earlier. You will find that those methods are not public. LinkedList operates on these basic methods. Let's see what methods we can call.

// Delete header element
public E removeFirst() {
    final Node<E> f = first;
    if (f == null)
        throw new NoSuchElementException();
    return unlinkFirst(f);
}

// Delete footer element
public E removeLast() {
    final Node<E> l = last;
    if (l == null)
        throw new NoSuchElementException();
    return unlinkLast(l);
}

// Insert new header node
public void addFirst(E e) {
    linkFirst(e);
}

// /Insert new footer node
public void addLast(E e) {
    linkLast(e);
}

// Size of linked list
public int size() {
    return size;
}

// Add element to footer
public boolean add(E e) {
    linkLast(e);
    return true;
}

// Deletes the specified element
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;
}

// Gets the element of the specified subscript
public E get(int index) {
    checkElementIndex(index);
    return node(index).item;
}

// Replaces the value of the specified subscript
public E set(int index, E element) {
    checkElementIndex(index);
    Node<E> x = node(index);
    E oldVal = x.item;
    x.item = element;
    return oldVal;
}

// Inserts a node at the specified location
public void add(int index, E element) {
    checkPositionIndex(index);

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

// Deletes the node with the specified subscript
public E remove(int index) {
    checkElementIndex(index);
    return unlink(node(index));
}

// Get the value of the header node. If the header is empty, null will be returned
public E peek() {
    final Node<E> f = first;
    return (f == null) ? null : f.item;
}

// Get the value of the header node. If the header is empty, an exception will be thrown
public E element() {
    return getFirst();
}

// Get the value of the header node and delete the header node. If the header is empty, null will be returned
public E poll() {
    final Node<E> f = first;
    return (f == null) ? null : unlinkFirst(f);
}

// Add element to header
public void push(E e) {
    addFirst(e);
}

// Delete header element
public E pop() {
    return removeFirst();
}

4, Summary

  1. The underlying structure of LinkedList is a two-way linked list with head / tail pointers, which can quickly operate the head / tail nodes.
  2. Compared with array, the characteristic of linked list is that it is more efficient to insert and delete elements at the specified position, but the efficiency of search is not as high as array.

Posted by adam119 on Wed, 03 Nov 2021 23:57:51 -0700