【深入理解Java集合框架 - (3)】 | LinkedList

这篇文章深入讲解了 Java 中的 LinkedList,包括基本概念(如链表定义、结点组成、类型等)、单向链表及相关手写实现(如增删查等方法)、特殊的链表(带哨兵的单向链表、双向链表、循环链表)、LinkedList 的源码详解(类的继承关系、成员变量、核心方法等)、关键特性(如双向链表、动态大小、访问特点、增删效率、内存占用等)、优缺点及适用场景。

关联问题:单向链表怎么高效增删双向链表如何遍历LinkedList内存如何优化

前言

LinkedListJava常用的集合类之一,其核心价值有如下3点:

  • 高效的增删操作
  • 充分利用内存空间
  • 用作其他数据结构(如队列双端队列)。

千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意


一、基本概念

1.1、定义

链表是一种物理存储单元非连续非顺序存储结构数据元素逻辑顺序是通过链表中的指针链接次序实现的。

1.2、结点

链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成

每个结点包括两个部分:

  • 数据域存储数据元素
  • 指针域存储下一个结点的地址

1.3、链表的类型

1.4、LinkedList的概述

LinkedList 是 java.util 包中的一个类,它实现了 List 接口。基于双向链表实现,具备高效的增删操作的能力和充分利用内存空间,允许在运行时动态生成结点


二、单向链表及其手写实现

2.1、特点

  • 每个元素只知道其下一个元素,意味着访问链表中的元素必须从头指针开始。
  • 尾结点(tail)指向null,是结束遍历的标识

2.2、实现步骤:

  • 1、定义一个单向链表类SingleLinkedList
  • 2、定义一个结点类Node
  • 3、链表是由一系列结点组成,它们之间是组合关系,这种组合关系可使用内部类方式实现。
  • 4、根据链表的特点还需要定义一个头指针
  • 5、分别实现链表的遍历等方法。

完整代码实现详细注释如下:

 

Java

代码解读

复制代码

import java.util.Iterator; import java.util.function.Consumer; /** * 单向链表 */ public class SingleLinkedList implements Iterable<Integer> { /** * 头指针: 代表链表的起始位置 * 根据头指针才能访问到链表中的其他元素 * 初始时, 头指针指向null, 意味着链表中无结点 */ private Node head = null; public void addFirst(int value) { /** * 1.链表为空: * 创建新结点, 赋值给头指针 * 意味着头指针指向该结点, 然后将结点链入链表中, 此时新节点就是第一个结点 */ // head = new Node(value, null); //2.链表非空: 创建新结点, 其next指向head 然后再让head指向新结点后链入链表 // /** * 2.链表非空: * 创建新结点, 其next指向head * 然后再让head指向新结点后链入链表中 * 代码包含1场景, 保留如下代码即可, 但分析步骤不可少, 可细细体会下该过程 */ head = new Node(value, head); } /** * 查找最后一个结点 */ private Node findLast() { //空链表处理 if (head == null) { return null; } Node p = head; while (p.next != null) { p = p.next; } return p; } /** * 链表尾部添加数据, 需要先知晓最后一个结点 */ public void addLast(int value) { //1.查找到最后一个结点 Node last = findLast(); //2.处理链表为空的场景 if (last == null) { addFirst(value); return; } //3.创建新结点, 新结点指针指向null, 最后一个结点指向新结点 last.next = new Node(value, null); } /** * 根据索引插入数据 * 1.需要先找到该索引的上一个结点 视为前结点 * 2.创建新结点: * a.新结点的next指向前结点的next * b.前结点的next指向新结点 */ public void add(int index, int value) { if (index == 0) { addFirst(value); return; } Node prev = findNode(index - 1); if (prev == null) { throw new IndexOutOfBoundsException(); } prev.next = new Node(value, prev.next); } /** * 1.根据索引获取结点的值 索引是在遍历的过程才知晓的 * 2.若在Node中加index属性, 增删时维护起来比较困难 */ private Node findNode(int index) { int i = 0; for (Node p = head; p != null; p = p.next, i++) { if (i == index) { return p; } } return null; } public int get(int index) { Node node = findNode(index); if (node == null) { throw new IndexOutOfBoundsException(); } return node.value; } /** * 删除第一个节点, 通过改变头指针的指向就能实现 */ public void removeFirst() { if (head == null) { throw new IndexOutOfBoundsException(); } head = head.next; } /** * 根据索引删除元素, 关键也是找到上一个结点 */ public void remove(int index) { if (index == 0) { removeFirst(); return; } //查找索引对应的上一个结点 Node prev = findNode(index - 1); if (prev == null) { throw new IndexOutOfBoundsException(); } //获取删除的节点 Node removed = prev.next; if (removed == null) { throw new IndexOutOfBoundsException(); } //处理结点直接的指向 prev.next = removed.next; } /** * while 循环遍历 */ public void loop(Consumer<Integer> consumer) { Node p = head; while (p != null) { // 循环中对链表的操作, 最好不要写到循环中, 而是把它当做参数传递进来 // System.out.println(p.value); consumer.accept(p.value); //更新数据 p = p.next; } } /** * for 循环遍历 */ public void loop1(Consumer<Integer> consumer) { for (Node p = head; p != null; p = p.next) { consumer.accept(p.value); } } /** * 迭代器 遍历 */ @Override public Iterator<Integer> iterator() { //此处也可使用匿名内部类 return new NodeIterator(); } /** * 1.结点类: 对结点描述 * 2.静态内部类: 未使用到外部类的属性时使用 * 3.内部类: 使用到外部类的属性时使用 * 4.为啥使用内部类? * 1.与外部类是组合关系(链表是有多个结点组成), 实际应用中, * 若遇到类似场景时, 需要考虑是否使用外部类和内部类组合实现 * 2.封装思想: 避免外部知晓更多信息, * 对外暴露的信息越少越好, 外部不需要知道内部更底层的实现 */ private static class Node { /** * 值 */ int value; /** * 指向下一个结点的指针 */ Node next; public Node(int value, Node next) { this.value = value; this.next = next; } } /** * 内部类 使用到了外部类的head属性, 不能使用static */ private class NodeIterator implements Iterator<Integer> { //指针初始值 Node p = head; @Override public boolean hasNext() { //询问是否有下一个元素 return p != null; } @Override public Integer next() { // 1.返回当前值 int value = p.value; // 2.并指向下一个元素 p = p.next; return value; } } }


三、单向链表(带哨兵)及其手写实现

单向链表中还有一种特殊的结点称为哨兵结点,它不存储数据,通常用作头尾,用来简化边界的判断,可对比一下不带哨兵的实现。

完整代码实现详细注释如下:

 

Java

代码解读

复制代码

/** * 单向链表(带哨兵): 主要简化单链表的边界判断 */ public class SentinelLinkedList implements Iterable<Integer> { private Node head = new Node(666, null); public void addFirst(int value) { add(0, value); } public void addLast(int value) { //带哨兵的最后一个结点不可能为null Node last = findLast(); last.next = new Node(value, null); } public void add(int index, int value) { Node prev = findNode(index - 1); if (prev == null) { throw new IndexOutOfBoundsException(); } prev.next = new Node(value, prev.next); } public void removeFirst() { remove(0); } public void remove(int index) { //查找索引对应的上一个结点 Node prev = findNode(index - 1); if (prev == null) { throw new IndexOutOfBoundsException(); } //获取删除的节点 Node removed = prev.next; if (removed == null) { throw new IndexOutOfBoundsException(); } //处理结点直接的指向 prev.next = removed.next; } private Node findNode(int index) { int i = -1; for (Node p = head; p != null; p = p.next, i++) { if (i == index) { return p; } } return null; } public int get(int index) { Node node = findNode(index); if (node == null) { throw new IndexOutOfBoundsException(); } return node.value; } private Node findLast() { Node p = head; while (p.next != null) { p = p.next; } return p; } @Override public Iterator<Integer> iterator() { //此处也可使用匿名内部类 return new NodeIterator(); } private static class Node { int value; Node next; public Node(int value, Node next) { this.value = value; this.next = next; } } private class NodeIterator implements Iterator<Integer> { //指针初始值 从哨兵的下一个值开始遍历 Node p = head.next; @Override public boolean hasNext() { return p != null; } @Override public Integer next() { int value = p.value; p = p.next; return value; } } }


四、双向链表(带哨兵)及其手写实现

4.1、特点

  • 每个元素知道其上一个元素和下一个元素
  • 头结点(head)指向null
  • 尾结点(tail)指向null
 

Java

代码解读

复制代码

/** * 双向链表(带哨兵): 主要简化单链表的边界判断 */ public class DoubleLinkedList implements Iterable<Integer> { private final Node head;//头哨兵 private final Node tail;//尾哨兵 public DoubleLinkedList() { head = new Node(null, 666, null); tail = new Node(null, 888, null); head.next = tail; tail.prev = head; } /** * 根据索引位置查找其对应的节点 */ private Node findNode(int index) { int i = -1; for (Node p = head; p != tail; p = p.next, i++) { if (i == index) { return p; } } return null; } public void addFirst(int value) { add(0, value); } public void add(int index, int value) { //查找上一个结点 Node prev = findNode(index - 1); if (prev == null) { throw new IndexOutOfBoundsException(); } //查找下一个节点 Node next = prev.next; Node inserted = new Node(prev, value, next); prev.next = inserted; next.prev = inserted; } public void addLast(int value) { //带哨兵的最后一个结点不可能为null Node last = tail.prev; Node added = new Node(last, value, tail); last.prev = added; tail.next = added; } public void removeFirst() { remove(0); } public void remove(int index) { //查找索引对应的上一个结点 Node prev = findNode(index - 1); if (prev == null) { throw new IndexOutOfBoundsException(); } //获取待删除的节点 Node removed = prev.next; if (removed == tail) { throw new IndexOutOfBoundsException(); } //获取待删除的节点的下一个结点 Node next = removed.next; prev.next = next; next.prev = prev; } public void removeLast() { Node removed = tail.prev; if (removed == head) { throw new IndexOutOfBoundsException(); } Node prev = removed.prev; prev.next = tail; tail.prev = prev; } @Override public Iterator<Integer> iterator() { //此处也可使用匿名内部类 return new NodeIterator(); } private static class Node { Node prev; int value; Node next; public Node(Node prev, int value, Node next) { this.prev = prev; this.value = value; this.next = next; } } private class NodeIterator implements Iterator<Integer> { //指针初始值 从哨兵的下一个值开始遍历 Node p = head.next; @Override public boolean hasNext() { return p != tail; } @Override public Integer next() { int value = p.value; p = p.next; return value; } } }


五、循环链表

5.1、特点

  • 每个元素只知道其下一个元素
  • 尾结点(tail)指向头结点(head)。
  • 实际开发中不常用,做简单了解就好。

六、LinkedList的源码详解(基于JDK1.8)

通过上述手动实现,我们对LinkedList有了一个初步的认知,接下来深入探索JDKLinkedList源码的实现,继续加深理解程度。关于如何阅读源码,可从如下3个方面入手:

  • 类的继承关系及接口实现
  • 成员变量和构造函数
  • 核心方法

图像表示:

6.1、类的继承关系及接口实现

 

Java

代码解读

复制代码

public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable { ... }

知识以图片的形式进入大脑,记忆更为深刻

其他接口在上一章节已详细描述,本章节主要介绍Deque

Deque双端队列Double-Ended Queue)的缩写,它允许在队列的两端进行插入和删除操作。LinkedList实现了 Deque接口,提供了双端队列的功能。

以下是LinkedList作为Deque的主要作用:

  • 1、两端插入和删除

    • addFirst(E e):在队列头部添加一个元素。
    • addLast(E e):在队列尾部添加一个元素。
    • removeFirst():删除并返回队列头部的元素。
    • removeLast():删除并返回队列尾部的元素。
  • 2、两端访问

    • getFirst():返回队列头部的元素,但不删除。
    • getLast():返回队列尾部的元素,但不删除。
  • 3、迭代器

    • iterator():返回一个从队列头部开始的迭代器。
    • descendingIterator():返回一个从队列尾部开始的迭代器。

6.2、成员变量和构造函数

 

Java

代码解读

复制代码

/** 链表中元素的个数 */ transient int size = 0; /** 头结点 */ transient Node<E> first; //** 尾结点 */ transient Node<E> last; //** 无参数构造 */ public LinkedList() { } /** 集合构造 */ public LinkedList(Collection<? extends E> c) { this(); addAll(c); }


6.3、核心方法(add(index,value)node(index))

 

Java

代码解读

复制代码

/** * 添加指定元素到列表的末尾 */ public boolean add(E e) { linkLast(e); return true; } /** * 链入末尾的元素 做简单的指针变更操作 * 1.创建新结点 前驱指向链表中的尾结点 后继指向null * 2.变更尾结点的指向 * 3.若原始尾结点为null,将新结点即为头结点 * 4.若原始尾节点不为null,将尾结点的后继指向新结点 */ void linkLast(E e) { final Node<E> l = last; final Node<E> newNode = new Node<>(l, e, null); last = newNode; if (l == null) first = newNode; else l.next = newNode; size++; modCount++; } /** * 向指定的位置插入数据 * 1.下标越界检测 * 2.判断位置是否最后 * 3.查找node的位置 * 4.创建新结点插入指定位置 */ public void add(int index, E element) { checkPositionIndex(index); if (index == size) linkLast(element); else linkBefore(element, node(index)); } /** * 通过折半查找法 * 1. 小于size一半的从前往后遍历获取指定的节点 * 2. 大于size一半的从后往前遍历获取指定的节点 */ Node<E> node(int index) { // assert isElementIndex(index); if (index < (size >> 1)) { Node<E> x = first; for (int i = 0; i < index; i++) x = x.next; return x; } else { Node<E> x = last; for (int i = size - 1; i > index; i--) x = x.prev; return x; } } /** * 对插入的元素做指针变换操作 */ void linkBefore(E e, Node<E> succ) { // assert succ != null; final Node<E> pred = succ.prev; final Node<E> newNode = new Node<>(pred, e, succ); succ.prev = newNode; if (pred == null) first = newNode; else pred.next = newNode; size++; modCount++; }

熟练掌握上述的核心方法,删除修改查询等操作也类似。就不一一讲述了。LinkedList相关面试攻略可查看ArrayList章节。


七、LinkedList的关键特性

7.1、双向链表:

  • 基于双向链表实现,每个结点有前驱数据域后继,能高效地从两端进行操作(核心价值)。

7.2、动态大小:

  • 大小是动态的,可根据需要增长或缩小。

7.3、较慢的随机访问:

  • 随机访问:时间复制度为O(n),因为需要从头或尾开始逐个遍历结点。
  • 顺序访问:虽然随机访问较慢,但顺序访问(如使用迭代器)仍然非常高效。

7.4、高效的增删操作:

  • 中间增删:在查找到增删位置的前提下,增删操作复杂度为O(1)
  • 两端操作: 其实现了Deque接口,能进行高效的两端操作

7.5、内存占用:

  • 内存开销大:其内部结点需要额外的前驱及后继指针。

7.6、泛型支持:

  • 支持泛型,可以指定存储的元素类型

7.7、非线程安全:

  • 非线程安全的,如需在多线程环境中使用,可以使用 Collections.synchronizedList方法将其包装成线程安全的列表。

八、总结

8.1、优点:

  • 高效的增删操作
  • 充分利用内存空间
  • 运行时动态生成结点

8.2、缺点:

  • 较慢的随机访问
  • 内存开销较大
  • 非线程安全,多线程环境下需要外部同步。

8.3、适用场景:

  • 频繁的任意位置的增删操作
  • 用作其他数据结构(如队列双端队列)。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

老王的代码

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值