template <class T> struct node { T value; node<T>* prev; node<T>* next; node(const T& value = T(), node<T>* prev = nullptr, node<T>* next = nullptr) { this->value = value; this->prev = prev; this->next = next; } };
The default parameter value T()
requires a bit of explanation. How do you specify a default value for a parameter whose data type is a template parameter? If the data type of T
is a class or struct type, then the default constructor for the class or struct will be used to supply the default value. For built-in types like int
or double
, the default value supplied for the parameter will be 0.
Case 1: List is not empty
Assume that our linked list has three data members: l_front
(a pointer to the front or first node in the list), l_back
(a pointer to the back or last node in the list), and l_size
(the number of values stored in the linked list.
Let's also assume that the linked list contains one or more nodes and that we have allocated a new list node using the temporary pointer new_node
. A diagram of the list might look like this:
To insert the new node at the rear of the list, we have to set three pointers: the prev
pointer in new_node
, the next
pointer in the current last node in the list, and l_back
, which needs to be updated to
point to the new last node in the list. (The next
pointer in new_node
has already been set to nullptr
by the constructor, so we don't need to change that.) Once we've finished, the list should look like this:
Here is the C++ code to perform these three steps:
new_node->prev = l_back; // Point new node's prev pointer at current last node in the list
l_back->next = new_node; // Point current last node's next pointer at new last node in the list
l_back = new_node; // Point l_back at new last node in the list
The order of these steps is important. We can code Steps 1 and 2 in either order with no problems, but Step 3 must be done last. (Why?)
Case 2: List is empty
The steps above work as long as there is at least one node in the linked list. But what if the list is empty?
If we use the same steps as above:
new_node->prev = l_back; // Point new node's prev pointer at current last node in the list
l_back->next = new_node; // Since l_back == nullptr, this step causes a segmentation fault
l_back = new_node;
To insert the new node at the rear of an empty list, we once again have to set three pointers: the prev
pointer in new_node
, l_front
, which needs to point to the new first node in the list,
and l_back
, which needs to be updated to point to the new last node in the list. Once we've finished, the list should look like this:
So, for an empty list, the correct C++ code to perform these three steps is:
new_node->prev = l_back; // Since l_back == nullptr, new_node->prev will be set to nullptr as well
l_front = new_node; // Point l_front at new first node in the list
l_back = new_node; // Point l_back at new last node in the list
To combine the two cases and minimize repetition of code, we can
The steps for insertion at the back of a doubly-linked list and the steps for insertion at the front
of a doubly-linked list are symmetric. This means that to write the code for push_front()
,
take the code you've written for push_back()
and
l_front
to l_back
, and vice versanext
to prev
, and vice versaCase 1: List contains more than one node
Assume that our linked list contains two or more nodes:
The steps in C++ to remove the last node in the list look like this:
node<T>* del_node = l_back; // Save address of node to delete in a pointer
l_back = del_node->prev; // Point l_back at the new last node in the list
l_back->next = nullptr; // Set the new last node's next pointer to nullptr
delete del_node;
Here's a diagram of the list just after Step 3:
Case 2: List contains one node
The steps above work as long as there are at least two nodes in the linked list. But what if the list only contains one node?
If we use the same steps as above:
node<T>* del_node = l_back; // Save address of node to delete in a pointer
l_back = del_node->prev; // This makes l_back nullptr, which is what it should be
l_back->next = nullptr; // Segmentation fault!
delete del_node;
Once again, this is a special case that needs to be handled a bit differently. The correct sequence of steps in this case is:
node<T>* del_node = l_back; // Save address of node to delete in a pointer
l_back = del_node->prev; // This makes l_back nullptr
l_front = nullptr; // If l_back == nullptr, l_front should be as well since the list is now empty
delete del_node;
Here's a diagram of the list just after Step 3:
As with insertion, to combine the two cases and minimize repetition of code, we can
l_back == nullptr
)The steps for deletion at the back of a doubly-linked list and the steps for deletion at the front
of a doubly-linked list are also symmetric. This means that to write the code for pop_front()
,
take the code you've written for pop_back()
and
l_front
to l_back
, and vice versanext
to prev
, and vice versa