Reverse a singly linked list.
New to this one? Start here. Below is the core idea in everyday language, plus a real-world analogy—then the sections that follow build on it with the formal reasoning, code, and step-by-step walkthroughs.
Picture a conga line where everyone holds the shoulders of the person in front. To reverse the line without anyone leaving, each dancer lets go of the person ahead and instead grabs the person behind them. Do this one dancer at a time, front to back, and the whole line now faces the other way. The trick: before you let go of who is in front, you must remember them, or you will never reach the rest of the line.
A naive strategy is to store the values of the linked list in an array and reconstruct the linked list by traversing the array in reverse order. However, this solution does not reverse the original linked list; it just creates a new one. Could we try performing the reversal in place?
Let’s think about the problem in terms of pointer manipulation. The key observation here is that if we "flip" the direction of the pointers, we're effectively reversing the linked list:
Now, we just need to figure out how to perform this pointer manipulation. Consider the example below:
To reverse the direction of all pointers, we iterate through the nodes one by one. In this process, we need access to the current node (curr_node) and the previous node (prev_node) to adjust the current node's next pointer to the previous node. Note that prev_node will initially point at null since the first node has no previous node:
To reverse the next pointer, we'll need a way to shift the curr_node and prev_node pointers one node over. To shift prev_node, we can set it to the position of curr_node. However, we can't move curr_node to node 2 because we lost our reference to node 2:
This suggests we should have preserved a reference to node 2 before reversing the curr_node. This can be done by creating a variable next_node and setting it to curr_node.next. Let’s assume we did this. Now, we can advance prev_node and curr_node forward by one:
Note, we don't need to shift next_node, as it can be set by curr_node.next in the next iteration.
We can summarize this logic in three steps. At each node in the linked list:
Save a reference to the next node (next_node = curr_node.next).
Change the current node’s next pointer to link to the previous node (curr_node.next = prev_node).
Move both prev_node and curr_node forward by one (prev_node = curr_node,curr_node = next_node).
Let's repeat these steps for the rest of the linked list:
We can stop the reversal when curr_node becomes null, indicating there are no more nodes to reverse.
The final step is to return the head of the reversed linked list, which is pointed to by prev_node once curr_node becomes null.
from ds import ListNode
def linked_list_reversal(head: ListNode) -> ListNode:
curr_node, prev_node = head, None
# Reverse the direction of each node's pointer until 'curr_node' is null.
while curr_node:
next_node = curr_node.next
curr_node.next = prev_node
prev_node = curr_node
curr_node = next_node # 'prev_node' will be pointing at the head of the reversed linked list.
return prev_nodeListNode* linkedListReversal(ListNode* head) {
ListNode* prev = nullptr; // everything behind curr, already flipped
ListNode* curr = head;
while (curr != nullptr) {
ListNode* nextNode = curr->next; // 1. save the rest of the list
curr->next = prev; // 2. flip this node's arrow backward
prev = curr; // 3. prev catches up
curr = nextNode; // 4. move on
}
return prev; // prev is the new head
}public ListNode linkedListReversal(ListNode head) {
ListNode prev = null; // everything behind curr, already flipped
ListNode curr = head;
while (curr != null) {
ListNode nextNode = curr.next; // 1. save the rest of the list
curr.next = prev; // 2. flip this node's arrow backward
prev = curr; // 3. prev catches up
curr = nextNode; // 4. move on
}
return prev; // prev is the new head
}Time complexity: The time complexity of linked_list_reversal is , where denotes the length of the linked list. This is because we perform constant-time pointer manipulation at each node of the linked list.
Space complexity: The space complexity is .
Sometimes, the interviewer may want the problem solved using recursion. Let’s see what a recursive solution to this problem would look like.
In a recursive solution, the problem is solved by solving smaller instances of the same problem. To solve these smaller subproblems, we would need to use the linked list reversal function (linked_list_reversal) in its own implementation. The process of solving smaller problems using recursive calls continues until the smallest version of the problem is solved.
The smallest version of this problem involves reversing a linked list of size 0 or 1. These are linked lists which are inherently the same as their reverse. So, these can be our base cases.
With this in mind, let's try crafting the logic of the recursive function using the example below:
Think about which subproblem we should solve. To reverse the entire linked list, we can use our linked_list_reversal function to reverse the sublist after the current node. This way, we only need to focus on reversing the pointer of the current node. Let’s see how this works.
As mentioned, let’s first recursively call linked_list_reversal on the sublist starting at head.next. Let’s assume this recursive call reverses this sublist and returns its head as intended.
When designing a recursive function, assume any recursive call to that function will behave as intended, even if the function hasn’t been fully implemented.
Next, we need the tail of the reversed sublist (node 2) to point to node 1. We can reference node 2 using head.next. So, all we need to do is set head.next.next to head as illustrated below:
The linked list is almost fully reversed now, but node 1 is still pointing to node 2. To remove this link, just set head.next to null. Then, we can return new_head, which is the head of the reversed linked list:
from ds import ListNode
def linked_list_reversal_recursive(head: ListNode) -> ListNode:
# Base cases.
if (not head) or (not head.next):
return head
# Recursively reverse the sublist starting at the next node.
new_head = linked_list_reversal_recursive(head.next)
# Connect the reversed sublist to the head node to fully reverse the entire linked list.
head.next.next = head
head.next = None
return new_head
Time complexity: The time complexity of linked_list_reversal_recursive is because it involves a single recursive traversal through the linked list, visiting each node exactly once.
Space complexity: The space complexity is due to the stack space taken up by the recursive call stack, which grows up to levels deep because recursive calls are made.
Two step-by-step walkthroughs of the algorithm below: a valid case (the expected happy path) and an invalid / edge case that exercises the tricky parts. Each step shows the program state as the code runs—the same steps apply to Python, C++ and Java.
prev (reversed part) curr (flipping now)
| Iteration | next saved | flip performed | prev | curr |
|---|---|---|---|---|
| start | — | — | null | 1 |
| 1 | 2 | 1 → null | 1 | 2 |
| 2 | 3 | 2 → 1 | 2 | 3 |
| 3 | null | 3 → 2 | 3 | null |
prev (reversed part) curr (flipping now)
| Iteration | next saved | flip performed | prev | curr |
|---|---|---|---|---|
| start | — | — | null | 6 |
| 1 | 1 | 6 → null | 6 | 1 |
| 2 | 8 | 1 → 6 | 1 | 8 |
| 3 | 4 | 8 → 1 | 8 | 4 |
| 4 | null | 4 → 8 | 4 | null |
Contrast with Dry run 1: the mechanics are identical — four nodes simply means four flips. The loop always ends with curr = null and prev resting on the old tail, which becomes the new head.
Both runs step exactly as the tables above. Python-specific: save with next_node = curr.next, flip with curr.next = prev; the empty link is None; return prev.
Same steps. C++-specific: ListNode* nextNode = curr->next; then curr->next = prev; the empty link is nullptr.
Same steps. Java-specific: ListNode nextNode = curr.next; then curr.next = prev; the empty link is null.
Tip: Visualize pointer manipulations.
Often, it can be tricky to figure out exactly what to do when dealing with linked list manipulation. Drawing pointers as arrows between nodes can be quite helpful. By observing how these arrows should be reoriented to represent changes in the linked list's structure, we can deduce the necessary pointer manipulation logic. This approach also helps identify which nodes we need references to when making these changes.