LinkedList原理
摘要:传统的动态数组如ArrayList在数据量增加到一定程度时需要扩容,而LinkedList作为一种基本的程序数据结构,采用了不同的内存管理机制。本文将介绍LinkedList如何在运行时动态管理内存,以及它为什么不需要进行数组式的扩容操作。我们还将讨论链表的性能特点,以及它是如何成为特定场景下的理想选择。
引言
在一片被神秘力量覆盖的草原上,灰太狼是一位被赋予特殊智慧的狼族首领。某日,他为了更好地管理他的羊群——每只羊都被一个独特的魔法球体所代表——发明了一种神奇的组织方式。这就是我们今天要讲述的,灰太狼的“魔法链表”(Magical LinkedList)。
灰太狼的“魔法链表”
在草原的边界,有一棵会说话的古老魔法树,它负责记录着链表的起始点。灰太狼每次获得一只新羊时,魔法树就会生出一个闪闪发光的球体,球体中有一个小小的窗口展示着羊的形象。球体的两侧各有一条细丝,一条连接前一只羊,一条指向下一只羊。
故事展开
灰太狼的羊群是独一无二的,因为这些魔法球体并不需要排成一行等待扩大空间。每当灰太狼决定添加新的球体时,他只需通知魔法树,树便会在链表的尾端添加一个新的光球。
这种方法意味着无论何时,灰太狼都能轻松地添加或移除羊群中的任何一只羊。他只需要告诉树,树就会用它强大的魔法重新编织光球之间的连接丝,无需其他复杂的调整。
灰太狼的顿悟
随着时间的推移,灰太狼意识到他的“魔法链表”并不需要像其他狼族那样担心扩大领地。每只羊的球体是独立存在的,只要草原上还有空间,链表就能无限扩展。
故事的启示
灰太狼的“魔法链表”故事向我们展示了链表数据结构的基本原理。每个元素(或在我们的故事中,每个球体)是独立的,并且包含了两部分信息:它存储的值(羊的形象)和它在结构中的位置(通过细丝连接的前后球体)。这允许了链表在添加或删除元素时的高效性,而无需进行数组那样的扩容操作。
虽然灰太狼无法像倒数羊一样迅速地找到特定的球体(因为他需要沿着光球的连接丝一路追溯),但当涉及到将新羊引入群体或让任何羊退出时,这种方式却显得异常迅速。
最终,灰太狼因其高效的管理方式而成为了草原上的传说。他的“魔法链表”羊群在广阔的草原上自由生长,永远不用担心空间不够用。
目录
1. 链表简介
LinkedList,顾名思义,是一个由节点链组成的列表。每个节点包含两个基本组成部分:一是存储的数据,二是指向列表中下一个节点的引用。在Java的LinkedList实现中,这是一个双向链表,也就是说,每个节点还包含一个指向前一个节点的引用。
2. 内存管理
由于链表的这种结构,它在内存中的存储是分散的。这意味着,当你添加一个新元素到LinkedList时,你只需简单地创建一个新节点,并更新前一个节点的引用,即可将其连接到链表中。这个过程如下:
- 创建一个新节点,将其数据部分设置为要添加的元素。
- 将新节点的前一个节点引用设置为当前链表的最后一个节点。
- 更新当前最后一个节点的下一个节点引用,使其指向新节点。
- 如果链表是空的,在添加第一个节点时,同时更新链表的头引用。
这种方式意味着链表动态地、按需分配内存。当一个新节点被添加到链表中时,并没有一个预先分配的内存块需要被填充,新节点占据的内存是在添加操作发生时分配的。
3. 为什么不需要扩容
与ArrayList等需要预先分配一个容量然后在容量用尽时进行扩容的数据结构不同,LinkedList由于其节点是独立分配的,因此不存在“容量”概念。你可以无限添加元素到LinkedList中,只要系统的内存允许。每次添加操作只是简单地创建一个新的节点并将其连接到链表的适当位置。
LinkedList的这种设计也导致了其性能特点的不同:它在添加和删除元素时具有常数时间复杂度(即O(1)),因为这些操作仅涉及到改变节点之间的引用。但是,它在随机访问元素时的性能较低,因为需要从头(或尾)开始遍历节点来找到特定索引的元素,这具有线性时间复杂度(即O(n))。
4. 源码精细分析
transient int size = 0; //transient表示字段不会被序列化size存储LinkedList元素数量,初始化为0
transient Node<E> first; // 链表的头节点,初始时没有任何元素,所以是null
transient Node<E> last; // 链表的尾节点,初始时也是null
protected transient int modCount = 0;// 更改次数
//构造函数,创建一个空的LinkedList
public LinkedList() {
}
// 内部的Node类,表示链表中的每个节点
private static class Node<E> {
E item; // 节点存储的元素
Node<E> next; // 指向链表中下一个节点的引用
Node<E> prev; // 指向链表中上一个节点的引用
// Node的构造函数,用于创建节点
Node(Node<E> prev, E element, Node<E> next) {
this.item = element; // 设置节点存储的元素
this.next = next; // 设置节点的下一个节点引用
this.prev = prev; // 设置节点的前一个节点引用
}
}
// 在链表的末尾添加一个新元素
public boolean add(E e) {
linkLast(e); // 调用一个私有的辅助方法来实现添加操作
return true; // 由于添加总是会发生,我们返回true表示成功
}
// 灰太狼找到一片空旷的地方准备埋下自己的宝藏(元素e)。
// 在链表中指定位置插入一个新元素
public void add(int index, E element) {
checkPositionIndex(index); // 首先检查指定的索引是否有效
if (index == size) // 如果索引等于链表的当前长度
linkLast(element); // 调用linkLast来在链表末尾添加新元素
else
linkBefore(element, node(index)); // 否则,在指定位置之前插入新元素
}
// 灰太狼在他的藏宝图(链表)的特定位置(index)准备打开一个新的宝藏洞穴(插入新元素element)。
// 辅助方法:在链表的尾部链接一个新节点
void linkLast(E e) {
final Node<E> l = last; // 保存链表的最后一个节点的引用
final Node<E> newNode = new Node<>(l, e, null); // 创建新的节点,将当前最后一个节点作为前驱
last = newNode; // 更新链表的末尾为新节点
if (l == null) // 如果链表为空(last为null)
first = newNode; // 那么新节点同时也是链表的第一个节点
else // 如果链表不为空
l.next = newNode; // 让原来的最后一个节点指向新节点
size++; // 链表的大小增加1
modCount++; // 链表结构改变的次数增加1
}
// 灰太狼在藏宝图的末尾成功挖开了一个新洞,并且将宝藏(newNode)藏进去。
// 辅助方法:在指定节点之前链接一个新节点
void linkBefore(E e, Node<E> succ) {
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++; // 链表的大小增加1
modCount++; // 链表结构改变的次数增加1
}
// 灰太狼巧妙地在宝藏图上某个节点(succ)的前面挖了一个新洞,并且藏上了宝藏(newNode)。
// 辅助方法:寻找链表中指定索引位置的节点
Node<E> node(int 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; // 返回找到的节点
}
}
// 灰太狼决定研究他的藏宝图,从最近的一端开始,找到要挖掘的新地点。
// 辅助方法:检查给定的索引是否超出了链表的范围
private void checkPositionIndex(int index) {
if (!isPositionIndex(index)) // 如果索引无效
throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); // 抛出异常
}
// 灰太狼确认他不会在地图上不存在的位置挖洞,否则他会陷入困境(抛出异常)。
// 辅助方法:确定索引是否在有效范围内
private boolean isPositionIndex(int index) {
return index >= 0 && index <= size; // 检查索引是否在0和链表大小之间
}
// 灰太狼总是会确保他的计划在藏宝图的范围之内,以避免无谓的努力。
// 将一个集合中所有的元素添加到链表的指定位置
public boolean addAll(int index, Collection<? extends E> c) {
checkPositionIndex(index); // 检查索引位置是否合法
Object[] a = c.toArray(); // 将集合转换为数组
int numNew = a.length; // 要添加的新元素的数量
if (numNew == 0)
return false; // 如果没有元素要添加,返回false
Node<E> pred, succ;
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; // 更新前一个节点为新添加的节点
}
if (succ == null) {
last = pred; // 如果没有后继节点,那么最后一个添加的节点将成为新的尾节点
} else {
pred.next = succ; // 否则,将最后一个新节点链接到后继节点之前
succ.prev = pred; // 并将后继节点的前一个节点更新为新节点
}
size += numNew; // 更新链表的大小
modCount++; // 更新链表修改次数
return true; // 操作成功,返回true
}
// 灰太狼在藏宝图(链表)的一个特定位置找到了一批宝石(集合c)。他巧妙地将这些宝石一个接一个地放置到了宝图的秘密通道中,从而大大扩展了他的宝藏。
在这个故事中,灰太狼有一个藏宝图(代表链表),他在寻找最佳的位置来埋藏他的珍贵宝藏(元素)。有时,他会在图的尽头(链表的末尾)埋宝,有时则会在特定的位置(链表中的特定索引)。他使用不同的方法(辅助函数)来确保每次挖掘都是在正确的位置。当灰太狼找到了一批宝藏(一个集合),他就会仔细地将宝藏逐一放入藏宝图的特定位置。通过精心的策划和对藏宝图的深入了解,灰太狼成功地将他的宝藏安全地藏了起来。
5. 结论
总而言之,LinkedList之所以不需要类似于ArrayList的扩容操作是因为每个新元素都是单独分配内存的一个独立节点。这种灵活的内存管理方法,虽然在某些操作上可能效率较低,但它提供了无与伦比的操作灵活性。在需要经常插入和删除元素的场景下,LinkedList是一个非常合适的选择。
希望这篇文章能够帮助你理解链表的基本原理以及它是如何在内存中管理数据的。如果你对动态数组感兴趣,也可以查看我ArrayList原理的文章。
6. 参考文献
- Oracle. (n.d.). Class LinkedList. Java Platform SE 8.
- Sedgewick, R., & Wayne, K. (2011). Algorithms (4th ed.). Addison-Wesley.
- Bloch, J. (2008). Effective Java (2nd ed.). Addison-Wesley.
关于作者:猿究院——屠龙少年SlayMaster
版权声明:创作不易,版权需敬, 笑一笑,传播正能量!
评论区:灵感闪现,友谊加码, 一句顶俏,乐开花!