首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >【java-数据结构篇】揭秘 Java LinkedList:链表数据结构的 Java 实现原理与核心概念

【java-数据结构篇】揭秘 Java LinkedList:链表数据结构的 Java 实现原理与核心概念

作者头像
学无止尽5
发布2025-05-31 15:16:55
发布2025-05-31 15:16:55
31600
代码可运行
举报
运行总次数:0
代码可运行

1. Java LinkedList 基础

1.1 LinkedList 简介 LinkedList 是 Java 集合框架中的一个类,它实现了 ListDequeQueue 接口,底层基于双向链表实现。 LinkedList的底层是双向链表结构,由于链表没有将元素存储在连续的空间中,元素存储在单独的节点中,然后通过引⽤将节点连接起来了,因此在在任意位置插⼊或者删除元素时,不需要搬移元素,效率⽐较⾼。

在集合框架中,LinkedList也实现了List接⼝,具体如下:

【说明】 1. LinkedList实现了List接⼝ 2. LinkedList的底层使⽤了双向链表 3. LinkedList没有实现RandomAccess接⼝,因此LinkedList不⽀持随机访问 4. LinkedList的任意位置插⼊和删除元素时效率⽐较⾼,时间复杂度为O(1) 5. LinkedList⽐较适合任意位置插⼊的场景

1.2 LinkedList 的实现原理 LinkedList 的底层是一个双向链表,由节点(Node)组成。每个节点包含数据和指向前后节点的引用。

1.3 LinkedList 与 ArrayList 的区别

  • 存储结构LinkedList 是链表,ArrayList 是动态数组。
  • 访问速度ArrayList 随机访问快,LinkedList 插入和删除快。
  • 内存消耗LinkedList 因为维护节点引用,内存开销更大。

代码示例:创建和使用 LinkedList

代码语言:javascript
代码运行次数:0
运行
复制
import java.util.LinkedList;

public class LinkedListExample {
    public static void main(String[] args) {
        LinkedList<String> list = new LinkedList<>();
        list.add("A");
        list.add("B");
        list.add("C");
        list.addFirst("Start");
        list.addLast("End");

        System.out.println("LinkedList: " + list);

        list.remove("B");
        System.out.println("After removing 'B': " + list);
        
        System.out.println("First Element: " + list.getFirst());
        System.out.println("Last Element: " + list.getLast());
    }
}

2. 链表基础

2.1 链表的定义与种类 链表是一种通过指针将数据节点连接起来的数据结构,链表是⼀种物理存储结构上⾮连续存储结构,数据元素的逻辑顺序是通过链表中的引⽤链接次序实现的 。主要有以下几种:

注意: 1.从上图可看出,链式结构在逻辑上是连续的,但是在物理上不一定连续 2.现实中的结点一般都是从堆上申请出来的 3.从堆上申请的空间,是按照一定的策略来分配的,两次申请的空间可能连续,也可能不连续

  1. 单链表:每个节点只指向下一个节点。

  1. 双链表:每个节点有两个指针,分别指向前后节点。

  1. 带头
  1. 不带头
在这里插入图片描述
在这里插入图片描述

  1. 循环链表:链表尾部的指针指向头部节点,形成一个环。

重点掌握两种 1.⽆头单向⾮循环链表:结构简单,⼀般不会单独⽤来存数据。实际中更多是作为其他数据结构的⼦结构,如哈希桶、图的邻接表等等。另外这种结构在笔试⾯试中出现很多。

2.⽆头双向链表:在Java的集合框架库中LinkedList底层实现就是⽆头双向循环链表。

2.2 单链表与双链表的区别

1.结构定义 单链表是由一系列节点组成的数据结构。每个节点包含两个部分,一个是数据元素本身,另一个是指向下一个节点的指针(引用)。 例如,用Java语言简单定义一个单链表节点类可以是这样:

代码语言:javascript
代码运行次数:0
运行
复制
class ListNode {
    int val; // 数据元素,这里以整数为例
    ListNode next; // 指向下一个节点的引用
    ListNode(int val) {
        this.val = val;
        this.next = null;
    }
}

整个单链表就像是一条单向的链条,从表头开始,通过每个节点的next指针依次访问下一个节点,直到最后一个节点的next指针为null,表示链表的末尾。

代码语言:javascript
代码运行次数:0
运行
复制
    - 双链表的节点除了包含数据元素和指向下一个节点的指针外,还包含一个指向前一个节点的指针。
    - 以下是用Java定义双链表节点类的示例:
代码语言:javascript
代码运行次数:0
运行
复制
class DoubleListNode {
    int val;
    DoubleListNode prev;
    DoubleListNode next;
    DoubleListNode(int val) {
        this.val = val;
        this.prev = null;
        this.next = null;
    }
}
代码语言:javascript
代码运行次数:0
运行
复制
    - 双链表就像一个双向的通道,既可以从表头顺着`next`指针向后遍历,也可以从表尾顺着`prev`指针向前遍历。
  1. 遍历方式
    • 单链表
      • 只能单向遍历,从链表的头部开始,通过当前节点的next指针逐个访问下一个节点,直到到达链表尾部。
      • 例如,在Java中遍历一个单链表可以这样做:
代码语言:javascript
代码运行次数:0
运行
复制
ListNode head = new ListNode(1);
// 假设已经构建好链表,这里简单示例添加一个节点
ListNode second = new ListNode(2);
head.next = second;
ListNode current = head;
while (current!= null) {
    System.out.println(current.val);
    current = current.next;
}
代码语言:javascript
代码运行次数:0
运行
复制
双链表:
    - 可以双向遍历。既可以从头部开始,通过`next`指针向后遍历,也可以从尾部开始,通过`prev`指针向前遍历。
    - 例如,遍历双链表向后的代码如下:
代码语言:javascript
代码运行次数:0
运行
复制
DoubleListNode headDouble = new DoubleListNode(1);
DoubleListNode secondDouble = new DoubleListNode(2);
headDouble.next = secondDouble;
secondDouble.prev = headDouble;
DoubleListNode currentDouble = headDouble;
while (currentDouble!= null) {
    System.out.println(currentDouble.val);
    currentDouble = currentDouble.next;
}
代码语言:javascript
代码运行次数:0
运行
复制
    - 向前遍历的代码如下:
代码语言:javascript
代码运行次数:0
运行
复制
DoubleListNode tail = secondDouble;
while (tail!= null) {
    System.out.println(tail.val);
    tail = tail.prev;
}
  1. 插入操作
    • 单链表:
      • 在指定节点之后插入新节点相对简单。假设要在节点p之后插入节点newNode,步骤如下:
      • 首先将newNodenext指针指向p节点的下一个节点(即newNode.next = p.next),然后将p节点的next指针指向newNode(即p.next = newNode)。
      • 例如:
代码语言:javascript
代码运行次数:0
运行
复制
// 在单链表节点p之后插入newNode
ListNode p = head;
ListNode newNode = new ListNode(3);
newNode.next = p.next;
p.next = newNode;

但是如果要在指定节点之前插入新节点,需要先遍历链表找到该节点的前驱节点,这会比较复杂,尤其是在没有额外的头节点辅助的情况下。

  • 双链表:
    • 在指定节点之后插入新节点的操作和单链表类似,但是由于有prev指针,操作更加方便。在节点p之后插入newNode,步骤如下:
    • 首先将newNodenext指针指向p节点的下一个节点(即newNode.next = p.next),然后将p节点的下一个节点(假设为q)的prev指针指向newNode(即q.prev = newNode),接着将p节点的next指针指向newNode(即p.next = newNode),最后将newNodeprev指针指向p(即newNode.prev = p)。
    • 在指定节点之前插入新节点也比较方便,因为可以直接通过prev指针找到前驱节点进行操作。
  1. 删除操作
    • 单链表
      • 要删除指定节点p,需要先找到p的前驱节点(假设为q),然后将qnext指针指向p的下一个节点(即q.next = p.next)。
      • 如果没有额外的头节点辅助,删除表头节点时需要特殊处理,因为表头节点没有前驱节点。
    • 双链表
      • 要删除指定节点p,可以直接通过pprev指针找到前驱节点(假设为q),将qnext指针指向p的下一个节点(即q.next = p.next),同时通过pnext指针找到后继节点(假设为r),将rprev指针指向q(即r.prev = q)。
      • 无论是删除表头、表尾还是中间节点,操作相对单链表更加统一和方便。

    2.3 循环链表与普通链表

普通链表由节点依次连接而成,有明确的头节点,最后一个节点的指针指向空,以此标识链表的结尾。例如单链表通过各节点的指针顺序串联数据。而循环链表则是一种特殊形式,它的最后一个节点指针并非指向空,而是指向头节点,从而形成一个闭环结构。在遍历方面,普通链表遇空指针停止,循环链表则需特定条件控制遍历次数以避免死循环。插入操作时,普通链表需关注前驱节点处理,循环链表除插入逻辑外,还得维护循环特性,二者在不同应用场景下各有优势。

3. Java LinkedList 的常见操作

3.1 添加元素 LinkedList 提供了多种方法来添加元素,常用的方法包括:

  • add(E element): 在链表末尾添加元素。
  • add(int index, E element): 在指定索引处添加元素。
  • addFirst(E element): 在链表头部添加元素。
  • addLast(E element): 在链表尾部添加元素。

代码示例:添加元素

代码语言:javascript
代码运行次数:0
运行
复制
import java.util.LinkedList;

public class LinkedListAddExample {
    public static void main(String[] args) {
        LinkedList<String> list = new LinkedList<>();

        list.add("A"); // 添加到尾部
        list.addFirst("Start"); // 添加到头部
        list.addLast("End"); // 添加到尾部
        list.add(1, "Middle"); // 指定位置添加

        System.out.println("LinkedList: " + list);
    }
}

3.2 删除元素 LinkedList 提供的方法来删除元素:

  • remove(): 移除头部的元素。
  • remove(int index): 移除指定位置的元素。
  • remove(Object o): 移除第一次出现的指定元素。
  • removeFirst(): 移除链表头部的元素。
  • removeLast(): 移除链表尾部的元素。

代码示例:删除元素

代码语言:javascript
代码运行次数:0
运行
复制
import java.util.LinkedList;

public class LinkedListRemoveExample {
    public static void main(String[] args) {
        LinkedList<String> list = new LinkedList<>();
        list.add("A");
        list.add("B");
        list.add("C");
        list.add("D");

        System.out.println("Original List: " + list);

        list.remove("B"); // 删除元素 B
        System.out.println("After removing 'B': " + list);

        list.remove(1); // 删除索引为 1 的元素
        System.out.println("After removing index 1: " + list);

        list.removeFirst(); // 删除头部元素
        System.out.println("After removing first: " + list);

        list.removeLast(); // 删除尾部元素
        System.out.println("After removing last: " + list);
    }
}

3.3 查找元素 LinkedList 提供的方法来查找元素:

  • get(int index): 获取指定索引位置的元素。
  • getFirst(): 获取链表头部的元素。
  • getLast(): 获取链表尾部的元素。
  • indexOf(Object o): 返回第一次出现的元素的索引。
  • lastIndexOf(Object o): 返回最后一次出现的元素的索引。
  • contains(Object o): 检查链表中是否包含指定元素。

代码示例:查找元素

代码语言:javascript
代码运行次数:0
运行
复制
import java.util.LinkedList;

public class LinkedListFindExample {
    public static void main(String[] args) {
        LinkedList<String> list = new LinkedList<>();
        list.add("A");
        list.add("B");
        list.add("C");
        list.add("B");

        System.out.println("LinkedList: " + list);

        System.out.println("Element at index 1: " + list.get(1)); // 获取索引为1的元素
        System.out.println("First Element: " + list.getFirst()); // 获取头部元素
        System.out.println("Last Element: " + list.getLast()); // 获取尾部元素
        System.out.println("Index of 'B': " + list.indexOf("B")); // 查找 B 的第一次出现位置
        System.out.println("Last Index of 'B': " + list.lastIndexOf("B")); // 查找 B 的最后一次出现位置
        System.out.println("Contains 'C': " + list.contains("C")); // 检查是否包含 C
    }
}

3.4 修改元素 修改元素通常使用 set(int index, E element) 方法,可以替换指定索引处的元素。

代码示例:修改元素

代码语言:javascript
代码运行次数:0
运行
复制
import java.util.LinkedList;

public class LinkedListModifyExample {
    public static void main(String[] args) {
        LinkedList<String> list = new LinkedList<>();
        list.add("A");
        list.add("B");
        list.add("C");

        System.out.println("Original List: " + list);

        list.set(1, "X"); // 将索引为1的元素替换为 "X"
        System.out.println("After modification: " + list);
    }
}

3.5 遍历元素 LinkedList 的遍历可以使用 for 循环、增强 for 循环以及迭代器。

代码示例:遍历元素

代码语言:javascript
代码运行次数:0
运行
复制
import java.util.LinkedList;
import java.util.Iterator;

public class LinkedListTraverseExample {
    public static void main(String[] args) {
        LinkedList<String> list = new LinkedList<>();
        list.add("A");
        list.add("B");
        list.add("C");

        // 使用 for 循环
        System.out.println("Using for loop:");
        for (int i = 0; i < list.size(); i++) {
            System.out.print(list.get(i) + " ");
        }
        System.out.println();

        // 使用增强的 for 循环
        System.out.println("Using enhanced for loop:");
        for (String element : list) {
            System.out.print(element + " ");
        }
        System.out.println();

        // 使用迭代器
        System.out.println("Using iterator:");
        Iterator<String> iterator = list.iterator();
        while (iterator.hasNext()) {
            System.out.print(iterator.next() + " ");
        }
        System.out.println();
    }
}

4. Java LinkedList 的高级操作

4.1 迭代器的使用 LinkedList 支持两种迭代器:

  • Iterator:用于单向遍历。
  • ListIterator:用于双向遍历,并支持在遍历时添加或修改元素。

代码示例:使用 Iterator 和 ListIterator

代码语言:javascript
代码运行次数:0
运行
复制
import java.util.LinkedList;
import java.util.Iterator;
import java.util.ListIterator;

public class LinkedListIteratorExample {
    public static void main(String[] args) {
        LinkedList<String> list = new LinkedList<>();
        list.add("A");
        list.add("B");
        list.add("C");

        // 使用 Iterator 单向遍历
        System.out.println("Using Iterator:");
        Iterator<String> iterator = list.iterator();
        while (iterator.hasNext()) {
            System.out.print(iterator.next() + " ");
        }
        System.out.println();

        // 使用 ListIterator 双向遍历
        System.out.println("Using ListIterator (forward):");
        ListIterator<String> listIterator = list.listIterator();
        while (listIterator.hasNext()) {
            System.out.print(listIterator.next() + " ");
        }
        System.out.println();

        System.out.println("Using ListIterator (backward):");
        while (listIterator.hasPrevious()) {
            System.out.print(listIterator.previous() + " ");
        }
        System.out.println();
    }
}

4.2 Queue 和 Deque 接口中的操作 LinkedList 实现了 Deque 接口,因此它可以作为双端队列使用。

常用操作:

  • 队列操作
    • offer(E e):将元素添加到队列尾部。
    • poll():移除并返回队列头部的元素。
    • peek():返回队列头部的元素但不移除。
  • 双端队列操作
    • offerFirst(E e):在头部添加元素。
    • offerLast(E e):在尾部添加元素。
    • pollFirst():移除并返回头部的元素。
    • pollLast():移除并返回尾部的元素。

代码示例:作为队列使用

代码语言:javascript
代码运行次数:0
运行
复制
import java.util.LinkedList;
import java.util.Queue;

public class LinkedListQueueExample {
    public static void main(String[] args) {
        Queue<Integer> queue = new LinkedList<>();

        // 添加元素
        queue.offer(1);
        queue.offer(2);
        queue.offer(3);

        System.out.println("Queue: " + queue);

        // 查看头部元素
        System.out.println("Peek: " + queue.peek());

        // 移除头部元素
        System.out.println("Poll: " + queue.poll());
        System.out.println("Queue after poll: " + queue);
    }
}

代码示例:作为双端队列使用

代码语言:javascript
代码运行次数:0
运行
复制
import java.util.Deque;
import java.util.LinkedList;

public class LinkedListDequeExample {
    public static void main(String[] args) {
        Deque<String> deque = new LinkedList<>();

        // 双端操作
        deque.offerFirst("A");
        deque.offerLast("B");
        deque.offerFirst("C");

        System.out.println("Deque: " + deque);

        // 头部和尾部操作
        System.out.println("Poll First: " + deque.pollFirst());
        System.out.println("Poll Last: " + deque.pollLast());
        System.out.println("Deque after poll: " + deque);
    }
}

4.3 使用 Collections 对 LinkedList 进行排序和操作 LinkedList 可以使用 Collections 工具类进行排序、反转等操作。

代码示例:对 LinkedList 排序

代码语言:javascript
代码运行次数:0
运行
复制
import java.util.LinkedList;
import java.util.Collections;

public class LinkedListSortExample {
    public static void main(String[] args) {
        LinkedList<Integer> list = new LinkedList<>();
        list.add(5);
        list.add(2);
        list.add(8);
        list.add(1);

        System.out.println("Original List: " + list);

        // 排序
        Collections.sort(list);
        System.out.println("Sorted List: " + list);

        // 反转
        Collections.reverse(list);
        System.out.println("Reversed List: " + list);

        // 随机打乱
        Collections.shuffle(list);
        System.out.println("Shuffled List: " + list);
    }
}

4.4 克隆 LinkedList 使用 clone() 方法可以创建 LinkedList 的浅拷贝。

代码示例:克隆 LinkedList

代码语言:javascript
代码运行次数:0
运行
复制
import java.util.LinkedList;

public class LinkedListCloneExample {
    public static void main(String[] args) {
        LinkedList<String> original = new LinkedList<>();
        original.add("A");
        original.add("B");
        original.add("C");

        LinkedList<String> cloned = (LinkedList<String>) original.clone();

        System.out.println("Original List: " + original);
        System.out.println("Cloned List: " + cloned);

        // 修改克隆的列表不会影响原列表
        cloned.add("D");
        System.out.println("Modified Cloned List: " + cloned);
        System.out.println("Original List after modification: " + original);
    }
}

4.5 将 LinkedList 转为其他集合类型 LinkedList 可以轻松地转为 ArrayList 或其他集合类型。

代码示例:转为 ArrayList

代码语言:javascript
代码运行次数:0
运行
复制
import java.util.LinkedList;
import java.util.ArrayList;

public class LinkedListToArrayListExample {
    public static void main(String[] args) {
        LinkedList<String> linkedList = new LinkedList<>();
        linkedList.add("A");
        linkedList.add("B");
        linkedList.add("C");

        // 转为 ArrayList
        ArrayList<String> arrayList = new ArrayList<>(linkedList);

        System.out.println("LinkedList: " + linkedList);
        System.out.println("ArrayList: " + arrayList);
    }
}

5. 链表的基本操作与算法

5.1 单链表的反转 反转单链表的核心思想是改变节点的 next 指针的指向。

代码示例:反转单链表

代码语言:javascript
代码运行次数:0
运行
复制
public class ReverseLinkedList {
    static class Node {
        int data;
        Node next;

        Node(int data) {
            this.data = data;
        }
    }

    public static Node reverse(Node head) {
        Node prev = null;
        Node current = head;
        while (current != null) {
            Node next = current.next;
            current.next = prev;
            prev = current;
            current = next;
        }
        return prev;
    }

    public static void printList(Node head) {
        Node temp = head;
        while (temp != null) {
            System.out.print(temp.data + " -> ");
            temp = temp.next;
        }
        System.out.println("null");
    }

    public static void main(String[] args) {
        Node head = new Node(1);
        head.next = new Node(2);
        head.next.next = new Node(3);

        System.out.println("Original List:");
        printList(head);

        head = reverse(head);
        System.out.println("Reversed List:");
        printList(head);
    }
}

5.2 链表中环的检测 使用快慢指针(Floyd 判圈法)检测链表中是否存在环。

代码示例:检测环

代码语言:javascript
代码运行次数:0
运行
复制
public class DetectCycle {
    static class Node {
        int data;
        Node next;

        Node(int data) {
            this.data = data;
        }
    }

    public static boolean hasCycle(Node head) {
        Node slow = head;
        Node fast = head;

        while (fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;

            if (slow == fast) {
                return true;
            }
        }
        return false;
    }

    public static void main(String[] args) {
        Node head = new Node(1);
        head.next = new Node(2);
        head.next.next = new Node(3);
        head.next.next.next = head; // Creates a cycle

        System.out.println("Has Cycle: " + hasCycle(head));
    }
}

6. LinkedList 的性能分析与优化

6.1 时间复杂度分析

LinkedList 的操作性能受链表结构的限制,具体复杂度如下:

操作

时间复杂度

说明

添加元素

- 头部插入

O(1)

直接修改头节点指针即可。

- 尾部插入

O(1)

如果有尾指针,直接修改尾节点指针;否则需要遍历到尾部。

- 指定位置插入

O(n)

需要遍历链表找到指定位置。

删除元素

- 头部删除

O(1)

修改头节点指针即可。

- 尾部删除

O(n)

需要遍历找到尾部的前一个节点。

- 指定位置删除

O(n)

需要遍历找到该位置。

访问元素

O(n)

需要从头开始逐个遍历找到指定位置。

搜索元素

O(n)

需要从头到尾遍历链表。

总结: 对于频繁的插入、删除操作(尤其是头部或尾部操作),LinkedList 性能优于 ArrayList。 对于频繁的随机访问或搜索操作,ArrayList 是更优选择。


6.2 内存消耗分析

LinkedList 的内存使用较高,因为每个节点不仅存储数据,还需要额外存储前驱和后继的指针。

特性

LinkedList

ArrayList

存储结构

双向链表

动态数组

节点开销

每个节点有两个额外指针

仅存储数据

空间利用率

较低

较高

示例:分析内存消耗

代码语言:javascript
代码运行次数:0
运行
复制
import java.util.LinkedList;
import java.util.ArrayList;

public class MemoryUsageExample {
    public static void main(String[] args) {
        LinkedList<Integer> linkedList = new LinkedList<>();
        ArrayList<Integer> arrayList = new ArrayList<>();

        for (int i = 0; i < 1000; i++) {
            linkedList.add(i);
            arrayList.add(i);
        }

        System.out.println("LinkedList size: " + linkedList.size());
        System.out.println("ArrayList size: " + arrayList.size());

        // 虽然无法直接显示内存使用,但 LinkedList 的开销更高。
    }
}

6.3 性能优化方法

选择适合的场景

  • 使用 LinkedList 时应避免频繁的随机访问操作。
  • 如果操作多为插入、删除,尤其是在头部或尾部,优先选择 LinkedList

减少遍历操作

  • 避免多次调用 get(index) 来访问元素,可以使用 Iterator 或增强的 for 循环提高效率。

示例:用迭代器代替索引遍历

代码语言:javascript
代码运行次数:0
运行
复制
import java.util.LinkedList;
import java.util.Iterator;

public class OptimizedTraversal {
    public static void main(String[] args) {
        LinkedList<Integer> list = new LinkedList<>();
        for (int i = 0; i < 1000; i++) {
            list.add(i);
        }

        // 遍历方式优化
        Iterator<Integer> iterator = list.iterator();
        while (iterator.hasNext()) {
            Integer value = iterator.next();
            // 处理数据
        }
    }
}

预估链表大小

  • 如果使用 LinkedList 构建大规模数据结构,尽量减少动态扩展,预先分配节点空间。
  • 在某些场景中,可以考虑手动实现链表来减少不必要的开销。

结合其他数据结构

  • 如果随机访问和插入删除操作都需要较高效率,可以结合 HashMapLinkedList,例如实现 LRU 缓存

6.4 性能测试与比较

代码示例:LinkedList 与 ArrayList 性能对比

代码语言:javascript
代码运行次数:0
运行
复制
import java.util.LinkedList;
import java.util.ArrayList;

public class PerformanceTest {
    public static void main(String[] args) {
        LinkedList<Integer> linkedList = new LinkedList<>();
        ArrayList<Integer> arrayList = new ArrayList<>();

        long startTime, endTime;

        // 测试插入性能
        startTime = System.nanoTime();
        for (int i = 0; i < 100000; i++) {
            linkedList.add(0, i);
        }
        endTime = System.nanoTime();
        System.out.println("LinkedList insertion time: " + (endTime - startTime) + " ns");

        startTime = System.nanoTime();
        for (int i = 0; i < 100000; i++) {
            arrayList.add(0, i);
        }
        endTime = System.nanoTime();
        System.out.println("ArrayList insertion time: " + (endTime - startTime) + " ns");

        // 测试访问性能
        startTime = System.nanoTime();
        for (int i = 0; i < linkedList.size(); i++) {
            linkedList.get(i);
        }
        endTime = System.nanoTime();
        System.out.println("LinkedList access time: " + (endTime - startTime) + " ns");

        startTime = System.nanoTime();
        for (int i = 0; i < arrayList.size(); i++) {
            arrayList.get(i);
        }
        endTime = System.nanoTime();
        System.out.println("ArrayList access time: " + (endTime - startTime) + " ns");
    }
}

测试结果:

  • 插入操作:LinkedList 优于 ArrayList,尤其是在头部插入时。
  • 访问操作:ArrayList 明显优于 LinkedList,因为它支持随机访问。

7. LinkedList 的应用场景

LinkedList 是基于双向链表实现的,适用于某些特定的应用场景。以下列出 LinkedList 的主要应用及实现方法。

7.1 实现队列 (Queue)

LinkedList 实现了 Queue 接口,因此可以直接作为队列使用,支持先进先出的操作。

常用方法:

  • offer(E e):在队列尾部添加元素。
  • poll():移除并返回队列头部的元素。
  • peek():返回队列头部的元素但不移除。

代码示例:实现队列

代码语言:javascript
代码运行次数:0
运行
复制
import java.util.LinkedList;
import java.util.Queue;

public class LinkedListQueueExample {
    public static void main(String[] args) {
        Queue<Integer> queue = new LinkedList<>();

        // 添加元素到队列
        queue.offer(10);
        queue.offer(20);
        queue.offer(30);

        System.out.println("Queue: " + queue);

        // 获取并移除头部元素
        System.out.println("Poll: " + queue.poll());

        // 获取头部元素但不移除
        System.out.println("Peek: " + queue.peek());

        System.out.println("Queue after operations: " + queue);
    }
}

7.2 实现栈 (Stack)

LinkedList 也可以用作栈的实现,支持后进先出的操作。可以使用以下方法:

  • push(E e):将元素压入栈顶。
  • pop():移除并返回栈顶元素。
  • peek():返回栈顶元素但不移除。

代码示例:实现栈

代码语言:javascript
代码运行次数:0
运行
复制
import java.util.LinkedList;

public class LinkedListStackExample {
    public static void main(String[] args) {
        LinkedList<Integer> stack = new LinkedList<>();

        // 压栈
        stack.push(10);
        stack.push(20);
        stack.push(30);

        System.out.println("Stack: " + stack);

        // 弹栈
        System.out.println("Pop: " + stack.pop());

        // 查看栈顶元素
        System.out.println("Peek: " + stack.peek());

        System.out.println("Stack after operations: " + stack);
    }
}

7.3 数据历史记录的存储

LinkedList 的双向链表特性,支持快速遍历前后节点,因此适用于实现历史记录功能,如浏览器的前进和后退。

代码示例:实现简单的历史记录功能

代码语言:javascript
代码运行次数:0
运行
复制
import java.util.LinkedList;

public class BrowserHistory {
    private LinkedList<String> history = new LinkedList<>();
    private int current = -1;

    public void visit(String url) {
        while (history.size() > current + 1) {
            history.removeLast(); // 清除前进历史
        }
        history.add(url);
        current++;
        System.out.println("Visited: " + url);
    }

    public void back() {
        if (current > 0) {
            current--;
            System.out.println("Back to: " + history.get(current));
        } else {
            System.out.println("No more back history.");
        }
    }

    public void forward() {
        if (current < history.size() - 1) {
            current++;
            System.out.println("Forward to: " + history.get(current));
        } else {
            System.out.println("No more forward history.");
        }
    }

    public static void main(String[] args) {
        BrowserHistory browser = new BrowserHistory();
        browser.visit("google.com");
        browser.visit("youtube.com");
        browser.back();
        browser.visit("github.com");
        browser.back();
        browser.forward();
    }
}

8. 链表的代码实现

8.1 单链表的手动实现 代码示例:实现单链表

代码语言:javascript
代码运行次数:0
运行
复制
class MyLinkedList {
    private Node head;

    private static class Node {
        int data;
        Node next;

        Node(int data) {
            this.data = data;
        }
    }

    public void add(int data) {
        if (head == null) {
            head = new Node(data);
            return;
        }
        Node temp = head;
        while (temp.next != null) {
            temp = temp.next;
        }
        temp.next = new Node(data);
    }

    public void printList() {
        Node temp = head;
        while (temp != null) {
            System.out.print(temp.data + " -> ");
            temp = temp.next;
        }
        System.out.println("null");
    }

    public static void main(String[] args) {
        MyLinkedList list = new MyLinkedList();
        list.add(1);
        list.add(2);
        list.add(3);
        list.printList();
    }
}

9. 常见问题与调试技巧

9.1 空指针异常(NullPointerException)

问题描述: 当操作一个空的 LinkedList 时(例如访问头部或尾部元素),会抛出 NullPointerException

示例代码:

代码语言:javascript
代码运行次数:0
运行
复制
import java.util.LinkedList;

public class NullPointerExample {
    public static void main(String[] args) {
        LinkedList<String> list = new LinkedList<>();

        // 空列表访问会导致 NullPointerException
        System.out.println(list.getFirst());
    }
}

解决方法:

在操作之前检查列表是否为空:

代码语言:javascript
代码运行次数:0
运行
复制
if (!list.isEmpty()) {
    System.out.println(list.getFirst());
} else {
    System.out.println("List is empty.");
}

使用带有默认返回值的方法(如 peekFirstpeekLast),这些方法不会抛出异常:

代码语言:javascript
代码运行次数:0
运行
复制
System.out.println(list.peekFirst()); // 返回 null 而非抛出异常

9.2 并发修改异常(ConcurrentModificationException)

问题描述: 在遍历 LinkedList 的同时修改其内容(例如添加或删除元素),会抛出 ConcurrentModificationException

示例代码:

代码语言:javascript
代码运行次数:0
运行
复制
import java.util.LinkedList;

public class ConcurrentModificationExample {
    public static void main(String[] args) {
        LinkedList<String> list = new LinkedList<>();
        list.add("A");
        list.add("B");
        list.add("C");

        for (String element : list) {
            if (element.equals("B")) {
                list.remove(element); // 在遍历时修改列表
            }
        }
    }
}

解决方法:

使用 迭代器remove() 方法:

代码语言:javascript
代码运行次数:0
运行
复制
import java.util.Iterator;

Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
    String element = iterator.next();
    if (element.equals("B")) {
        iterator.remove(); // 使用迭代器的 remove 方法
    }
}

使用 CopyOnWriteArrayList 或其他线程安全的集合来避免并发问题。


9.3 性能问题

问题描述: 对 LinkedList 进行频繁的随机访问或搜索操作时,性能可能会很低。

示例代码:

代码语言:javascript
代码运行次数:0
运行
复制
LinkedList<Integer> list = new LinkedList<>();
for (int i = 0; i < 100000; i++) {
    list.add(i);
}

// 随机访问
for (int i = 0; i < list.size(); i++) {
    System.out.println(list.get(i)); // 低效:每次 get 都需要从头遍历
}

解决方法:

  • 遍历操作时使用迭代器代替随机访问。
  • 如果需要频繁随机访问,考虑使用 ArrayList 替代 LinkedList

9.4 死循环问题

问题描述: 在手动实现链表时,可能会不小心创建一个环,导致死循环。

示例代码:

代码语言:javascript
代码运行次数:0
运行
复制
class Node {
    int data;
    Node next;

    Node(int data) {
        this.data = data;
    }
}

public class InfiniteLoopExample {
    public static void main(String[] args) {
        Node head = new Node(1);
        Node second = new Node(2);
        head.next = second;
        second.next = head; // 创建一个循环

        Node current = head;
        while (current != null) {
            System.out.println(current.data);
            current = current.next; // 无限循环
        }
    }
}

解决方法:

使用快慢指针检测环:

代码语言:javascript
代码运行次数:0
运行
复制
public static boolean hasCycle(Node head) {
    Node slow = head;
    Node fast = head;

    while (fast != null && fast.next != null) {
        slow = slow.next;
        fast = fast.next.next;

        if (slow == fast) {
            return true; // 检测到环
        }
    }
    return false;
}

小心修改链表的 next 指针,避免手动制造环。


9.5 IndexOutOfBoundsException

问题描述: 在尝试访问 LinkedList 中不存在的索引时,会抛出 IndexOutOfBoundsException

示例代码:

代码语言:javascript
代码运行次数:0
运行
复制
import java.util.LinkedList;

public class IndexOutOfBoundsExample {
    public static void main(String[] args) {
        LinkedList<String> list = new LinkedList<>();
        list.add("A");

        System.out.println(list.get(5)); // 超出范围
    }
}

解决方法:

确保访问的索引有效:

代码语言:javascript
代码运行次数:0
运行
复制
if (index >= 0 && index < list.size()) {
    System.out.println(list.get(index));
} else {
    System.out.println("Index out of bounds.");
}

使用异常捕获机制:

代码语言:javascript
代码运行次数:0
运行
复制
try {
    System.out.println(list.get(5));
} catch (IndexOutOfBoundsException e) {
    System.out.println("Invalid index: " + e.getMessage());
}

9.6 内存泄漏问题

问题描述: 长时间持有链表的引用,或者链表节点之间存在循环引用,可能导致内存泄漏。

解决方法:

使用弱引用(WeakReference)来避免不必要的对象持有。

手动清理链表不再需要的节点:

代码语言:javascript
代码运行次数:0
运行
复制
list.clear(); // 释放所有元素

9.7 链表的深拷贝问题

问题描述: 默认的 clone() 方法只进行浅拷贝,对于复杂对象可能会引发问题。

示例代码:

代码语言:javascript
代码运行次数:0
运行
复制
LinkedList<Node> list = new LinkedList<>();
list.add(new Node(1));
LinkedList<Node> clonedList = (LinkedList<Node>) list.clone();
clonedList.get(0).data = 100; // 修改克隆后的数据会影响原链表

解决方法:

实现深拷贝:

代码语言:javascript
代码运行次数:0
运行
复制
LinkedList<Node> deepClone = new LinkedList<>();
for (Node node : list) {
    deepClone.add(new Node(node.data)); // 手动复制节点
}

10. 总结与扩展

10.1 LinkedList 在实际开发中的适用性

  • 优点
    1. 高效的插入和删除操作
      • 在头部、尾部插入或删除元素效率极高,时间复杂度为 O(1)。
    2. 双向链表的灵活性
      • 可以作为队列(Queue)或双端队列(Deque)使用。
    3. 动态扩展性
      • 无需考虑容量问题,链表节点可以动态分配。
  • 缺点
    1. 随机访问性能较差
      • 随机访问需要从头遍历,时间复杂度为 O(n)。
    2. 内存开销大
      • 每个节点额外存储两个指针(前驱和后继)。
  • 适用场景
    • 数据量较小且插入、删除操作频繁。
    • 需要使用队列或双端队列功能。
    • 需要频繁改变数据结构,例如在中间插入或删除节点。

10.2 链表的扩展实现

实现栈(Stack)功能

  • LinkedList 可以很方便地实现栈,利用 addFirst()removeFirst() 模拟栈的 pushpop 操作。

代码示例:使用 LinkedList 实现栈

代码语言:javascript
代码运行次数:0
运行
复制
import java.util.LinkedList;

public class LinkedListStack {
    private LinkedList<Integer> stack = new LinkedList<>();

    public void push(int value) {
        stack.addFirst(value);
    }

    public int pop() {
        return stack.removeFirst();
    }

    public int peek() {
        return stack.getFirst();
    }

    public boolean isEmpty() {
        return stack.isEmpty();
    }

    public static void main(String[] args) {
        LinkedListStack stack = new LinkedListStack();
        stack.push(10);
        stack.push(20);
        System.out.println(stack.pop()); // 输出 20
        System.out.println(stack.peek()); // 输出 10
    }
}

实现队列(Queue)功能

  • LinkedList 本身实现了 Queue 接口,天然适合用于队列操作。

实现 LRU 缓存(最近最少使用缓存)

  • 利用 LinkedHashMapLinkedList 的组合,可以实现一个简单的 LRU 缓存。

代码示例:LRU 缓存

代码语言:javascript
代码运行次数:0
运行
复制
import java.util.LinkedHashMap;
import java.util.Map;

public class LRUCache<K, V> extends LinkedHashMap<K, V> {
    private final int capacity;

    public LRUCache(int capacity) {
        super(capacity, 0.75f, true);
        this.capacity = capacity;
    }

    @Override
    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
        return size() > capacity;
    }

    public static void main(String[] args) {
        LRUCache<Integer, String> cache = new LRUCache<>(3);
        cache.put(1, "A");
        cache.put(2, "B");
        cache.put(3, "C");
        cache.get(1); // 使用键 1
        cache.put(4, "D"); // 淘汰键 2

        System.out.println(cache); // 输出 {3=C, 1=A, 4=D}
    }
}

跳表(Skip List)

  • 跳表是一种基于链表的扩展数据结构,支持快速插入、删除和搜索,时间复杂度为 O(log n)。
  • 可用于实现有序的键值存储。

10.3 学习和实践资源推荐

  1. 官方文档
  2. 经典书籍
    • 《数据结构与算法分析》:理解链表的基本原理和应用。
    • 《Java 编程思想》:深入理解 LinkedList 在集合框架中的地位。
  3. 开源项目
    • Github 上的 LRU Cache 实现。
    • 使用链表构建的自定义数据结构(如双端队列、循环链表)。
  4. 实践题目
    • 在在线平台(如 LeetCode、HackerRank)上练习与链表相关的算法题,例如:
      • 反转链表
      • 合并两个有序链表
      • 检测链表中的环
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2024-11-30,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. Java LinkedList 基础
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档