Introduction
The data structures of LinkedList
and ArrayList
are completely different. The bottom layer of ArrayList is the structure of an array, while the bottom layer of LinkedList is the structure of a linked list. It can perform efficient insertion and removal operations. It is based on a doubly linked list structure.
The overall structure of LinkedList
It can also be seen from the figure that LinkedList has a lot of Nodes, and there are two variables first
and last
to save the information of the head and tail nodes; and it is not a circular doubly linked list, because it is null before and after , this is 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 the List
interface, but also the Queue
and Deque
interfaces, so it can be used as a List, as a double-ended queue, and of course as a stack.
Source code analysis
main attribute
// 元素个数
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 ways to add elements, one is to add elements at the end of the queue, and the other is to add elements to the head of the queue. These two forms are mainly implemented in LinkedList by 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, the typical method of adding elements in the middle of a double-linked list.
The three ways to add elements are roughly as follows:
Adding elements to the head and tail of the queue is efficient and has a time complexity of O(1).
It is inefficient to add elements in the middle. First, you must find the node at the insertion position, and then modify the pointers of the front and rear nodes. The time complexity is O(n).
remove 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 intermediate deletion of elements, so there are three methods for deleting elements, 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 following figure.
[
Deleting elements at the beginning and end of the queue is very efficient, and the time complexity is O(1).
It is inefficient to delete elements in the middle. First, find the node at the deleted position, and then modify the front and back pointers. The time complexity is O(n).
stack
As we said earlier, LinkedList is a double-ended queue. Remember that double-ended queues can be used as stacks?
package org.example.test;
import java.util.LinkedList;
/**
* 利用LinkedList来模拟栈
* 栈的特点:先进后出
*/
public class Test12 {
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 Test13 {
public static void main(String[] args) {
// 测试栈
Test12 test12 = new Test12();
test12.push("我是第1个进去的");
test12.push("我是第2个进去的");
test12.push("我是第3个进去的");
test12.push("我是第4个进去的");
test12.push("我是第5个进去的");
// 取出
while (!test12.isEmpty()){
String pop = test12.pop();
System.out.println(pop);
}
// 打印结果
/*我是第5个进去的
我是第4个进去的
我是第3个进去的
我是第2个进去的
我是第1个进去的*/
}
}
The characteristic of the stack is LIFO(Last In First Out)
, so it is also very simple to use as a stack. To add and delete elements, only operate the first node of the queue.
Summarize
(1) LinkedList is a List implemented with a double-linked list, so there is no problem of insufficient capacity, so there is no method for expansion.
(2) LinkedList is also a double-ended queue, with the characteristics of queue, double-ended queue, and stack.
(3) LinkedList is very efficient to add and delete elements at the beginning and end of the queue, and the time complexity is O(1).
(4) LinkedList is 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.
The essential difference comes from the underlying implementation of the two: 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 located directly through subscripts; linked lists can only be traversed when querying elements, which is less efficient than arrays.
The efficiency of adding and deleting elements of 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 the pointer of the corresponding position needs to be adjusted.
The above is a popular comparison between arrays and linked lists. In daily use, both can work well in their own 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 maintains a pointer size of the current capacity, the time complexity of adding elements directly to ArrayList is O( 1), it is very convenient to use.
LinkedList is often used as the implementation class of Queue queue. Because the bottom layer is a doubly linked list, it can easily provide first-in, first-out operations.
The answer can be divided into two parts: one is the difference between the underlying implementation of arrays and linked lists, and the other is the implementation details of ArrayList and LinkedList.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。