快递分拣中心里的 LinkedList 冒险:从源码到实战的趣味解析

一、快递分拣中心的神奇传送带:LinkedList 的基本结构

想象你来到一个高科技快递分拣中心,这里的包裹传送带不像普通仓库那样排成直线,而是每个包裹都有两个 "小助手"------ 前面的助手指向下一个包裹,后面的助手指向前一个包裹。这就是 Java 中的 LinkedList,一个由双向链表构成的智能包裹管理系统。

1.1 包裹节点的秘密

每个包裹(节点)都有三个关键信息:

  • 包裹里的物品(item

  • 指向后面包裹的指针(next

  • 指向前面包裹的指针(prev

java

kotlin 复制代码
// 快递包裹节点类
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;
    }
}

1.2 分拣中心的核心控制室

分拣中心有三个重要的控制变量:

  • first:第一个包裹的位置

  • last:最后一个包裹的位置

  • size:当前包裹的数量

java

java 复制代码
transient Node<E> first;   // 第一个包裹
transient Node<E> last;    // 最后一个包裹
transient int size = 0;    // 包裹数量

二、包裹入库:LinkedList 的添加操作

2.1 尾部入库:最常用的包裹存放方式

当新包裹到达时,最常见的操作是直接放在传送带尾部。就像分拣中心的传送带会自动把新包裹接在最后一个位置:

java

ini 复制代码
public boolean add(E e) {
    linkLast(e);  // 调用尾部添加方法
    return true;
}

void linkLast(E e) {
    Node<E> l = last;  // 获取当前最后一个包裹
    Node<E> newNode = new Node<>(l, e, null);  // 新建包裹,前一个是原最后一个,后一个为空
    last = newNode;  // 更新最后一个包裹
    
    if (l == null) {
        first = newNode;  // 如果原本没有包裹,新包裹也是第一个
    } else {
        l.next = newNode;  // 原最后一个包裹的下一个指向新包裹
    }
    size++;  // 包裹数量加1
}

2.2 插队入库:在指定位置插入包裹

有时需要把包裹插入到特定位置,比如在第 3 个包裹后面插入新包裹,分拣中心会启动智能定位系统:

java

perl 复制代码
public void add(int index, E element) {
    checkPositionIndex(index);  // 检查位置是否合法
    
    if (index == size) {
        linkLast(element);  // 如果是尾部,直接调用尾部添加
    } else {
        linkBefore(element, node(index));  // 否则找到指定位置并插入
    }
}

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;
    }
}

三、包裹出库:LinkedList 的删除操作

3.1 紧急出库:删除第一个包裹

当有紧急包裹需要优先处理时,分拣中心会直接取出第一个包裹:

java

ini 复制代码
public E remove() {
    return removeFirst();  // 调用删除第一个包裹方法
}

public E removeFirst() {
    Node<E> f = first;  // 获取第一个包裹
    if (f == null) throw new NoSuchElementException();  // 如果没有包裹,报错
    return unlinkFirst(f);  // 执行删除
}

private E unlinkFirst(Node<E> f) {
    E element = f.item;  // 取出包裹物品
    Node<E> next = f.next;  // 记录下一个包裹
    
    f.item = null;  // 清空当前包裹物品
    f.next = null;  // 断开下一个包裹引用,帮助垃圾回收
    
    first = next;  // 新的第一个包裹
    if (next == null) {
        last = null;  // 如果没有下一个包裹,最后一个也置空
    } else {
        next.prev = null;  // 新第一个包裹的前一个置空
    }
    
    size--;  // 包裹数量减1
    return element;
}

3.2 按位置出库:删除指定位置的包裹

如果需要删除第 5 个包裹,分拣中心会先定位再操作:

java

ini 复制代码
public E remove(int index) {
    checkElementIndex(index);  // 检查位置合法性
    return unlink(node(index));  // 定位并删除
}

E unlink(Node<E> x) {
    E element = x.item;  // 取出物品
    Node<E> next = x.next;  // 下一个包裹
    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--;  // 数量减1
    return element;
}

四、包裹查询:LinkedList 的获取与修改

4.1 按位置找包裹

想知道第 3 个包裹是什么?分拣中心会按位置查找:

java

scss 复制代码
public E get(int index) {
    checkElementIndex(index);  // 检查位置合法性
    return node(index).item;  // 定位并返回物品
}

4.2 修改包裹内容

如果发现第 2 个包裹贴错了标签,需要修改内容:

java

ini 复制代码
public E set(int index, E element) {
    checkElementIndex(index);  // 检查位置合法性
    Node<E> x = node(index);  // 定位包裹
    E oldVal = x.item;  // 保存原物品
    x.item = element;  // 更新物品
    return oldVal;  // 返回原物品
}

五、包裹流水线:LinkedList 的迭代器

分拣中心的传送带可以从头遍历到尾,也可以从尾遍历到头,这就是迭代器的神奇之处:

java

csharp 复制代码
public Iterator<E> iterator() {
    return new ListItr(0);  // 从头开始迭代
}

private class ListItr implements ListIterator<E> {
    private Node<E> lastReturned;  // 上一个返回的包裹
    private Node<E> next;  // 下一个包裹
    private int nextIndex;  // 下一个索引
    private int expectedModCount = modCount;  // 期望的修改次数,用于并发检查
    
    ListItr(int index) {
        next = (index == size)? null : node(index);
        nextIndex = index;
    }
    
    public E next() {
        checkForComodification();  // 检查是否有并发修改
        if (!hasNext()) throw new NoSuchElementException();
        
        lastReturned = next;
        next = next.next;
        nextIndex++;
        return lastReturned.item;
    }
    
    // 还有更多方法如 hasPrevious(), previous(), remove() 等
}

六、多功能传送带:LinkedList 作为队列和栈

6.1 作为队列(先进先出)

当分拣中心作为普通仓库使用时,遵循先进先出规则:

java

typescript 复制代码
// 入队:尾部添加
public boolean offer(E e) {
    return add(e);
}

// 出队:头部取出
public E poll() {
    Node<E> f = first;
    return (f == null)? null : unlinkFirst(f);
}

// 查看队首:不取出
public E peek() {
    return (first == null)? null : first.item;
}

6.2 作为栈(后进先出)

当分拣中心作为紧急仓库时,遵循后进先出规则:

java

scss 复制代码
// 入栈:头部添加
public void push(E e) {
    addFirst(e);
}

// 出栈:头部取出
public E pop() {
    return removeFirst();
}

七、分拣中心 vs 仓库货架:LinkedList vs ArrayList

场景 LinkedList(快递分拣中心) ArrayList(普通仓库货架)
随机取包裹 需要从头到尾找,慢(O (n)) 直接按编号取,快(O (1))
中间插入包裹 只需修改指针,快(O (1)) 需移动后面所有包裹,慢(O (n))
尾部添加包裹 直接接在末尾,快(O (1)) 可能需要扩容,平均快(均摊 O (1))
空间占用 每个包裹有两个指针,更占空间 连续数组,空间紧凑

八、实战建议:什么时候用 LinkedList

  1. 推荐场景

    • 需要频繁在中间位置插入 / 删除(如聊天消息列表)
    • 作为队列或栈使用(如任务调度)
    • 数据量不确定,需要动态扩展
  2. 不推荐场景

    • 频繁随机访问(如按索引查元素)
    • 数据量小且固定,插入删除不频繁

九、代码实战:LinkedList 的日常使用

java

arduino 复制代码
public static void main(String[] args) {
    // 创建 LinkedList
    LinkedList<String> packageList = new LinkedList<>();
    
    // 添加包裹
    packageList.add("包裹1");
    packageList.add("包裹2");
    packageList.addFirst("包裹0");  // 头部添加
    
    // 查看包裹
    System.out.println("第一个包裹:" + packageList.getFirst());
    System.out.println("第三个包裹:" + packageList.get(2));
    
    // 修改包裹
    packageList.set(1, "新包裹2");
    
    // 删除包裹
    packageList.remove(0);  // 删除第一个
    
    // 迭代遍历
    System.out.println("所有包裹:");
    for (String pkg : packageList) {
        System.out.println(pkg);
    }
    
    // 作为栈使用
    LinkedList<Integer> stack = new LinkedList<>();
    stack.push(1);
    stack.push(2);
    System.out.println("出栈:" + stack.pop());  // 输出 2
    
    // 作为队列使用
    LinkedList<String> queue = new LinkedList<>();
    queue.offer("任务1");
    queue.offer("任务2");
    System.out.println("出队:" + queue.poll());  // 输出 任务1
}

通过这个快递分拣中心的比喻,我们深入理解了 LinkedList 的核心原理:双向链表的结构让它在插入删除时高效如快递分拣,而迭代器和双端队列的支持让它成为多功能的数据结构工具。记住:当你的数据需要频繁 "插队" 或 "紧急出库" 时,LinkedList 就是你的最佳选择!

相关推荐
用户20187928316736 分钟前
MagiskHidePropsConf 原理与实战故事
android
whysqwhw1 小时前
Egloo 项目结构分析
android
Wgllss1 小时前
大型异步下载器(二):基于kotlin+Compose+协程+Flow+Channel+ OKhttp 实现多文件异步同时分片断点续传下载
android·架构·android jetpack
yzpyzp1 小时前
KAPT 的版本如何升级,是跟随kotlin的版本吗
android·kotlin·gradle
泓博1 小时前
KMP(Kotlin Multiplatform)简单动画
android·开发语言·kotlin
柿蒂2 小时前
Flutter 拖动会比原生更省资源?分析 GPU呈现模式条形图不动的秘密
android·flutter
_一条咸鱼_2 小时前
Android Runtime内存管理全体系解构(46)
android·面试·android jetpack
猫头虎3 小时前
【Python系列PyCharm实战】ModuleNotFoundError: No module named ‘sklearn’ 系列Bug解决方案大全
android·开发语言·python·pycharm·bug·database·sklearn
鹤渺3 小时前
React Native 搭建iOS与Android开发环境
android·react native·ios
liang_jy4 小时前
Android 窗口显示(一)—— Activity、Window 和 View 之间的联系
android·面试