快递分拣中心里的 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 就是你的最佳选择!

相关推荐
踢球的打工仔4 小时前
PHP面向对象(7)
android·开发语言·php
安卓理事人4 小时前
安卓socket
android
安卓理事人10 小时前
安卓LinkedBlockingQueue消息队列
android
万能的小裴同学11 小时前
Android M3U8视频播放器
android·音视频
q***577411 小时前
MySql的慢查询(慢日志)
android·mysql·adb
JavaNoober12 小时前
Android 前台服务 "Bad Notification" 崩溃机制分析文档
android
城东米粉儿12 小时前
关于ObjectAnimator
android
zhangphil13 小时前
Android渲染线程Render Thread的RenderNode与DisplayList,引用Bitmap及Open GL纹理上传GPU
android
火柴就是我14 小时前
从头写一个自己的app
android·前端·flutter
lichong95115 小时前
XLog debug 开启打印日志,release 关闭打印日志
android·java·前端