How does a sentinel node offer benefits over NULL?
I think that a little code example would be a better explanation than a theoretic discussion.
The following is the code for node deletion in a doubly-linked list of nodes where NULL
is used to mark the end of the list and where two pointers first
and last
are used to hold the address of first and last node:
// 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;
and this is the same code where instead there is a special dummy node to mark the end of the list and where the address of first node in the list is stored in the next
field of the special node and where the last node in the list is stored in the prev
field of the special dummy node:
// Using the dummy node
n->prev->next = n->next;
n->next->prev = n->prev;
The same kind of simplification is also present for node insertion; for example to insert node n
before node x
(having x == NULL
or x == &dummy
meaning insertion in last position) the code would be:
// 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;
and
// Using the dummy node
n->next = x;
n->prev = x->prev;
n->next->prev = n;
n->prev->next = n;
As you can see the dummy node approach removed for a doubly-linked list all special cases and all conditionals.
The following picture represents the two approaches for the same list in memory...
There's no advantage with sentinels if you're just doing simple iteration and not looking at the data in the elements.
However, there's some real gain when using it for "find" type algorithms. For example, imagine a linked list list std::list
where you want to find a specific value x
.
What you would do without sentinels is:
for (iterator i=list.begin(); i!=list.end(); ++i) // first branch here
{
if (*i == x) // second branch here
return i;
}
return list.end();
But with sentinels (of course, end actually has to be a real node for this...):
iterator i=list.begin();
*list.end() = x;
while (*i != x) // just this branch!
++i;
return i;
You see there's no need for the additional branch to test for the end of the list - the value is always guaranteed to be there, so you will automatically return end()
if x
cannot be found in your "valid" elements.
For another cool and actually useful application of sentinels, see "intro-sort", which is the sorting algorithm that's used in most std::sort
implementations. It has a cool variant of the partition algorithm that uses sentinels to remove a few branches.