LC 987 Vertical Order Traversal of a Binary Tree
Interview pattern · Tree geometry + total ordering

Vertical Order Traversal of a Binary Tree

This looks like a traversal problem. It isn't. It's a sorting problem wearing a traversal costume — and the single line most people forget is the one the whole problem was built around.

LeetCode 987 Difficulty Hard Time O(n log n) Core trick (col, row, value) triples Asked at Amazon · Meta · Google
01

The Problem in Plain English

Draw the tree on graph paper. Not sketched loosely — drawn on a real grid, with real coordinates.

The root sits at the origin. Every time you go down-left, you move one square down and one square left. Every time you go down-right, you move one square down and one square right. That's it — that's the entire coordinate system.

Now here's the thing you're being asked for. Forget the branches. Forget parents and children. Just look at the piece of paper and the dots on it.

▸ The whole task

Sweep across the paper in vertical strips, from the leftmost strip to the rightmost strip. For each strip, read out the numbers you find in it — top row first, working downwards.

And if two numbers happen to be sitting on the exact same square, read out the smaller number first.

Return one list per strip. Skip strips with nothing in them.

That last rule — smaller number first when two nodes land on the same square — sounds like a footnote. It is not a footnote. It is the entire reason this problem is rated Hard instead of Medium, and it is the reason a perfectly reasonable solution you'll write in ninety seconds will fail.

Because yes: two different nodes can land on exactly the same square. A node deep in the left subtree that drifted right, and a node deep in the right subtree that drifted left, can collide on the same coordinate. They're unrelated. They've never met. And now you have to decide which one gets printed first, and the tree structure gives you no answer — so the problem hands you one: smaller value wins.

1 / \ 2 3 / \ / \ 4 6 5 7 on graph paper: col: -2 -1 0 1 2 row 0: 1 row 1: 2 3 row 2: 4 6,5 7 ^^^ same square!

Reading strip by strip, left to right: [4], [2], then the middle strip — which contains 1 on row 0, and both 5 and 6 on row 2. Smaller first, so [1, 5, 6]. Then [3], then [7].

[[4], [2], [1, 5, 6], [3], [7]]
02

Why Most People Get Stuck

There are three traps here, stacked on top of each other. Most people fall into the first two, fix them, feel clever, and then get destroyed by the third.

Trap 1 — "I'll just DFS and bucket by column"

Completely natural. Recurse, carry a column number, dump values into a map keyed by column.

The problem: DFS visits in depth order, not row order. A node three levels deep in the left subtree gets visited long before a node one level deep in the right subtree — and they can share a column. So the list for that column comes out vertically scrambled.

Trap 2 — "Fine, BFS then, so rows come out in order"

Better! BFS is level order, so within any single column, nodes now genuinely appear top-to-bottom. Row ordering: solved, free, no extra work.

And this is where people get comfortable. This solution passes a satisfying number of test cases. It is also the accepted solution to a different problem — LeetCode 314, "Binary Tree Vertical Order Traversal," which has no tie-break rule.

Trap 3 — the one that actually fails you

⚠️ The mental trap

You assume that because BFS gave you correct row ordering for free, it will also give you correct ordering within a row. It does not. BFS visits nodes in a row left-to-right by tree position, not by value. When two nodes collide on the same square, BFS hands them to you in whichever order their parents happened to sit — and the problem wants them in value order.

Here's the exact test case that exposes it. Watch node 6 and node 5:

Press the button to see what BFS actually produces.

Node 6 is the right child of 2. Node 5 is the left child of 3. Both land at column 0, row 2. Same square. They are not siblings, not cousins in any useful sense — they just happen to have drifted into each other.

BFS reaches 2 before 3, so it emits 6 then 5. The answer requires 5 then 6. Your output is [1, 6, 5]; the expected output is [1, 5, 6]. One test case, and it's over.

Trap 4 (bonus) — negative columns

Columns go negative. You can't index an array with −2. And a HashMap gives you no ordering, so even after you've collected everything perfectly, printing it in hash order scrambles the strips. Small trap, but it catches people at the very last line.

03

The Key Observation

💡 Key Observation

Every node reduces to a triple: (column, row, value). The required output is exactly that list of triples sorted lexicographically. The traversal has nothing to do with the ordering.

Let me convince you properly, because this sounds like a restatement and it isn't.

Step 1 — read the problem's rules as a comparator

The problem tells you, in plain English, exactly how to compare any two nodes:

  • Different column? The smaller column comes first.
  • Same column, different row? The smaller row comes first.
  • Same column, same row? The smaller value comes first.

Look at what that is. That is a total order on nodes. Given any two nodes in the tree, those three rules always produce a decisive answer. There are no unresolved ties, no "it depends," no ambiguity.

Step 2 — realise what that buys you

Any collection of items with a total order defined on them has exactly one correct sorted sequence. And any sorting algorithm will find it.

So the answer isn't something you have to discover through clever traversal. It's already determined the moment you know each node's triple. The output is a mathematical consequence of the data, not a consequence of the walk.

💡 The reframe that fixes everything

Stop trying to make the traversal produce the right order. No traversal can — no walk of a tree visits nodes in value order for collisions, because value order has nothing to do with tree shape.

The traversal has exactly one job: hand out coordinates. That's it. Then you sort. The two concerns are completely independent, and the moment you separate them, the problem collapses.

This is why the DFS-versus-BFS argument that dominates most explanations is a red herring. Use whichever you like. DFS is three lines shorter. Neither one affects correctness, because neither one is responsible for ordering.

Step 3 — why the coordinates work the way they do

The rule left → (row+1, col−1) and right → (row+1, col+1) is just a statement about drawing. Going left moves you left. Going down moves you down. There's nothing to derive.

But notice one consequence that does matter: the column is not unique to a node. Unlike the seat index in a perfect-tree layout, where each position belongs to exactly one node, here −1 + 1 = 0 and +1 − 1 = 0. Two entirely different paths can arrive at the same column. And if they also happen to be at the same depth, you get a genuine collision — which is precisely why the problem needs a third tie-break key.

Click any node to see its coordinate and light up its column strip.

Click any node.

Step 4 — see the comparator actually matter

Below is the same tree shape, with the root relabelled 9 so each broken comparator produces a visibly different answer. Switch the sort keys and watch the output break in four distinct ways.

04

The Movie

Welcome to the village of Treeton, and its famously over-engineered post office. 📮

Treeton has one Main Street, running north to south, and it is numbered 0. Streets to the west are −1, −2, −3. Streets to the east are +1, +2, +3. The village council thought this was elegant. The postal service has never forgiven them.

Every building has a floor number too: floor 0 at the top of the hill, floor 1 below it, floor 2 below that. (Treeton is built into a hillside. Don't overthink it.)

So every letter that arrives has three things written on it:

┌──────────────────────────────┐ │ To: street 0 │ ← which vertical strip │ floor 2 │ ← how far down │ name "Five" │ ← the value on the node └──────────────────────────────┘

Meet Priya, the postal clerk. Her job has exactly three phases, and she never mixes them up.

Phase 1 — stamping. The letters arrive in a jumbled sack, in no useful order whatsoever. Priya doesn't care. She picks up each letter and stamps the street and floor onto it. She does this in whatever order the letters come out of the sack, because the order she stamps them in has no bearing on anything. Stamping is not sorting.

Phase 2 — pigeonholes. Behind her is a wall of pigeonholes, one per street, arranged west to east: −2, −1, 0, +1, +2. She throws each stamped letter into the hole for its street.

Phase 3 — ordering each hole. Now she takes each pigeonhole and puts its letters in delivery order: lowest floor first, because the postman walks downhill. And here's the rule from the manual, Section 4, subclause (c):

▸ Treeton Postal Manual, §4(c)

"Where two letters are addressed to the same street and the same floor, they shall be ordered alphabetically by name. The postman cannot be expected to guess."

Then she walks the pigeonholes west to east and hands out the bundles. Done.

✉ TREETON POST OFFICE ✉
west ← main street → east
📮

Priya has a sack of unsorted letters and a wall of pigeonholes. Press the button.

The subplot: Bob

Last summer the post office hired a temp named Bob. Bob noticed that the mail truck always delivers letters floor by floor — all the floor-0 letters, then all the floor-1 letters, and so on. Bob concluded, reasonably, that the truck had already done the sorting for him, and he could just stack letters into pigeonholes in arrival order and skip Phase 3 entirely.

Bob was right about floors. Bob was catastrophically wrong about §4(c).

Two letters arrived for street 0, floor 2: one for Six, one for Five. The truck had picked up Six first, because Six lives on the eastern edge of the western district and the truck goes west to east. Bob stacked Six, then Five. The postman delivered them in that order. Section 4(c) was violated. There was an inquiry.

Bob is the BFS-only solution. Bob is what you write in the first ninety seconds. Don't be Bob.

Cast list

A letter
A node
Street number (can be negative)
Column, from col−1 / col+1
Floor number
Row, from row+1
Name on the letter
node.val
Stamping the address
The traversal — DFS or BFS, doesn't matter
The wall of pigeonholes
TreeMap<Integer, List<Integer>>
Holes arranged west to east
Sorted keys — the reason it isn't a HashMap
Lowest floor delivered first
Secondary sort key: row
Manual §4(c), alphabetical
Tertiary sort key: value
Bob the temp
BFS with no tie-break — solves LC 314, fails LC 987

Remember Priya's three phases: stamp, file, order. Never let them bleed into each other, and this problem cannot hurt you.

05

Build the Algorithm Naturally

Let's invent this from nothing, failing our way forward.

Attempt 0 — "DFS, bucket by column"

dfs(node, col): map[col].add(node.val) dfs(node.left, col - 1) dfs(node.right, col + 1)

Columns are right. Contents are scrambled. On the tree [3,9,20,null,null,15,7], column 0 gets 3 first and 15 later, which happens to be fine — but flip the shape and a deep left node beats a shallow right node into the same bucket. ❌

Lesson: depth order ≠ row order. We need row information.

Attempt 1 — "BFS instead"

Now nodes enter each bucket in genuine top-to-bottom order, free of charge, because BFS is level order. Rows: solved. ✅

Then you hit [1,2,3,4,6,5,7] and get [1,6,5] instead of [1,5,6]. ❌

Lesson: BFS orders rows, but within a row it orders by tree position, and tree position is not value.

Attempt 2 — "Patch it: sort each row's slice"

Since BFS already isolates one row at a time, collect that row's (col, value) pairs into a small list, sort it by (col, value), then flush it into the buckets. Ties inside a row are now resolved by value, and rows still arrive in order.

This works. It's a genuinely good solution. But notice what just happened: you gave up on the traversal doing the ordering, and started sorting. You just did it in small batches.

Attempt 3 — "Why am I sorting in batches?"

Ask the honest question: if I'm sorting anyway, why am I bending the traversal into level order to help? What if I sort everything, once?

for every node: record (col, row, value) sort all records by col, then row, then value group consecutive records by col

And now the traversal genuinely doesn't matter — use a three-line DFS. Every ordering requirement lives in one comparator, where you can read it and check it against the problem statement line by line.

💡 Why this is the version to write

The comparator is a direct transcription of the problem statement. Three rules in the prompt, three lines in the comparator, in the same order. There is nowhere for a subtle ordering bug to hide, because there is no implicit ordering anywhere — the traversal contributes nothing but coordinates.

Attempt 4 — "Can I make the data structure do the sorting?"

Yes, and it's worth knowing because interviewers sometimes like it:

TreeMap<Integer, // column, sorted TreeMap<Integer, // row, sorted PriorityQueue<Integer>>> // values at that exact cell, min-heap

Insert every node and the structure maintains all three orderings automatically; then you just walk it. It's elegant and it reads beautifully. It's also slower in practice (three levels of tree-map overhead per insert), more allocation, and much easier to fumble under pressure. Know it, mention it, don't lead with it.

The final algorithm

  1. DFS from the root with (row = 0, col = 0). Recurse left with (row+1, col−1), right with (row+1, col+1).
  2. At each node, append the triple {col, row, value} to a flat list.
  3. Sort the list: by col, then row, then value.
  4. Walk the sorted list; start a new output group every time col changes.
🎯 Interview Tip

Say the reframe out loud before you write anything: "The output order is a total order on (col, row, value), so this is really a sort, not a traversal — the traversal just assigns coordinates." That single sentence tells the interviewer you've seen the actual structure of the problem rather than pattern-matched to "tree question, do BFS."

06

Dry Run

Tree: [1, 2, 3, 4, 6, 5, 7] — the collision case.

1 / \ 2 3 / \ / \ 4 6 5 7

Watch the three phases stay strictly separate: stamp (assign coordinates, in DFS preorder), sort (one comparator, everything at once), group (walk and cut on column change).

Step-by-step execution 1 / 15
node
row
col
prevCol
cells — list of {col, row, value}
answer
[[4], [2], [1, 5, 6], [3], [7]]

The moment that matters

Look back at the stamping phase. Node 6 was stamped fourth and node 5 was stamped sixth — DFS reached 6 long before 5. Look at the sorted list: 5 comes before 6.

The sort completely erased the traversal's opinion. That's not a side effect; that's the design. The traversal was never allowed to have an opinion in the first place.

07

Implementation Intuition

For each line, one question: what disaster happens if we delete it or change it? Click to open.

cells.add(new int[]{col, row, node.val});
Why col first, not row first?

Purely so the array's natural element order matches the comparator's key order — it makes the comparator readable top-to-bottom and makes an off-by-one in the index far less likely. Functionally you could store {row, col, val} and compare [1] then [0], but now you have two orderings to keep in your head and one of them is a lie.

Why store row at all, when BFS would give it for free?

Because we're using DFS, where row is not free. But more importantly: storing it makes the solution traversal-independent. The comparator is now the single source of truth for ordering. If ordering lives partly in the comparator and partly in "well, BFS happens to visit in the right order," you have a solution you cannot reason about.

if (a[0] != b[0]) return Integer.compare(a[0], b[0]);
Disaster if this isn't first

Column must be the primary key, because the output is grouped by column. If any other key comes first, the sorted list interleaves columns, your grouping loop cuts a new group every few elements, and you get more groups than there are columns — with the same column appearing multiple times.

Why Integer.compare and not a[0] - b[0]

Subtraction overflows. With this problem's constraints (values 0–1000, columns bounded by n) it can't actually overflow, so you'd get away with it — but it's a habit that will silently break you on a problem with Integer.MIN_VALUE in it. Integer.compare costs nothing and is always right.

if (a[1] != b[1]) return Integer.compare(a[1], b[1]);
Disaster if removed

Within a column, ordering falls through to value. A node with value 1 sitting at row 9 would print before a node with value 500 sitting at row 0 — the column reads bottom-to-top-ish, in an order with no relationship to the picture.

Disaster if swapped with the value key

Same thing. Row must outrank value. Value is only ever a tie-break, and it only fires when the rows are already equal. Getting this backwards is the single most common comparator bug on this problem — and it passes plenty of tests, because it only differs when a column contains multiple rows with out-of-order values.

return Integer.compare(a[2], b[2]); // the value tie-break
Disaster if removed

You have written the solution to LeetCode 314, not 987. Everything passes until a test has two nodes on the same square — then the collision resolves by whatever order the sort happened to leave them in, which for a stable sort is traversal order and for an unstable sort is undefined.

This is the line the entire problem exists to test. If you remember one thing about 987, remember that it has a third key.

int prevCol = Integer.MIN_VALUE;
Disaster if you initialise to 0

Column 0 always exists (the root is there), and it is frequently the first column when the tree leans right. If prevCol starts at 0, the first group never gets created, and you crash on answer.get(answer.size() - 1) with an empty list — or, worse, silently merge column 0 into nothing.

Integer.MIN_VALUE is a sentinel meaning "no column yet." It works because no real column can ever equal it: columns are bounded by ±n.

if (cell[0] != prevCol) { answer.add(new ArrayList<>()); prevCol = cell[0]; }
Why this simple check is enough

Because the list is sorted by column, all entries for a column are guaranteed contiguous. You never need a map at this stage — a single "did the column just change?" test is sufficient, and it automatically skips empty columns, since a column with no nodes simply never appears in the list.

Disaster if you use a HashMap here instead

You'd re-introduce the unordered-keys problem you already solved by sorting. Sorting made the map unnecessary; keeping the map makes the sort half-wasted.

collect(node.left, row + 1, col - 1);
Disaster if the sign flips

Left becomes col + 1 and your whole answer comes out mirrored — every column list is correct internally, but the columns are in reverse order. It passes symmetric test trees and fails everything else, which makes it maddening to spot.

Why row + 1 on both branches

Row is depth. Both children are one level deeper; only the horizontal direction differs. If you accidentally write row instead of row + 1 on one branch, that subtree flattens into its parent's row and starts colliding with nodes it should sit below.

if (node == null) return;
Disaster if removed

NPE on node.val the first time you hit a leaf. Putting the null check at the top of the recursive function (rather than guarding each call site) means you write it once instead of twice and can't forget one branch.

08

Common Bugs

Thirteen ways this goes wrong, roughly ordered by how often they actually happen in interviews.

The thirteen classic failures 13 items
01 No value tie-break at all

You solved LC 314. Fails the instant a test has two nodes on the same square. Symptom: exactly one column is wrong, by exactly one swap. If your output differs from expected by a single adjacent transposition, this is always the cause.

02 Comparator keys in the wrong order

Value before row is the classic. Row must outrank value; value only fires on a row tie. Passes many tests, which is what makes it dangerous — it only differs when a column holds several rows with values that aren't already ascending.

03 HashMap instead of TreeMap

Columns come out in hash order. With small integer keys, Java's HashMap often looks sorted for non-negative keys, so this can pass a couple of tests and then explode the moment a negative column appears. A near-perfect trap.

04 Indexing an array by column

ArrayIndexOutOfBoundsException on the first left-leaning node, because column −1 is not an array index. If you want an array, you must first find minCol and offset every index by it — which is two extra passes for no benefit over sorting.

05 DFS without recording the row

Column contents come out in depth-first order, so a deep left node outranks a shallow right node in the same column. The output looks almost right, which is the worst kind of wrong.

06 Sorting an entire column by value

A "fix" people reach for after seeing bug 1. It resolves the collision and destroys the row ordering at the same time. Value sorting must be scoped to within a single (col, row) cell — which the three-key comparator does automatically and a blanket sort does not.

07 Sign flip on the column

Left going to col + 1. Output is perfectly mirrored. Symmetric test trees pass, so you'll only catch it on an asymmetric case. Sanity check: on a left-only chain, the answer should be columns from most-negative upward.

08 prevCol initialised to 0

Column 0 is never empty (the root lives there), so this either crashes on the first element or silently drops it. Use Integer.MIN_VALUE as the "no column yet" sentinel.

09 Subtraction in the comparator

a[0] - b[0] overflows for extreme values. Safe here given the constraints, but it's a habit that breaks silently elsewhere, and interviewers notice. Integer.compare always.

10 Forgetting computeIfAbsent in the TreeMap version

columns.get(col).add(val) NPEs on the first node in each column. computeIfAbsent(col, k -> new ArrayList<>()).add(val) creates the bucket lazily and is one line.

11 Instance state not reset

If cells is a field on the Solution object and the judge reuses the instance across test cases, results bleed between tests. Either declare it locally inside the method or clear it first. LeetCode usually creates a fresh instance, so this bites you in real code more than on the judge.

12 Relying on sort stability instead of a third key

"Java's List.sort is stable, so ties keep insertion order, so I'll just insert in the right order." Now correctness depends on an invariant living in a different function, and switching DFS to BFS silently breaks it. Encode the rule in the comparator where it can be read.

13 Emitting empty columns

Only a risk in the array-offset version, where you allocate a slot for every column between min and max and then dutifully print the empty ones. The problem wants only non-empty columns. Sorting sidesteps this entirely: a column with no nodes never appears.

⚠️ Common Pitfall — the interview-losing one

Bugs 1 and 2 are the same underlying mistake: you let the traversal own part of the ordering. The instant any ordering rule lives outside the comparator, you can no longer verify correctness by reading the comparator against the problem statement — and that's the only verification you have time for in an interview.

09

How to Recognise This Pattern Again

Trigger words in the prompt

  • "vertical," "column," "same column," "left to right"
  • "top to bottom," "row," "position (row, col)"
  • "if two nodes are in the same position / same row and column, order by value" — this sentence is the problem. The moment you see it, you know there are three sort keys, and you know the naive traversal solution is wrong.
  • Any explicit statement of the form "sort by A, then by B, then by C"

Structural tells

  • The problem describes the output order in words rather than implying it from a traversal. That's a comparator specification in disguise.
  • The output is grouped by something that isn't depth.
  • Two nodes with no relationship to each other can compete for the same output slot.
💡 The general rule to internalise

When a problem specifies an output order that no traversal naturally produces, stop traversing and start sorting. Use the traversal only to attach coordinates, then transcribe the problem's ordering rules directly into a comparator.

The wider family — every one of these is "assign a coordinate, then order by it":

ProblemCoordinateOrdering
Vertical order, LC 987 (this one)col ∓1, row +1col, row, value — three keys
Vertical order, LC 314col ∓1, row +1col, row — no tie-break
Top view / bottom viewcol ∓1first / last seen per column in BFS
Diagonal traversalleft → d+1, right → ddiagonal index, then order of arrival
Maximum width, LC 662seat 2i, 2i+1no sort — just min and max per level
Boundary traversalnonethree separate walks, concatenated
🎯 Interview Tip — the disambiguating question

If an interviewer gives you a vertical-order problem verbally, ask: "If two nodes end up at the same row and column, what order do you want them in?" Either they say "by value" (it's 987, and you've just shown you spotted the hard part unprompted) or they say "doesn't matter" (it's 314, and you've just saved yourself a sort). It's the single highest-value question you can ask on this problem.

10

Memory Hook

🧠 Memory Hook

"The traversal only hands out addresses. The sort delivers the mail."

And the one that encodes the actual comparator:

🧠 Memory Hook — the three keys

"Street, then floor, then name. Three keys, always three."

If you remember nothing else six months from now, remember Priya's three phases — stamp, file, order — and remember Bob, who thought the truck had already sorted his letters and got hauled into an inquiry over Section 4(c). Those two images regenerate the whole solution, including the part everyone forgets.

11

Interview Explanation — 60 seconds

The interviewer asks: "Walk me through your approach." Here is the answer, near-verbatim.

🎯 Say this

"The output order here is fully specified by the problem: column ascending, then row ascending, then value ascending. That's a total order on nodes — which means this isn't really a traversal problem, it's a sort. The traversal's only job is to assign each node its coordinate.

So: one DFS from the root carrying (row, col), starting at (0, 0). Left child gets (row+1, col−1), right gets (row+1, col+1). At each node I record the triple (col, row, value) into a flat list.

Then I sort that list once with a three-key comparator that transcribes the problem statement directly. Finally I walk the sorted list and start a new output group whenever the column changes — which works because sorting by column first makes each column's entries contiguous, and empty columns simply never appear.

The reason the third key matters is that two unrelated nodes can land on the exact same (row, col) — a right child in the left subtree and a left child in the right subtree — and no traversal will order those two by value. That's the case a BFS-only solution silently gets wrong.

O(n log n) time from the sort, O(n) space. I could also do BFS and sort each row's slice, which gets row ordering free — same complexity, slightly more code."

The three beats, if you forget the script

BeatThe one sentence
ReframeThe output order is a total order on (col, row, value) — so it's a sort, not a traversal.
MechanismDFS assigns coordinates; one three-key comparator does all the ordering; group on column change.
Hidden catchTwo unrelated nodes can occupy the same cell — that's why there's a third key, and why BFS alone fails.
12

Complexity

Where every unit of cost comes from O(n log n)

Time: O(n log n)

Three phases, three costs:

  • Stamping: the DFS touches each node once and does O(1) work — one array allocation, one list append. O(n).
  • Sorting: n triples, comparison-based sort, each comparison O(1) (at most three integer compares). O(n log n) — and this dominates everything.
  • Grouping: one linear walk of the sorted list, O(1) per element. O(n).

Total: O(n) + O(n log n) + O(n) = O(n log n).

▸ Could it be faster?

In principle, a little. Columns are bounded to [−n, n], so you could bucket by column in O(n) and only sort inside buckets. But you still need to sort within each bucket by (row, value), and the sum of those sorts is still O(n log n) in the worst case — a single tall column holding every node. So the asymptotic answer doesn't move, and the code gets considerably worse. Not worth it. Say this if asked; don't volunteer it as your main solution.

Why the BFS variant is the same

The BFS-and-sort-each-row version sorts k₁, k₂, … kₘ elements where the kᵢ sum to n. That's Σ kᵢ log kᵢ ≤ Σ kᵢ log n = n log n. Slightly better in practice on wide, shallow trees (many small sorts beat one big one), identical asymptotically. Worth one sentence, not worth choosing over the simpler code.

Space: O(n)

WhatCostWhy
The triple listO(n)one int[3] per node — unavoidable, it's the whole dataset
Sort workspaceO(n)Java's TimSort on objects needs auxiliary space
Recursion stackO(h)h = height; O(n) on a skewed tree, O(log n) balanced
OutputO(n)every node appears exactly once

All of it collapses to O(n). Note that unlike the width problem, there's no clever way to avoid storing every node — the sort fundamentally needs the whole dataset in hand before it can produce the first element of the answer.

A note on the TreeMap-of-TreeMap-of-heap version

Same O(n log n) — each insert is O(log n) across the nested structures, done n times. But the constant factor is meaningfully worse: three levels of red-black tree traversal plus a heap sift per node, and a great deal more allocation. It reads elegantly and runs slower. Good to know it exists; don't reach for it first.

13

Java Implementation Walkthrough

Solution A — DFS + one sort write this one

Line by line

List<int[]> cells — declared as a local inside the public method and passed down, rather than as a field. That kills bug 11 (state bleeding across calls) for free. Each element is {col, row, value}: three ints, no object overhead, and the element order deliberately mirrors the comparator's key order so the two can be read against each other at a glance.

collect(root, 0, 0) — the root is the origin. Nothing special about (0,0); any starting point works since all coordinates are relative, but 0 makes the negative columns visible and obvious.

if (node == null) return; — at the top of the recursion, so it's written once and covers both branches. Handles the empty-tree input as well: the list stays empty, the sort does nothing, the grouping loop never runs, and you return an empty list. No special case needed anywhere.

collect(node.left, row + 1, col - 1) — down and left. row + 1 on both branches because both children are one level deeper; only the column direction differs. There's no return value and no accumulation on the way back up — this recursion is pure descent, which is why it's three lines.

cells.sort(...) — the entire ordering logic of the problem, in one place. Read it against the problem statement: column ascending, row ascending, value ascending. Three rules in the prompt, three lines here, same order. That correspondence is the thing that makes this version verifiable under interview pressure.

Integer.compare(a[0], b[0]) — never a[0] - b[0]. Costs nothing, can't overflow, signals care.

int prevCol = Integer.MIN_VALUE; — the "no column yet" sentinel. Cannot collide with a real column, since columns are bounded by ±n.

if (cell[0] != prevCol) — start a new group on every column change. This is correct only because the list is sorted by column, which guarantees each column's entries are contiguous. Empty columns never appear in cells, so they're skipped automatically — no filtering needed.

answer.get(answer.size() - 1).add(cell[2]) — append to the group we're currently building. cell[2] is the value; the column and row have done their job and are discarded.

Solution B — BFS + TreeMap + per-row sort mention this one

Why this works

int levelSize = queue.size(); — the same level-fence snapshot as in the width problem. Freeze it before the inner loop, or pushing children extends the loop into the next row and the whole row-isolation guarantee collapses.

List<int[]> level — holds {col, value} for this row only. No row field needed: every element here is on the same row by construction. That's the entire benefit BFS buys you.

level.sort(...) — two keys instead of three, because row is already handled by the outer loop. Sort by column, then by value for same-cell ties. This is the line Bob skipped.

columns.computeIfAbsent(cell[0], k -> new ArrayList<>()).add(cell[1]) — lazily create each column's bucket. Without computeIfAbsent, you NPE on the first node of every column.

TreeMap, not HashMap — the keys must come out west-to-east. answer.addAll(columns.values()) then walks them in ascending column order for free.

▸ When would you actually choose this?

When the tree is very wide and shallow: many small sorts beat one large one in practice, and you avoid materialising n three-element arrays. It's also the more natural answer if the interviewer has already pushed you down a BFS path. But it has more moving parts — a pair class, a level fence, a lazily-created map — and therefore more places to slip.

Solution C — let the data structure do the sorting know it, don't lead with it

Every ordering is maintained on insert: outer TreeMap sorts columns, inner TreeMap sorts rows, and the PriorityQueue is a min-heap that resolves same-cell ties by value. Walking the structure emits the answer directly, with no comparator anywhere.

It reads beautifully and it is genuinely clever. It is also slower (three levels of tree overhead plus a heap sift per insert), allocates far more, and puts the ordering logic into type declarations rather than a readable comparator — which means a reviewer has to reverse-engineer the intent from the generics. Mention it as an alternative; write Solution A.

Which one do you actually write? decision table
A · DFS + sortB · BFS + row sortC · Nested maps
Lines of code~25~35~30
Where ordering livesone comparator ✅comparator + BFS invarianttype declarations
TimeO(n log n)O(n log n)O(n log n)
Practical speedfastestfast on wide treesslowest
Risk on a deep treestack overflow (recursion)nonestack overflow if DFS-fed
Interview default✅ this onegood fallbackname-drop only

Default to A. It's the shortest, the ordering is verifiable in one glance, and the traversal choice is visibly irrelevant — which is exactly the insight you want to demonstrate. If the interviewer specifically wants an iterative solution or flags recursion depth, switch to B; the reframe you already explained still holds, you're just sorting in batches.

14

Visual Cheat Sheet

The whole lesson, compressed to the size of an index card.

IdeaRemember
ObservationEvery node is a triple (col, row, value). The answer is those triples sorted lexicographically. The traversal only assigns coordinates.
CoordinatesRoot at (0,0). Left → (row+1, col−1). Right → (row+1, col+1).
The comparatorcol ascending → row ascending → value ascending. Three keys, in that order, always.
Why three keysTwo unrelated nodes can occupy the same cell (a right child in the left subtree meets a left child in the right subtree). No traversal orders those by value.
PatternCoordinate assignment + explicit total order. Traversal choice is irrelevant to correctness.
Data structureList<int[]> of triples + one sort. Alternative: TreeMap<col, List> with a per-row sort.
Pitfall 1No value tie-break → you solved LC 314, not 987. Symptom: one column off by a single swap.
Pitfall 2Value before row in the comparator. Row must outrank value; value only fires on a row tie.
Pitfall 3HashMap for columns (unordered), array indexed by a negative column, or prevCol = 0.
Recognition cue"If two nodes are in the same position, order by value" → three sort keys, and the naive BFS answer is wrong.
Question to ask"What order do you want for two nodes at the same row and column?" — distinguishes 987 from 314 in one sentence.
Memory hook"The traversal only hands out addresses. The sort delivers the mail."
ComplexityTime O(n log n), dominated by the sort. Space O(n) for the triples, plus O(h) recursion.
The moviePriya at the Treeton Post Office: stamp, file, order. Negative streets to the west. Manual §4(c) breaks name ties. Bob skipped Phase 3 and there was an inquiry.
Lesson complete

Type "next" when you're ready.

Or name the problem you want — the next lesson uses this exact structure.