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

news2024/12/26 13:39:47

在这里插入图片描述

我的个人主页
我的专栏Java-数据结构,希望能帮助到大家!!!点赞❤ 收藏❤


目录

1. Java LinkedList 基础

1.1 LinkedList 简介
1.2 LinkedList 的实现原理
1.3 LinkedList 与 ArrayList 的区别

2. 链表基础

2.1 链表的定义与种类
2.2 单链表与双链表的区别
2.3 循环链表与普通链表

3. Java LinkedList 的常见操作

3.1 添加元素 (add, addFirst, addLast)
3.2 删除元素 (remove, removeFirst, removeLast)
3.3 查找元素 (get, indexOf, contains)
3.4 修改元素

4. Java LinkedList 的高级操作

4.1 迭代器的使用 (Iterator, ListIterator)
4.2 Queue 和 Deque 接口中的操作
4.3 使用 Collections 对 LinkedList 进行排序和操作

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

5.1 单链表的反转
5.2 链表中环的检测
5.3 链表的合并与分割
5.4 链表的删除与插入操作

6. LinkedList 的性能分析与优化

6.1 时间复杂度分析
6.2 内存消耗与管理
6.3 大数据量时的性能优化

7. LinkedList 的应用场景

7.1 实现队列 (Queue)
7.2 实现栈 (Stack)
7.3 使用链表存储数据历史记录

8. 链表的代码实现

8.1 单链表的手动实现
8.2 双链表的手动实现
8.3 循环链表的手动实现

9. 常见问题与调试技巧

9.1 空指针异常的处理
9.2 ConcurrentModificationException 的避免
9.3 常见逻辑错误与解决方法

10. 总结与扩展

10.1 LinkedList 在实际开发中的适用性
10.2 链表的扩展实现 (如跳跃表)
10.3 学习和实践资源推荐


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

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语言简单定义一个单链表节点类可以是这样:

class ListNode {
    int val; // 数据元素,这里以整数为例
    ListNode next; // 指向下一个节点的引用
    ListNode(int val) {
        this.val = val;
        this.next = null;
    }
}

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

    - 双链表的节点除了包含数据元素和指向下一个节点的指针外,还包含一个指向前一个节点的指针。
    - 以下是用Java定义双链表节点类的示例:
class DoubleListNode {
    int val;
    DoubleListNode prev;
    DoubleListNode next;
    DoubleListNode(int val) {
        this.val = val;
        this.prev = null;
        this.next = null;
    }
}
    - 双链表就像一个双向的通道,既可以从表头顺着`next`指针向后遍历,也可以从表尾顺着`prev`指针向前遍历。
  1. 遍历方式
    • 单链表
      • 只能单向遍历,从链表的头部开始,通过当前节点的next指针逐个访问下一个节点,直到到达链表尾部。
      • 例如,在Java中遍历一个单链表可以这样做:
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;
}
双链表:
    - 可以双向遍历。既可以从头部开始,通过`next`指针向后遍历,也可以从尾部开始,通过`prev`指针向前遍历。
    - 例如,遍历双链表向后的代码如下:
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;
}
    - 向前遍历的代码如下:
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)。
      • 例如:
// 在单链表节点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): 在链表尾部添加元素。

代码示例:添加元素

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(): 移除链表尾部的元素。

代码示例:删除元素

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): 检查链表中是否包含指定元素。

代码示例:查找元素

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) 方法,可以替换指定索引处的元素。

代码示例:修改元素

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 循环以及迭代器。

代码示例:遍历元素

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

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():移除并返回尾部的元素。

代码示例:作为队列使用

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

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

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 排序

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

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

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 指针的指向。

代码示例:反转单链表

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 判圈法)检测链表中是否存在环。

代码示例:检测环

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 的内存使用较高,因为每个节点不仅存储数据,还需要额外存储前驱和后继的指针。

特性LinkedListArrayList
存储结构双向链表动态数组
节点开销每个节点有两个额外指针仅存储数据
空间利用率较低较高

示例:分析内存消耗

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 性能优化方法

  1. 选择适合的场景

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

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

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

    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();
                // 处理数据
            }
        }
    }
    
  3. 预估链表大小

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

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

6.4 性能测试与比较

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

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():返回队列头部的元素但不移除。

代码示例:实现队列

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():返回栈顶元素但不移除。

代码示例:实现栈

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

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

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 单链表的手动实现
代码示例:实现单链表

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

示例代码:

import java.util.LinkedList;

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

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

解决方法:

  1. 在操作之前检查列表是否为空:
    if (!list.isEmpty()) {
        System.out.println(list.getFirst());
    } else {
        System.out.println("List is empty.");
    }
    
  2. 使用带有默认返回值的方法(如 peekFirstpeekLast),这些方法不会抛出异常:
    System.out.println(list.peekFirst()); // 返回 null 而非抛出异常
    

9.2 并发修改异常(ConcurrentModificationException)

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

示例代码:

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); // 在遍历时修改列表
            }
        }
    }
}

解决方法:

  1. 使用 迭代器remove() 方法:
    import java.util.Iterator;
    
    Iterator<String> iterator = list.iterator();
    while (iterator.hasNext()) {
        String element = iterator.next();
        if (element.equals("B")) {
            iterator.remove(); // 使用迭代器的 remove 方法
        }
    }
    
  2. 使用 CopyOnWriteArrayList 或其他线程安全的集合来避免并发问题。

9.3 性能问题

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

示例代码:

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 死循环问题

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

示例代码:

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; // 无限循环
        }
    }
}

解决方法:

  1. 使用快慢指针检测环:
    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;
    }
    
  2. 小心修改链表的 next 指针,避免手动制造环。

9.5 IndexOutOfBoundsException

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

示例代码:

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)); // 超出范围
    }
}

解决方法:

  1. 确保访问的索引有效:
    if (index >= 0 && index < list.size()) {
        System.out.println(list.get(index));
    } else {
        System.out.println("Index out of bounds.");
    }
    
  2. 使用异常捕获机制:
    try {
        System.out.println(list.get(5));
    } catch (IndexOutOfBoundsException e) {
        System.out.println("Invalid index: " + e.getMessage());
    }
    

9.6 内存泄漏问题

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

解决方法:

  1. 使用弱引用(WeakReference)来避免不必要的对象持有。
  2. 手动清理链表不再需要的节点:
    list.clear(); // 释放所有元素
    

9.7 链表的深拷贝问题

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

示例代码:

LinkedList<Node> list = new LinkedList<>();
list.add(new Node(1));
LinkedList<Node> clonedList = (LinkedList<Node>) list.clone();
clonedList.get(0).data = 100; // 修改克隆后的数据会影响原链表

解决方法:

  • 实现深拷贝:
    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 链表的扩展实现

  1. 实现栈(Stack)功能

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

    代码示例:使用 LinkedList 实现栈

    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
        }
    }
    
  2. 实现队列(Queue)功能

    • LinkedList 本身实现了 Queue 接口,天然适合用于队列操作。
  3. 实现 LRU 缓存(最近最少使用缓存)

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

    代码示例:LRU 缓存

    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}
        }
    }
    
  4. 跳表(Skip List)

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

10.3 学习和实践资源推荐

  1. 官方文档

    • Java LinkedList 文档
    • 深入理解 LinkedList 提供的所有方法及其实现原理。
  2. 经典书籍

    • 《数据结构与算法分析》:理解链表的基本原理和应用。
    • 《Java 编程思想》:深入理解 LinkedList 在集合框架中的地位。
  3. 开源项目

    • Github 上的 LRU Cache 实现。
    • 使用链表构建的自定义数据结构(如双端队列、循环链表)。
  4. 实践题目

    • 在在线平台(如 LeetCode、HackerRank)上练习与链表相关的算法题,例如:
      • 反转链表
      • 合并两个有序链表
      • 检测链表中的环

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2252928.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

北斗道路运输车辆管理应用:违规驾驶行为监测、车辆编队管理、安全跟踪(车辆历史轨迹查询)、车辆动态位置数据的实时查看和管理

文章目录 场景概述解决方案应用案例合作构想场景概述 面向旅游大巴车、危险品运输车及重型载货运输车等车辆,利用北斗定位导航服务,结合互联网通信技术,实现车辆安全驾驶管理与调度,有效降低道路事故发生风险,提升道路运输管理水平及车辆调度能力。 解决方案 在车辆上安…

【ABAP——DAILOG_2】

文章目录 使用Tabstrip控件实现分页签效果标签页的修改使用Table Control控件实现表单输出表格控件使用向导创建Table ControlTable Control列的修改 用户通过界面输入数据&#xff0c;数据通过屏幕控件传递到ABAP/4程序中的变量&#xff0c;程序在PBO中准备数据并显示界面&…

资料文件夹转移工具5.2.3 |快速转移到D盘,释放C盘空间

这是一款支持将C盘的常用文件夹转移到其他磁盘分区的工具&#xff0c;提供仅变目录、复制资料和转移资料三种转移方式。该工具完全免费&#xff0c;单文件免安装&#xff0c;大小仅为546KB&#xff0c;非常适合需要释放C盘空间的用户。 大小&#xff1a;546KB 下载地址&#…

使用STM32CubeMX配置串口各种功能

使用STM32CubeMX配置串口各种功能 STM32CubeMX软件的安装接收空闲中断STM32CubeMX配置1.新建工程2. 选择芯片3. 选择时钟和下载方式4. 配置串口5.设置工程消息6.生成代码7.修改生成的代码 空闲中断DMA转运STM32CubeMX配置4.配置串口5.设置工程消息6.生成代码7.修改生成的代码 S…

Javascript中DOM操作和事件监听综合练习 (具备三种功能的轮播图案例)

#如何去实现图片轮播效果图&#xff0c;通过创建一个基本的 HTML 页面结构&#xff0c;包含用于展示轮播图片的区域、左右切换箭头以及放置轮播图片的容器。# 整体架构 一、CSS 样式 接下来创建一个 styles.css 文件来设置页面的样式&#xff0c;让轮播效果看起来更美观。定义…

无人设备遥控器之防水性能篇

无人设备遥控器的防水性能是评估其耐用性和适应不同环境能力的重要指标。随着无人设备技术的不断发展&#xff0c;越来越多的遥控器在设计时融入了防水元素&#xff0c;以满足用户在不同天气条件下的使用需求。 一、防水等级与标准 无人设备遥控器的防水性能通常通过防水等级来…

【JAVA】Java入门 - 循环结构进阶

第1关 for循环的进阶使用-嵌套循环&#xff08;1&#xff09; 第2关 for循环的进阶使用-嵌套循环&#xff08;2&#xff09; 第3关 99乘法表 第4关 综合练习之ATM取款机 第5关 选择题 D、BC、B

【AI系统】AI 编译器后端优化

AI 编译器后端优化 AI 编译器分为多层架构&#xff0c;最顶层由各种 AI 训练框架编写的神经网络模型架构&#xff0c;一般由 Python 编写&#xff0c;常见的 AI 训练框架有 PyTorch、MindSpore、PaddlePaddle 等。在导入 AI 编译器时需要用对应框架的 converter 功能转换为 AI…

力扣 三角dp

动态规划基础题&#xff0c;当前所在元素来自上一行的两列的值。 题目 从图可以看出&#xff0c;每一行的第一个数与最后一个数都是1&#xff0c;然后中间的数是来自它左上方和右上方的数的和。当然并不是要打印这个三角形的形状&#xff0c;因此可以想到正常的打印方式应该是…

Oracle检查加强版本

支持更丰富了&#xff0c;代码也更乱了 #!/bin/bash## 实例个数 告警日志 实例状态 会话 活动会话 锁 集群状态 服务状态 磁盘空间 侦听日志 ## 单机、RAC Linux、AIX 11g、19c、23ai ## 依赖adrci配置正常&#xff0c;也可以改为 getAlert() ## ver 1.2case uname inAIX)ps…

RoBERTa- 稳健优化的 BERT 预训练模型详解

一、引言 自 BERT&#xff08;Bidirectional Encoder Representations from Transformers&#xff09;问世&#xff0c;预训练语言模型在自然语言处理&#xff08;NLP&#xff09;领域掀起革命浪潮&#xff0c;凭卓越表现大幅刷新诸多任务成绩。RoBERTa 承继 BERT 架构&#x…

【流程图】各元素形状和含义

判定、文档、数据、数据库、流程处理节点 矩形 - 动词 平行四边形 - 图像 下波浪 - 数据 图片来源http://baike.cu12.com/bkss/62449.shtml

「Mac畅玩鸿蒙与硬件41」UI互动应用篇18 - 多滑块联动控制器

本篇将带你实现一个多滑块联动的控制器应用。用户可以通过拖动多个滑块&#xff0c;动态控制不同参数&#xff08;如红绿蓝三色值&#xff09;&#xff0c;并实时显示最终结果。我们将以动态颜色调节为例&#xff0c;展示如何结合状态管理和交互逻辑&#xff0c;打造一个高级的…

PyQt6 开发基础

<?xml version"1.0" encoding"UTF-8"?> <ui version"4.0"><class>Form</class><widget class"QWidget" name"Form"><property name"geometry"><rect><x>0<…

Kafka如何保证消息可靠?

大家好&#xff0c;我是锋哥。今天分享关于【Kafka如何保证消息可靠&#xff1f;】面试题。希望对大家有帮助&#xff1b; Kafka如何保证消息可靠&#xff1f; 1000道 互联网大厂Java工程师 精选面试题-Java资源分享网 Kafka通过多种机制来确保消息的可靠性&#xff0c;主要包…

pytest(二)excel数据驱动

一、excel数据驱动 excel文件内容 excel数据驱动使用方法 import openpyxl import pytestdef get_excel():excel_obj openpyxl.load_workbook("../pytest结合数据驱动-excel/data.xlsx")sheet_obj excel_obj["Sheet1"]values sheet_obj.valuescase_li…

类和对象(中)(类的默认成员函数)+日期类实现

1.类的默认成员函数 默认成员函数就是用户没有显示实现&#xff0c;编译器会自动生成的成语函数称为默认成员函数。一个类&#xff0c;我们不写的情况下编译器会默认生成6个默认成员函数。C11后还增加了两个默认成员函数&#xff0c;移动构造和移动赋值&#xff08;这一节暂时…

详解版本控制工作原理及优势,常见的版本控制系统对比(HelixCore、Git、SVN等)

什么是版本控制软件&#xff1f;从基础层面来说&#xff0c;版本控制&#xff08;也可称版本管理&#xff09;就是随时间跟踪和管理文件变更的过程&#xff0c;而版本控制软件有助于实现这一过程的自动化。但这仅仅是其功能及其重要性的开端。 什么是版本控制&#xff1f; 版本…

记录一次网关异常

记一次网关异常 网关时不时就会出现下面的异常。关键是不知道什么时候就会报错&#xff0c;并且有时候就算什么都不操作&#xff0c;也会导致这个异常。 ERROR org.springframework.scheduling.support.TaskUtils$LoggingErrorHandler - Unexpected error occurred in schedul…

SpringMVC跨域问题解决方案

当Web应用程序尝试从一个源&#xff08;例如 http://localhost:9090&#xff09;向另一个不同的源&#xff08;例如 http://localhost:8080&#xff09;发起请求时&#xff0c;发现报错&#xff1a; 报错原因&#xff1a;请求被CORS策略拦截了 跨域问题概述 当Web应用程序尝试…