Java Collection Series 3, LinkedList of Skeleton Surprise

1. Overview of LinkedList

We have introduced in the previous article that ArrayList and Vector in the List family are like twin brothers. They are implemented from the bottom and have similar functions, except for some personal behaviors (member variables, constructors and method threads). Safety). Next, we'll get to know their other powerful brother: LinkedList

Dependencies of LinkedList:

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
  • 1. Inheriting from AbstractSequentialList, in essence, there is no difference from inheriting AbstractList. AbstractSequentialList improves the methods that are not implemented in AbstractList.
  • 2. Serializable: The member variable Node is modified with transient, and serialization is realized by rewriting the read/writeObject method.
  • 3. Cloneable: Rewrite the clone() method, by creating a new LinkedList object, traversing the copied data to copy the object.
  • 4. Deque: Implements the queue interface in the Collection family, indicating that it has the function of a double-ended queue.

eng~ From the above implementation interface, the overall difference between LinkedList and ArrayList is that LinkedList implements the Queue (Deque) interface in the Collection family and has the function of a double-ended queue . (Just like a child, he not only has the characteristics of his parents, but some of them also have some characteristics of an uncle, such as a nephew who looks like an uncle).

2. LinkedList member variable

    transient int size = 0;

    /**
     * Pointer to first node.
     * Invariant: (first == null && last == null) ||
     *            (first.prev == null && first.item != null)
     */
    transient Node<E> first;

    /**
     * Pointer to last node.
     * Invariant: (first == null && last == null) ||
     *            (last.next == null && last.item != null)
     */
    transient Node<E> last;
    
    
    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;
        }
    }

The member variables of LinkedList are mainly composed of size (data size), first (head node) and last (tail node). Combined with the idea of ​​double-ended linked list in the data structure, each node needs to own and save data (E item), point to the next node (Node next) and point to the previous node (Node prev).

Compared with the member variables of ArrayLit and Vector, LinkedList obviously does not provide the maximum limit of MAX_ARRAY_SIZE . This is because the linked list has no length limit, and its memory address does not need to be allocated a fixed length for storage, only the next node needs to be recorded. The storage address can complete the continuity of the entire linked list.

Extended thinking : What are the differences between JDK 1.8 and JDK 1.6 in LinkedList?

The main difference is that LinkedList in version 1.6 and before only saves the head and tail of the queue through a header head pointer. This kind of operation can be said to be very deep, but in terms of code readability, it deepens the difficulty of reading the code. Therefore, in subsequent JDK updates, the head node and the tail node are distinguished. The node class has also been renamed Node.

3. LinkedList constructor

LinkedList provides only two constructors:

  • LinkedList()
  • LinkedList(Collection<? extends E> c)

In JDK1.8, LinkedList's constructor LinkedList() is an empty method and does not provide any special operations. Different from JDK1.6, the header is initialized as an empty pointer object.

3.1 LinkedList()

JDK 1.6

private transient Entry<E> header = new Entry<E>(null, null, null);
    public LinkedList() {
        header.next = header.previous = header;
    }

When JDK 1.8 is in use, the first node will be created.

    public LinkedList() {
    }

3.2 LinkedList(Collection<? extends E> c)

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

This construction method mainly creates objects by calling addAll, which will be described in detail when the LinkedList addition method is introduced.

3.3 Summary

In the implementation of the new version of LinkedList, in addition to distinguishing the head node and the tail node, it pays more attention to memory allocation during use, which is similar to ArrayList (the default constructor of ArrayList is to create an empty array object).

4. Add method (Add)

LinkedList inherits AbstractSequentialList (AbstractList) and implements the Deque interface at the same time. Therefore, he is adding methods, including the operations of both:

AbstractSequentialList:

  • add (E and)
  • add(int index,E e)
  • addAll(Collection<? extends E> c)
  • addAll(int index, Collection<? extends E> c)

About

  • addFirst (E and)
  • addLast(E e)
  • offer(E e)
  • offerFirst(E e)
  • offerLast (E e)

4.1 add(E e) & addLast(E e) & offer(E e) & offerLast(E e)

Although LinkedList implements the adding methods of List and Deque respectively, but in a sense, these methods have something in common. For example, when we call the add(E e) method, whether it is a list such as ArrayList or Vector, it is added at the end of the array by default, so it has the same charm as adding a node addLast(E e) at the end of the queue. Therefore, from the source code of LinkedList, the underlying operations of these methods are actually the same.

    public boolean add(E e) {
        linkLast(e);
        return true;
    }
    
    public void addLast(E e) {
        linkLast(e);
    }
    
     public boolean offer(E e) {
        return add(e);
    }
    
    public boolean offerLast(E e) {
        addLast(e);
        return true;
    }
    
    void linkLast(E e) {
        final Node<E> l = last;
        final Node<E> newNode = new Node<>(l, e, null);
        last = newNode;
        if (l == null)
            first = newNode;
        else
            l.next = newNode;
        size++;
        modCount++;
    }

We mainly analyze the linkLast method:

  • Get the tail node (last)
  • Create an insert node and set the previous node to last and the next node to null.
  • Set the new node as the last node (last)
  • If l (initial end node) == null, it means this is the first operation, and the newly added head node
  • Otherwise, set the next node of l (initial end node) as the newly added node
  • size + 1, operation count + 1

Extended thinking : Why does the internal variable Node l need to be modified with final?

4.2 addFirst(E e) & offerFirst(E e)

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

    private void linkFirst(E e) {
        final Node<E> f = first;
        final Node<E> newNode = new Node<>(null, e, f);
        first = newNode;
        if (f == null)
            last = newNode;
        else
            f.prev = newNode;
        size++;
        modCount++;
    }

As can be seen from the above code, offerFirst and addFirst are actually the same operation, but the returned data types are different. The linkFirst method is actually the same idea as linkLast, and it will not be described in detail here.

4.3 add(int index,E e)

Here we mainly talk about why LinkedList is better than ArrayList in adding and removing elements.

    public void add(int index, E element) {
        checkPositionIndex(index);
        // 如果插入节点为末尾,直接插入
        if (index == size)
            linkLast(element);
        // 否则,找到该节点,进行插入
        else
            linkBefore(element, node(index));
    }
    
    Node<E> node(int index) {
        // 这里顺序查找元素,通过二分查找的方式,决定从头或尾节点开始进行查找,时间复杂度为 n/2
        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;
        }
    }
    
    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++;
    }

The flow of LinkedList in add(int index, Element e) method

  • Judging the validity of subscripts
  • If the caret position is the end, insert directly
  • Otherwise, traverse 1/2 of the linked list to find the node with index subscript
  • Set the front and back nodes of the new node through succ

The reason why LinkedList is better than ArrayList in inserting data is mainly because in the process of inserting data (linkBefore), the insertion calculation only needs to set the front and rear nodes of the node, while ArrayList needs to move the entire array data backward. (

System.arraycopy(elementData, index, elementData, index + 1,size - index);

4.4 addAll(Collection<? extends E> c)

In the two addAll methods provided in LinkedList, the internal implementation is the same, mainly through: addAll(int index, Collection<? extends E> c):

    public boolean addAll(Collection<? extends E> c) {
        return addAll(size, c);
    }
    
      public boolean addAll(int index, Collection<? extends E> c) {
        checkPositionIndex(index);
        //将集合转化为数组
        Object[] a = c.toArray();
        int numNew = a.length;
        if (numNew == 0)
            return false;

        Node<E> pred, succ;
        //获取插入节点的前节点(prev)和尾节点(next)
        if (index == size) {
            succ = null;
            pred = last;
        } else {
            succ = node(index);
            pred = succ.prev;
        }
        //将集合中的数据编织成链表
        for (Object o : a) {
            @SuppressWarnings("unchecked") E e = (E) o;
            Node<E> newNode = new Node<>(pred, e, null);
            if (pred == null)
                first = newNode;
            else
                pred.next = newNode;
            pred = newNode;
        }
        //将 Collection 的链表插入 LinkedList 中。
        if (succ == null) {
            last = pred;
        } else {
            pred.next = succ;
            succ.prev = pred;
        }

        size += numNew;
        modCount++;
        return true;
    }
    

4.5 Summary

LinkedList is better than ArrayList in inserting data, mainly because it only needs to modify the pointer's point, and does not need to transfer the data of the entire array. However, LinkedList is better than not implementing RandomAccess, or because it does not support index search. It takes more time to operate (n/2) in the operation of finding elements.

5. Remove method (Remove)

AbstractSequentialList

  • remove(int index)
  • remove(Object o)

About

  • remove()
  • removeFirst()
  • removeLast()
  • removeFirstOccurrence(Object o)
  • removeLastOccurrence(Object o)

5.1 remove(int index)&remove(Object o)

In ArrayList, the remove(Object o) method is to traverse the array, find the subscript, and delete it through fastRemove (similar to remove(int i)). LinkedList, on the other hand, traverses the linked list, finds the target node (node), and deletes it through unlink: Here we mainly look at the unlink method:

    E unlink(Node<E> x) {
        // assert x != null;
        final E element = x.item;
        final Node<E> next = x.next;
        final Node<E> prev = x.prev;

        if (prev == null) {
            first = next;
        } else {
            prev.next = next;
            x.prev = null;
        }

        if (next == null) {
            last = prev;
        } else {
            next.prev = prev;
            x.next = null;
        }

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

The whole process is:

  • Get the next and prev of the target node
  • If prev is empty, the target node is the head node
  • Set first as the next node of the target node (next)
  • Otherwise, set the next node of the prev node to next (that is, remove it from the re-linked list)
  • If next is empty, the target node is the tail node
  • Set last to the previous node of the target node
  • Otherwise, set the previous node of the next node to prev
  • set the target node to null

It can be seen that the delete method is similar to the add method, only the node relationship needs to be modified, which avoids the array translation similar to ArrayList and greatly reduces the time loss.

5.2 Remove in Deque

The main process of removeFirstOccurrence and removeLastOccurrence in Deque is to traverse from the first/last node first. When the first target object is found, use remove (Object o) to delete the object. Nothing special in general.

The slight difference is the removeFirst() and removeLast() methods in Deque. In the underlying implementation, since it is clear that the deleted object is the first/last object, the deletion operation will be simpler:

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

    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;
        if (next == null)
            last = null;
        else
            next.prev = null;
        size--;
        modCount++;
        return element;
    }

The overall operation is to set the next of the first node to be the new head node, and then clear f. The removeLast operation is similar.

Combined with the idea of ​​​​queue, both removeFirst and removeLast return data E, which is equivalent to our dequeue operation ( pollFirst / pollLast )

6 LinkedList Double-ended linked list

The reason why we say LinkedList is a double-ended linked list is because it implements the Deque interface and supports some operations of the queue. Let's take a look at what methods are implemented:

  • pop()
  • poll()
  • push()
  • peek()
  • offer()

It can be seen that the methods provided in Deque mainly include the above methods. Next, let's take a look at how these methods are implemented in LinkedList.

6.1 pop() & poll()

Source code of LinkedList#pop :

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

As can be seen from the above code, the operation of Pop() is that the head element of the queue is dequeued, and an exception will be thrown if the first is empty.

Source code of LinkedList#poll :

    public E poll() {
       final Node<E> f = first;
       return (f == null) ? null : unlinkFirst(f);
   }

Comparing the source code of pop and poll, you can see that although first is also dequeued, the difference is that if first is null, the pop() method will throw an exception .

6.2 push()

The underlying implementation of the push() method is actually calling addFirst(Object o):

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

The operation of the push() method is mainly similar to the push operation in the stack.

6.3 peek()

The LinkedList#peek operation is mainly to take the value of the head element of the queue (according to the FIFO of the queue, peek is to take the head data)

    public E peek() {
       final Node<E> f = first;
       return (f == null) ? null : f.item;
   }
   

6.3 offer()

The offer() method adds methods for direct invocation.

    public boolean offer(E e) {
       return add(e);
   }

7 LinkedList Traversal

Because LinkedList does not implement RandomAccess, it will be very ineffective when traversing in the form of random access. In addition, LinkedList provides similar traversal through Iterator, node prev or next traversal, and for loop traversal, all of which have good results.

8 Summary

There is not much expansion thinking, and the brain is not clear enough. Generally speaking, the source code and analysis of the small family under the List interface are completed. I have a better understanding of each member. During the interview, I will not give a simple answer. LinkedList has better insertion and deletion performance, ArrayList can locate elements quickly, and Vector is thread-safe. Only when you fully understand its implementation, you will find that although your answer is correct, it is only 60 points. If you want to answer every question perfectly, then please think carefully and understand it carefully.

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325391243&siteId=291194637