DSA sheet · Linked List · Reversal pattern
Palindrome Linked List
The second question of the reversal pattern. We must say whether a linked list reads the same forwards and backwards. The teacher builds the answer step by step: first the array solution with two pointers, then a brute force that copies the list into an array, and finally the optimal one that combines two patterns: fast & slow pointers to find the middle, and reversal to turn the second half around so that the "right pointer" can walk backwards. This "find middle + reverse second half + compare" recipe is reused straight away in the next problem (Maximum Twin Sum) and later in Reorder List.
Every part below follows the same order:
① the question in simple words → ② what the constraints tell us → ③ intuition → ④ building the logic from examples → ⑤ approach steps → ⑥ code → ⑦ code line by line → ⑧ dry run → ⑨ complexity & remember
- Part 0 · Linked lists, fast & slow pointers and reversal from scratch
- Part A · Brute force: copy into an array, then two pointers
- Part B · Optimal: middle → reverse second half → compare
- Part C · Revision page
Part 0 · What you must know before starting
Nodes, head and None
A linked list is a chain of nodes. Each node holds a value (val) and a link (next) to the following node. The last node's next is None. We are only given the first node, the head.
class ListNode:
def __init__(self, val=0, next=None):
self.val = val # the value in this node
self.next = next # the following node, or None at the end[1] → [2] → [3] → [2] → [1] → None head
To visit the nodes we use a walker: curr = head, then curr = curr.next again and again until curr is None. We walk with a copy so that head still marks the start.
There is no index access: to reach the i-th node you must follow i arrows from the head, which costs O(i). And the arrows only go forward. You can't step back from a node to the one before it. This one fact shapes the whole solution below.
Rule for changing arrows: save next first
A node's next is the only road to the rest of the list. If you overwrite it without saving it, everything after it is lost. So we always do nxt = curr.next before changing curr.next.
Tool 1: fast & slow pointers (finding the middle)
Two walkers start at head. slow moves 1 step at a time, fast moves 2 steps. When fast reaches the end, slow has covered half the distance, so it's standing in the middle. One pass, no counting.
slow = fast = head
while fast and fast.next: # fast can still take 2 steps
slow = slow.next # 1 step
fast = fast.next.next # 2 steps
# slow is now the middle- Odd length (5 nodes): fast lands on the last node,
fast.nextis None → stop. slow is on the exact middle (3rd node). - Even length (4 nodes): fast jumps past the end and becomes None → stop. slow is on the second of the two middle nodes (3rd node).
- Why both checks?
fastis None handles even length;fast.nextis None handles odd length. Checkingfastfirst also protectsfast.nextfrom crashing.
Tool 2: reversing a list with prev / curr / nxt (from the previous video)
prev = None
while curr:
nxt = curr.next # save the road ahead
curr.next = prev # flip the arrow backward
prev = curr # step prev forward
curr = nxt # step curr forward
# prev is the new head of the reversed pieceRemember one detail: the first node we reverse ends up pointing to None (because prev starts as None). We'll need that to know when to stop comparing.
What is a palindrome?
A sequence that reads the same from both ends: 1 2 3 2 1, 1 2 2 1, 7. The first value equals the last, the second equals the second-last, and so on. We compare the values in the nodes, not the node objects.
Part A · Brute force: copy into an array, then two pointers
LeetCode 234
1The question in simple words
Given the head of a singly linked list, return True if its values form a palindrome, else False.
[1] → [2] → [3] → [2] → [1] → None → True (odd length) [1] → [2] → [2] → [1] → None → True (even length) [1] → [2] → None → False
2What the constraints tell us
- Number of nodes: 1 to 10⁵ → the list is never empty, but it can be a single node (which is a palindrome).
- An O(n²) idea (for example, walking to the k-th node from the end for every k) would be 10⁵ × 10⁵ = 10¹⁰ steps → TLE. Roughly 10⁸ simple steps is the limit.
- Values are 0 to 9: small, nothing special.
- The teacher's usual reminder: in linked-list questions O(n²) is rare. Aim for linear time, and then try to bring the space down to O(1).
3Intuition: solve it on an array first
Forget the list. On an array [1, 2, 3, 2, 1] we'd use two pointers: left at the start, right at the end. Compare the two values. If they match, move left forward and right backward, and compare again. If any pair doesn't match, it can't be a palindrome, so stop at once.
[1, 2, 3, 2, 1]
L R 1 = 1 ✓ → move both
L R 2 = 2 ✓ → move both
LR left == right → stop → True
4Building the logic: when do the pointers stop?
The teacher looks at both lengths:
- Odd (5 values): after two moves left and right land on the same index (the middle). A value always equals itself, so stop.
- Even (4 values,
[1, 2, 2, 1]): L=0,R=3 → L=1,R=2 → next move gives L=2,R=1. They never meet. Left crosses right, and every pair has been checked.
Both cases are covered by one rule: keep going while left < right.
arr[left] != arr[right] at any point (say 4 against 1), return False right away. Checking the remaining pairs can't change the answer.But we have a linked list, not an array. The right pointer would have to step backwards, and the arrows don't allow that. So the brute force simply copies the values into an array first and then runs the same two-pointer check.
(In the video she calls the list "doubly linked" for a moment. That's a slip: it's a singly linked list, and that's exactly why we can't walk backwards.)
5Approach steps
- Walk the list and append every value to
vals. - Set
i = 0,j = len(vals) - 1. - While
i < j: ifvals[i] != vals[j]return False; elsei += 1,j -= 1. - If the loop finishes without a mismatch, return True.
6Code (Python)
class Solution:
def isPalindrome(self, head):
vals = []
curr = head
while curr: # copy the values out
vals.append(curr.val)
curr = curr.next
i, j = 0, len(vals) - 1 # two pointers on the array
while i < j: # stop when they meet (odd) or cross (even)
if vals[i] != vals[j]:
return False # one mismatch is enough
i += 1
j -= 1
return True7Code line by line
| line | what it means |
|---|---|
| vals = [] | The extra array. This costs O(n) space. |
| while curr: vals.append(curr.val) curr = curr.next | One pass over the list, copying each value. |
| i, j = 0, len(vals) - 1 | Left pointer on the first index, right pointer on the last index (length − 1). |
| while i < j: | Odd length ends with i == j; even length ends with i > j. Either way, all pairs are checked. |
| if vals[i] != vals[j]: return False | A pair doesn't match → not a palindrome. Stop now. |
| i += 1 j -= 1 | Move both pointers one step towards the middle, in the same round. |
| return True | Reached only if no pair failed. |
8Dry run
| step | vals | i, j | compare | result |
|---|---|---|---|---|
| 0 | [1, 2, 3, 2, 1] | – | copied from the list | |
| 1 | 0, 4 | 1 vs 1 | equal → move | |
| 2 | 1, 3 | 2 vs 2 | equal → move | |
| 3 | 2, 2 | i < j is false | stop → True |
With [1, 2, 3, 4, 1]: step 1 compares 1 vs 1 ✓, step 2 compares 2 vs 4 → False, and we never look at the middle.
9Complexity & remember
- Time O(n) + O(n/2) = O(3n/2): n steps to copy, then n/2 rounds of comparing, because both pointers move in the same round.
- Space O(n) for the array.
while i < j. Return False on the first mismatch. O(n) extra space is the weakness.Part B · Optimal: middle → reverse second half → compare
1The question (same), with a new goal
Same input and output, but now with O(1) extra space: no array.
2What the constraints tell us
- At least 1 node →
headis never None, soslow = fast = headis safe. - Up to 10⁵ nodes → a few linear passes are fine. O(n²) is not.
3Intuition: make the right pointer able to walk backwards
The teacher goes through the patterns one by one to pick a tool:
- Basic operations (insert, delete, search) → we aren't changing the list's content, so no.
- Fast & slow → used to find the middle or detect a cycle. Not the main idea… but keep it in mind.
- Reversal → here's the key. The two-pointer idea failed only because the right pointer can't go backwards. But if the arrows of the second half pointed backwards, a walker starting at the last node could move towards the middle using plain
.next. So we only need to reverse half the list. - And how do we find where the half starts? That's exactly what fast & slow is for. So both patterns work together.
before: [1] → [2] → [3] → [2] → [1] → None
second half reversed:[1] → [2] → [3] ← [2] ← [1]
head1 head2
Now head1 walks right and head2 walks left, both with .next
4Building the logic step by step
Step 1: find the middle with fast & slow
Odd example 1 2 3 2 1:
start [1] → [2] → [3] → [2] → [1] → None slow/fast move 1 [1] → [2] → [3] → [2] → [1] → None slow fast move 2 [1] → [2] → [3] → [2] → [1] → None slow fast stop: fast.next is None → middle = slow = [3] (position 2)
Even example 1 2 2 1:
start [1] → [2] → [2] → [1] → None slow/fast move 1 [1] → [2] → [2] → [1] → None slow fast move 2 [1] → [2] → [2] → [1] → None slow fast stop: fast is None → middle = slow = [2] (position 2)
In both cases slow ends on the node where the second half starts (for odd length, the middle node itself).
Step 2: reverse from slow to the end
Keep head where it is: if we moved it, we'd lose the first half. Reverse the piece that starts at slow, using the four lines from the previous video. For the odd example, the piece is [3] → [2] → [1]:
start: prev = None, curr = slow None [3] → [2] → [1] → None prev curr round 1: nxt = curr.next, curr.next = prev (red), then move None ← [3] [2] → [1] → None prev curr nxt prev curr ← after moving round 2: nxt = curr.next, curr.next = prev (red), then move None ← [3] ← [2] [1] → None prev curr nxt prev curr ← after moving round 3: nxt = curr.next, curr.next = prev (red), then move None ← [3] ← [2] ← [1] None prev curr nxt prev curr ← after moving
Now prev is on the last node, the head of the reversed half. The teacher calls it head2, and the original head head1. The whole picture:
[1] → [2] → [3] ← [2] ← [1]
head1 ↓ head2 (= prev)
None
head1's walk: 1, 2, 3, then None head2's walk: 1, 2, 3, then None
→ Nothing was lost. The first half still ends at [3], because we never touched [2]'s arrow. And [3] was the first node we reversed, so it now points to None (prev started as None). Both walks end at [3] and then None. The middle node simply belongs to both halves, and comparing it with itself is harmless.
→ It depends on where you start reversing. Her drawing reverses only the nodes after the middle. Her code (and ours) reverses from
slow itself, so the middle node is shared. Both work: in the odd case head2's piece is never longer than head1's, and in the even case the pieces are equal. That's why we loop on head2.Step 3: compare, walking both pointers forward
- Loop while head2 is not None. Head2's piece is the shorter one (or equal), so it decides when we're done. It ends in None because of the reversal.
- If
head1.val != head2.val→ return False immediately. - Else move both:
head1 = head1.next,head2 = head2.next. - If the loop ends without a mismatch → return True.
5Approach steps
slow = fast = head. Whilefast and fast.next: slow 1 step, fast 2 steps.- Reverse the list starting at
slowwith prev / curr / nxt. The new head isprev. left = head,right = prev.- While
right: if the values differ → False; else move both one step. - Return True.
6Code (Python)
class Solution:
def isPalindrome(self, head):
# 1. find the middle
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
# 2. reverse the second half (starting at slow)
prev = None
curr = slow
while curr:
nxt = curr.next
curr.next = prev
prev = curr
curr = nxt
# 3. compare the first half with the reversed second half
left, right = head, prev # head1 and head2
while right:
if left.val != right.val:
return False
left = left.next
right = right.next
return True7Code line by line
| line | what it means |
|---|---|
| slow = fast = head | Both walkers start at the first node. Safe, because the list has at least 1 node. |
| while fast and fast.next: | Stop when fast falls off (even length) or sits on the last node (odd length). |
| slow = slow.next fast = fast.next.next | Slow moves 1, fast moves 2. When the loop ends, slow is at the start of the second half. |
| prev = None curr = slow | Set up the reversal of the piece from slow to the end. The first reversed node will point to None. |
| nxt = curr.next … curr = nxt | The standard four lines: save, flip, move prev, move curr. |
| left, right = head, prev | head1 at the very start, head2 at the old last node (start of the reversed half). |
| while right: | head2's piece is the shorter one or equal, so it controls the loop. |
| if left.val != right.val: return False | A matching pair from the two ends is different → not a palindrome. |
| left = left.next right = right.next | Left moves towards the middle from the front; right moves towards the middle from the back (thanks to the reversed arrows). |
| return True | All pairs matched. |
8Dry runs
Odd: 1 → 2 → 3 → 2 → 1
After steps 1 and 2 (drawn above): head1 = [1] (first), head2 = [1] (last).
| round | pointers | compare | what changes | answer so far |
|---|---|---|---|---|
| 1 | left = 1st node, right = 5th node | 1 vs 1 | both move | still True |
| 2 | left = 2nd node, right = 4th node | 2 vs 2 | both move | still True |
| 3 | left = 3rd node, right = 3rd node (the same node) | 3 vs 3 | left → None, right → None | still True |
| 4 | right is None | – | loop ends | return True |
Only 3 rounds for 5 nodes, which matches the teacher's count.
Even: 1 → 2 → 2 → 1
slow ends on the 3rd node (the second 2). Reversing [2] → [1] gives [1] → [2] → None:
[1] → [2] → [2] ← [1]
head1 ↓ head2
None
head1's walk: 1, 2, 2 head2's walk: 1, 2
| round | pointers | compare | what changes | answer so far |
|---|---|---|---|---|
| 1 | left = 1st, right = 4th | 1 vs 1 | both move | still True |
| 2 | left = 2nd, right = 3rd | 2 vs 2 | right → None | still True |
| 3 | right is None | – | loop ends | return True |
Here head1's piece is longer (it still reaches the second 2), and that's fine: we stop on head2.
Not a palindrome: 1 → 2 → 3 → 4 → 1
slow ends on [3]. Reversing [3] → [4] → [1] gives head2 = [1] → [4] → [3] → None. Round 1: 1 vs 1 ✓. Round 2: 2 vs 4 → return False.
→ LeetCode doesn't check, so the teacher leaves it. In an interview, it's good manners to mention it and, if asked, reverse the second half again (
reverse(prev)) before returning, to put the list back. That costs one more n/2 pass and keeps O(1) space. (This point is mine, not from the video.)9Complexity & remember
- Finding the middle: O(n/2), because fast moves 2 steps per round.
- Reversing the second half: O(n/2), the normal reversal is O(n) but here it only touches half the list.
- Comparing: O(n/2), both pointers move in the same round. For 5 nodes, only 3 rounds.
- Total O(3n/2) = O(n) time, O(1) space. We used a few pointers and no array.
The teacher says the brute force was "2n" when she compares at the end, but by her own counting earlier it's also n + n/2 = 3n/2. So the time is about the same. The real win is space: O(n) → O(1). On LeetCode the optimal version also ran faster, since it doesn't build an array.
Part C · Revision page
| Brute force | Optimal | |
|---|---|---|
| idea | copy to array, two pointers from both ends | reverse the second half so the right pointer can move with .next |
| patterns used | two pointers on an array | fast & slow + reversal + two pointers |
| stop condition | while i < j | while right (the reversed half ends in None) |
| changes the list? | no | yes (second half reversed; can be undone) |
| time | O(n + n/2) | O(n/2 + n/2 + n/2) |
| space | O(n) | O(1) |
| length | where slow stops | head1's walk | head2's walk |
|---|---|---|---|
| odd (1 2 3 2 1) | exact middle [3] | 1, 2, 3 | 1, 2, 3 |
| even (1 2 2 1) | second middle (the second 2) | 1, 2, 2 | 1, 2 |
2. Array: two pointers,
while i < j, return False on the first mismatch.3. A list can't walk backwards, so reverse the second half.
4. Find that half with slow (1 step) / fast (2 steps),
while fast and fast.next.5. Compare head vs prev while prev isn't None. O(n) time, O(1) space.
head while finding the middle (you lose the first half)✗
while fast.next and fast (checks fast.next before fast → crash on even length)✗ looping the comparison on head1 instead of head2 (for even length head1's walk is one node longer, so head2 becomes None first and
right.val crashes)✗ comparing nodes (
left != right) instead of values (left.val != right.val)✗ forgetting the early
return False and checking everything anywayclass ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
def build(values):
dummy = ListNode()
tail = dummy
for v in values:
tail.next = ListNode(v)
tail = tail.next
return dummy.next
s = Solution()
print(s.isPalindrome(build([1, 2, 3, 2, 1]))) # True
print(s.isPalindrome(build([1, 2, 2, 1]))) # True
print(s.isPalindrome(build([1, 2]))) # False
print(s.isPalindrome(build([7]))) # True
print(s.isPalindrome(build([1, 2, 3, 4, 1]))) # FalseBased on this video: Palindrome Linked List | Reversal pattern