【数据结构】关于链表中哨兵结点(sentinel)问题的深入剖析

1. 概述

最近正在学习UC Berkeley的CS61B这门课,主要是采用Java语言去实现一些数据结构以及运用数据结构去做一些project。这门课不仅告诉你这个东西怎么做,而且一步一步探寻为什么要这样做以及为什么会有这些功能。我们有时在接触某段代码或功能的实现时,可能直接就看到了它最终的面貌,而不知道如何一步步演化而来,其实每一个功能的添加或优化都是对应一个问题的解决。下面就这门课中关于链表中哨兵结点的相关问题进行总结。

2. 什么是哨兵结点

哨兵顾名思义有巡逻、检查的功能,在我们程序中通过增加哨兵结点往往能够简化边界条件,从而防止对特殊条件的判断,使代码更为简便优雅,在链表中应用最为典型。

2.1 单链表中的哨兵结点

首先讨论哨兵结点在单链表中的运用,如果不加哨兵结点在进行头尾删除和插入时需要进行特殊判断。比如在尾部插入结点的代码如下:

void addLast(int x) {
    if (first == null) {   //需要判断头节点是否存在
    	first = new Node(x, null);
        return;
    }
    Node p = first;
    while (p.next != null) { //如果头结点存在,则不断的找到尾节点,并将当前的节点添加至尾部
        p = p.next;
    }
    p.next = new Node(x, null);
}

如上所示需要对结点为空的特殊情况进行判断,头部加了一个哨兵结点后就可以不需要判断了(不会为空):
在这里插入图片描述

2.2 双链表中的哨兵结点

2.2.1 Version 1: 双哨兵

这种模式用的比较多
在这里插入图片描述c++代码:

template <typename T> struct LinkedListNode    //定义node节点结构
{
    T val;  //节点值
    LinkedListNode<T> * prev;   //前驱节点,pre指针
    LinkedListNode<T> * succ;   //后继节点, next指针

    LinkedListNode()
    {
        prev = succ = nullptr;
    }

    LinkedListNode(T val, LinkedListNode<T> * prev = nullptr, LinkedListNode<T> * succ = nullptr)
    {
        this -> val = val;
        this -> prev = prev;
        this -> succ = succ;
    }
};

template <typename T> struct LinkedList       //链表结构
{
    int size;   //链表的长度
    LinkedListNode<T> * head;   //头哨兵节点
    LinkedListNode<T> * tail;   //尾哨兵节点

    LinkedList()
    {
        size = 0;
        head = new LinkedListNode<T>();   //构造函数中初始化 head哨兵
        tail = new LinkedListNode<T>();   //构造函数中初始化 tail哨兵
        head -> succ = tail;
        tail -> prev = head;
    }

    LinkedListNode<T> * insert(LinkedListNode<T> * prev, T val) //在prev节点后插入val值
    {
        LinkedListNode<T> * node = new LinkedListNode<T>(val, prev, prev -> succ);
        prev -> succ -> prev = node;
        prev -> succ = node;
        ++size;
        return node;
    }

    LinkedListNode<T> * insert(T val)   //在链表头部插入val值
    {
        return insert(head, val);
    }

    void remove(LinkedListNode<T> * node)   //在链表中删除node节点
    {
        node -> prev -> succ = node -> succ;
        node -> succ -> prev = node -> prev;
        --size;
    }

    LinkedListNode<T> * get(int index)  //返回链表index位置的节点
    {
        LinkedListNode<T> * node = head -> succ;
        while (index--)
            node = node -> succ;
        return node;
    }

    T & operator[](int index)   //重载[]运算符
    {
        return get(index) -> val;
    }
};

注意:并不是在链表中带有head和tail属性就是哨兵节点!而是指会在构造函数或初始化的地方去初始化节点的实例:
在这里插入图片描述

2.2.2 Version 2:循环双链表

上述Version1需要两个哨兵结点,可以对其进行改进。可以使用头部结点的prev指针指向尾部,尾部结点的next指针指向哨兵,这样就只需要一个哨兵结点,使链表变成循环链表,比Version1更为简洁优雅。
在这里插入图片描述


参考:
《关于链表中哨兵结点问题的深入剖析》 参考图
《[算法/数据结构] 链表和哨兵节点》 参考代码

  • 1
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值