知识框架图:
LinkedList是一种常用的数据结构。底层是一个双向链表。每个节点包含数据以及指向前一个节点和后一个节点的引用。
一:LinkedList的使用
1.1 LinkedList的构造方法
方法 | 解释 |
LinkedList() | 无参构造 |
public LinkedList(Collection<? extends E> c) | 使用其他集合容器中元素构造List |
代码示例:
public static void main(String[] args) {
// 构造一个空的LinkedList
List<Integer> list1 = new LinkedList<>();
List<String> list2 = new ArrayList<>();
list2.add("三国演义");
list2.add("西游记");
list2.add("水浒传");
// 使用ArrayList构造LinkedList
List<String> list3 = new LinkedList<>(list2);
}
1.2 LinkedList的常用方法
方法 | 解释 |
boolean add (E e) | 尾插 e |
void add (int index, E element) | 将 e 插入到 index 位置 |
boolean addAll (Collection<? extends E> c) | 尾插 c 中的元素 |
E remove (int index) | 删除 index 位置元素 |
boolean remove (Object o) | 删除遇到的第一个 o |
E get (int index) | 获取下标 index 位置元素 |
E set (int index, E element) | 将下标 index 位置元素设置为 element |
void clear () | 清空 |
boolean contains (Object o) | 判断 o 是否在线性表中 |
int indexOf (Object o) | 返回第一个 o 所在下标 |
int lastIndexOf (Object o) | 返回最后一个 o 的下标 |
List<E> subList (int fromIndex, int toIndex) | 截取部分 list |
部分方法代码示例:
public static void main(String[] args) {
LinkedList<Integer> list = new LinkedList<>();
list.add(1); // add(elem): 表示尾插
list.add(2);
list.add(3);
list.add(4);
list.add(5);
list.add(6);
list.add(7);
System.out.println(list.size());
System.out.println(list);
// 在起始位置插入0
list.add(0, 0); // add(index, elem): 在index位置插入元素elem
System.out.println(list);
list.remove(); // remove(): 删除第一个元素,内部调用的是removeFirst()
list.removeFirst(); // removeFirst(): 删除第一个元素
list.removeLast(); // removeLast(): 删除最后元素
list.remove(1); // remove(index): 删除index位置的元素
System.out.println(list);
// contains(elem): 检测elem元素是否存在,如果存在返回true,否则返回false
if(!list.contains(1)){
list.add(0, 1);
}
list.add(1);
System.out.println(list);
System.out.println(list.indexOf(1)); // indexOf(elem): 从前往后找到第一个elem的位置
System.out.println(list.lastIndexOf(1)); // lastIndexOf(elem): 从后往前找第一个1的位置
int elem = list.get(0); // get(index): 获取指定位置元素
list.set(0, 100); // set(index, elem): 将index位置的元素设置为elem
System.out.println(list);
// subList(from, to): 用list中[from, to)之间的元素构造一个新的LinkedList返回
List<Integer> copy = list.subList(0, 3);
System.out.println(list);
System.out.println(copy);
list.clear(); // 将list中元素清空
System.out.println(list.size());
}
1.3 LinkedList的遍历
1.使用普通 for 循环遍历
//由于LinkedList不能像数组那样通过索引直接快速访问元素,使用普通 for 循环遍历需要借助size()方法和
//get(int index)方法来逐个获取元素。这种方式效率较低,不推荐在频繁遍历的场景中使用。
for (int i = 0; i < list.size(); i++) {
String element = list.get(i);
System.out.println(element);
}
2.使用增强型 for 循环遍历
//增强for循环可以简洁地遍历集合中的元素。
for (String element : list) {
System.out.println(element);
}
3. 使用迭代器(Iterator)遍历
//1.使用迭代器的hasNext和next方法进行遍历,避免在遍历过程中频繁调用size方法
//或通过索引访问元素。这样可以提高遍历的效率,特别是在处理大型链表时。
//2.在遍历过程中尽量避免对链表进行修改操作,因为这可能会导致迭代器失效,需要重新获取迭代器。
//如果必须在遍历过程中进行修改,可以使用迭代器的remove方法来安全地删除元素。
LinkedList<String> list = new LinkedList<>();
list.add("计算机科学与技术");
list.add("软件工程");
list.add("人工智能");
//1.获取迭代器:可以通过LinkedList的iterator()方法获取一个Iterator对象。
Iterator<String> iterator = list.iterator();
//2.使用迭代器遍历:通过hasNext()方法判断是否还有下一个元素,使用next()方法获取下一个元素。
while (iterator.hasNext()) {
String element = iterator.next();
System.out.println(element);
}
4. 从链表尾部向头部遍历
LinkedList<String> list = new LinkedList<>();
list.add("计算机科学与技术");
list.add("软件工程");
list.add("人工智能");
//1.获取列表迭代器并指向尾部:可以使用listIterator(int index)方法获取一个ListIterator对象,
//并将其初始位置设置为链表的尾部。
ListIterator<String> listIterator = list.listIterator(list.size());
//2.向头部遍历:使用hasPrevious()方法判断是否还有前一个元素,使用previous()方法获取前一个元素。
while (listIterator.hasPrevious()) {
String element = listIterator.previous();
System.out.println(element);
}
二:使用案例
2.1实现队列和栈数据结构
1. 队列(Queue):
• LinkedList 可以很方便地用来实现队列。在队列中,元素遵循先进先出(FIFO)的原则。可以利用 LinkedList 的 addLast 方法在队尾添加元素,使用 removeFirst 方法在队首取出元素。
• 例如:在一个任务调度系统中,可以使用队列来管理待执行的任务。新任务添加到队尾,执行引擎从队首获取任务并执行。
代码示例:
import java.util.LinkedList;
public class TaskQueue {
private LinkedList<String> queue = new LinkedList<>();
public void addTask(String task) {
queue.addLast(task);
}
public String getNextTask() {
return queue.removeFirst();
}
public static void main(String[] args) {
TaskQueue taskQueue = new TaskQueue();
taskQueue.addTask("Task 1");
taskQueue.addTask("Task 2");
System.out.println(taskQueue.getNextTask()); // 输出:Task 1
System.out.println(taskQueue.getNextTask()); // 输出:Task 2
}
}
2. 栈(Stack):
• LinkedList 也可以用于实现栈。在栈中,元素遵循先进后出(LIFO)的原则。可以使用 addFirst 方法将元素压入栈顶,使用 removeFirst 方法弹出栈顶元素。
• 例如:在表达式求值、函数调用栈等场景中可以使用栈。
import java.util.LinkedList;
public class StackExample {
private LinkedList<String> stack = new LinkedList<>();
public void push(String item) {
stack.addFirst(item);
}
public String pop() {
return stack.removeFirst();
}
public static void main(String[] args) {
StackExample stack = new StackExample();
stack.push("Item 1");
stack.push("Item 2");
System.out.println(stack.pop()); // 输出:Item 2
System.out.println(stack.pop()); // 输出:Item 1
}
}
2.2. 频繁插入和删除的场景
1. 日志记录系统:
• 在一个日志记录系统中,新的日志条目不断产生并需要添加到日志列表中。同时,可能需要根据时间范围或其他条件删除一些旧的日志条目。由于 LinkedList 在插入和删除操作上的高效性,特别是在列表中间进行插入和删除时,非常适合这种场景。
• 代码示例:
import java.util.LinkedList;
public class LoggingSystem {
private LinkedList<String> logs = new LinkedList<>();
public void addLog(String logEntry) {
logs.add(logEntry);
}
public void removeOldLogs(int daysAgo) {
// 假设日志中有时间戳,可以根据时间戳删除超过指定天数的日志
// 这里只是一个简单的示例,实际实现可能更复杂
for (String log : logs) {
// 根据条件判断是否删除日志
}
}
public static void main(String[] args) {
LoggingSystem loggingSystem = new LoggingSystem();
loggingSystem.addLog("Log entry 1");
loggingSystem.addLog("Log entry 2");
// 后续可以根据需要添加和删除日志
}
}
2. 实时消息处理:
• 在一个实时消息处理系统中,新的消息不断到来需要添加到消息队列中,同时已处理的消息可以从队列中删除。LinkedList 的高效插入和删除操作可以满足这种实时性要求。
• 代码示例:
import java.util.LinkedList;
public class MessageProcessingSystem {
private LinkedList<String> messages = new LinkedList<>();
public void addMessage(String message) {
messages.add(message);
}
public String processNextMessage() {
return messages.removeFirst();
}
public static void main(String[] args) {
MessageProcessingSystem messageSystem = new MessageProcessingSystem();
messageSystem.addMessage("Message 1");
messageSystem.addMessage("Message 2");
System.out.println(messageSystem.processNextMessage()); // 输出:Message 1
System.out.println(messageSystem.processNextMessage()); // 输出:Message 2
}
}
三:LinkedList源码分析:
3.1 主要成员变量
• size:表示链表中元素的个数。
• first:指向链表的第一个节点。
• last:指向链表的最后一个节点。
3.2 内部节点类
• item:存储节点中的元素。
• next:指向下一个节点的引用。
• prev:指向前一个节点的引用。
3.3 构造方法
默认构造方法:创建一个空的链表。
包含集合参数的构造方法:可以从另一个集合创建LinkedList,将集合中的元素依次添加到链表中。
3.4添加元素方法
1. add(E e):在链表末尾添加元素。
内部调用linkLast方法,将新元素作为最后一个节点添加到链表中。
2. add(int index, E element):在指定位置插入元素。
首先检查索引是否合法,然后根据索引位置决定是在末尾添加(调用linkLast)还是在指定位置插入(调用linkBefore)。
3.5 删除元素方法
1. remove(int index): 删除指定位置的元素。
首先检查索引是否合法,然后调用unlink方法删除指定节点。
public E remove(int index) {
checkElementIndex(index);
return unlink(node(index));
}
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;
}
2. remove(Object o):删除指定的元素。
遍历链表找到指定元素的节点,然后调用unlink方法删除该节点。
public boolean remove(Object o) {
if (o == null) {
for (Node<E> x = first; x!= null; x = x.next) {
if (x.item == null) {
unlink(x);
return true;
}
}
} else {
for (Node<E> x = first; x!= null; x = x.next) {
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false;
}
同理,其他的方法都可以通过源码阅读知道底层是怎么去实现的。
四:模拟实现LinkedList
public class MyLinkedList {
static class ListNode {
public int val;
public ListNode prev;
public ListNode next;
public ListNode(int val) {
this.val = val;
}
}
public ListNode head;//标记双向链表的头部
public ListNode tail;//标记双向链表的尾部
/**
* 打印双向链表的每个节点 的值
*/
public void display() {
ListNode cur = this.head;
while (cur != null) {
System.out.print(cur.val + " ");
cur = cur.next;
}
System.out.println();
}
//得到链表的长度
public int size() {
int count = 0;
ListNode cur = this.head;
while (cur != null) {
cur = cur.next;
count++;
}
return count;
}
//查找是否包含关键字key是否在单链表当中
public boolean contains(int key) {
ListNode cur = this.head;
while (cur != null) {
if (cur.val == key) {
return true;
}
cur = cur.next;
}
return false;
}
public void addFirst(int data) {
ListNode node = new ListNode(data);
if (this.head == null) {
this.head = node;
this.tail = node;
} else {
node.next = this.head;
this.head.prev = node;
head = node;
}
}
//尾插法
public void addLast(int data) {
ListNode node = new ListNode(data);
if (this.head == null) {
this.head = node;
this.tail = node;
} else {
tail.next = node;
node.prev = tail;
tail = node;
}
}
//任意位置插入,第一个数据节点为0号下标
public void addIndex(int index, int data) {
//1、判断Index位置的合法性
if (index < 0 || index > size()) {
throw new IndexWrongFulException("index位置不合法!");
}
//2、判断特殊位置,头插 和 尾插
if (index == 0) {
addFirst(data);
return;
}
if (index == size()) {
addLast(data);
return;
}
//3、找到index位置节点的地址
ListNode cur = findIndexListNode(index);
//4、修改4个指向
ListNode node = new ListNode(data);
node.next = cur;
cur.prev.next = node;
node.prev = cur.prev;
cur.prev = node;
}
private ListNode findIndexListNode(int index) {
ListNode cur = head;
while (index != 0) {
cur = cur.next;
index--;
}
return cur;
}
//删除第一次出现关键字为key的节点
public void remove(int key) {
ListNode cur = head;
while (cur != null) {
if (cur.val == key) {
if (cur == head) {
head = head.next;
if (head != null) {
head.prev = null;
} else {
tail = null;
}
} else {
cur.prev.next = cur.next;
if (cur.next != null) {
cur.next.prev = cur.prev;
} else {
this.tail = cur.prev;
}
}
return;
}
cur = cur.next;
}
}
//删除所有值为key的节点
public void removeAllKey(int key) {
ListNode cur = head;
while (cur != null) {
if (cur.val == key) {
if (cur == head) {
head = head.next;
if (head != null) {
head.prev = null;
} else {
tail = null;
}
} else {
cur.prev.next = cur.next;
if (cur.next != null) {
cur.next.prev = cur.prev;
} else {
this.tail = cur.prev;
}
}
}
cur = cur.next;
}
}
public void clear() {
ListNode cur = head;
while (cur != null) {
ListNode curNext = cur.next;
cur.prev = null;
cur.next = null;
cur = curNext;
}
head = null;
tail = null;
}
}
注意:上述代码中,只是简单模拟一下LinkedList中常用方法的实现逻辑,用的是基本数据类型,并没有像源码那样使用泛型,改成泛型更加通用,因为节点中的数据可能是引用数据类型或者自定义类型 。
五:LinkedList细节
5.1 插入和删除操作
• LinkedList在进行插入和删除操作时非常高效,尤其是在链表中间插入或删除元素。但是,在进行大量的随机插入和删除操作时,需要注意维护链表的结构完整性,避免出现指针错误。
在 Java 的LinkedList中进行频繁的插入和删除操作时,可以通过以下方法避免性能下降: 如果要在链表中间插入或删除元素,可以先使用迭代器找到目标位置,然后使用迭代器的add和remove方法进行操作。这样可以避免在遍历过程中频繁地调用get方法,提高性能。
LinkedList<String> list = new LinkedList<>();
list.add("apple");
list.add("banana");
list.add("cherry");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String element = iterator.next();
if (element.equals("banana")) {
iterator.remove(); // 删除元素
}
}
iterator = list.iterator();
while (iterator.hasNext()) {
String element = iterator.next();
if (element.equals("apple")) {
iterator.add("orange"); // 在元素后插入
}
}
5.2 遍历方式选择
• 可以使用迭代器、增强型 for 循环和普通 for 循环进行遍历。如果只需要顺序遍历链表,迭代器和增强型 for 循环是比较好的选择,简洁且高效。如果需要在遍历过程中同时进行插入或删除操作,迭代器是更安全的方式,因为它可以正确地处理链表结构的变化。
5.3 LinkedList插入和删除元素的平均时间复杂度为 O(1),但在某些特殊情况下可能接近 O(n)。
插入操作:
在链表头部插入元素:只需要更新一个指针,将新节点的next指向原来的头节点,然后更新头节点为新节点。时间复杂度为 O(1)。
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++;
}
在链表尾部插入元素:同样只需要更新一个指针,将新节点的prev指向原来的尾节点,然后更新尾节点为新节点。时间复杂度为 O(1)。
public void addLast(E e) {
linkLast(e);
}
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++;
}
在链表中间插入元素:需要先找到插入位置,然后调整前后节点的指针。如果已知插入位置的前驱节点,那么插入操作的时间复杂度为 O(1);如果需要从头开始遍历找到插入位置,那么时间复杂度接近 O(n),其中 n 是链表的长度。
删除链表中间元素:需要先找到要删除的节点,然后调整前后节点的指针。如果已知要删除节点的前驱节点,那么删除操作的时间复杂度为 O(1);如果需要从头开始遍历找到要删除的节点,那么时间复杂度接近 O(n),其中 n 是链表的长度。
5.4 类型安全问题
在使用LinkedList时,需要注意类型安全问题。如果存储不同类型的元素,可能会引发ClassCastException异常。可以使用泛型来确保链表中存储的元素类型一致。
LinkedList list = new LinkedList();
list.add(10);
list.add("hello");
int number = (int) list.get(1); // 这里会抛出 ClassCastException
5.5集合类之间的转换
可以将LinkedList转换为其他集合类,例如使用toArray()方法将其转换为数组,或者使用ArrayList的构造函数将其转换为ArrayList。但在进行转换时,需要注意数据类型的兼容性和性能开销。
5.6 并发修改异常
• 在使用迭代器遍历LinkedList的同时进行修改操作(如添加或删除元素),可能会导致ConcurrentModificationException异常。因为迭代器在遍历过程中会维护一个修改计数器,当检测到集合被外部修改时,就会抛出异常。为了避免这种情况,可以使用迭代器的remove()方法在遍历过程中进行安全的删除操作,或者在修改后重新获取迭代器。
LinkedList<String> list = new LinkedList<>();
list.add("apple");
list.add("banana");
list.add("cherry");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String element = iterator.next();
if (element.equals("banana")) {
list.remove(element); // 这里会抛出 ConcurrentModificationException
}
}
1. 如果在多线程环境下使用LinkedList,可以考虑使用并发安全的集合类,
如:ConcurrentLinkedQueue。这些集合类在设计时考虑了多线程并发访问的情况,提供了更高的性能和线程安全性。
2. 如果必须使用LinkedList,可以使用同步机制来保护对链表的访问。例如,可以使用synchronized关键字或者ReentrantLock来确保在多线程环境下对链表的操作是线程安全的。但需要注意同步带来的性能开销。
5.7 空指针处理
• 当从LinkedList中获取元素时,需要注意处理可能返回的null值,以避免空指针异常。特别是在使用迭代器遍历链表时,要确保对每个元素进行适当的空指针检查。
5.8 内存管理问题
1. 内存泄漏:如果在使用LinkedList的过程中,没有正确地管理对链表中元素的引用,可能会导致内存泄漏。例如,如果一个元素被插入到链表中,但在后续的操作中不再被需要,却仍然被链表中的节点引用着,那么这个元素所占用的内存就无法被垃圾回收器回收。方案:及时清理无用的引用:如果一个元素从链表中被删除,确保其引用被及时设置为null,以便垃圾回收器可以回收其占用的内存。
2. 内存浪费:由于LinkedList的每个节点都需要额外的内存来存储前后节点的引用,对于存储大量小元素的情况,可能会导致相对较高的内存开销。相比之下,数组或其他更紧凑的数据结构可能会更节省内存。(方案:选择合适的数据结构)
5.9特殊用途
1. 双向链表的优势:LinkedList是双向链表,这意味着可以从链表的头部和尾部两个方向进行快速的插入和删除操作。例如,可以使用addFirst和removeFirst方法在链表头部进行高效的操作,这在一些特定的算法和数据结构中可能非常有用,如实现栈或队列的数据结构。
2. 作为队列或栈的实现:虽然 Java 中有专门的Queue和Stack接口以及相应的实现类,但LinkedList可以很容易地用作队列或栈。例如,通过只使用addLast和removeFirst方法,可以将LinkedList用作队列;通过只使用addFirst和removeFirst方法,可以将其用作栈。
3. 与其他数据结构的转换:LinkedList可以与其他数据结构进行转换,这在一些特定的场景下可能很有用。例如,可以将LinkedList转换为数组,或者将其转换为另一种集合类型。但在进行转换时,需要注意性能和类型安全问题。
5.10 内存布局细节
1. 节点结构:LinkedList的节点不仅包含数据元素,还包含前后节点的引用。这使得每个节点占用的内存相对较大,尤其是在存储小数据量时,与其他数据结构相比可能存在一定的内存浪费。例如,存储大量小整数时,ArrayList可能在内存使用上更加紧凑,因为它是基于数组实现的,而LinkedList的每个节点都需要额外的空间来存储指针。
2. 内存碎片化:由于LinkedList的节点在内存中是分散存储的,频繁的插入和删除操作可能导致内存碎片化。虽然 Java 的垃圾回收机制会处理不再使用的节点,但在某些情况下,内存碎片化可能会影响程序的性能,尤其是在长时间运行的程序中。
5.11 性能特性的微妙之处
1. 遍历性能:虽然在插入和删除操作上表现出色,但LinkedList的遍历性能相对较差。这是因为在遍历过程中,需要逐个节点地通过指针进行访问,而不能像ArrayList那样通过索引快速定位元素。例如,在一个大型的LinkedList中进行顺序遍历可能比在同等大小的ArrayList中遍历要慢得多。
2. 大尺寸链表的性能下降:当LinkedList变得非常大时,一些操作的性能可能会急剧下降。例如,在一个包含数十万甚至更多节点的链表中进行插入或删除操作,可能会因为需要遍历较长的距离来找到插入或删除位置而变得非常耗时。