算法之旅(七)链表进阶 虚拟节点

深入理解链表中的虚拟节点(Dummy Node)

链表(Linked List)是一种常用的数据结构,在各种算法和应用中发挥着重要作用。在链表操作中,虚拟节点(Dummy Node)是一种常用的技巧,能够简化代码逻辑,提高代码的可读性和健壮性。


一、链表的数据结构特点

1. 定义

链表是一种线性数据结构,由一系列节点(Node)组成,这些节点通过指针(或引用)连接,形成一个链式的结构。每个节点包含两个部分:

  • 数据域(Data):存储节点的数据内容。
  • 指针域(Next):存储指向下一个节点的引用。

2. 类型

  • 单链表(Singly Linked List):每个节点只包含指向下一个节点的指针。
  • 双向链表(Doubly Linked List):每个节点包含指向前一个节点和后一个节点的指针。
  • 循环链表(Circular Linked List):链表的最后一个节点指向头节点,形成一个环。

3. 特点

  • 动态性:链表的长度可以动态变化,节点可以在运行时动态分配和释放。
  • 插入和删除效率高:在已知位置进行插入和删除操作时,只需修改指针,时间复杂度为 O(1)。
  • 随机访问效率低:不能通过索引直接访问元素,需要从头节点开始遍历,时间复杂度为 O(n)。

二、虚拟节点的定义

1. 什么是虚拟节点

虚拟节点(Dummy Node),又称为哨兵节点(Sentinel Node),是一个不存储实际有效数据的节点。它的主要作用是简化链表在插入、删除等操作时对头节点和尾节点的特殊处理。

2. 虚拟节点的属性

  • 数据域通常为默认值:例如 null-10 等,不参与实际的数据存储。
  • 指针域指向实际的链表头节点或尾节点:根据具体实现,虚拟节点的 next 指向实际的第一个节点。

虚拟指针(Dummy Node)的应用主要集中在链表结构中,但在其他数据结构中,虚拟节点的概念可能以不同的形式或实现来帮助简化某些操作逻辑。

三、为什么虚拟节点常见于链表

1. 链表结构的特性

  • 链表的动态性:链表中的节点是动态分配的,节点之间通过指针连接,不像数组那样有固定的索引访问。这种结构使得在链表头部进行插入、删除等操作时需要特别处理头节点。
  • 简化头节点的特殊处理:在链表中,修改头节点时需要额外处理(如插入或删除第一个节点)。虚拟节点的引入可以避免特殊的头节点判断,统一处理所有节点。

四、 虚拟节点的主要作用

  • 简化操作:无论链表是空链表还是已有节点的链表,虚拟节点用于简化头节点的特殊处理,使插入、删除等操作更为统一和简单。
    例如,在反转链表、合并链表、删除节点等操作中,虚拟节点使代码逻辑更清晰和简洁。
  • 避免空指针问题:在链表操作中,如果涉及到操作第一个节点时指针为空或链表为空的情况,虚拟节点可以帮助避免这些边界问题。

五、虚拟节点的应用示例

下面通过两个经典的链表问题,演示虚拟节点的实际应用,并讲解代码思路和算法复杂度。

示例一:删除链表中的指定元素

问题描述:

给定一个链表和一个值 val,删除链表中所有等于 val 的节点,并返回新的链表头节点。

示例:

输入:head = [1,2,6,3,4,5,6], val = 6
输出:[1,2,3,4,5]

代码实现

public class RemoveElements {
    // 定义链表节点
    static class ListNode {
        int val;
        ListNode next;
        ListNode(int val) { this.val = val; }
    }

    public static ListNode removeElements(ListNode head, int val) {
        // 创建虚拟节点,指向头节点
        ListNode dummy = new ListNode(0);
        dummy.next = head;

        // 初始化当前节点和前驱节点
        ListNode current = head;
        ListNode prev = dummy;

        while (current != null) {
            if (current.val == val) {
                // 删除当前节点
                prev.next = current.next;
            } else {
                // 前驱节点移动
                prev = current;
            }
            // 当前节点移动
            current = current.next;
        }
        // 返回新的头节点
        return dummy.next;
    }

    public static void main(String[] args) {
        // 创建链表 1->2->6->3->4->5->6
        ListNode head = new ListNode(1);
        head.next = new ListNode(2);
        head.next.next = new ListNode(6);
        head.next.next.next = new ListNode(3);
        head.next.next.next.next = new ListNode(4);
        head.next.next.next.next.next = new ListNode(5);
        head.next.next.next.next.next.next = new ListNode(6);

        int val = 6;
        ListNode newHead = removeElements(head, val);

        // 输出结果
        System.out.print("删除指定元素后的链表:");
        while (newHead != null) {
            System.out.print(newHead.val);
            if (newHead.next != null) {
                System.out.print("->");
            }
            newHead = newHead.next;
        }
    }
}

输出结果:

删除指定元素后的链表:1->2->3->4->5

思路讲解

  • 引入虚拟节点:

    • 创建一个虚拟节点 dummy,其 next 指向原链表的头节点 head
    • 这样,无论头节点是否需要被删除,都可以统一处理。
  • 遍历链表:

    • 使用两个指针:
      • prev:指向当前节点的前驱节点,初始为 dummy
      • current:指向当前节点,初始为 head
    • current.val == val 时,删除当前节点:prev.next = current.next
    • 否则,前驱节点 prev 移动到当前节点。
    • 无论是否删除,当前节点 current 都移动到下一个节点。
  • 返回结果:

    • 最终返回 dummy.next,即新的链表头节点。

算法复杂度

  • 时间复杂度:O(n),其中 n 是链表的长度。需要遍历整个链表。
  • 空间复杂度:O(1),只使用了常数级别的额外空间。

示例二:合并两个有序链表

问题描述:

将两个升序的单链表合并为一个新的升序单链表,并返回新的链表头节点。

示例:

输入:l1 = [1,2,4], l2 = [1,3,4]
输出:[1,1,2,3,4,4]

代码实现

public class MergeTwoSortedLists {
    // 定义链表节点
    static class ListNode {
        int val;
        ListNode next;
        ListNode(int val) { this.val = val; }
    }

    public static ListNode mergeTwoLists(ListNode l1, ListNode l2) {
        // 创建虚拟节点
        ListNode dummy = new ListNode(-1);
        ListNode current = dummy;

        // 遍历两个链表
        while (l1 != null && l2 != null) {
            if (l1.val <= l2.val) {
                current.next = l1; // 连接 l1 节点
                l1 = l1.next;      // 移动 l1 指针
            } else {
                current.next = l2; // 连接 l2 节点
                l2 = l2.next;      // 移动 l2 指针
            }
            current = current.next; // 移动 current 指针
        }

        // 连接剩余部分
        current.next = (l1 != null) ? l1 : l2;

        // 返回合并后的链表头节点
        return dummy.next;
    }

    public static void main(String[] args) {
        // 创建第一个链表:1->2->4
        ListNode l1 = new ListNode(1);
        l1.next = new ListNode(2);
        l1.next.next = new ListNode(4);

        // 创建第二个链表:1->3->4
        ListNode l2 = new ListNode(1);
        l2.next = new ListNode(3);
        l2.next.next = new ListNode(4);

        // 合并链表
        ListNode merged = mergeTwoLists(l1, l2);

        // 输出结果
        System.out.print("合并后的链表:");
        while (merged != null) {
            System.out.print(merged.val);
            if (merged.next != null) {
                System.out.print("->");
            }
            merged = merged.next;
        }
    }
}

输出结果:

合并后的链表:1->1->2->3->4->4

思路讲解

  • 引入虚拟节点:

    • 创建一个虚拟节点 dummy,其 next 将指向合并后的链表头节点。
    • 使用指针 current 指向 dummy,用于构建新链表。
  • 合并过程:

    • 遍历 l1l2,比较当前节点的值。
    • 将较小值的节点连接到 current.next,并移动对应的链表指针。
    • 无论连接哪个节点,都需要移动 current 指针。
  • 处理剩余节点:

    • 当其中一个链表遍历完后,直接将另一个链表的剩余部分连接到 current.next
  • 返回结果:

    • 最终返回 dummy.next,即合并后的链表头节点。

算法复杂度

  • 时间复杂度:O(n + m),其中 n 和 m 分别为两个链表的长度。需要遍历两个链表。
  • 空间复杂度:O(1),只使用了常数级别的额外空间。

五、总结

  • 虚拟节点的作用:

    • 简化对头节点的特殊处理,统一链表操作逻辑。
    • 提高代码的可读性和维护性。
    • 避免空指针异常,增加代码的健壮性。
  • 使用场景:

    • 插入、删除链表节点(特别是头节点)。
    • 合并链表、反转链表、删除链表中的指定元素等操作。
  • 实践建议:

    • 在涉及链表头节点操作时,考虑使用虚拟节点简化代码。
    • 注意虚拟节点的 next 指向,最终返回结果时应返回 dummy.next

链表同样对双指针的应用非常多,本章主要讲解链表算法的特定处理-虚拟节点

©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,056评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,842评论 2 378
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,938评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,296评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,292评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,413评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,824评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,493评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,686评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,502评论 2 318
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,553评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,281评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,820评论 3 305
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,873评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,109评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,699评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,257评论 2 341

推荐阅读更多精彩内容