DSA sheet · Linked List · Merge & sort pattern

Partition List

This problem sits in the merge & sort part of the linked list sheet, and the teacher calls it a very common interview question. We must rearrange a list so that all the small values come first, while keeping the original order inside each group. She first solves it the "array way" (copy values out, arrange them, write them back), then shows the proper linked list way: split the nodes into two chains with two dummy nodes, then join the chains. The second idea, "build two lists side by side and glue them", shows up again and again in linked list problems.

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 · Before starting

What is a linked list node?

A linked list is a chain of small boxes called nodes. Each node holds two things: a value (val) and a link to the next box (next). The last node's next is None, which means "the chain ends here". The first node is called the head. If we only have the head, we can still reach every node by following the links.

given by LeetCode, don't write this in the solution
class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val      # the number stored in this box
        self.next = next    # the next box, or None at the end
head
 ↓
[1] → [4] → [3] → [2] → [5] → [2] → None

Walking through a list

We keep a pointer (a variable that refers to a node), usually called curr. We start at the head and move with curr = curr.next until curr becomes None.

the basic walk
curr = head
while curr is not None:
    print(curr.val)      # do something with this node
    curr = curr.next     # step to the next box

No index access. In an array, arr[4] jumps straight to position 4. A linked list has no such jump. To reach position i you must walk i steps from the head, which costs O(n). That's why linked list solutions are built around walking forward once.

Save next before you change a pointer

A node's next is the only road to the rest of the list. If you overwrite node.next and haven't kept the old value somewhere, the rest of the chain is lost. In this problem we stay safe in a different way: we change small.next or large.next (the end of a chain we are building), and we keep our walking pointer curr moving along the original links.

The dummy node

A dummy node is a fake node, for example ListNode(0), that we put in front of a list we are building. Its value never matters. Why use it?

The tail pointer (and why we need a second name for it)

To add nodes at the end of a chain quickly, we keep a pointer to the last node of that chain, called the tail. To add a node: tail.next = node, then tail = tail.next. If we used the dummy itself as the moving pointer, it would drift to the end and we would lose the start of the chain. So we keep two names: the dummy stays still (it remembers the start), and a copy of it moves (it remembers the end).

[0] → [1] → [2] → [2]
 ↑                 ↑
small_dummy       small     (dummy stays, small moves)

Part A · Brute force: copy the values, arrange, write back

LeetCode 86 · Partition List

1The question in simple words

You get the head of a linked list and a number x. Rearrange it so that every node with value less than x comes before every node with value greater than or equal to x. Inside each group, the nodes must stay in the same order as in the original list.

input   x = 3
[1] → [4] → [3] → [2] → [5] → [2] → None

small ( < 3 ):  1, 2, 2        (in original order)
large (>= 3 ):  4, 3, 5        (in original order)

output
[1] → [2] → [2] → [4] → [3] → [5] → None

The teacher goes through it node by node: 1 is smaller than 3 → left side. 4 is bigger → right side. 3 is equal to x → it counts as "greater than or equal", so right side. 2 → left. 5 → right. 2 → left.

Doubt: the large group is 4, 3, 5. Shouldn't it be sorted to 3, 4, 5?
→ No. This is a partition, not a sort. We only move each node to the correct side. Inside a side, the original order is kept: 4 came before 3 in the input, so 4 stays before 3.

2What the constraints tell us

3Intuition: pretend it's an array

Forget the links for a moment and picture the values in a normal Python list: [1, 4, 3, 2, 5, 2]. Arranging them is easy. The teacher gives two ways:

Both keep the original order inside each group, because we read the values left to right. Once we have the arranged values, we walk the linked list one more time and overwrite each node's value with the next value from our arranged list.

4Building the logic

The linked list part is just "read the values" and "write the values back":

  1. Read: walk with curr, and collect values that are < x.
  2. Read again: walk from the head again, and collect values that are ≥ x.
  3. Write: walk from the head a third time with an index i. Set curr.val = values[i], then move both.
Doubt: does it matter whether I use Way 1 or Way 2?
→ Not for complexity. Way 2 keeps one list of n values. Way 1 keeps two lists of about n/2 each. Either way the extra space is O(n). The teacher picks Way 2 for the code.
Doubt: we didn't move any node. Is overwriting values allowed?
→ LeetCode accepts it, because the output values are right. But it isn't the "linked list way" an interviewer wants. Linked list questions are about re-linking nodes without an extra array (an in-place solution). That's what Part B does.

5Approach steps

  1. If the list is empty, return None.
  2. Walk the list, add every value < x to values.
  3. Walk again, add every value ≥ x to values.
  4. Walk a third time, writing values[0], values[1], … into the nodes.
  5. Return head (same nodes, new values).

6Code (Python)

Brute force: store values, overwrite
class Solution:
    def partition(self, head, x):
        if head is None:                 # 0 nodes allowed
            return None

        values = []
        curr = head
        while curr is not None:          # pass 1: the small values
            if curr.val < x:
                values.append(curr.val)
            curr = curr.next

        curr = head
        while curr is not None:          # pass 2: the large values
            if curr.val >= x:
                values.append(curr.val)
            curr = curr.next

        curr = head
        i = 0
        while curr is not None:          # pass 3: write them back
            curr.val = values[i]
            i += 1
            curr = curr.next
        return head

7Code line by line

linewhat it means
if head is None: return NoneEmpty list (allowed by the constraints). Nothing to rearrange.
values = []A normal Python list. This is the extra space.
if curr.val < x: values.append(curr.val)Pass 1 keeps only the small values, in the order we meet them.
curr = headGo back to the start for the next pass. (We can do this because head never moved.)
if curr.val >= x: values.append(curr.val)Pass 2 adds the large values after the small ones. Note the >=: a value equal to x goes here.
curr.val = values[i]Pass 3 overwrites the node with the arranged value at the same position.
return headSame first node, now holding the arranged values.

8Dry run (x = 3)

passwhat we read / writevalues after the passlist after the pass
11 ✓ · 4 ✗ · 3 ✗ · 2 ✓ · 5 ✗ · 2 ✓ (keep < 3)[1, 2, 2]unchanged
21 ✗ · 4 ✓ · 3 ✓ · 2 ✗ · 5 ✓ · 2 ✗ (keep ≥ 3)[1, 2, 2, 4, 3, 5]unchanged
3node 1 ← 1, node 2 ← 2, node 3 ← 2, node 4 ← 4, node 5 ← 3, node 6 ← 5same[1] → [2] → [2] → [4] → [3] → [5] → None

9Complexity & remember

Remember the brute forceCopy values out (small ones first, then large ones), then write them back into the same nodes. Correct, but it uses an array, so it's not the answer an interviewer is looking for.

Part B · Optimal: two dummy nodes (small chain + large chain)

1The question (same as Part A), with a new rule

Same input, same output. But now we are not allowed to store values anywhere. We have to work with the nodes themselves and change only the next links.

2What the constraints tell us

3Intuition: two queues at a counter

Picture people walking past a counter one by one. Each person is sent to one of two lines: the "small" line or the "large" line. Each new person joins the end of their line, so each line keeps the arrival order. When everyone has passed, the small line is followed by the large line.

That's exactly the plan. One pointer can't look after two groups at once, so the teacher says we need two chains: one for nodes < x, one for nodes ≥ x. At the end, we connect the last node of the small chain to the first node of the large chain.

4Building the logic, the way the teacher derives it

Step 1: where do the two chains start? → two dummy nodes

The first node we meet (1) must be attached somewhere. Attaching to "the next of small" means small must already be a node. So we make two fake nodes with value 0: small_dummy and large_dummy.

small_dummy → [0] → None
large_dummy → [0] → None

Step 2: attach the node to the right chain

Walk with curr. If curr.val < x, attach it to the small chain. Otherwise (≥ x), attach it to the large chain.

Step 3: why we need moving pointers small and large

The teacher's first thought is to write small_dummy.next = curr. That works for the first node. But the next small node must go after 1, not after the dummy. If we moved small_dummy forward to make room, we would no longer know where the chain starts, and we could never read it from the beginning again.

Her fix: keep the dummy still, and make a separate alias that moves, just like we use curr to walk without moving head. So: small = small_dummy and large = large_dummy. These are the tail pointers of the two chains.

Attach rulesmall.next = curr then small = small.next
(or the same with large). Attach first, then move the tail onto the node you just attached, so the next node goes after it.

Step 4: always move curr

Whether the if-branch or the else-branch ran, we are done with this node, so after the if/else we always do curr = curr.next. The loop runs while curr is not None.

Doubt: after small.next = curr, is curr.next still the original next node?
→ Yes. We changed small.next (the old tail's link), not curr.next. The node curr still points to its original neighbour, so curr = curr.next continues along the original list. That's why no extra "save next" variable is needed here.

Step 5: after the loop, join the chains

Now we have two separate chains: 0 → 1 → 2 → 2 and 0 → 4 → 3 → 5. The last small node (where small stands) must point to the first real large node. We don't have a variable on 4, but we know it is large_dummy.next:

Joinsmall.next = large_dummy.next

Step 6: cut the end of the large chain → large.next = None

This is the line the teacher adds while coding, and it's the one most people forget. Node 5 was originally followed by the last 2. We never changed 5.next, so it still points to that 2. But that 2 is now inside the small chain, which leads back to 4 … 5 … 2 … forever. That's a cycle.

without large.next = None:

[1] → [2] → [2] → [4] → [3] → [5]
              ↑                 │
              └─────────────────┘   5 still points to the old last 2 → endless loop

So after joining we set large.next = None. Now the list really ends after 5.

Doubt: why don't we also need small.next = None?
→ Because the very next line overwrites small.next anyway (it becomes large_dummy.next). Only the large chain's end is left with a stale link.

Step 7: what do we return?

Reading from small_dummy gives 0 → 1 → 2 → 2 → 4 → 3 → 5. The 0 is our fake node, not part of the answer. The real head is small_dummy.next.

Doubt: what if no value is smaller than x (or the list is empty)?
→ Then the small chain is only the dummy, and small is still small_dummy. The join makes small_dummy.next = large_dummy.next, so we return the large chain. For an empty list both chains are empty and we return None. The dummy nodes remove all these special cases, so no base case is needed.

5Approach steps

  1. Make small_dummy and large_dummy. Set tails small, large to them.
  2. Walk curr from head to None.
  3. If curr.val < x: attach to small and move small. Else: attach to large and move large.
  4. Move curr.
  5. After the loop: small.next = large_dummy.next, then large.next = None.
  6. Return small_dummy.next.

6Code (Python)

Optimal: two dummy nodes
class Solution:
    def partition(self, head, x):
        small_dummy = ListNode(0)        # start of the "< x" chain
        large_dummy = ListNode(0)        # start of the ">= x" chain
        small = small_dummy              # tail of the small chain (moves)
        large = large_dummy              # tail of the large chain (moves)

        curr = head
        while curr is not None:
            if curr.val < x:
                small.next = curr        # attach to the small chain
                small = small.next
            else:
                large.next = curr        # attach to the large chain
                large = large.next
            curr = curr.next             # always move on

        small.next = large_dummy.next    # join: small chain → large chain
        large.next = None                # cut the old link (avoids a cycle)
        return small_dummy.next

7Code line by line

linewhat it means
small_dummy = ListNode(0) large_dummy = ListNode(0)Two fake heads, so each chain always has a node to attach after. They never move.
small = small_dummy large = large_dummyMoving aliases: they always stand on the last node of their chain.
while curr is not None:Visit every original node once.
if curr.val < x:Decide the side. Equal to x goes to the else branch (large side).
small.next = curr small = small.nextHook the node at the end of the small chain, then make it the new end.
large.next = curr large = large.nextSame for the large chain.
curr = curr.nextMove on. curr.next is still the original neighbour, because we only changed the tail's link.
small.next = large_dummy.nextGlue the last small node to the first real large node.
large.next = NoneThe last large node may still point to an old neighbour. Cut it so the list ends here.
return small_dummy.nextSkip the fake 0. This is the real head.

8Dry run (x = 3, list 1 → 4 → 3 → 2 → 5 → 2)

I'll name the two 2s as 2a (4th node) and 2b (6th node), so we can track them. S = small_dummy, L = large_dummy.

stepcurr< 3?what changessmall chainlarge chain
start1small = S, large = LSL
11yesS.next = 1, small → 1S → 1L
24noL.next = 4, large → 4S → 1L → 4
33no (equal)4.next = 3 (it already was), large → 3S → 1L → 4 → 3
42ayes1.next = 2a, small → 2aS → 1 → 2aL → 4 → 3
55no3.next = 5, large → 5S → 1 → 2aL → 4 → 3 → 5
62byes2a.next = 2b, small → 2bS → 1 → 2a → 2bL → 4 → 3 → 5
endNoneloop stopsbut 5.next is still 2b (old link)
join2b.next = L.next (= 4)S → 1 → 2a → 2b → 4 → 3 → 5 → 2b … (cycle!)
cut5.next = NoneS → 1 → 2a → 2b → 4 → 3 → 5 → None ✓

Snapshot after step 4:

S → [0] → [1] → [2a]                L → [0] → [4] → [3]
                  ↑                                  ↑
                small                              large
curr is on [5]   (curr moves along the ORIGINAL links: 2a.next was 5)

Snapshot after the loop, before joining:

S → [0] → [1] → [2a] → [2b]         L → [0] → [4] → [3] → [5] ─→ (old link to 2b)
                         ↑                                  ↑
                       small                              large

Final, after join and cut, returning S.next:

[1] → [2] → [2] → [4] → [3] → [5] → None
Doubt: in step 4, when we set 1.next = 2a, didn't we lose 4?
→ No. 4 is already safe inside the large chain (L.next = 4). And curr was already standing on 2a. Every node is held by one of the chains by the time its old link is overwritten.

9Complexity & remember

Remember Partition ListTwo dummies, two tails. Each node goes to the end of the small chain (< x) or the large chain (≥ x). Then small.next = large_dummy.next, large.next = None, return small_dummy.next.

Part C · Revision page

Brute force (Part A)Two dummies (Part B)
ideacopy values into an array in the right order, write them backre-link nodes into two chains, then join
walks3 (small values, large values, overwrite)1
timeO(3n) = O(n)O(n)
extra spaceO(n) arrayO(1) (two dummy nodes)
empty listneeds if head is Nonehandled by the dummies
interview?works, but uses the "array trick"the expected in-place answer
If you remember only 5 lines 1. Partition keeps the original order inside each side; it is not a sort.
2. < x goes to the small chain; ≥ x (equal included) goes to the large chain.
3. Dummy nodes stay still; the tails small/large move.
4. Attach, then move the tail: t.next = curr; t = t.next.
5. After the loop: join, then large.next = None, return small_dummy.next.
Mistakes to avoid ✗ moving the dummy itself (you lose the start of the chain)
✗ forgetting large.next = None (the old link makes a cycle)
✗ returning small_dummy instead of small_dummy.next (the fake 0 shows up)
✗ joining to large_dummy instead of large_dummy.next
✗ putting values equal to x on the small side
test it yourself (paste under either solution)
def build(vals):
    dummy = ListNode(0)
    t = dummy
    for v in vals:
        t.next = ListNode(v)
        t = t.next
    return dummy.next

def to_list(head):
    out = []
    while head:
        out.append(head.val)
        head = head.next
    return out

s = Solution()
print(to_list(s.partition(build([1, 4, 3, 2, 5, 2]), 3)))   # [1, 2, 2, 4, 3, 5]
print(to_list(s.partition(build([2, 1]), 2)))               # [1, 2]
print(to_list(s.partition(build([]), 0)))                   # []

Based on this video: Partition List