维基百科的注释简要提到了使用哨兵节点来简化链表的实现。
哨兵节点是位于列表前面的虚拟节点。
在双向链表中,哨兵节点指向列表的第一个和最后一个元素。我们不再需要为列表的头部和尾部保留单独的指针,就像我们对单链表所做的那样。
我们也不必担心更新头和尾指针,因为正如我们将看到的,如果我们在哨兵节点之后插入,这会自动发生,因此将项目添加到列表中,或者在哨兵节点之前插入,因此将一个项目附加到列表中。
我们可以消除用于单链表的容器对象,因为哨兵节点可以跟踪列表中的第一个和最后一个元素。如果我们这样做了,那么我们将返回一个指向哨兵节点的指针给用户。
但是,数据结构一般设计有一个容器对象,该容器对象介导数据结构的用户和数据结构的实现之间的通信,所以我们会保留容器对象。
@6502 在How does a sentinel node offer benefits over NULL? 上的回答非常有帮助。
以下是在节点的双向链表中删除节点的代码,其中NULL用于标记链表的结束,两个指针first和last用于保存第一个和最后一个节点的地址:
// Using NULL and pointers for first and last
if (n->prev) n->prev->next = n->next;
else first = n->next;
if (n->next) n->next->prev = n->prev;
else last = n->prev;
这是相同的代码,其中有一个特殊的虚拟节点来标记列表的结尾,列表中第一个节点的地址存储在特殊节点的下一个字段中,最后一个节点在哪里该列表存储在特殊虚拟节点的 prev 字段中:
// Using the dummy node
n->prev->next = n->next;
n->next->prev = n->prev;
节点插入也有同样的简化;例如,在节点 x 之前插入节点 n(具有 x == NULL 或 x == &dummy 表示在最后一个位置插入),代码将是:
// Using NULL and pointers for first and last
n->next = x;
n->prev = x ? x->prev : last;
if (n->prev) n->prev->next = n;
else first = n;
if (n->next) n->next->prev = n;
else last = n;
和
// Using the dummy node
n->next = x;
n->prev = x->prev;
n->next->prev = n;
n->prev->next = n;
如您所见,为双向链表删除了所有特殊情况和所有条件的虚拟节点方法。