Facebook Pixel

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:

  1. 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.

  2. Reverse the second half of the linked list starting from the node after the slow pointer.

  3. 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.

  4. Return true if all values match, false if any mismatch is found.

For example:

  • Input: 1 -> 2 -> 3 -> 2 -> 1 returns true
  • Input: 1 -> 2 -> 3 -> 4 returns false
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. 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.

  2. 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.

  3. 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 (initially None)
  • cur: the current node being processed
  • t: temporarily stores the next node

The reversal works by:

  1. Saving the next node in t
  2. Pointing current node's next to the previous node
  3. Moving pre and cur 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 beginning
  • pre 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 Evaluator

Example 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 2
  • fast moves to node 3 (one step) then node 2 (second step)

Iteration 2:

  • slow moves to node 3
  • fast 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:

  1. 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.
  2. Reversing the second half: The second half of the list is reversed by iterating through approximately n/2 nodes. This takes O(n/2) time.
  3. Comparing both halves: The comparison loop iterates through at most n/2 nodes to check if values match. This takes O(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 (with 3.next still pointing to the old 2)

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.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

How does merge sort divide the problem into subproblems?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More