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更为简洁优雅。
参考:
《关于链表中哨兵结点问题的深入剖析》 参考图
《[算法/数据结构] 链表和哨兵节点》 参考代码