Skip to main content

Section 28.8 Recursion in Linked Lists

In the last chapter, we learned to implement core list algorithms iteratively (printing a list, deleting all its memory, copying a list, etc...). However, it is also possible to use recursion to do any of these jobs. In fact, recursion is often a natural fit for working with linked lists since each node points to the next node, which is itself a linked list (or null if it is the end of the list). This means that many operations on linked lists can be defined in terms of operations on smaller linked lists.
Learning to think recursively about linked lists is also good practice for learning to think about recursion in more complex pointer based structures like trees. Many algorithms on trees are defined recursively and most naturally implemented using recursion. Practicing recursion with linked lists is a good stepping stone to understanding those more complex algorithms.
In Sectionย 24.6 we learned to focus on the following while designing recursive functions:
  • Identify the inputs and outputs for the function
  • Determine the base case for the recursion
  • Define the recursive case to break down the problem into a smaller version that is one step closer to the base case
We will continue to use this process as we design recursive algorithms for linked lists.
The output of every recursive function on a linked list will differ. But the inputs will often be similar. Each recursive call will be responsible for processing one node. The rest of the list is โ€œsomebody elseโ€™s problemโ€. So, a recursive function will always take as an input a pointer to the node it is responsible for. We will call that pointer current.
A function has a pointer for current. It points at a list node. We donโ€™t know what links to the node. We donโ€™t know anything about the next node or what may be beyond it.๐Ÿ”—
Figure 28.8.1. A recursive function will be passed a pointer that points to node (current) it is responsible for.
The function has access to the data in the current node, and to the next pointer. But it should generally not care about what is actually being pointed to. Any node that is beyond the current node is the responsibility of the next recursive call.
The first recursive call will typically be passed the head pointer of the list. Each subsequent recursive call will be passed the next pointer of the current node, which points to the next node in the list (or nullptr if we are at the end of the list).
The main stack frame has a linked list with a head pointer. It points at a list with nodes for 10, 20, and 30. Then there are successive stack frames for a recursive function. The first has a current pointer pointing at the 10 node. The next has a current pointer pointing at the 20 node. The next has a current pointer pointing at the 30 node. The final stack frame is holding a null pointer in its current.๐Ÿ”—
Figure 28.8.2. A series of stack frames for a recursive function. The first call was passed a copy of the head pointer from myList. Each subsequent call was passed the next pointer from the previous call.
This diagram helps us think about how to identify a base case. The base case should be a situation where we can answer the given question, or do the operation, without having to break the problem down any further. In the case of a linked list, that will often be when the current pointer is nullptr, indicating we have reached the end of the list. So our basic logic will often look like this:
if (current == nullptr) {
    // reached end of the list
    // base case logic
    return ...;
}
// recursive case logic
Defining our base case this way means that we can gracefully handle empty lists as well. If the list is empty, the head pointer will be nullptr. The first call to our recursive function will be passed that nullptr head pointer. The base case will immediately be satisfied and we can return the appropriate result for an empty list.
We can also define our base case by looking to see if we are in the last node (i.e., if current->next is nullptr):
if (current->next == nullptr) {
    // this is the last node
    // base case logic
    return ...;
}
// recursive case logic
However, using that approach, we have to be careful to handle the empty list case separately. If the list is empty, the first call will be passed a nullptr head pointer. Trying to access current->next in that case would lead to a runtime error. So unless there is a good reason to stop at the last node, it is often better to let the recursion continue until we actually reach nullptr.

Insight 28.8.1.

This will also be common in more complex structures. It is often easier to end recursion at โ€œone step too farโ€ rather than trying to stop exactly at the last valid element.

Checkpoint 28.8.1.

Why is current == nullptr a better base case than current->next == nullptr?
  • It handles the empty list case gracefully without requiring special logic.
  • It stops one step earlier, reducing the number of recursive calls.
  • It makes sure we reach the end of the list before stopping.
You have attempted of activities on this page.