Skip to main content

Section 27.11 Improvements for SimpleLinkedList

Subsection 27.11.1 Tracking the size

We have already identified some improvements that could be made to our SimpleLinkedList class. One simple addition we mentioned would be to add a size variable that keeps track of how many elements are in the list.
Adding a size variable would provide a few benefits. Currently, to find the size of the list, we would need to loop through all the nodes and count them. (This would take \(O(n)\) time.) If we kept a size variable updated as we add and remove elements, we could return the size in constant time, \(O(1)\text{.}\)
Once getting the size is cheap, we can use it to simplify the implementation of other methods by using it to do bounds checking before we start traversing the list. All of our traversal functions currently have to keep checking if they have hit a nullptr as they walk through the list. Instead, we can check the index against the size at the start of the function and throw an exception if it is out of bounds.
template<typename T>
T LinkedList<T>::retrieveAt(int index) const {
  if (index < 0 || index >= size)
    throw out_of_range("Bad index in retrieveAt");

  ListNode<T>* current = head;
  for (int i = 0; i < index; i++) {
    current = current->next;
    // No need to check for nullptr here
  }

  return current->data;
}
This does not come for free. Maintaining the size variable will add a slight amount of complexity to our code. We will need to remember to update the size variable whenever we add or remove an element.
void LinkedList<T>::insertStart(const T& value) {
  // Do work to insert new node at start of list...
  size++;  //update size variable
}

void LinkedList<T>::removeAt(int index) {
  //Do work to remove node at index...
  size--;  //update size variable
}

Insight 27.11.1.

Usually we want to avoid storing the same data more than once. We already implicitly are storing the size of the list by the number of nodes it contains, so adding a size variable is a form of data duplication.
But, the small amount of extra record keeping involved with the size variable seems like a reasonable trade-off for the performance benefits it provides for a critical operation like getting the size of the list.

Subsection 27.11.2 Tracking the tail

Another issue we have not addressed yet is working at the end of the list. Currently, if we want to add an element to the end of the list, we have to start at the head and walk through all the nodes until we reach the end. This takes \(O(n)\) time. If we kept a pointer to the tail node of the list, we could add elements to the end in constant time, \(O(1)\text{.}\)
Below is an animation of what using a tail pointer to add a new element to the end of the list would look like.

Activity 27.11.1. Singly Linked List with Tail Pointer.

This version of the linked list keeps track of the size of the list and a tail pointer to the last node in the list. It has started to insert 20 at the end of the list. Use > to step through the process.
Instructions.
When the > button is highlighted, an animation is prepared to run. Use the Step button to step through the animation one step at a time.
When no animation is in progress, you can use the data controls to start a new animation.
You can click and drag on the animation area to pan around. Use the Ctrl + mouse wheel (or touchpad scroll gesture) while over the animation area to zoom in and out.
  • Core Controls.
    • << : Skip to the beginning of the animation.
    • < : Step backwards one step in the animation.
    • > : Step forward one step in the animation.
    • >> : Skip to the end of the animation.
    • Auto Step Speed Set to anything other than off to automatically step through the animation at the selected speed.
    • Zoom Set the zoom level. You can also use Ctrl + Mouse Wheel to zoom in and out.
  • Data Controls.
    • Value Use this area to enter a value when inserting, finding, deleting, etc...
Now, adding an element to the end of the list only takes a few steps, regardless of how long the list is. We do have to worry about one special case - inserting into an empty list. In that case, the new node is both the head and the tail of the list.
Listing 27.11.1. insertEnd Implementation
void LinkedList<T>::insertEnd(T value) {
    if (size == 0) {
        // add a node and set both head and tail to point to it
    } else {
        // make the new node
        // update the next pointer of the current tail to point to the new node
        // update the list's tail pointer to point to the new node
    }
    size++;
}
As with size, maintaining a tail pointer will result in some extra complexity in our code. We will need to make sure to update the tail pointer whenever we add or remove elements from the end of the list. Here are some new special cases we will need to handle:
  • When inserting into an empty list insertStart needs to set both the head and tail pointers to point to the new node.
  • If removeAt is called on the index of the last node, we need to make sure to update the tail pointer.
  • if removeStart is called when the list has only one node, we need to make sure the tail pointer is set to nullptr.
Below are some snippets showing the new logic we will need. For the full code, see SectionΒ 27.19.
void LinkedList<T>::insertStart(const T& value) {
    // ... do the insertion ...
    // if list was empty, new node is also tail
    if (size == 0)
        tail = head;
    size++;
}
...
void LinkedList<T>::removeAt(int index) {
    // ... Make current point to the node before the one we are deleting ...
    // ... do the removal ...
    // Is there nothing left after current node? Then it is the new tail
    if (current->next == nullptr)
        tail = current;
    size--;
}
Although this allows us to efficiently insert at the end of the list, we still can’t efficiently remove an element from the end of the list. To remove the last element, we need to set the tail pointer to the second-to-last node and set the next pointer of that node to nullptr. But we have no way to reach the second-to-last node without starting at the head and walking through the list.

Checkpoint 27.11.1.

What does adding a tail pointer to our simple linked list allow us to do? Check all that apply.
  • Efficiently insert elements at the end of the list
  • Efficiently remove elements from the end of the list
  • Efficiently access the last element in the list
  • Efficiently access the second-to-last element in the list
  • Efficiently print the list in reverse order
You have attempted of activities on this page.