[JAVA collection articles] LinkedList detailed explanation


Introduction

In the last article, we analyzed ArrayListthe usage and source code in detail: [JAVA collection articles] Detailed explanation of the source code of ArrayList . In this chapter, we will talk about LinkedListthe usage and source code, LinkedListwhich ArrayListis completely different from the data structure. The bottom layer of ArrayList is an array structure, while the bottom layer of LinkedList It is a linked list structure, which can perform efficient insertion and removal operations, and it is based on a doubly linked list structure.

The overall structure diagram of LinkedList

image-20220210193621308

It can also be seen from the figure that LinkedList has a lot of Nodes, and it also stores the information of the head and tail nodes firstwith lastthese two variables; in addition, it is not a circular doubly linked list, because it is null before and after, this It is also where we need to pay attention.

inheritance system

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
    
    ...}

Through the inheritance system, we can see that LinkedList not only implements Listthe interface, but also implements Queuethe and Dequeinterface, so it can be used not only as a List, but also as a double-ended queue, and of course it can also be used as a stack.

Source code analysis

main attributes

// 元素个数
transient int size = 0;
// 链表首节点
transient Node<E> first;
// 链表尾节点
transient Node<E> last;

Node node

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

Construction method

public LinkedList() {
    
    
}

public LinkedList(Collection<? extends E> c) {
    
    
    this();
    //将集合C中的所有的元素都插入到链表中
    addAll(c);
}

add element

As a double-ended queue, there are two main types of adding 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 implemented in LinkedList through the following two methods.

// 从队列首添加元素
private void linkFirst(E e) {
    
    
    // 首节点
    final Node<E> f = first;
    // 创建新节点,新节点的next是首节点
    final Node<E> newNode = new Node<>(null, e, f);
    // 让新节点作为新的首节点
    first = newNode;
    // 判断是不是第一个添加的元素
    // 如果是就把last也置为新节点
    // 否则把原首节点的prev指针置为新节点
    if (f == null)
        last = newNode;
    else
        f.prev = newNode;
    // 元素个数加1
    size++;
    // 修改次数加1,说明这是一个支持fail-fast的集合
    modCount++;
}

// 从队列尾添加元素
void linkLast(E e) {
    
    
    // 队列尾节点
    final Node<E> l = last;
    // 创建新节点,新节点的prev是尾节点
    final Node<E> newNode = new Node<>(l, e, null);
    // 让新节点成为新的尾节点
    last = newNode;
    // 判断是不是第一个添加的元素
    // 如果是就把first也置为新节点
    // 否则把原尾节点的next指针置为新节点
    if (l == null)
        first = newNode;
    else
        l.next = newNode;
    // 元素个数加1
    size++;
    // 修改次数加1
    modCount++;
}

public void addFirst(E e) {
    
    
    linkFirst(e);
}

public void addLast(E e) {
    
    
    linkLast(e);
}

// 作为无界队列,添加元素总是会成功的
public boolean offerFirst(E e) {
    
    
    addFirst(e);
    return true;
}

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

The above is viewed as a double-ended queue. Its added elements are divided into first and last added elements. As a List, it is necessary to support adding elements in the middle, mainly through the following method.

// 在节点succ之前添加元素
void linkBefore(E e, Node<E> succ) {
    
    
    // succ是待添加节点的后继节点
    // 找到待添加节点的前置节点
    final Node<E> pred = succ.prev;
    // 在其前置节点和后继节点之间创建一个新节点
    final Node<E> newNode = new Node<>(pred, e, succ);
    // 修改后继节点的前置指针指向新节点
    succ.prev = newNode;
    // 判断前置节点是否为空
    // 如果为空,说明是第一个添加的元素,修改first指针
    // 否则修改前置节点的next为新节点
    if (pred == null)
        first = newNode;
    else
        pred.next = newNode;
    // 修改元素个数
    size++;
    // 修改次数加1
    modCount++;
}

// 寻找index位置的节点
Node<E> node(int index) {
    
    
    // 因为是双链表
    // 所以根据index是在前半段还是后半段决定从前遍历还是从后遍历
    // 这样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;
    }
}

// 在指定index位置处添加元素
public void add(int index, E element) {
    
    
    // 判断是否越界
    checkPositionIndex(index);
    // 如果index是在队列尾节点之后的一个位置
    // 把新节点直接添加到尾节点之后
    // 否则调用linkBefore()方法在中间添加节点
    if (index == size)
        linkLast(element);
    else
        linkBefore(element, node(index));
}

The method of adding elements in the middle is also very simple, a typical double-linked list method of adding elements in the middle.

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

qrcode

Adding elements at the head and tail of the queue is very efficient, and the time complexity is O(1).

Adding elements in the middle is relatively inefficient. First, find the node at the insertion position, and then modify the pointers of the front and back nodes. The time complexity is O(n).

delete element

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

As a List, it also supports deleting elements in the middle, so there are three methods for deleting elements, which are as follows.

// 删除首节点
private E unlinkFirst(Node<E> f) {
    
    
    // 首节点的元素值
    final E element = f.item;
    // 首节点的next指针
    final Node<E> next = f.next;
    // 添加首节点的内容,协助GC
    f.item = null;
    f.next = null; // help GC
    // 把首节点的next作为新的首节点
    first = next;
    // 如果只有一个元素,删除了,把last也置为空
    // 否则把next的前置指针置为空
    if (next == null)
        last = null;
    else
        next.prev = null;
    // 元素个数减1
    size--;
    // 修改次数加1
    modCount++;
    // 返回删除的元素
    return element;
}
// 删除尾节点
private E unlinkLast(Node<E> l) {
    
    
    // 尾节点的元素值
    final E element = l.item;
    // 尾节点的前置指针
    final Node<E> prev = l.prev;
    // 清空尾节点的内容,协助GC
    l.item = null;
    l.prev = null; // help GC
    // 让前置节点成为新的尾节点
    last = prev;
    // 如果只有一个元素,删除了把first置为空
    // 否则把前置节点的next置为空
    if (prev == null)
        first = null;
    else
        prev.next = null;
    // 元素个数减1
    size--;
    // 修改次数加1
    modCount++;
    // 返回删除的元素
    return element;
}
// 删除指定节点x
E unlink(Node<E> x) {
    
    
    // x的元素值
    final E element = x.item;
    // x的前置节点
    final Node<E> next = x.next;
    // x的后置节点
    final Node<E> prev = x.prev;

    // 如果前置节点为空
    // 说明是首节点,让first指向x的后置节点
    // 否则修改前置节点的next为x的后置节点
    if (prev == null) {
    
    
        first = next;
    } else {
    
    
        prev.next = next;
        x.prev = null;
    }

    // 如果后置节点为空
    // 说明是尾节点,让last指向x的前置节点
    // 否则修改后置节点的prev为x的前置节点
    if (next == null) {
    
    
        last = prev;
    } else {
    
    
        next.prev = prev;
        x.next = null;
    }

    // 清空x的元素值,协助GC
    x.item = null;
    // 元素个数减1
    size--;
    // 修改次数加1
    modCount++;
    // 返回删除的元素
    return element;
}
// remove的时候如果没有元素抛出异常
public E removeFirst() {
    
    
    final Node<E> f = first;
    if (f == null)
        throw new NoSuchElementException();
    return unlinkFirst(f);
}
// remove的时候如果没有元素抛出异常
public E removeLast() {
    
    
    final Node<E> l = last;
    if (l == null)
        throw new NoSuchElementException();
    return unlinkLast(l);
}
// poll的时候如果没有元素返回null
public E pollFirst() {
    
    
    final Node<E> f = first;
    return (f == null) ? null : unlinkFirst(f);
}
// poll的时候如果没有元素返回null
public E pollLast() {
    
    
    final Node<E> l = last;
    return (l == null) ? null : unlinkLast(l);
}
// 删除中间节点
public E remove(int index) {
    
    
    // 检查是否越界
    checkElementIndex(index);
    // 删除指定index位置的节点
    return unlink(node(index));
}

The three methods of deleting elements are typical methods of deleting elements in a double-linked list, and the general process is shown in the figure below.

[qrcode

Deleting elements at the head and tail of the queue is very efficient, and the time complexity is O(1).

Deleting elements in the middle is relatively inefficient. First, find the node at the deletion position, and then modify the front and rear pointers. The time complexity is O(n).

the stack

We said earlier that LinkedList is a double-ended queue. Do you remember that double-ended queues can be used as stacks?

/**
 * 利用LinkedList来模拟栈
 * 栈的特点:先进后出
 */
public class Test {
    
    

    private LinkedList<String> linkList = new LinkedList<String>();

    // 压栈
    public void push(String str){
    
    
        linkList.addFirst(str);
    }

    // 出栈
    public String pop(){
    
    
        return linkList.removeFirst();
    }

    // 查看
    public String peek(){
    
    
        return linkList.peek();
    }

    // 判断是否为空
    public boolean isEmpty(){
    
    
        return linkList.isEmpty();
    }
}

class Test1 {
    
    
    public static void main(String[] args) {
    
    
        // 测试栈
        Test test = new Test();
        test.push("我是第1个进去的");
        test.push("我是第2个进去的");
        test.push("我是第3个进去的");
        test.push("我是第4个进去的");
        test.push("我是第5个进去的");
        // 取出
        while (!test.isEmpty()){
    
    
            String pop = test.pop();
            System.out.println(pop);
        }
        // 打印结果
        /*我是第5个进去的
        我是第4个进去的
        我是第3个进去的
        我是第2个进去的
        我是第1个进去的*/
    }
}

The characteristic of the stack is LIFO(Last In First Out)that it is very simple to use as a stack. Adding and deleting elements can only operate the first node of the queue.

Summarize

(1) LinkedList is a List implemented by a double-linked list, so there is no problem of insufficient capacity, so there is no way to expand the capacity.

(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 relatively 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 elements other than the head and tail of the queue.

(6) LinkedList is functionally equal to ArrayList + ArrayDeque.

(7) LinkedList is not thread-safe.

(8) LinkedList can store null values.

classic interview questions

Talk about the difference between ArrayList and LinkedList.

It can be divided into two parts: one is the difference between the underlying implementation of the array and the linked list, and the other is the implementation details of ArrayList and LinkedList.

  • The bottom layer of ArrayList is an array, and the bottom layer of LinkedList is a doubly linked list.

  • Arrays have O(1) query efficiency, and elements can be directly located by subscripts; linked lists can only be queried by traversal when querying elements, which is less efficient than arrays.

  • The efficiency of adding and deleting elements in an array is relatively low, usually accompanied by the operation of copying the array; the efficiency of adding and deleting elements in a linked list is very high, and only need to adjust the pointer of the corresponding position.

The above is a popular comparison between arrays and linked lists. In daily use, both can play a good role in their applicable scenarios.

We often use ArrayList instead of arrays, because it encapsulates many easy-to-use APIs, and it implements an automatic expansion mechanism internally. Since it internally maintains a current capacity pointer size, the time complexity of directly adding elements to ArrayList is O( 1), very convenient to use. LinkedList is often used as the implementation class of Queue queue. Since the bottom layer is a doubly linked list, it can easily provide first-in-first-out operations.

Guess you like

Origin blog.csdn.net/jiang_wang01/article/details/131214996