LinkedList overview
LinkedList is an important implementation of the Java Collections framework. Let’s briefly describe some of the features of LinkedList:
LinkedList
Underlying adoptedTwo-way linked list
Structure;LinkedList
Support for null and duplicate values (a feature of lists);LinkedList
Deque interface, with the characteristics of a double-ended queue, can also be used as a stack;LinkedList
When storing elements, you do not need to expand them as you would with an ArrayList, but the nodes that store elements need extra space to store precursor and subsequent references.LinkedList
The insertion efficiency is relatively high at the head and tail of the linked list. However, when the insertion is performed at the specified position, the node at this position needs to be located. The time complexity of this operation isO(N)
;LinkedList
A non-thread-safe collection class, where multiple threads working on the LinkedList at the same time can raise unexpected exception errors in a concurrent environment.
LinkedList inherits the architecture
View the inheritance system of LinkedList directly through IDEA. The architecture is complicated, so take a look at it one by one.
- AbstractSequentialList;
- Implement List and Deque interface;
- Implement serialization interface;
- Cloneable interface is implemented
AbstractSequentialList class AbstractSequentialList class AbstractSequentialList class AbstractSequentialList class AbstractSequentialList class AbstractSequentialList class AbstractSequentialList class AbstractSequentialList class AbstractSequentialList class AbstractSequentialList AbstractSequentialList provides methods that are basically implemented through ListIterator, such as the get and Add methods below. However, although LinkedList inherits AbstractSequentialList, it does not directly use the methods of the parent class. Instead, it re-implements a set of methods, the implementation of which will be discussed later.
public E get(int index) {
try {
return listIterator(index).next();
} catch (NoSuchElementException exc) {
throw new IndexOutOfBoundsException("Index: "+index); }}public void add(int index, E element) {
try {
listIterator(index).add(element);
} catch (NoSuchElementException exc) {
throw new IndexOutOfBoundsException("Index: "+index); }}// Leave it to subclasses
public abstract ListIterator<E> listIterator(int index);
Copy the code
In addition, as outlined at the beginning of this article, LinkedList implements the Deque interface and features a dual-ended queue.
Member properties of the LinkedList
// Record the actual number of elements in the list
transient int size = 0;
// Maintain the first reference of the linked list
transient Node<E> first;
// Maintain the last node references of the linked list
transient Node<E> last;
Copy the code
You can see that first and last are of type Node, so let’s take a quick look at the inner class in LinkedList
private static class Node<E> {
E item; // The actual element in the node
Node<E> next; // Maintain the successor of the node
Node<E> prev; // Maintain the precursor of the node
// create a new node with parameters: precursor node, insert element reference, successor node
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev; }}Copy the code
It can be seen that the structure of the Node static internal class is relatively simple, each Node maintains its own stored element information + precursor Node reference + successor Node reference. Without going into too much detail here, let’s take a quick look at how the LinkedList is constructed
Constructor of LinkedList
// Create an empty set (the list is empty)
public LinkedList(a) {}// Call your own no-argument constructor to construct an empty Collection, and then add all the elements in the Collection to the list
// If the Collection is passed empty, a null-pointer exception will be thrown
public LinkedList(Collection<? extends E> c) {
this(a); addAll(c); }Copy the code
The main method of LinkedList
The add method
The main methods for adding LinkedList implementations are as follows
-
Add nodes to the end of the list (linkLast method)
-
Add an element to the top of the list (linkFirst method)
-
Add elements to the middle of the list (linkBefore method)
Let’s look at the implementation of these three methods.
(1) linkLast method
public void addLast(E e) {
linkLast(e);
}
Copy the code
LinkLast () : linkLast () : linkLast (); linkLast () : linkLast ();
void linkLast(E e) {
// (1) Obtain the global successor node of the current list instance
final Node<E> l = last;
// create a new Node, as we know from the Node constructor
// The element item in this new node is the generic reference passed in at the moment. The precursor node is the global successor node, and the successor node is null
//(that is, the new node is the new successor node of the linked list)
final Node<E> newNode = new Node<>(l, e, null);// Node(Node<E> prev, E element, Node<E> next){}
// (3) Update the reference to the global successor node
last = newNode;
// (4) If the successor of the original list is null, then the global header reference is also required to point to the new node
if (l == null)
first = newNode;
// the prev of newNode is set to last when a newNode is created. So here we have to put the original last
// Set the successor of the node to newNode
else
l.next = newNode;
// (6) Update the number of sizes in the current list
size++;
// (7) Here is the parameter used by the fast-fail mechanism
modCount++;
}
Copy the code
Let’s briefly simulate this process with an example diagram
- When the list is initially empty, we call add to add a new node
- The list is not empty when the add method is called to add nodes at the end of the list
(2) linkFirst method
This method is a private method that is exposed to the consumer through the addFirst method call.
public void addFirst(E e) {
linkFirst(e);
}
Copy the code
Let’s focus on the implementation logic of the linkFirst method
private void linkFirst(E e) {
// (1) Get the global head node
final Node<E> f = first;
// create a new node whose predecessor is null and whose successor is the current global first node
final Node<E> newNode = new Node<>(null, e, f);
// (3) Update global header references
first = newNode;
// if the first node is null, the last node points to the new node
if (f == null)
last = newNode;
// (5) is not null, the precursor of the original head node is newNode
else
f.prev = newNode;
size++;
modCount++;
}
Copy the code
The logic above is also relatively simple, which is to set the newly added node as the head node, and then update the link between the nodes in the linked list. Let’s use the following diagram to understand briefly.
(3) linkBefore method
public void add(int index, E element) {
// Check the validity of index: if the value is greater than or equal to 0 and less than or equal to size, an exception will be raised
checkPositionIndex(index);
//index = size, insert a new node at the end, as mentioned above
if (index == size)
linkLast(element);
// Insert node at index (node(index));
else
linkBefore(element, node(index));
}
private void checkPositionIndex(int index) {
if(! isPositionIndex(index))throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private boolean isPositionIndex(int index) {
return index >= 0 && index <= size;
}
Copy the code
The main logic in the add(index,element) method is linkBefore, so let’s look at this method. Before that, we call node(index) and find the node at index
Node<E> node(int index) {
//index < size/2
if (index < (size >> 1)) {
// Use the global header node to find (traverse the list)
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
//index > size / 2
Node<E> x = last;
// Use the global tail node to look forward
for (int i = size - 1; i > index; i--)
x = x.prev;
returnx; }}Copy the code
Node method makes use of the two characteristics of bidirectional linked list and record the total length of the linked list. It is divided into two parts to traverse and query the nodes at the position of JIndex. After this node is found, the linkBefore method is called as an argument, as shown below
void linkBefore(E e, Node<E> succ) {
//succ ! = null; Succ is the node at the specified location
// Element =succ
final Node<E> pred = succ.prev;
// Create a new node
// A precursor is a precursor of an incoming node
// The successor is the node passed in
final Node<E> newNode = new Node<>(pred, e, succ);
// Update the precursor reference to the index node
succ.prev = newNode;
// the index node is null, which is equivalent to inserting the node in the head and updating first
if (pred == null)
first = newNode;
// not null, then its successor is the new node
else
pred.next = newNode;
size++;
modCount++;
}
Copy the code
The logic of this method is also relatively simple. It is to insert a new node between succ and succ.prev. We understand this process by simple illustration
delete
As a double-ended queue, there are two ways to delete elements: at the beginning of the queue, at the end of the queue. As a List, you need to support intermediate deletion of elements, so there are three methods for deleting elements.
(1) unlinkFirst method
The following are two public methods that call unlinkFirst. The main difference is that removeFirst raises an exception when first is null, while pollFirst returns NULL.
// remove throws an exception if no element is present
public E removeFirst(a) {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}
// poll returns null if there are no elements
public E pollFirst(a) {
final Node<E> f = first;
return (f == null)?null : unlinkFirst(f);
}
Copy the code
Mainly look at the implementation of the method unlinkFirst
private E unlinkFirst(Node<E> f) {
// assert f == first && f ! = null;
// Get the element value of the header
final E element = f.item;
// Get the successor of the header
final Node<E> next = f.next;
// Delete item and its successor, GC
f.item = null;
f.next = null; // help GC
// Update the header reference (the successor of the original header)
first = next;
// If there is only one node in the list, then the last node is null
if (next == null)
last = null;
// Set the precursor of the new head node to NULL
else
next.prev = null;
// Update size and modCount
size--;
modCount++;
// Returns the value of the original head node
return element;
}
Copy the code
(2) unlinkLast method
The following are two public methods that call unlinkLast. The main difference is that removeLast raises an exception when first is null, while pollLast returns NULL.
// remove throws an exception if no element is present
public E removeLast(a) {
final Node<E> l = last;
if (l == null)
throw new NoSuchElementException();
return unlinkLast(l);
}
// poll returns null if there are no elements
public E pollLast(a) {
final Node<E> l = last;
return (l == null)?null : unlinkLast(l);
}
Copy the code
The following is an implementation of the unlinkLast method
// Delete the tail node
private E unlinkLast(Node<E> l) {
// The element value of the last node
final E element = l.item;
// The leading pointer to the tail node
final Node<E> prev = l.prev;
// Clear the contents of the tail node to assist GC
l.item = null;
l.prev = null; // help GC
// Make the front node the new tail node
last = prev;
// If there is only one element, delete and set first to null
// Otherwise, empty next for the front node
if (prev == null)
first = null;
else
prev.next = null;
// Update size and modCount
size--;
modCount++;
// Returns the deleted element
return element;
}
Copy the code
(4) Unlink method
// Delete the intermediate node
public E remove(int index) {
// Check for overbounds
checkElementIndex(index);
// Delete the index node
return unlink(node(index));
}
Copy the code
// Delete node X
E unlink(Node<E> x) {
// The element value of x
final E element = x.item;
// The front node of x
final Node<E> next = x.next;
// the post-node of x
final Node<E> prev = x.prev;
// If the front node is empty
// specify the first node, so that first points to the node after x
// Otherwise change next of the front node to the back node of x
if (prev == null) {
first = next;
} else {
prev.next = next;
x.prev = null;
}
// If the back node is empty
// Last points to the front node of x
// Otherwise, change the prev of the rear node to the front node of X
if (next == null) {
last = prev;
} else {
next.prev = prev;
x.next = null;
}
// Empty the element value of x to assist GC
x.item = null;
// The number of elements is reduced by 1
size--;
// The number of changes is increased by 1
modCount++;
// Returns the deleted element
return element;
}
Copy the code
To find the
LinkedList is based on a LinkedList structure and does not randomly access elements in a given location as an ArrayList does. The LinkedList lookup is a bit more cumbersome, going back from the head (or tail) of the list in O(N) time. The relevant source code is as follows:
public E get(int index) {
checkElementIndex(index); // Check the validity of index
// Call the node method to iterate through the node at index and return the node's item, as described above
return node(index).item;
}
Copy the code
traverse
The list traversal process is also very simple, similar to the above search process, we go back to the beginning of the node on the line. However, you need to be careful about traversing the LinkedList, which can lead to inefficient code. Typically, we would iterate over the LinkedList using foreach, which ultimately translates to an iterator. So the core of analyzing LinkedList traversal is its iterator implementation, which looks like this:
public ListIterator<E> listIterator(int index) {
checkPositionIndex(index);
return new ListItr(index);
}
private class ListItr implements ListIterator<E> {
private Node<E> lastReturned;
private Node<E> next;
private int nextIndex;
private int expectedModCount = modCount;
The /** constructor refers next to the node */ at the specified location
ListItr(int index) {
// assert isPositionIndex(index);
next = (index == size) ? null : node(index);
nextIndex = index;
}
public boolean hasNext(a) {
return nextIndex < size;
}
public E next(a) {
checkForComodification();
if(! hasNext())throw new NoSuchElementException();
lastReturned = next;
next = next.next;
nextIndex++;
return lastReturned.item;
}
/ /... other method
}
Copy the code
Here’s a quick note about traversing the LinkedList. Linkedlists are not good at random location access, and it would be inefficient to traverse a LinkedList using random access. For example:
List<Integet> list = new LinkedList<>();
list.add(1)
list.add(2)...for (int i = 0; i < list.size(); i++) {
Integet item = list.get(i);
// do something
}
Copy the code
When there are many elements in the list, the above traversal method is very inefficient. The reason is that the above method requires traversal of the beginning node(or the end node) for every element acquired (call get(I) method, the implementation of which is described above), which is inefficient and inefficient in the case of large data volume. This should be avoided in everyday use.
conclusion
Finally, summarize the differences between ArrayList and LinkedList that are often asked in interviews. For ArrayList, please refer to my previous source analysis of ArrayList.
-
ArrayList is based on dynamic array implementation, LinkedList is based on bidirectional LinkedList implementation;
-
ArrayList(array subscript access) is superior to LinkedList(traversal list access) for random access;
-
Without adding data directly to the tail, an ArrayList adds/deletes data to and from the specified index by copying the array. LinkedList is implemented by addressing node pointing; So LinkedList (changing the next and prev points of a node) is better than ArrayList (moving array elements).
-
LinkedList does not waste space on the data store. Dynamic expansion of ArrayList results in some space being wasted.