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

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

Bandy AI
Bandy AI

全球领先的电商设计Agent

下载

在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
指针前进。双向链表在这里提供了一个直观且高效的模型来管理操作历史。

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

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
c语言中null和NULL的区别
c语言中null和NULL的区别

c语言中null和NULL的区别是:null是C语言中的一个宏定义,通常用来表示一个空指针,可以用于初始化指针变量,或者在条件语句中判断指针是否为空;NULL是C语言中的一个预定义常量,通常用来表示一个空值,用于表示一个空的指针、空的指针数组或者空的结构体指针。

236

2023.09.22

java中null的用法
java中null的用法

在Java中,null表示一个引用类型的变量不指向任何对象。可以将null赋值给任何引用类型的变量,包括类、接口、数组、字符串等。想了解更多null的相关内容,可以阅读本专题下面的文章。

458

2024.03.01

if什么意思
if什么意思

if的意思是“如果”的条件。它是一个用于引导条件语句的关键词,用于根据特定条件的真假情况来执行不同的代码块。本专题提供if什么意思的相关文章,供大家免费阅读。

778

2023.08.22

treenode的用法
treenode的用法

​在计算机编程领域,TreeNode是一种常见的数据结构,通常用于构建树形结构。在不同的编程语言中,TreeNode可能有不同的实现方式和用法,通常用于表示树的节点信息。更多关于treenode相关问题详情请看本专题下面的文章。php中文网欢迎大家前来学习。

538

2023.12.01

C++ 高效算法与数据结构
C++ 高效算法与数据结构

本专题讲解 C++ 中常用算法与数据结构的实现与优化,涵盖排序算法(快速排序、归并排序)、查找算法、图算法、动态规划、贪心算法等,并结合实际案例分析如何选择最优算法来提高程序效率。通过深入理解数据结构(链表、树、堆、哈希表等),帮助开发者提升 在复杂应用中的算法设计与性能优化能力。

17

2025.12.22

深入理解算法:高效算法与数据结构专题
深入理解算法:高效算法与数据结构专题

本专题专注于算法与数据结构的核心概念,适合想深入理解并提升编程能力的开发者。专题内容包括常见数据结构的实现与应用,如数组、链表、栈、队列、哈希表、树、图等;以及高效的排序算法、搜索算法、动态规划等经典算法。通过详细的讲解与复杂度分析,帮助开发者不仅能熟练运用这些基础知识,还能在实际编程中优化性能,提高代码的执行效率。本专题适合准备面试的开发者,也适合希望提高算法思维的编程爱好者。

27

2026.01.06

空指针异常处理
空指针异常处理

本专题整合了空指针异常解决方法,阅读专题下面的文章了解更多详细内容。

22

2025.11.16

页面置换算法
页面置换算法

页面置换算法是操作系统中用来决定在内存中哪些页面应该被换出以便为新的页面提供空间的算法。本专题为大家提供页面置换算法的相关文章,大家可以免费体验。

408

2023.08.14

俄罗斯Yandex引擎入口
俄罗斯Yandex引擎入口

2026年俄罗斯Yandex搜索引擎最新入口汇总,涵盖免登录、多语言支持、无广告视频播放及本地化服务等核心功能。阅读专题下面的文章了解更多详细内容。

158

2026.01.28

热门下载

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

精品课程

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

共21课时 | 3.1万人学习

Kotlin 教程
Kotlin 教程

共23课时 | 3万人学习

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

共13课时 | 0.9万人学习

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

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