当前位置: 首页 > news >正文

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

在这里插入图片描述

我的个人主页
我的专栏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"); // 删除元素 BSystem.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");// 转为 ArrayListArrayList<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 cycleSystem.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<>();// 空列表访问会导致 NullPointerExceptionSystem.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()); // 输出 20System.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;}@Overrideprotected 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); // 使用键 1cache.put(4, "D"); // 淘汰键 2System.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)上练习与链表相关的算法题,例如:
      • 反转链表
      • 合并两个有序链表
      • 检测链表中的环

相关文章:

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

我的个人主页 我的专栏&#xff1a;Java-数据结构&#xff0c;希望能帮助到大家&#xff01;&#xff01;&#xff01;点赞❤ 收藏❤ 目录 1. Java LinkedList 基础 1.1 LinkedList 简介 1.2 LinkedList 的实现原理 1.3 LinkedList 与 ArrayList 的区别 2. 链表基础 2.1 链…...

macOS运行amd64的镜像

在macOS上运行amd64&#xff08;x86_64&#xff09;架构的镜像&#xff0c;通常通过虚拟化或仿真工具来实现。例如&#xff0c;如果你使用的是基于Apple Silicon&#xff08;M1或M2等&#xff09;芯片的Mac&#xff0c;那么你的处理器是ARM架构的&#xff0c;而amd64是x86架构&…...

轻量的基于图结构的RAG方案LightRAG

LightRAG出自2024年10月的论文《LIGHTRAG: SIMPLE AND FASTRETRIEVAL-AUGMENTED GENERATION》(github)&#xff0c;也是使用图结构来索引和搜索相关文本。 LightRAG作者认为已有的RAG系统有如下两个限制&#xff0c;导致难以回答类似"How does the rise of electric vehi…...

计算机的错误计算(一百七十三)

摘要 给定多项式 在 MATLAB 中计算 的值。输出是错误结果。 例1. 已知 计算 直接贴图吧&#xff1a; 这样&#xff0c;MATLAB 输出了错误结果。因为准确值为 0.2401e-16 . 注&#xff1a;可参看计算机的错误计算&#xff08;六&#xff09;。...

【力扣】—— 二叉树的前序遍历、字典序最小回文串

Hi~&#xff01;这里是奋斗的明志&#xff0c;很荣幸您能阅读我的文章&#xff0c;诚请评论指点&#xff0c;欢迎欢迎 ~~ &#x1f331;&#x1f331;个人主页&#xff1a;奋斗的明志 &#x1f331;&#x1f331;所属专栏&#xff1a;数据结构 &#x1f4da;本系列文章为个人学…...

linux替换更高版本gcc

实际使用时对与gcc版本有很多要求, 需要在centos上安装更高版本的gcc 1、安装centos-release-scl sudo yum install centos-release-scl2、安装devtoolset&#xff0c;注意&#xff0c;如果想安装7.版本的&#xff0c;就改成devtoolset-7-gcc&#xff0c;以此类推 sudo yum …...

在Java中使用Apache POI导入导出Excel(六)

本文将继续介绍POI的使用&#xff0c;上接在Java中使用Apache POI导入导出Excel&#xff08;五&#xff09; 使用Apache POI组件操作Excel&#xff08;六&#xff09; 43、隐藏和取消隐藏行 使用 Excel&#xff0c;可以通过选择该行&#xff08;或行&#xff09;来隐藏工作表…...

`uni.setClipboardData` 是 uni-app 提供的一个 API 设置系统剪贴板的内容

uni.setClipboardData是uni-app提供的一个API&#xff0c;用于设置系统剪贴板的内容。 使用说明&#xff1a; 使用此API可以将指定的文本内容复制到系统剪贴板&#xff0c;使用户能够在其他应用或页面中粘贴这些内容。 uni.setClipboardData({data: , // 需要复制的内容 suc…...

【大模型微调】pdf转markdown

目前市面上大部分都是pdf文档,要想转换成能训练的文本,调研了各种工具。 觉得MinerU确实不错。 参考此链接进行操作 MinerU/docs/README_Ubuntu_CUDA_Acceleration_en_US.md at master opendatalab/MinerU GitHub 需要注意的几个点: 1. 使用root账户安装的,配置文件在…...

Vue 3 结合 TypeScript基本使用

Vue 3 结合 TypeScript 使用可以提供更加强大的类型检查和开发体验。以下是一些基本的步骤来开始使用 Vue 3 和 TypeScript&#xff1a; 1. 创建项目 你可以使用 Vue CLI 来快速创建一个支持 TypeScript 的 Vue 项目。首先确保你已经安装了 Node.js 和 npm。然后全局安装或更…...

Trotter steps的复杂性分析

总结 • 我们开发了使用汉密尔顿系数结构执行 Trotter 步骤的递归方法&#xff0c;超越了顺序方法。 • #Gate/Step 在汉密尔顿项数上是次线性的&#xff0c;而 #Step 仍然保持交换子缩放。 • 新结果给出了实空间中第二量化电子结构汉密尔顿的最快量子模拟。对第一量化量子模…...

mean,median,mode,var,std,min,max函数

剩余的函数都放在这篇里面吧 m e a n mean mean函数可以求平均值 a a a为向量时&#xff0c; m e a n ( a ) mean(a) mean(a)求向量中元素的平均值 a a a为矩阵时&#xff0c; m e a n ( a , 1 ) mean(a,1) mean(a,1)求矩阵中各列元素的平均值&#xff1b; m e a n ( a , 2 )…...

JavaScript实现tab栏切换

JavaScript实现tab栏切换 代码功能概述 这段代码实现了一个简单的选项卡&#xff08;Tab&#xff09;切换功能。它通过操作 HTML 元素的类名&#xff08;class&#xff09;来控制哪些选项卡&#xff08;Tab&#xff09;和对应的内容板块显示&#xff0c;哪些隐藏。基本思路是先…...

精确电压输出,家电和工业设备的完美选择,宽输入电压线性稳压器

WD5201线性稳压器的核心内容概述&#xff1a; 主要特点 • 高精度输出电压&#xff1a;2%精度。 • 输出电压可调&#xff1a;支持5V、3.3V、2.7V三档输出。 • 优化控制方式&#xff1a;提升效率。 • 宽输入电压范围&#xff1a;80305VAC。 • 无需功率电感和输入高压电…...

深入理解定时器:优先队列与时间轮实现

文章目录 1. 线程池概述线程池的基本特点&#xff1a; 2. 使用线程池的优先队列定时器实现2.1 优先队列定时器实现2.2 解释&#xff1a; 3. 使用时间轮的线程池定时器实现3.1 时间轮定时器实现 4. 总结 在定时器设计中&#xff0c;使用线程池来执行定时任务可以有效提高程序的性…...

autogen-agentchat 0.4.0.dev8版本的安装

1. 安装命令 pip install autogen-agentchat0.4.0.dev8 autogen-ext[openai]0.4.0.dev82. 版本检查 import autogen_agentchat print(autogen_agentchat.__version__)0.4.0.dev8import autogen_ext print(autogen_ext.__version__)0.4.0.dev83. 第一个案例 使用 autogen-age…...

JAVA |日常开发中读写XML详解

JAVA &#xff5c;日常开发中读写XML详解 前言一、XML 简介二、在 Java 中读取 XML2.1 使用 DOM&#xff08;Document Object Model&#xff09;方式读取 XML2.2 使用 SAX&#xff08;Simple API for XML&#xff09;方式读取 XML 三、在 Java 中写入 XML3.1 使用 DOM 方式写入…...

React 路由与组件通信:如何实现路由参数、查询参数、state和上下文的使用

&#x1f90d; 前端开发工程师、技术日更博主、已过CET6 &#x1f368; 阿珊和她的猫_CSDN博客专家、23年度博客之星前端领域TOP1 &#x1f560; 牛客高级专题作者、打造专栏《前端面试必备》 、《2024面试高频手撕题》 &#x1f35a; 蓝桥云课签约作者、上架课程《Vue.js 和 E…...

帮我写一篇关于AI搜索网页上编写的文章是否存在版权问题的文章, 字数在 3000 字左右。文心一言提问, 记录后用.

AI搜索网页上编写的文章是否存在版权问题&#xff1f; 在当今科技飞速发展的时代&#xff0c;AI搜索工具如雨后春笋般涌现&#xff0c;为人们获取信息提供了极大的便利。然而&#xff0c;随之而来的问题是&#xff0c;AI搜索案例中常常出现很多内容缺乏依据&#xff0c;这引发…...

电脑关机的趣味小游戏——system函数、strcmp函数、goto语句的使用

文章目录 前言一. system函数1.1 system函数清理屏幕1.2 system函数暂停运行1.3 system函数电脑关机、重启 二、strcmp函数三、goto语句四、电脑关机小游戏4.1. 程序要求4.2. 游戏代码 总结 前言 今天我们写一点稍微有趣的代码&#xff0c;比如写一个小程序使电脑关机&#xf…...

AttributeError: ‘DataFrame‘ object has no attribute ‘append‘的参考解决方法

文章目录 写在前面一、问题描述二、解决方法参考链接 写在前面 自己的测试环境&#xff1a; Ubuntu20.04 一、问题描述 运行开源的python代码的时候&#xff0c;遇到如下问题 AttributeError: DataFrame object has no attribute append二、解决方法 报错中的DataFrame是在…...

java垃圾回收机制介绍

Java垃圾回收机制&#xff08;Garbage Collection, GC&#xff09;是Java编程语言中的一项重要特性&#xff0c;它自动管理内存&#xff0c;释放不再使用的对象 1. 堆&#xff08;Heap&#xff09;&#xff1a; • Java虚拟机&#xff08;JVM&#xff09;中用于存储对象实例的内…...

SpringMVC跨域问题解决方案

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

【语音识别】Zipformer

Zipformer 是kaldi 团队于2024研发的序列建模模型。相比较于 Conformer、Squeezeformer、E-Branchformer等主流 ASR 模型&#xff0c;Zipformer 具有效果更好、计算更快、更省内存等优点。并在 LibriSpeech、Aishell-1 和 WenetSpeech 等常用数据集上取得了当时最好的 ASR 结果…...

vue+uniapp+echarts的使用(H5环境下echarts)

1.安装 npm install echarts4.9.0 --save // 带版本号 2.main.js中全局引用 // import echarts from echarts // 如果是5.0以上版本用这个 import * as echarts from echarts Vue.prototype.$echartsecharts 3.使用 <template><view id"box" style"w…...

【Python网络爬虫笔记】7-网络爬虫的搜索工具re模块

目录 一、网络爬虫中的正则表达式和re模块&#xff08;一&#xff09;数据提取的精确性&#xff08;二&#xff09;处理复杂的文本结构&#xff08;三&#xff09;提高数据处理效率 二、正则表达式的内涵&#xff08;一&#xff09;、常用元字符&#xff08;二&#xff09;、量…...

为什么选择 React Native 作为跨端方案

为什么选择 React Native 作为跨端方案 我深刻地知道&#xff0c;没有完美的跨端技术&#xff0c;只有适合的场景。脱离适用场景去谈跨端技术没有什么意义。 适用场景 1. 业务更新迭代较快的团队与出海团队 React Native 特别适合那些业务更新频繁、需要快速响应市场的团队…...

服务器与普通电脑有什么区别?

服务器和普通电脑&#xff08;通常指的是个人计算机&#xff0c;即PC&#xff09;有众多相似之处&#xff0c;主要构成包含&#xff1a;CPU&#xff0c;内存&#xff0c;芯片&#xff0c;I/O总线设备&#xff0c;电源&#xff0c;机箱及操作系统软件等&#xff0c;鉴于使用要求…...

Oracle 12c Data Guard 环境中的 GAP 修复方法

概述 上文中提到Oracle 12c 引入了多项新技术来简化 Data Guard 环境中的 GAP 修复过程&#xff0c;如&#xff08;RECOVER … FROM SERVICE&#xff09;。这些新特性不仅减少了操作步骤&#xff0c;还提高了效率和准确性。本文档将详细说明如何利用这些新特性进行 GAP 修复。…...

力扣 三角dp

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