How does a sentinel node offer benefits over NULL?
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.
How to properly use sentinel nodes?
Answer 1
Yes this is a valid position for the sentinelnode to take. head
and tail
can point to the actual beginning and end of the data. But your add
and delete
functions will need to be aware of the aberrations caused at the list boundaries by virtue of the sentinel node
Answer 2
Yes this is a valid search strategy and is infact called the Elephant in Cairo technique
Answer 3
Yes, the purpose of the sentinel node is to let you know that it is the sentinel node. You could just maintain a constant pointer (or whatever your lang of choice supports) to this sentinel node to check if you are at the sentinel node or just stick a flag in the node.
Sentinel approach with Doubly Linked List
The Wikipedia notes briefly mention using a sentinel node to simplify the implementation of linked lists.
A sentinel node is a dummy node that goes at the front of a list.
In a doubly-linked list, the sentinel node points to the first and last elements of the list. We no longer need to keep separate pointers for the head and tail of the list, like we had to do with singly-linked lists.
We also do not have to worry about updating the head and tail pointers, since as we shall see, this happens automatically if we insert after a sentinel node, hence prepending an item to the list, or insert before a sentinel node, hence appending an item to the list.
We could eliminate the container object that we used for singly linked lists, since the sentinel node can keep track of both the first and last elements in the list. If we did so, then we would return a pointer to the sentinel node to the user.
However, data structures are generally designed with a container object that mediates the communication between the user of the data structure and the implementation of the data structure, so we will retain the container object.
An answer by @6502 on How does a sentinel node offer benefits over NULL? is very helpful.
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.
Dummy nodes in linked lists
Look, there is a significant difference between a "Dummy" node and a "Sentinel" node.
Dummy nodes "Sometimes get used as first and last nodes in the list".
When you initiate a linked list a common approach is to create a dummy node, and interestingly it's the last node at the same time.
Obviously not always first or last nodes of a LL are dummy records.
Please note that you can use a dummy node without any data and a null pointer as a sentinel that shows the last node in the LL.
You may wonder is it possible to have a LL without any dummy node?
Answer: Yes. You can hold initialization of your LL until the insertion of the first data entry, and to that point just have null pointer as the LL, and after insertion(s) hold a pointer to the head node and always use a null pointer as "Next" node of the tail node.
I refer you to this page for more insight.
doubly linked list empty situation
There are two common ways to store a double linked-list:
Let
head
andtail
refer to actual nodes in the tree.head variable tail variable
| |
V V
first -> second -> third -> nullIn this case, you are correct -
head
will benull
.Let
head
andtail
be special nodes (called "sentinel nodes", as pointed out in the other answer) which don't actually contain any data and point to the first and last nodes in the list throughnext
andprev
respectively.head variable tail variable
| |
V V
head -> first -> second -> third -> tail -> null
node nodeIn this case,
header.next == tail
will mean the list is empty.One might use this approach instead of the above one to simplify the implementation a bit and make the code a bit faster - there's no need to make special provision for removing or inserting the first or last nodes.
need for header node
Having sentinel nodes prevents you from having to handle certain edge cases.
The biggest is the null check: You always know that there will be a node at the top of the list that you can insert nodes after, so you don't have to deal with checking if head is null. (also it helps having a tail node for similar reasons)
Consider the two cases:
With a head and a tail node:
addNewDataAtHead( data ):
newNode = new Node(data);
newNode.next = head.next;
newNode.prev = head;
head.next.prev = newNode;
head.next = newNode;
Without:
addNewDataAtHead( data ):
newNode = new Node(data);
if (head == null):
head = newNode;
newNode.next = head;
head.prev = newNode;
head = newNode;
the intent of the first one is a lot clearer because it is like inserting anywhere else. The second case requires you to check for a special circumstance.
need for header node
Having sentinel nodes prevents you from having to handle certain edge cases.
The biggest is the null check: You always know that there will be a node at the top of the list that you can insert nodes after, so you don't have to deal with checking if head is null. (also it helps having a tail node for similar reasons)
Consider the two cases:
With a head and a tail node:
addNewDataAtHead( data ):
newNode = new Node(data);
newNode.next = head.next;
newNode.prev = head;
head.next.prev = newNode;
head.next = newNode;
Without:
addNewDataAtHead( data ):
newNode = new Node(data);
if (head == null):
head = newNode;
newNode.next = head;
head.prev = newNode;
head = newNode;
the intent of the first one is a lot clearer because it is like inserting anywhere else. The second case requires you to check for a special circumstance.
Related Topics
Using Boost Adaptors with C++11 Lambdas
Iterator Adapter to Iterate Just the Values in a Map
How to Draw a Point (On Mouseclick) on a Qgraphicsscene
Should Std::Common_Type Use Std::Decay
Function Already Defined Error in C++
Lifetime of Object Is Over Before Destructor Is Called
Conversion from Int** to Const Int**
Does Copy List Initialization Invoke Copy Ctor Conceptually
Efficiently Convert Between Hex, Binary, and Decimal in C/C++
Glut Deprecation in MAC Osx 10.9, Ide: Qt Creator
Why Do Two Functions Have the Same Address
C++ - Std::Thread Crashes Upon Execution
Passing Constexpr Objects Around
C++ Circular Dependency in Header Files