链表的数据结构与java实现

84 阅读4分钟

链表详解

1. 什么是链表

链表(Linked List)是一种线性数据结构,由一系列节点(Node)组成,每个节点包含数据域和指针域。与数组不同,链表中的元素在内存中不是连续存储的,而是通过指针链接在一起。

链表主要分为:

  • 单链表(Singly Linked List):每个节点只有一个指针指向下一个节点
┌───┐    ┌───┐    ┌───┐
│ A │ ──>│ B │ ──>│ C │ ──> null
└───┘    └───┘    └───┘
  • 双向链表(Doubly Linked List):每个节点有两个指针,分别指向前驱和后继节点
null <── ┌───┐ <──> ┌───┐ <──> ┌───┐ <──> null
         │ A │      │ B │      │ C │
         └───┘      └───┘      └───┘
  • 循环链表(Circular Linked List):尾节点指向头节点形成环状结构
┌───┐    ┌───┐    ┌───┐
│ A │ ──>│ B │ ──>│ C │
└───┘    └───┘    └───┘
  ▲                  │
  └──────────────────┘

2. 链表的特性

  1. 动态大小:链表可以根据需要动态增长或缩小,不需要预先分配固定大小的内存空间
  2. 高效插入/删除:在已知节点位置的情况下,插入和删除操作的时间复杂度为O(1)
  3. 非连续存储:节点在内存中分散存储,通过指针连接
  4. 顺序访问:只能从头节点开始顺序访问,无法直接访问任意位置的节点

3. 链表的优缺点

3.1 优点

  1. 动态内存分配,不需要预先知道数据规模
  2. 插入和删除操作高效,只需修改指针指向
  3. 可以高效实现栈、队列等抽象数据类型
  4. 不会造成内存空间的浪费

3.2 缺点

  1. 访问元素需要从头遍历,随机访问效率低(O(n))
  2. 需要额外的内存空间存储指针
  3. 缓存不友好,因为节点在内存中不连续
  4. 反向遍历困难(单链表)

4. 链表与数组性能对比

操作链表时间复杂度数组时间复杂度
访问元素O(n)O(1)
插入/删除O(1)O(n)
查找元素O(n)O(n)
内存占用较高(指针开销)较低

5. 链表的基本使用示例

// 单链表基本操作示例
// 1. 创建链表实例
SinglyLinkedList<String> list = new SinglyLinkedList<>();

// 2. 添加元素 - 时间复杂度O(1)
list.add("Java");  // 头节点指向新节点
list.add("Python"); // 新节点插入链表末尾
list.add("C++");   // 再次插入末尾

// 3. 遍历链表 - 时间复杂度O(n)
Node<String> current = list.getHead(); // 获取头节点
while (current != null) {
    System.out.println(current.data); // 打印当前节点数据
    current = current.next; // 移动到下一个节点
}

6. 单链表核心方法实现

6.1 插入操作

// 在指定位置插入元素
public void insert(int index, T data) {
    if (index < 0 || index > size) {
        throw new IndexOutOfBoundsException();
    }

    Node<T> newNode = new Node<>(data);
    if (index == 0) {
        // 插入到头部
        newNode.next = head;
        head = newNode;
    } else {
        // 找到插入位置的前驱节点
        Node<T> prev = head;
        for (int i = 0; i < index - 1; i++) {
            prev = prev.next;
        }
        newNode.next = prev.next;
        prev.next = newNode;
    }
    size++;
}

6.2 删除操作

// 删除指定位置的元素
public T remove(int index) {
    if (index < 0 || index >= size) {
        throw new IndexOutOfBoundsException();
    }

    T removedData;
    if (index == 0) {
        // 删除头节点
        removedData = head.data;
        head = head.next;
    } else {
        // 找到要删除节点的前驱节点
        Node<T> prev = head;
        for (int i = 0; i < index - 1; i++) {
            prev = prev.next;
        }
        removedData = prev.next.data;
        prev.next = prev.next.next;
    }
    size--;
    return removedData;
}

6.3 查找操作

// 查找元素是否存在
public boolean contains(T data) {
    Node<T> current = head;
    while (current != null) {
        if (current.data.equals(data)) {
            return true;
        }
        current = current.next;
    }
    return false;
}

常见错误分析:

  1. 忘记检查current是否为null导致NullPointerException
  2. 在遍历过程中修改链表结构可能导致ConcurrentModificationException
  3. 循环链表未正确处理终止条件会导致无限循环

7. 内存占用分析

  • 单链表节点内存:数据域(4-8字节) + 指针域(4-8字节)
  • 双向链表节点内存:数据域(4-8字节) + 前驱指针(4-8字节) + 后继指针(4-8字节)
  • 32位系统指针4字节,64位系统指针8字节

8. 并发环境下的线程安全

  1. 使用锁机制保护链表操作
  2. 使用并发集合如ConcurrentLinkedQueue
  3. 采用不可变链表设计
  4. 读写锁优化读多写少场景

9. 实际项目应用案例

  1. Java LinkedList实现
  2. Redis的列表数据结构
  3. Linux内核任务调度
  4. 浏览器历史记录管理
  5. 游戏中的对象池管理

10. 完整代码

本文中的代码示例可以在我的GitHub仓库中找到:查看完整代码