0

0

java代码如何实现双向链表并处理节点关系 java代码双向链表的基础编写教程​

蓮花仙者

蓮花仙者

发布时间:2025-08-11 17:14:02

|

305人浏览过

|

来源于php中文网

原创

双向链表在需要双向遍历、频繁删除已知节点或实现撤销/重做等场景下优于单向链表,1. 当需支持前后导航(如浏览器历史、播放列表)时,双向链表可高效实现;2. 当需o(1)删除已知节点(如lru缓存)时,prev指针避免了遍历查找前驱;3. 当实现复杂数据结构或操作历史管理时,双向链接提供了灵活的节点关系维护;尽管其内存开销略高且逻辑更复杂,但在上述场景中性能和便利性优势显著,因此应根据具体需求权衡选择。

java代码如何实现双向链表并处理节点关系 java代码双向链表的基础编写教程​

在Java中实现双向链表,核心在于定义一个包含数据、前驱节点和后继节点的内部类,然后构建链表类来管理这些节点,确保每个操作(如添加、删除)都正确更新前驱(

prev
)和后继(
next
)指针,从而维护双向连接关系。

解决方案

实现一个双向链表,我们通常需要两个主要部分:

Node
类和
DoublyLinkedList
类。

1.

Node
类: 这是链表的基本构成单元。每个节点不仅存储数据,还需要指向前一个节点(
prev
)和后一个节点(
next
)的引用。

class Node {
    int data;
    Node prev;
    Node next;

    public Node(int data) {
        this.data = data;
        this.prev = null; // 初始时,前驱和后继都为空
        this.next = null;
    }
}

2.

DoublyLinkedList
类: 这个类负责管理整个链表,包括头节点(
head
)和尾节点(
tail
),以及提供各种操作方法。

public class DoublyLinkedList {
    private Node head;
    private Node tail;
    private int size; // 维护链表大小,方便快速获取

    public DoublyLinkedList() {
        this.head = null;
        this.tail = null;
        this.size = 0;
    }

    // 添加到链表头部
    public void addFirst(int data) {
        Node newNode = new Node(data);
        if (head == null) { // 链表为空
            head = newNode;
            tail = newNode;
        } else {
            newNode.next = head; // 新节点的next指向原head
            head.prev = newNode; // 原head的prev指向新节点
            head = newNode;      // 更新head为新节点
        }
        size++;
        // System.out.println("Added " + data + " to head. Current size: " + size);
    }

    // 添加到链表尾部
    public void addLast(int data) {
        Node newNode = new Node(data);
        if (tail == null) { // 链表为空
            head = newNode;
            tail = newNode;
        } else {
            tail.next = newNode; // 原tail的next指向新节点
            newNode.prev = tail; // 新节点的prev指向原tail
            tail = newNode;      // 更新tail为新节点
        }
        size++;
        // System.out.println("Added " + data + " to tail. Current size: " + size);
    }

    // 在指定索引处添加(这里只是一个简单的示例,实际可能需要更多边界检查)
    public void addAtIndex(int index, int data) {
        if (index < 0 || index > size) {
            throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size);
        }
        if (index == 0) {
            addFirst(data);
            return;
        }
        if (index == size) {
            addLast(data);
            return;
        }

        Node newNode = new Node(data);
        Node current = head;
        // 遍历到目标位置的前一个节点
        for (int i = 0; i < index - 1; i++) {
            current = current.next;
        }
        // current现在是新节点的前一个节点
        newNode.next = current.next;
        newNode.prev = current;
        current.next.prev = newNode; // 原来current.next的prev指向新节点
        current.next = newNode;      // current的next指向新节点
        size++;
        // System.out.println("Added " + data + " at index " + index + ". Current size: " + size);
    }

    // 删除链表头部
    public int removeFirst() {
        if (head == null) {
            throw new IllegalStateException("List is empty.");
        }
        int data = head.data;
        if (head == tail) { // 只有一个节点
            head = null;
            tail = null;
        } else {
            head = head.next; // head指向下一个节点
            if (head != null) { // 新的head的prev应该为null
                head.prev = null;
            }
        }
        size--;
        // System.out.println("Removed " + data + " from head. Current size: " + size);
        return data;
    }

    // 删除链表尾部
    public int removeLast() {
        if (tail == null) {
            throw new IllegalStateException("List is empty.");
        }
        int data = tail.data;
        if (head == tail) { // 只有一个节点
            head = null;
            tail = null;
        } else {
            tail = tail.prev; // tail指向前一个节点
            if (tail != null) { // 新的tail的next应该为null
                tail.next = null;
            }
        }
        size--;
        // System.out.println("Removed " + data + " from tail. Current size: " + size);
        return data;
    }

    // 删除指定索引处的节点
    public int removeAtIndex(int index) {
        if (index < 0 || index >= size) {
            throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size);
        }
        if (index == 0) {
            return removeFirst();
        }
        if (index == size - 1) {
            return removeLast();
        }

        Node current = head;
        // 遍历到目标节点
        for (int i = 0; i < index; i++) {
            current = current.next;
        }
        // current现在是需要删除的节点
        int data = current.data;
        current.prev.next = current.next; // 前一个节点的next指向删除节点的next
        current.next.prev = current.prev; // 后一个节点的prev指向删除节点的prev
        size--;
        // System.out.println("Removed " + data + " at index " + index + ". Current size: " + size);
        return data;
    }

    // 从头到尾遍历
    public void displayForward() {
        if (head == null) {
            System.out.println("List is empty.");
            return;
        }
        Node current = head;
        System.out.print("Forward: ");
        while (current != null) {
            System.out.print(current.data + " ");
            current = current.next;
        }
        System.out.println();
    }

    // 从尾到头遍历
    public void displayBackward() {
        if (tail == null) {
            System.out.println("List is empty.");
            return;
        }
        Node current = tail;
        System.out.print("Backward: ");
        while (current != null) {
            System.out.print(current.data + " ");
            current = current.prev;
        }
        System.out.println();
    }

    public int getSize() {
        return size;
    }

    public boolean isEmpty() {
        return size == 0;
    }
}

示例用法:

立即学习Java免费学习笔记(深入)”;

public class DoublyLinkedListDemo {
    public static void main(String[] args) {
        DoublyLinkedList list = new DoublyLinkedList();
        System.out.println("Is list empty? " + list.isEmpty()); // true

        list.addLast(10);
        list.addFirst(5);
        list.addLast(20);
        list.addAtIndex(1, 15); // 链表: 5 -> 15 -> 10 -> 20

        list.displayForward();  // Output: Forward: 5 15 10 20
        list.displayBackward(); // Output: Backward: 20 10 15 5
        System.out.println("List size: " + list.getSize()); // 4

        list.removeFirst(); // 移除 5
        list.displayForward(); // Output: Forward: 15 10 20
        list.removeLast();  // 移除 20
        list.displayForward(); // Output: Forward: 15 10
        list.removeAtIndex(0); // 移除 15
        list.displayForward(); // Output: Forward: 10

        System.out.println("List size: " + list.getSize()); // 1
        list.removeFirst(); // 移除 10
        list.displayForward(); // Output: List is empty.
        System.out.println("Is list empty? " + list.isEmpty()); // true
    }
}

双向链表与单向链表在实际应用中的选择考量是什么?

说实话,很多人一开始接触链表,可能觉得单向链表就够用了,毕竟它更简单,内存开销也小一点点。但实际开发中,双向链表的存在感是相当强的,它解决了一些单向链表“力所不能及”的痛点。

最直观的区别在于遍历方向。单向链表只能从头到尾,像一条单行道,一旦走过就回不去了。但双向链表,顾名思义,可以双向通行。这意味着,如果你需要频繁地“回溯”操作,比如浏览器历史记录(前进/后退)、文本编辑器的撤销/重做功能,或者像LRU缓存那样,需要快速将某个访问过的节点移动到链表头部,双向链表就显得非常自然且高效。

另一个关键点在于删除操作。在单向链表中,要删除一个节点,你必须找到它的“前一个”节点,然后修改那个前一个节点的

next
指针。这意味着如果你只知道要删除的节点本身,你还得从头遍历一遍才能找到它的前驱,这效率就很低了(O(N))。但双向链表呢?每个节点都带着
prev
指针,所以只要你拿到了要删除的节点引用,直接通过
node.prev
node.next
就能轻松地把这个节点“摘掉”,操作是 O(1) 的。当然,前提是你已经拿到了那个节点的引用,如果还是需要搜索才能找到,那整体还是 O(N)。

那么,代价是什么?内存。每个节点多了一个

prev
指针,这对于内存来说是额外的开销。如果你的链表非常庞大,或者节点本身就存储着大量数据,那么这个额外的指针累积起来也可能不容小觑。此外,维护双向链接关系也意味着每次添加或删除操作时,需要更新的指针数量翻倍,逻辑上稍微复杂一点点,出错的概率也相对高一点。

所以,选择哪个,真的要看具体需求。如果你的应用场景只需要顺序遍历,或者内存极其敏感,单向链表可能是更好的选择。但如果需要灵活的双向遍历,或者频繁地在已知节点位置进行删除操作,那么双向链表的优势就非常明显了,它带来的便利性和性能提升,往往能抵消那一点点内存和复杂度的增加。

KAIZAN.ai
KAIZAN.ai

使用AI来改善客户服体验,提高忠诚度

下载

在Java中实现双向链表时,如何优雅地处理边缘情况和空指针异常?

在Java里写双向链表,最让人头疼的不是核心逻辑,而是那些细碎的边缘情况和随之而来的空指针异常(

NullPointerException
)。一个健壮的双向链表实现,必须像个老练的工程师一样,对各种可能出现的“意外”做好防范。

首先,空链表是所有操作的起点和终点。当链表是空的(

head == null
tail == null
)时,任何添加或删除操作都必须特殊处理。比如,
addFirst
addLast
一个新节点时,如果链表为空,那么这个新节点既是
head
也是
tail
。而
removeFirst
removeLast
如果链表已经空了,那就得抛出
IllegalStateException
或返回一个特殊值,告诉调用者“没得删了”。

其次,单节点链表也是一个需要单独考虑的特殊情况。当链表中只有一个节点时,

head
tail
都指向它。这时候删除这个节点,就意味着
head
tail
都应该变回
null
。如果你不加区分地去处理
head.next
tail.prev
,很可能就会因为
head.next
null
而引发
NullPointerException

再来,

prev
next
指针的更新
是核心。每当添加或删除节点时,务必确保相关的
prev
next
指针都被正确地设置。例如,在
addFirst
中,新节点的
next
指向旧
head
,而旧
head
prev
必须指向新节点。如果旧
head
本身是
null
(即链表为空),那么这些操作就得跳过。同理,删除节点时,被删除节点的前驱的
next
应该跳过它指向其后继,后继的
prev
应该跳过它指向其前驱。如果被删除节点是
head
tail
,那么新的
head
tail
prev
/
next
就应该设为
null

具体到代码层面,这通常意味着大量的

if (node != null)
检查。例如,在
removeFirst()
后,新的
head
可能仍然是
null
(如果原来只有一个节点),所以在尝试设置
head.prev = null
之前,你需要检查
if (head != null)
。同样,在
addAtIndex
removeAtIndex
这样的操作中,索引的边界检查(
index < 0
index > size
)是必不可少的,防止越界访问。

一个好的实践是,将这些边缘情况的判断放在方法的开头,快速处理并返回或抛出异常。这样可以避免后续的复杂逻辑被这些特殊情况污染,让代码更清晰。同时,对

head
tail
这两个关键引用保持高度警惕,它们是链表的“入口”,它们的
null
状态直接决定了链表的空与否,以及各种操作的合法性。

双向链表的节点关系维护在哪些场景下显得尤为重要?

双向链表节点关系的维护,不仅仅是实现层面的技术细节,它更是决定了某些特定应用场景下数据结构选择的关键。当我们需要高效地执行以下操作时,双向链表的优势就会被放大:

1. 频繁的就近操作或上下文感知: 想象一下,你在一个播放器里,需要实现“上一曲”和“下一曲”的功能。如果用单向链表,实现“下一曲”很容易,但“上一曲”就麻烦了,你可能需要从头遍历才能找到当前歌曲的前一首。双向链表则天然支持这种双向导航,每个节点都知道它的“邻居”,操作效率极高。这同样适用于浏览器历史记录,你可以轻松地“回退”到上一个页面,或者“前进”到下一个页面。

2. 快速删除已知节点: 在某些缓存机制中,比如LRU(Least Recently Used)缓存,当一个数据被访问时,它需要被移动到链表的“最新”位置(比如头部)。当缓存满时,需要删除“最旧”的数据(比如尾部)。更复杂的是,如果某个数据在链表中间被访问了,它需要从中间被“取出”,然后放到头部。在双向链表中,由于每个节点都知道它的前驱和后继,只要拿到了这个节点的引用,删除它就变成了 O(1) 的操作:让它的前驱跳过它指向它的后继,让它的后继跳过它指向它的前驱。这种效率在高性能要求的缓存系统中至关重要。

3. 实现复杂数据结构的基础: 很多高级数据结构,比如一些复杂的图算法实现,或者某些自定义的内存管理系统,会使用双向链表作为其底层构建块。因为在这些场景下,数据元素之间的关系可能不是简单的线性顺序,但需要快速地在相邻元素之间跳转,甚至需要将某个元素从一个位置“剪切”到另一个位置,双向链表的灵活性提供了很好的支持。

4. 撤销/重做(Undo/Redo)功能: 在文本编辑器、图形设计软件等应用中,撤销和重做功能是不可或缺的。每一次操作都可以看作是链表中的一个节点。撤销就是沿着

prev
指针回溯,重做就是沿着
next
指针前进。双向链表在这里提供了一个直观且高效的模型来管理操作历史。

总的来说,当你的应用场景需要超越简单的线性遍历,涉及到对数据元素的“位置感知”、“双向导航”或“快速插入/删除”时,双向链表就是那个值得你投入精力去维护节点关系的选择。它带来的便利性和性能提升,往往能让你的系统设计更加优雅和高效。

相关专题

更多
java
java

Java是一个通用术语,用于表示Java软件及其组件,包括“Java运行时环境 (JRE)”、“Java虚拟机 (JVM)”以及“插件”。php中文网还为大家带了Java相关下载资源、相关课程以及相关文章等内容,供大家免费下载使用。

832

2023.06.15

java正则表达式语法
java正则表达式语法

java正则表达式语法是一种模式匹配工具,它非常有用,可以在处理文本和字符串时快速地查找、替换、验证和提取特定的模式和数据。本专题提供java正则表达式语法的相关文章、下载和专题,供大家免费下载体验。

738

2023.07.05

java自学难吗
java自学难吗

Java自学并不难。Java语言相对于其他一些编程语言而言,有着较为简洁和易读的语法,本专题为大家提供java自学难吗相关的文章,大家可以免费体验。

734

2023.07.31

java配置jdk环境变量
java配置jdk环境变量

Java是一种广泛使用的高级编程语言,用于开发各种类型的应用程序。为了能够在计算机上正确运行和编译Java代码,需要正确配置Java Development Kit(JDK)环境变量。php中文网给大家带来了相关的教程以及文章,欢迎大家前来阅读学习。

397

2023.08.01

java保留两位小数
java保留两位小数

Java是一种广泛应用于编程领域的高级编程语言。在Java中,保留两位小数是指在进行数值计算或输出时,限制小数部分只有两位有效数字,并将多余的位数进行四舍五入或截取。php中文网给大家带来了相关的教程以及文章,欢迎大家前来阅读学习。

398

2023.08.02

java基本数据类型
java基本数据类型

java基本数据类型有:1、byte;2、short;3、int;4、long;5、float;6、double;7、char;8、boolean。本专题为大家提供java基本数据类型的相关的文章、下载、课程内容,供大家免费下载体验。

446

2023.08.02

java有什么用
java有什么用

java可以开发应用程序、移动应用、Web应用、企业级应用、嵌入式系统等方面。本专题为大家提供java有什么用的相关的文章、下载、课程内容,供大家免费下载体验。

430

2023.08.02

java在线网站
java在线网站

Java在线网站是指提供Java编程学习、实践和交流平台的网络服务。近年来,随着Java语言在软件开发领域的广泛应用,越来越多的人对Java编程感兴趣,并希望能够通过在线网站来学习和提高自己的Java编程技能。php中文网给大家带来了相关的视频、教程以及文章,欢迎大家前来学习阅读和下载。

16926

2023.08.03

Golang gRPC 服务开发与Protobuf实战
Golang gRPC 服务开发与Protobuf实战

本专题系统讲解 Golang 在 gRPC 服务开发中的完整实践,涵盖 Protobuf 定义与代码生成、gRPC 服务端与客户端实现、流式 RPC(Unary/Server/Client/Bidirectional)、错误处理、拦截器、中间件以及与 HTTP/REST 的对接方案。通过实际案例,帮助学习者掌握 使用 Go 构建高性能、强类型、可扩展的 RPC 服务体系,适用于微服务与内部系统通信场景。

8

2026.01.15

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Git 教程
Git 教程

共21课时 | 2.7万人学习

Kotlin 教程
Kotlin 教程

共23课时 | 2.5万人学习

PHP新手语法线上课程教学
PHP新手语法线上课程教学

共13课时 | 0.9万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号