234. Palindrome Linked List
Problem Description
You are given the head of a singly linked list. Your task is to determine whether the linked list forms a palindrome when reading the values from start to end.
A palindrome reads the same forwards and backwards. For example, a linked list with values 1 -> 2 -> 2 -> 1
is a palindrome because reading from left to right gives [1, 2, 2, 1]
and reading from right to left also gives [1, 2, 2, 1]
.
The function should return true
if the linked list is a palindrome, and false
otherwise.
The solution uses a two-pointer technique to find the middle of the linked list. Once the middle is found, the second half of the list is reversed in-place. Then, both halves are compared node by node. The algorithm works as follows:
-
Use slow and fast pointers to locate the middle of the linked list. The slow pointer moves one step at a time while the fast pointer moves two steps.
-
Reverse the second half of the linked list starting from the node after the slow pointer.
-
Compare the values of nodes from the first half with the reversed second half. If all corresponding values match, the linked list is a palindrome.
-
Return
true
if all values match,false
if any mismatch is found.
For example:
- Input:
1 -> 2 -> 3 -> 2 -> 1
returnstrue
- Input:
1 -> 2 -> 3 -> 4
returnsfalse
Intuition
To check if a linked list is a palindrome, the straightforward approach would be to store all values in an array and check if the array reads the same forwards and backwards. However, this requires O(n)
extra space.
The key insight is that we only need to compare the first half with the second half. If we can somehow read the second half in reverse order, we can compare it with the first half directly.
Since we can't traverse a singly linked list backwards, we need to physically reverse the second half. This leads us to three main challenges:
-
Finding the middle point: We need to split the list into two halves. The fast and slow pointer technique elegantly solves this - when the fast pointer reaches the end (moving twice as fast), the slow pointer will be at the middle.
-
Reversing the second half: Once we find the middle, we can reverse the links in the second half. This is a standard linked list operation where we iterate through nodes and reverse their
next
pointers. -
Comparing both halves: After reversing, we can traverse both halves simultaneously from their respective starts, comparing values at each step.
The beauty of this approach is that it achieves O(1)
space complexity by modifying the linked list structure temporarily. We're essentially transforming the problem from "check if a sequence is a palindrome" to "check if two sequences are identical" by cleverly manipulating the list structure itself.
This technique of using the list's own structure to solve the problem without extra space is a common pattern in linked list problems where we want to optimize space complexity.
Solution Approach
The solution implements the fast and slow pointer technique to find the middle of the linked list, then reverses the second half for comparison.
Step 1: Find the Middle Node
slow, fast = head, head.next
while fast and fast.next:
slow, fast = slow.next, fast.next.next
We initialize slow
at the head and fast
at the second node. The fast
pointer moves two steps while slow
moves one step. When fast
reaches the end, slow
will be at the middle. For even-length lists, slow
stops at the last node of the first half. For odd-length lists, slow
stops at the middle node.
Step 2: Reverse the Second Half
pre, cur = None, slow.next
while cur:
t = cur.next
cur.next = pre
pre, cur = cur, t
Starting from slow.next
(the beginning of the second half), we reverse the linked list using three pointers:
pre
: tracks the previous node (initiallyNone
)cur
: the current node being processedt
: temporarily stores the next node
The reversal works by:
- Saving the next node in
t
- Pointing current node's
next
to the previous node - Moving
pre
andcur
forward
After this loop, pre
points to the head of the reversed second half.
Step 3: Compare Both Halves
while pre:
if pre.val != head.val:
return False
pre, head = pre.next, head.next
return True
We traverse both halves simultaneously:
head
traverses the first half from the beginningpre
traverses the reversed second half
If any values don't match, we return False
. If we complete the traversal without mismatches, the list is a palindrome.
Time Complexity: O(n)
- we traverse the list twice (once to find middle and reverse, once to compare)
Space Complexity: O(1)
- only using a constant number of pointers, no additional data structures
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through the solution with the linked list: 1 -> 2 -> 3 -> 2 -> 1
Initial State:
1 -> 2 -> 3 -> 2 -> 1 -> null
Step 1: Find the Middle
Initialize pointers:
slow
points to node 1 (head)fast
points to node 2 (head.next)
Iteration 1:
slow
moves to node 2fast
moves to node 3 (one step) then node 2 (second step)
Iteration 2:
slow
moves to node 3fast
moves to node 1 (one step) then null (second step)- Loop ends since
fast.next
is null
After finding middle:
1 -> 2 -> 3 -> 2 -> 1 -> null ^ slow
Step 2: Reverse Second Half
Start reversing from slow.next
(node 2):
Initial: pre = null
, cur = node 2
Iteration 1:
- Save
t = node 1
- Set
node 2.next = null
- Move:
pre = node 2
,cur = node 1
Iteration 2:
- Save
t = null
- Set
node 1.next = node 2
- Move:
pre = node 1
,cur = null
- Loop ends
After reversal:
First half: 1 -> 2 -> 3 ↓ null Second half: 1 -> 2 -> null ^ pre
Step 3: Compare Both Halves
Start comparison with head
at node 1 (first half) and pre
at node 1 (reversed second half):
Iteration 1:
- Compare: node 1.val (1) == node 1.val (1) ✓
- Move both pointers forward
Iteration 2:
- Compare: node 2.val (2) == node 2.val (2) ✓
- Move both pointers forward
Iteration 3:
pre
becomes null- Loop ends
All values matched, so return true
- the linked list is a palindrome!
Solution Implementation
1# Definition for singly-linked list.
2# class ListNode:
3# def __init__(self, val=0, next=None):
4# self.val = val
5# self.next = next
6
7class Solution:
8 def isPalindrome(self, head: Optional[ListNode]) -> bool:
9 # Find the middle of the linked list using slow and fast pointers
10 slow = head
11 fast = head.next
12
13 # Move slow pointer one step and fast pointer two steps each iteration
14 # When fast reaches the end, slow will be at the middle
15 while fast and fast.next:
16 slow = slow.next
17 fast = fast.next.next
18
19 # Reverse the second half of the linked list
20 # Initialize pointers for reversal
21 previous = None
22 current = slow.next
23
24 # Reverse the second half by changing the direction of pointers
25 while current:
26 temp = current.next # Store next node
27 current.next = previous # Reverse the pointer
28 previous = current # Move previous forward
29 current = temp # Move current forward
30
31 # Compare the first half with the reversed second half
32 # previous now points to the head of reversed second half
33 while previous:
34 if previous.val != head.val:
35 return False
36 previous = previous.next
37 head = head.next
38
39 return True
40
1/**
2 * Definition for singly-linked list.
3 * public class ListNode {
4 * int val;
5 * ListNode next;
6 * ListNode() {}
7 * ListNode(int val) { this.val = val; }
8 * ListNode(int val, ListNode next) { this.val = val; this.next = next; }
9 * }
10 */
11class Solution {
12 public boolean isPalindrome(ListNode head) {
13 // Use two pointers to find the middle of the linked list
14 ListNode slow = head;
15 ListNode fast = head.next;
16
17 // Move slow pointer one step and fast pointer two steps each iteration
18 // When fast reaches the end, slow will be at the middle
19 while (fast != null && fast.next != null) {
20 slow = slow.next;
21 fast = fast.next.next;
22 }
23
24 // Split the list into two halves
25 // The second half starts from slow.next
26 ListNode current = slow.next;
27 slow.next = null; // Disconnect the first half from the second half
28
29 // Reverse the second half of the linked list
30 ListNode previous = null;
31 while (current != null) {
32 ListNode temp = current.next; // Store the next node
33 current.next = previous; // Reverse the pointer
34 previous = current; // Move previous forward
35 current = temp; // Move to the next node
36 }
37
38 // Compare the first half with the reversed second half
39 // previous now points to the head of the reversed second half
40 while (previous != null) {
41 if (previous.val != head.val) {
42 return false; // Values don't match, not a palindrome
43 }
44 previous = previous.next;
45 head = head.next;
46 }
47
48 return true; // All values matched, it's a palindrome
49 }
50}
51
1/**
2 * Definition for singly-linked list.
3 * struct ListNode {
4 * int val;
5 * ListNode *next;
6 * ListNode() : val(0), next(nullptr) {}
7 * ListNode(int x) : val(x), next(nullptr) {}
8 * ListNode(int x, ListNode *next) : val(x), next(next) {}
9 * };
10 */
11class Solution {
12public:
13 bool isPalindrome(ListNode* head) {
14 // Find the middle of the linked list using two pointers
15 // slow moves one step, fast moves two steps
16 ListNode* slowPtr = head;
17 ListNode* fastPtr = head->next;
18
19 while (fastPtr != nullptr && fastPtr->next != nullptr) {
20 slowPtr = slowPtr->next;
21 fastPtr = fastPtr->next->next;
22 }
23
24 // Reverse the second half of the linked list
25 // slowPtr->next points to the start of second half
26 ListNode* prev = nullptr;
27 ListNode* curr = slowPtr->next;
28
29 while (curr != nullptr) {
30 ListNode* nextTemp = curr->next; // Store next node
31 curr->next = prev; // Reverse the link
32 prev = curr; // Move prev forward
33 curr = nextTemp; // Move curr forward
34 }
35
36 // Compare the first half and reversed second half
37 // prev now points to the head of reversed second half
38 while (prev != nullptr) {
39 if (prev->val != head->val) {
40 return false; // Values don't match, not a palindrome
41 }
42 prev = prev->next;
43 head = head->next;
44 }
45
46 return true; // All values matched, it's a palindrome
47 }
48};
49
1/**
2 * Definition for singly-linked list.
3 * class ListNode {
4 * val: number
5 * next: ListNode | null
6 * constructor(val?: number, next?: ListNode | null) {
7 * this.val = (val===undefined ? 0 : val)
8 * this.next = (next===undefined ? null : next)
9 * }
10 * }
11 */
12
13/**
14 * Checks if a linked list is a palindrome
15 * @param head - The head of the linked list
16 * @returns true if the linked list is a palindrome, false otherwise
17 */
18function isPalindrome(head: ListNode | null): boolean {
19 // Handle edge case of empty list
20 if (!head) {
21 return true;
22 }
23
24 // Step 1: Find the middle of the linked list using two pointers
25 // Slow pointer moves one step, fast pointer moves two steps
26 let slowPointer: ListNode = head;
27 let fastPointer: ListNode | null = head.next;
28
29 while (fastPointer !== null && fastPointer.next !== null) {
30 slowPointer = slowPointer.next!;
31 fastPointer = fastPointer.next.next;
32 }
33
34 // Step 2: Reverse the second half of the linked list
35 // Start from the node after the middle point
36 let currentNode: ListNode | null = slowPointer.next;
37 slowPointer.next = null; // Disconnect first half from second half
38
39 let previousNode: ListNode | null = null;
40
41 // Reverse the second half by changing next pointers
42 while (currentNode !== null) {
43 const nextTemp: ListNode | null = currentNode.next;
44 currentNode.next = previousNode;
45 previousNode = currentNode;
46 currentNode = nextTemp;
47 }
48
49 // Step 3: Compare the first half with the reversed second half
50 // previousNode now points to the head of the reversed second half
51 let firstHalfPointer: ListNode | null = head;
52 let secondHalfPointer: ListNode | null = previousNode;
53
54 while (secondHalfPointer !== null) {
55 if (secondHalfPointer.val !== firstHalfPointer!.val) {
56 return false;
57 }
58 secondHalfPointer = secondHalfPointer.next;
59 firstHalfPointer = firstHalfPointer!.next;
60 }
61
62 return true;
63}
64
Time and Space Complexity
Time Complexity: O(n)
, where n
is the length of the linked list.
The algorithm consists of three main phases:
- Finding the middle of the linked list: The two-pointer technique (slow and fast pointers) traverses approximately half the list with the fast pointer and the full first half with the slow pointer. This takes
O(n/2)
time. - Reversing the second half: The second half of the list is reversed by iterating through approximately
n/2
nodes. This takesO(n/2)
time. - Comparing both halves: The comparison loop iterates through at most
n/2
nodes to check if values match. This takesO(n/2)
time.
Total time complexity: O(n/2) + O(n/2) + O(n/2) = O(3n/2) = O(n)
Space Complexity: O(1)
The algorithm only uses a constant amount of extra space:
- A few pointer variables (
slow
,fast
,pre
,cur
,t
) are used regardless of the input size - The linked list is modified in-place by reversing the second half
- No additional data structures (arrays, stacks, or new nodes) are created
The space usage remains constant regardless of the size of the input linked list, resulting in O(1)
space complexity.
Common Pitfalls
1. Incorrect Fast Pointer Initialization
One of the most common mistakes is initializing both slow
and fast
pointers at head
instead of setting fast = head.next
. This causes the slow pointer to end up one position too far, leading to incorrect palindrome detection.
Incorrect approach:
slow = head
fast = head # Wrong! Should be head.next
while fast and fast.next:
slow = slow.next
fast = fast.next.next
Why this fails: When both pointers start at the same position, for even-length lists, the slow pointer will be at the first node of the second half instead of the last node of the first half. This causes misalignment during comparison.
Example where it fails:
- List:
1 -> 2 -> 2 -> 1
- With incorrect initialization: slow ends at the second
2
, causing comparison misalignment - With correct initialization: slow ends at the first
2
, properly dividing the list
Solution: Always initialize fast = head.next
to ensure proper middle detection:
slow = head
fast = head.next # Correct initialization
2. Not Handling Edge Cases
The code may fail for edge cases like:
- Single node lists (e.g.,
1
) - Two-node lists (e.g.,
1 -> 1
) - Empty lists (though typically the problem guarantees non-empty lists)
Potential issue: If head.next
is None
(single node), fast = head.next
would be None
, and the while loop wouldn't execute, leaving slow.next
as None
, which could cause issues.
Solution: Add edge case handling:
def isPalindrome(self, head: Optional[ListNode]) -> bool:
# Handle single node case
if not head or not head.next:
return True
# Continue with the regular algorithm...
3. Modifying the Original List Structure
The algorithm reverses the second half of the list in-place, which permanently modifies the original linked list structure. This might be problematic if the list needs to be preserved for other operations.
Impact: After checking for palindrome, the list structure is broken:
- Original:
1 -> 2 -> 3 -> 2 -> 1
- After algorithm:
1 -> 2 -> 3 <- 2 <- 1
(with3.next
still pointing to the old2
)
Solution: If preserving the original structure is required, reverse the second half back after comparison:
def isPalindrome(self, head: Optional[ListNode]) -> bool:
# ... existing code to find middle and reverse ...
# Store the head of reversed second half
reversed_head = previous
# Compare the halves
first_half = head
second_half = reversed_head
result = True
while second_half:
if first_half.val != second_half.val:
result = False
break
first_half = first_half.next
second_half = second_half.next
# Restore the original list structure
previous = None
current = reversed_head
while current:
temp = current.next
current.next = previous
previous = current
current = temp
# Reconnect the two halves
slow.next = previous
return result
4. Comparison Loop Boundary Issues
A subtle issue can occur if the comparison loop doesn't handle odd-length lists correctly. For odd-length lists, the middle element doesn't need to be compared with itself.
Example: For list 1 -> 2 -> 3 -> 2 -> 1
:
- First half:
1 -> 2 -> 3
- Second half (reversed):
1 -> 2
- The middle element
3
shouldn't affect palindrome check
The current solution handles this correctly by only iterating while previous
exists (the reversed second half is shorter for odd-length lists), but developers might mistakenly try to compare equal-length segments.
How does merge sort divide the problem into subproblems?
Recommended Readings
Stack Intro Imagine you have a pile of books on your desk If you want to add a new book you place it on top If you want to read a book you take it from the top And if you simply want to see which book is on the top you
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Linked List Cycle Given a linked list with potentially a loop determine whether the linked list from the first node contains a cycle in it For bonus points do this with constant space Parameters nodes The first node of a linked list with potentially a loop Result Whether there is a loop contained
Want a Structured Path to Master System Design Too? Don’t Miss This!