Before attempting this problem, you should be comfortable with:
Linked Lists - Understanding how to traverse a singly linked list using the getNext() method
Recursion - The call stack naturally reverses order, printing nodes as the stack unwinds
Stack Data Structure - An iterative alternative that provides LIFO ordering to reverse the print sequence
Two Pointers (Slow/Fast) - The divide and conquer approach uses this technique to find the middle of a list segment
1. Recursion
Intuition
Recursion naturally reverses the order of operations. When we recurse to the end of the list first and then print on the way back, we effectively print in reverse. The call stack holds all the nodes, and as each call returns, it prints its node's value.
Algorithm
If the current node is null, return (base case).
Recursively call the function on head.getNext().
After the recursive call returns, print the current node's value.
funcprintLinkedListInReverse(head ImmutableListNode){if head !=nil{printLinkedListInReverse(head.GetNext())
head.PrintValue()}}
class Solution {funprintLinkedListInReverse(head: ImmutableListNode?){if(head !=null){printLinkedListInReverse(head.getNext())
head.printValue()}}}
classSolution{funcprintLinkedListInReverse(_ head:ImmutableListNode?){if head !=nil{printLinkedListInReverse(head?.getNext())
head?.printValue()}}}
Time & Space Complexity
Time complexity: O(n)
Space complexity: O(n)
Where n is the size of the linked list.
2. Using Stack
Intuition
A stack provides Last-In-First-Out (LIFO) ordering, which is exactly what we need to reverse the print order. We traverse the list once to push all nodes onto the stack, then pop them one by one to print in reverse order.
Algorithm
Create an empty stack.
Traverse the linked list from head to end, pushing each node onto the stack.
Pop nodes from the stack one by one and print each node's value until the stack is empty.
funcprintLinkedListInReverse(head ImmutableListNode){
stack :=[]ImmutableListNode{}for head !=nil{
stack =append(stack, head)
head = head.GetNext()}forlen(stack)>0{
node := stack[len(stack)-1]
stack = stack[:len(stack)-1]
node.PrintValue()}}
class Solution {funprintLinkedListInReverse(head: ImmutableListNode?){val stack = java.util.Stack<ImmutableListNode>()var curr = head
while(curr !=null){
stack.push(curr)
curr = curr.getNext()}while(!stack.isEmpty()){val node = stack.pop()
node.printValue()}}}
classSolution{funcprintLinkedListInReverse(_ head:ImmutableListNode?){var stack =[ImmutableListNode]()var curr = head
while curr !=nil{
stack.append(curr!)
curr = curr?.getNext()}while!stack.isEmpty {let node = stack.removeLast()
node.printValue()}}}
Time & Space Complexity
Time complexity: O(n)
Space complexity: O(n)
Where n is the size of the linked list.
3. Square Root Decomposition
Intuition
To reduce space complexity, we can divide the list into blocks of size approximately sqrt(n). We only store pointers to the start of each block, requiring O(sqrt(n)) space. For each block, we use recursion to print in reverse. Since each block is small, the recursion depth stays at O(sqrt(n)).
Algorithm
Traverse the list once to count its size n.
Calculate block_size as ceil(sqrt(n)).
Traverse the list again, storing a pointer to the start of each block in a stack.
Pop each block from the stack and use recursion to print that block in reverse. Limit recursion to the block_size to avoid going into the next block.
funcprintLinkedListInReverseRecursively(head ImmutableListNode, size int){if size >0&& head !=nil{printLinkedListInReverseRecursively(head.GetNext(), size-1)
head.PrintValue()}}funcgetLinkedListSize(head ImmutableListNode)int{
size :=0for head !=nil{
size++
head = head.GetNext()}return size
}funcprintLinkedListInReverse(head ImmutableListNode){
linkedListSize :=getLinkedListSize(head)
blockSize :=int(math.Ceil(math.Sqrt(float64(linkedListSize))))
blocks :=[]ImmutableListNode{}
curr := head
for i :=0; i < linkedListSize; i++{if i%blockSize ==0{
blocks =append(blocks, curr)}
curr = curr.GetNext()}forlen(blocks)>0{
block := blocks[len(blocks)-1]
blocks = blocks[:len(blocks)-1]printLinkedListInReverseRecursively(block, blockSize)}}
class Solution {privatefunprintLinkedListInReverseRecursively(head: ImmutableListNode?, size: Int){if(size >0&& head !=null){printLinkedListInReverseRecursively(head.getNext(), size -1)
head.printValue()}}privatefungetLinkedListSize(head: ImmutableListNode?): Int {var size =0var curr = head
while(curr !=null){
size++
curr = curr.getNext()}return size
}funprintLinkedListInReverse(head: ImmutableListNode?){val linkedListSize =getLinkedListSize(head)val blockSize = kotlin.math.ceil(kotlin.math.sqrt(linkedListSize.toDouble())).toInt()val blocks = java.util.Stack<ImmutableListNode>()var curr = head
for(i in0 until linkedListSize){if(i % blockSize ==0){
blocks.push(curr)}
curr = curr?.getNext()}while(!blocks.isEmpty()){printLinkedListInReverseRecursively(blocks.pop(), blockSize)}}}
classSolution{privatefuncprintLinkedListInReverseRecursively(_ head:ImmutableListNode?,_ size:Int){if size >0&& head !=nil{printLinkedListInReverseRecursively(head?.getNext(), size -1)
head?.printValue()}}privatefuncgetLinkedListSize(_ head:ImmutableListNode?)->Int{var size =0var curr = head
while curr !=nil{
size +=1
curr = curr?.getNext()}return size
}funcprintLinkedListInReverse(_ head:ImmutableListNode?){let linkedListSize =getLinkedListSize(head)let blockSize =Int(ceil(sqrt(Double(linkedListSize))))var blocks =[ImmutableListNode]()var curr = head
for i in0..<linkedListSize {if i % blockSize ==0{
blocks.append(curr!)}
curr = curr?.getNext()}while!blocks.isEmpty {printLinkedListInReverseRecursively(blocks.removeLast(), blockSize)}}}
Time & Space Complexity
Time complexity: O(n)
Space complexity: O(n)
Where n is the size of the linked list.
4. Divide and Conquer
Intuition
We can use the slow and fast pointer technique to find the middle of any segment. By recursively printing the second half first, then the first half, we achieve reverse order. The recursion depth is O(log n) because we halve the problem at each level.
Algorithm
Define a helper function that takes a start and end pointer (end is exclusive).
Base case: if start is null or equals end, return. If start.getNext() equals end, print start and return.
Use slow and fast pointers to find the midpoint between start and end.
Recursively process the second half (from slow to end).
Recursively process the first half (from start to slow).
classSolution{/**
* @param {ImmutableListNode} start
* @param {ImmutableListNode} end
* @return {void}
*/helper(start, end){if(start ===null|| start === end){return;}if(start.getNext()=== end){
start.printValue();return;}let slow = start;let fast = start;while(fast !== end && fast.getNext()!== end){
slow = slow.getNext();
fast = fast.getNext().getNext();}this.helper(slow, end);this.helper(start, slow);}/**
* @param {ImmutableListNode} head
* @return {void}
*/printLinkedListInReverse(head){this.helper(head,null);}}
publicclassSolution{privatevoidHelper(ImmutableListNode start,ImmutableListNode end){if(start ==null|| start == end){return;}if(start.GetNext()== end){
start.PrintValue();return;}ImmutableListNode slow = start;ImmutableListNode fast = start;while(fast != end && fast.GetNext()!= end){
slow = slow.GetNext();
fast = fast.GetNext().GetNext();}Helper(slow, end);Helper(start, slow);}publicvoidPrintLinkedListInReverse(ImmutableListNode head){Helper(head,null);}}
funchelper(start, end ImmutableListNode){if start ==nil|| start == end {return}if start.GetNext()== end {
start.PrintValue()return}
slow := start
fast := start
for fast != end && fast.GetNext()!= end {
slow = slow.GetNext()
fast = fast.GetNext().GetNext()}helper(slow, end)helper(start, slow)}funcprintLinkedListInReverse(head ImmutableListNode){helper(head,nil)}
class Solution {privatefunhelper(start: ImmutableListNode?, end: ImmutableListNode?){if(start ==null|| start == end){return}if(start.getNext()== end){
start.printValue()return}var slow: ImmutableListNode?= start
var fast: ImmutableListNode?= start
while(fast != end && fast?.getNext()!= end){
slow = slow?.getNext()
fast = fast?.getNext()?.getNext()}helper(slow, end)helper(start, slow)}funprintLinkedListInReverse(head: ImmutableListNode?){helper(head,null)}}
classSolution{privatefunchelper(_ start:ImmutableListNode?,_ end:ImmutableListNode?){if start ==nil|| start === end {return}if start?.getNext()=== end {
start?.printValue()return}var slow = start
var fast = start
while fast !== end && fast?.getNext()!== end {
slow = slow?.getNext()
fast = fast?.getNext()?.getNext()}helper(slow, end)helper(start, slow)}funcprintLinkedListInReverse(_ head:ImmutableListNode?){helper(head,nil)}}
Time & Space Complexity
Time complexity: O(n⋅logn)
Space complexity: O(logn)
Where n is the size of the linked list.
5. Constant Space
Intuition
Without any extra data structures, we can print in reverse by repeatedly finding the last unprinted node. We maintain an end pointer that marks the boundary. Each iteration traverses from head to find the node just before end, prints it, and moves end backward. This uses constant space but requires O(n) traversals.
Algorithm
Initialize end to null (representing the position after the last node).
While head is not equal to end:
Start from head and traverse until finding the node whose next pointer equals end.
classSolution:defprintLinkedListInReverse(self, head:'ImmutableListNode')->None:
end =Nonewhile head != end:
curr = head
while curr.getNext()!= end:
curr = curr.getNext()
curr.printValue()
end = curr
classSolution{publicvoidprintLinkedListInReverse(ImmutableListNode head){ImmutableListNode curr;ImmutableListNode end =null;while(head != end){
curr = head;while(curr.getNext()!= end){
curr = curr.getNext();}
curr.printValue();
end = curr;}}}
classSolution{public:voidprintLinkedListInReverse(ImmutableListNode* head){
ImmutableListNode* curr;
ImmutableListNode* end =NULL;while(head != end){
curr = head;while(curr->getNext()!= end){
curr = curr->getNext();}
curr->printValue();
end = curr;}}};
classSolution{/**
* @param {ImmutableListNode} head
* @return {void}
*/printLinkedListInReverse(head){let curr;let end =null;while(head !== end){
curr = head;while(curr.getNext()!== end){
curr = curr.getNext();}
curr.printValue();
end = curr;}}}
publicclassSolution{publicvoidPrintLinkedListInReverse(ImmutableListNode head){ImmutableListNode curr;ImmutableListNode end =null;while(head != end){
curr = head;while(curr.GetNext()!= end){
curr = curr.GetNext();}
curr.PrintValue();
end = curr;}}}
funcprintLinkedListInReverse(head ImmutableListNode){var end ImmutableListNode =nilfor head != end {
curr := head
for curr.GetNext()!= end {
curr = curr.GetNext()}
curr.PrintValue()
end = curr
}}
class Solution {funprintLinkedListInReverse(head: ImmutableListNode?){var end: ImmutableListNode?=nullvar h = head
while(h != end){var curr = h
while(curr?.getNext()!= end){
curr = curr?.getNext()}
curr?.printValue()
end = curr
}}}
classSolution{funcprintLinkedListInReverse(_ head:ImmutableListNode?){var end:ImmutableListNode?=nilvar h = head
while h !== end {var curr = h
while curr?.getNext()!== end {
curr = curr?.getNext()}
curr?.printValue()
end = curr
}}}
Time & Space Complexity
Time complexity: O(n2)
Space complexity: O(1)
Where n is the size of the linked list.
Common Pitfalls
Stack Overflow with Deep Recursion
The simple recursive solution uses O(n) stack space. For very long linked lists, this can cause a stack overflow. Consider the square root decomposition or divide-and-conquer approaches to reduce recursion depth to O(n) or O(logn) respectively.
Printing in Forward Order Instead of Reverse
A common mistake in the recursive approach is calling printValue() before the recursive call, which prints nodes in forward order. The key insight is that the recursive call must happen first, and printing happens after the call returns, so the deepest nodes print first.
Incorrect Block Size Calculation in Square Root Decomposition
When implementing square root decomposition, using integer division for the block size can lead to issues. Use ceil(sqrt(n)) to ensure all nodes are covered. Also, ensure the recursion within each block stops at the block boundary, not at the end of the list.