LeetCode 206 - Reverse Linked List
The problem asks us to reverse a singly linked list. A singly linked list is a sequence of nodes where each node stores a value and a pointer to the next node in the sequence. The input, head, represents the first node of the linked list.
Difficulty: 🟢 Easy
Topics: Linked List, Recursion
Solution
Problem Understanding
The problem asks us to reverse a singly linked list. A singly linked list is a sequence of nodes where each node stores a value and a pointer to the next node in the sequence. The input, head, represents the first node of the linked list. Our task is to rearrange the connections between the nodes so that the direction of the list is completely reversed.
For example, if the original list is:
1 -> 2 -> 3 -> 4 -> 5
after reversal, it should become:
5 -> 4 -> 3 -> 2 -> 1
The important detail is that we are not creating a new list of values. Instead, we are modifying the next pointers of the existing nodes so that every link points in the opposite direction.
The constraints tell us that the list can contain anywhere from 0 to 5000 nodes. This means the list may be empty, may contain only one node, or may contain several thousand nodes. Since the input size is relatively moderate, both iterative and recursive approaches are feasible. However, recursion introduces additional call stack usage, so the iterative approach is generally more space efficient.
Several edge cases are important:
- The list may be empty (
head = None) - The list may contain only one node
- The list may contain two nodes, which is the smallest non-trivial reversal
- The implementation must correctly reconnect pointers without losing access to the remaining nodes
A naive implementation can easily break the list if it overwrites a pointer before saving the next node reference.
Approaches
A brute-force approach would be to extract all node values into an array, reverse the array, and then rebuild the linked list or overwrite the node values in reverse order. This works because arrays allow direct indexed access, making reversal straightforward. However, this approach uses additional memory proportional to the size of the list, which is unnecessary because the problem only requires pointer manipulation.
A more optimal approach recognizes that reversing a linked list only requires changing the direction of each next pointer. At any point during traversal, we only need to know three things:
- The current node being processed
- The previous node, which should become the new
next - The next node, so we do not lose access to the remainder of the list
By carefully updating these pointers in sequence, we can reverse the list in a single pass using constant extra space.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n) | O(n) | Store node values or nodes in an array, then rebuild or overwrite |
| Optimal | O(n) | O(1) | Reverse pointers in-place during traversal |
Algorithm Walkthrough
Iterative Reversal Algorithm
- Initialize two pointers,
previousandcurrent.
previous starts as None because the new tail of the reversed list should point to nothing. current starts at the original head because we begin processing from the front of the list.
2. Traverse the linked list while current is not None.
This loop ensures every node is visited exactly once. 3. Save the next node before modifying pointers.
Since we are about to change current.next, we must first store the original next node in a temporary variable, usually called next_node. Otherwise, we would lose access to the remainder of the list.
4. Reverse the pointer direction.
Set:
current.next = previous
This changes the direction of the link so the current node now points backward instead of forward. 5. Move both pointers forward.
After reversing the connection:
- Move
previoustocurrent - Move
currenttonext_node
This advances the traversal while preserving the reversed portion of the list. 6. Continue until all nodes are processed.
When current becomes None, the traversal is complete and previous points to the new head of the reversed list.
7. Return previous.
Since previous ends on the final processed node, it represents the head of the reversed list.
Why it works
The key invariant is that before each iteration:
previouspoints to a correctly reversed portion of the listcurrentpoints to the next node that still needs processing
Each iteration removes one node from the unreversed portion and attaches it to the front of the reversed portion. Since every node is processed exactly once, the entire list becomes reversed by the end of the traversal.
Python Solution
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
from typing import Optional
class Solution:
def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:
previous = None
current = head
while current:
next_node = current.next
current.next = previous
previous = current
current = next_node
return previous
The implementation directly follows the iterative algorithm described earlier.
The variable previous tracks the head of the already reversed portion of the list. Initially, it is None because the reversed list is empty.
The variable current traverses the original list node by node. During each iteration, the algorithm first saves current.next into next_node. This step is critical because the next pointer is about to be overwritten.
The line:
current.next = previous
reverses the direction of the pointer.
After reversing the link, both traversal pointers move forward. previous expands the reversed list, while current advances to the next unprocessed node.
When traversal finishes, previous points to the new head of the reversed list, so it is returned.
Go Solution
/**
* Definition for singly-linked list.
* type ListNode struct {
* Val int
* Next *ListNode
* }
*/
func reverseList(head *ListNode) *ListNode {
var previous *ListNode = nil
current := head
for current != nil {
nextNode := current.Next
current.Next = previous
previous = current
current = nextNode
}
return previous
}
The Go implementation is nearly identical to the Python version in logic and structure.
Go uses nil instead of Python's None. Pointer manipulation is explicit because linked list nodes are referenced using *ListNode.
No special handling for integer overflow is necessary because the algorithm only manipulates pointers, not arithmetic values.
The iterative structure remains the same:
- Save the next node
- Reverse the pointer
- Advance traversal pointers
Worked Examples
Example 1
Input:
1 -> 2 -> 3 -> 4 -> 5
Initial state:
| Step | previous | current | next_node | List State |
|---|---|---|---|---|
| Start | None | 1 | None | 1 -> 2 -> 3 -> 4 -> 5 |
Iteration 1:
- Save
2 - Reverse
1.nexttoNone
| Step | previous | current | next_node | Reversed Portion |
|---|---|---|---|---|
| After Iteration 1 | 1 | 2 | 2 | 1 -> None |
Iteration 2:
- Save
3 - Reverse
2.nextto1
| Step | previous | current | next_node | Reversed Portion |
|---|---|---|---|---|
| After Iteration 2 | 2 | 3 | 3 | 2 -> 1 -> None |
Iteration 3:
| Step | previous | current | next_node | Reversed Portion |
|---|---|---|---|---|
| After Iteration 3 | 3 | 4 | 4 | 3 -> 2 -> 1 -> None |
Iteration 4:
| Step | previous | current | next_node | Reversed Portion |
|---|---|---|---|---|
| After Iteration 4 | 4 | 5 | 5 | 4 -> 3 -> 2 -> 1 -> None |
Iteration 5:
| Step | previous | current | next_node | Reversed Portion |
|---|---|---|---|---|
| After Iteration 5 | 5 | None | None | 5 -> 4 -> 3 -> 2 -> 1 -> None |
Final output:
5 -> 4 -> 3 -> 2 -> 1
Example 2
Input:
1 -> 2
| Step | previous | current | Result |
|---|---|---|---|
| Start | None | 1 | Original list |
| Iteration 1 | 1 | 2 | 1 -> None |
| Iteration 2 | 2 | None | 2 -> 1 -> None |
Final output:
2 -> 1
Example 3
Input:
[]
Initial state:
| previous | current |
|---|---|
| None | None |
Since current is already None, the loop never executes.
Final output:
[]
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n) | Each node is visited exactly once |
| Space | O(1) | Only a few pointer variables are used |
The algorithm performs a single traversal of the linked list. Every node undergoes one pointer reversal operation, so the runtime grows linearly with the number of nodes.
The space complexity is constant because no additional data structures proportional to the input size are allocated. Only three pointer variables are maintained regardless of list length.
Test Cases
# Helper functions for testing
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
def build_list(values):
dummy = ListNode()
current = dummy
for value in values:
current.next = ListNode(value)
current = current.next
return dummy.next
def linked_list_to_list(head):
result = []
while head:
result.append(head.val)
head = head.next
return result
class Solution:
def reverseList(self, head):
previous = None
current = head
while current:
next_node = current.next
current.next = previous
previous = current
current = next_node
return previous
solution = Solution()
# Example 1
head = build_list([1, 2, 3, 4, 5])
assert linked_list_to_list(solution.reverseList(head)) == [5, 4, 3, 2, 1] # standard multi-node list
# Example 2
head = build_list([1, 2])
assert linked_list_to_list(solution.reverseList(head)) == [2, 1] # two-node reversal
# Example 3
head = build_list([])
assert linked_list_to_list(solution.reverseList(head)) == [] # empty list
# Single node
head = build_list([42])
assert linked_list_to_list(solution.reverseList(head)) == [42] # single-node edge case
# Repeated values
head = build_list([7, 7, 7])
assert linked_list_to_list(solution.reverseList(head)) == [7, 7, 7] # duplicate values
# Negative numbers
head = build_list([-1, -2, -3])
assert linked_list_to_list(solution.reverseList(head)) == [-3, -2, -1] # negative integers
# Long list
head = build_list(list(range(100)))
assert linked_list_to_list(solution.reverseList(head)) == list(range(99, -1, -1)) # larger input size
| Test | Why |
|---|---|
[1,2,3,4,5] |
Validates standard multi-node reversal |
[1,2] |
Smallest non-trivial linked list |
[] |
Ensures empty input is handled correctly |
[42] |
Verifies single-node lists remain unchanged |
[7,7,7] |
Confirms logic does not depend on unique values |
[-1,-2,-3] |
Ensures negative values work correctly |
| Large 100-node list | Tests scalability and pointer consistency |
Edge Cases
Empty List
An empty list means head is None. This case is important because attempting to access head.next without checking can cause runtime errors.
The implementation handles this naturally because the loop condition:
while current:
fails immediately when current is None. The function then returns previous, which is also None.
Single Node List
A list containing only one node is already reversed by definition. Some incorrect implementations accidentally disconnect the node or create cycles during pointer manipulation.
In this implementation, the single node's next pointer is reassigned to None, which it already is. The traversal then terminates correctly and returns the original node.
Losing the Remaining List
One of the most common bugs in linked list reversal is overwriting current.next before saving the reference to the remaining nodes.
For example, doing this incorrectly:
current.next = previous
current = current.next
would lose access to the unreversed portion of the list.
The implementation avoids this by first storing:
next_node = current.next
before changing any pointers. This guarantees that traversal can continue safely after reversal.