Skip to content
Reliable Data Engineering
Practice problem easy fast-slow-pointerslinked-listreversal
Practise with timer, notes and rubric

Palindrome Linked List in O(1) Space

Pattern: Fast & Slow Pointers (Floyd’s Cycle Detection) · Difficulty: Easy · Asked at: Meta, Amazon, Microsoft

Classic version: LeetCode 234

Problem

Return True if the values of a singly linked list read the same forwards and backwards. Use O(1) extra space, and leave the list unchanged when you return (callers may reuse it).

Examples

1 → 2 → 2 → 1   → True
1 → 2           → False

Starter code

class ListNode:
    def __init__(self, val, next=None):
        self.val, self.next = val, next


def is_palindrome_list(head: ListNode | None) -> bool:
    pass

Hints

Hint 1

When fast (2 steps) reaches the end, slow (1 step) is at the middle.

Hint 2

Reverse the list from the middle, compare the two halves node by node, then reverse the second half back.

Where this shows up in data engineering

Finding the middle in one pass with two speeds is the same idea as sampling the median position of a stream you can only read once with two cursors, and list reversal is an interview staple for pointer hygiene.

Solution

class ListNode:
    def __init__(self, val, next=None):
        self.val, self.next = val, next


def is_palindrome_list(head):
    def reverse(node):
        prev = None
        while node:
            node.next, prev, node = prev, node, node.next
        return prev

    slow = fast = head
    while fast and fast.next:
        slow, fast = slow.next, fast.next.next
    tail = reverse(slow)                 # second half, reversed
    ok, a, b = True, head, tail
    while b:
        if a.val != b.val:
            ok = False
            break
        a, b = a.next, b.next
    reverse(tail)                        # restore the original list
    return ok

Tests

Your solution should pass these:

class ListNode:
    def __init__(self, val, next=None):
        self.val, self.next = val, next

def build(values, cycle_at=-1):
    """Linked list from values; tail links to node `cycle_at` (-1 = no cycle)."""
    nodes = [ListNode(v) for v in values]
    for a, b in zip(nodes, nodes[1:]):
        a.next = b
    if nodes and cycle_at >= 0:
        nodes[-1].next = nodes[cycle_at]
    return (nodes[0] if nodes else None), nodes


def values(h):
    out = []
    while h:
        out.append(h.val)
        h = h.next
    return out

h, _ = build([1, 2, 2, 1])
assert is_palindrome_list(h) is True and values(h) == [1, 2, 2, 1]
h, _ = build([1, 2])
assert is_palindrome_list(h) is False and values(h) == [1, 2]
h, _ = build([1, 2, 3, 2, 1])
assert is_palindrome_list(h) is True and values(h) == [1, 2, 3, 2, 1]
h, _ = build([7])
assert is_palindrome_list(h) is True
assert is_palindrome_list(None) is True

Explanation

Steps: (1) fast/slow to the middle, (2) reverse from slow, (3) compare head-half with reversed tail-half (the tail is equal or one shorter, so loop on b), (4) reverse again to restore.

Odd lengths: the middle node ends up as the last node of both halves’ traversal; comparing it with itself is harmless.

Complexity: O(n) time, O(1) space. Copying values to a list and checking vals == vals[::-1] is O(n) space: fine first answer.

Pointer hygiene: the tuple assignment node.next, prev, node = prev, node, node.next evaluates the right side first, so it’s safe. Interviewers watch for lost references here.

Follow-up questions

Why restore the list?

Mutating an input that callers still hold is a side effect; in production code (or concurrent readers) it’s a bug. State the trade-off explicitly: O(1) space costs temporary mutation.