Analyze LinkedList from the design source code

Posted by hjunw on Mon, 24 Jan 2022 03:11:53 +0100

I'm so tired to write this blog. In addition, it took me 6 hours to write and read the source code. It can be said that I was extremely lucky and bitter. Too tired, too tired!!!
Linked list is a linear list with linked storage. The memory addresses of all elements are not necessarily continuous.

Dynamic array has an obvious disadvantage -------- -- > it may waste a lot of storage space, and the linked list can apply for as much memory as it can use.

Here, I design the source code, take the single linked list as an example, then expand it to double linked list, and then expand it to circular double linked list. Step by step, I will analyze the essence of LinkedList. Let's analyze it with me. (Note: the official LinkedList source code is acyclic)

A List interface is officially set to inherit the Collection interface. Here, we also set an interface as List, but do not inherit the Collection.

public interface List<E> extends Collection<E>//The official interface is designed like this

Most of the interfaces of the linked list are consistent with the dynamic array. List is the public interface of ArrayList and LinkedList (this is because I have imitated the official design of ArrayList source code before, so I don't need LinkedList to implement the list interface here, which is convenient for expanding other collections in the future). The method type is public by default. ( It is recommended to take a look at my article to analyze dynamic arrays from the design source code, and then take a look at this article for a better understanding: https://blog.csdn.net/boyas/article/details/117829733

public interface List<E> {
    static final int ELEMENT_NOTFOUND = -1;
    void clear();//Clear all elements
    int size();//Number of elements
    boolean isEmpty();//Is it empty
    boolean contains(E element);//Include an element
    void add(E element);//Add element to tail
    E get(int index);//Gets the element of the Index location
    E set(int index,E element);//Element that sets the index location
    void add(int index,E element);//Insert an element in the Index position
    E remove(int index);//Delete element at index position
    int indexOf(E element);//View the index of the element
}

Let's abstract a linear table AbstractList as the parent class of ArrayList and LinkedList, and implement the List interface. Here we extract the public code.

public abstract class AbstractList<E> implements List<E> {
    protected int size;
    //Exception thrown when subscript is out of bounds
    protected void outOfBounds(int index){
        throw new IndexOutOfBoundsException("Index:"+index+"Size:"+size);
    }
    //Check for subscript out of bounds (inaccessible or deleted location)
    protected void rangeCheck(int index){
        if (index<0||index>=size){
            outOfBounds(index);
        }
    }
    //Check that the subscript of add() is out of bounds (you can add an element at the size position)
    protected void  rangeCheckForAdd(int index){
        if (index<0||index>size){
            outOfBounds(index);
        }
    }

    @Override
    public int size() {
        return size;
    }

    @Override
    public boolean isEmpty() {
        return size==0;
    }

    @Override
    public void add(E element) {
        add(size,element);
    }
    @Override
    public boolean contains(E element) {
        return indexOf(element) != ELEMENT_NOTFOUND;
    }
}

Some students may have questions here. There are so many methods in the List interface in front. How can there be so many methods in the AbstractList class? Doesn't it mean that subclasses implementing interfaces need to override all methods in the interface?

Note: let me popularize here. In java, if a subclass is a non abstract class, you must implement all methods in the interface. If a subclass is an abstract class, you can not implement all methods in the interface, because abstract methods are allowed in the abstract class.

Next, we enter the design stage of LinkedList source code, where we design an internal static class Node (Node class, Node object is only used internally).

public class LinkedList<E> extends AbstractList<E> {
    private Node<E> first;
    private static class Node<E>{
        E element;//Each node stores one element
        Node<E> next;//Point to next node
        public Node(E element,Node<E> next){
            this.element=element;
            this.next=next;
        }
    }
  }

The implementation of other methods is different from ArrayList. Let's take a look at the clear() method. Clear means empty. To empty the linked list (note that it is a single linked list at this time), first, the size should be equal to 0, then the memory should be destroyed, and first should be equal to null. Some students may have questions here. Do you need to set next to null? (that is, whether the line between nodes should be broken)

The answer is No. Just set first to null and the remaining object nodes will hang in turn.

@Override
    public void clear() {
        size = 0;
        first = null;//next need not be set to null
    }

Next, we implement the add(int index, E element) method. Another add() method has been placed in the parent class. Here, we only need to implement this add() method. When writing the linked list, we should pay attention to the boundary test. For example, when the index is 0, size-1 and size, we only need to consider that the index is 0. When the incoming index is 0, the new node points to the first and the first pointer points to the new node. If the incoming index is not 0, we only need to find the previous node of the index. Here, we set a method Node node(int index) to obtain the corresponding node of the location. After this method is written, we can write out the E get(int index) and E set(int index,E element) methods.

 @Override
    public E get(int index) {//Get the element corresponding to a location
        return node(index).element;
    }

    @Override
    public E set(int index, E element) {
        Node<E> node = node(index);
        E old = node.element;//Save the previous element in old
        node.element = element;//cover
        return old;
    }
    
@Override
    public void add(int index, E element) {//Insert an element in the index position
        if(index == 0){//Add to 0 location
            first = new Node<>(element,first);
        }else {
            Node<E> prev = node(index-1);//Get previous node
            prev.next = new Node<>(element,prev.next);//Insert a node
        }
        size++;//Linked list length + 1
    }
    private Node<E> node(int index){//Get the node corresponding to a location
        rangeCheck(index);//Check index
        Node<E> node = first;
        for (int i = 0; i < index ; i++) {//Traversal node
            node = node.next;
        }
        return node;
    }

Next, let's look at the delete element - remove(int index) method. The delete and add methods are very similar. When the incoming index is not 0, the node in front of the index points to the node behind the index and overwrite the index. When the incoming index is 0, you only need to point first to first Next is OK. Isn't it very simple!

 @Override
    public E remove(int index) {
        Node<E> node = first;//Save deleted elements
        if (index == 0){//Delete element at 0
            first = first.next;
        }else{
            Node<E> prev= node(index-1);
            node = prev.next;
            prev.next=node.next;
        }
        size--;//Reduce the length of the linked list by one
        return node.element;
    }

Next, let's look at the indexOf(E element) method, which is very similar to the indexOf() method of ArrayList.

@Override
    public int indexOf(E element) {
        if(element == null){
            Node<E> node = first;
            for (int i = 0; i <size ; i++) {//Traversal node
                if (node.element == null) return i;
                node = node.next;
            }
        }else{
            Node<E> node = first;
            for (int i = 0; i < size; i++) {
                if(element.equals(node.element)) return i ;
                node = node.next;
            }
        }
        return ELEMENT_NOTFOUND;
    }

Next, we need to print the linked list and write a toString() method. There is nothing to talk about. The only thing to note is that the for loop can be replaced by a while loop, such as while(node!=null).

@Override
    public String toString() {
        StringBuilder string = new StringBuilder();
        string.append("size=").append(size).append(",[");
        Node<E> node = first;
        for (int i = 0; i < size; i++) {
            if (i!=0){
                string.append(",");
            }
            string.append(node.element);
            node = node.next;
        }
        string.append("]");
        return string.toString();
    }
}

After the test, the given data are as follows, and the final results should be 10,30,40.

public class Main {
    public static void main(String[] args) {
        List<Integer> list = new LinkedList<>();//Single linked list
        list.add(20);
        list.add(0,10);
        list.add(30);
        list.add(list.size(),40);
        list.remove(1);
        System.out.println(list);
    }
}


This is the end of the source code design of the single linked list. Then let's take a look at the double linked list (it's disgusting. You can feel it when you have wood. Hey, there's a circular double linked list behind it. It's even more disgusting). In fact, the linked list is not difficult. The red and black tree, hash table, bloom filter and jump list behind it are really disgusting. Take your time. I can only write this blog at my speed,
Wait a minute. I'm a little sleepy. I'll do it at night. Send it out first...

Topics: Java Algorithm data structure linked list abstract class