Topics › Linked List › Hard Problems of LL
Flattening of LL
Java source ↗ Edit this article ↗
Jump to code:
Problem
You are given a linked list in which every node has two pointers: next, which links the nodes of the top-level list, and bottom, which links a vertical sub-list hanging below that node. Each vertical list (including its top node) is sorted in ascending order through bottom pointers. Flatten the structure into a single list that is sorted in ascending order and linked only through bottom pointers; every next pointer in the result must be null. Return the head of the flattened list.
Constraints: 0 ≤ k ≤ 10^4 top-level nodes, total node count N ≤ 10^5, values fit in an int. The top-level nodes themselves are not necessarily sorted among each other, although in the classic statement they are.
Example 1
5 -> 10 -> 19 -> 28
| | | |
7 20 22 35
| | |
8 50 40
| |
30 45
answer = 5 -> 7 -> 8 -> 10 -> 19 -> 20 -> 22 -> 28 -> 30 -> 35 -> 40 -> 45 -> 50 (through bottom)
Example 2
5 -> 10 -> 19 -> 28 // every column has a single node
answer = 5 -> 10 -> 19 -> 28
Example 3
1 // a single column is already flat
|
2
|
3
answer = 1 -> 2 -> 3
The structure is really k sorted lists, so the problem is a disguised merge k sorted lists. Everything below follows from that observation.
Approach 1: Brute force, collect and sort
Intuition. Ignore the sorted columns entirely. Visit every node (walk next across the top, bottom down each column), gather all values, sort them, and build a brand-new chain of nodes linked through bottom. It is correct for any input, even unsorted columns, which is also a hint that it is doing more work than necessary.
Algorithm.
- For each top-level node, walk its column via
bottomand add every value to a list. - Sort the list.
- Create a new node for each value, linking them through
bottombehind a dummy node. - Return
dummy.bottom.
static Node bruteForce(Node head) {
List<Integer> vals = new ArrayList<>();
for (Node col = head; col != null; col = col.next) {
for (Node t = col; t != null; t = t.bottom) vals.add(t.val);
}
Collections.sort(vals);
Node dummy = new Node(0), tail = dummy;
for (int v : vals) { tail.bottom = new Node(v); tail = tail.bottom; }
return dummy.bottom;
}
Complexity. Time O(N log N) for the sort over all N nodes. Space O(N) for the value list and the new nodes.
The sorting step throws away the fact that each column is already sorted. Merging sorted sequences is cheaper than sorting from scratch, and the problem expects the original nodes to be reused rather than copied.
Approach 2: Better, merge the columns one at a time
Intuition. Two sorted lists can be merged into one sorted list in linear time, exactly as in merge sort. If the columns to the right of the current one have already been flattened into a single sorted list, we only need to merge the current column into it. Recursion expresses this directly: flatten everything from head.next onward, then merge head’s column with that result. The recursion bottoms out at the last column, which is already a sorted bottom list.
Algorithm.
- If
headisnullor has nonext, returnhead. - Let
rest = better(head.next), the flattened version of all columns to the right. - Set
head.next = nullso this column no longer points sideways. - Return
mergeBottom(head, rest).
The merge walks both lists, always attaching the smaller front node to the tail of the result and clearing its next pointer, then attaches whatever remains.
static Node better(Node head) {
if (head == null || head.next == null) return head; // a single column is already a sorted bottom list
Node rest = better(head.next); // flatten columns 2..k first
head.next = null; // this column no longer points sideways
return mergeBottom(head, rest);
}
static Node mergeBottom(Node a, Node b) {
Node dummy = new Node(0), tail = dummy;
while (a != null && b != null) {
if (a.val <= b.val) { tail.bottom = a; a = a.bottom; }
else { tail.bottom = b; b = b.bottom; }
tail = tail.bottom;
tail.next = null; // keep the result strictly one-dimensional
}
tail.bottom = (a != null) ? a : b; // whatever is left is already sorted
return dummy.bottom;
}
Complexity. Time O(N × k) in the worst case: the i-th merge from the right processes all nodes of columns i..k, so a node in the last column is touched once per merge. When all columns have similar length N/k, the total is about N/k × (1 + 2 + ... + k) = O(N × k). Space O(k) for the recursion stack; no new nodes are created.
This is the solution most often presented for the problem, and it is the right one to know: it reuses nodes, needs no auxiliary data structure, and the merge step is a standard tool. Its weakness is the repeated re-merging of the right-hand lists. With k = 10^4 columns of ten nodes each, the merges perform around 5 × 10^8 comparisons, which is borderline.
Approach 3: Optimal time, k-way merge with a min-heap
Intuition. Instead of merging lists pairwise, merge all of them at once. At any moment the next value of the output is the smallest of the current front nodes of the k columns. A min-heap keyed on node value gives us that smallest front in O(log k), and when we remove a node we push the node below it in the same column, so the heap always holds exactly one candidate per non-empty column. Each node enters and leaves the heap once.
Algorithm.
- Push every top-level node into a min-heap ordered by
val. - While the heap is not empty:
- Pop the smallest node
s. Ifs.bottomexists, push it. - Clear
s.next, appendsto the result throughbottom, and make it the new tail.
- Pop the smallest node
- Set the final tail’s
bottomtonulland returndummy.bottom.
static Node optimal(Node head) {
PriorityQueue<Node> heap = new PriorityQueue<>((x, y) -> Integer.compare(x.val, y.val));
for (Node col = head; col != null; col = col.next) heap.offer(col);
Node dummy = new Node(0), tail = dummy;
while (!heap.isEmpty()) {
Node smallest = heap.poll();
if (smallest.bottom != null) heap.offer(smallest.bottom); // the next node of the same column takes its place
smallest.next = null;
tail.bottom = smallest;
tail = smallest;
}
tail.bottom = null;
return dummy.bottom;
}
Dry run on the columns [5, 7, 8, 30], [10, 20], [19, 22, 50], [28, 35, 40, 45] (first few steps):
| heap contents (by value) | popped | pushed |
|---|---|---|
| 5, 10, 19, 28 | 5 | 7 |
| 7, 10, 19, 28 | 7 | 8 |
| 8, 10, 19, 28 | 8 | 30 |
| 10, 19, 28, 30 | 10 | 20 |
| 19, 20, 28, 30 | 19 | 22 |
| 20, 22, 28, 30 | 20 | (column exhausted) |
The output so far is 5 -> 7 -> 8 -> 10 -> 19 -> 20, and the heap never holds more than four nodes.
Complexity. Time O(N log k): N pops and at most N pushes, each O(log k) since the heap never exceeds k entries. Space O(k) for the heap; no new nodes.
The order of operations inside the loop matters. Appending to the result overwrites the bottom pointer of the previous tail, which was itself a popped node. That is safe only because every node’s bottom is read, and its successor pushed, at the moment the node is popped, before the pointer can be overwritten. Push first, then append.
Compared with Approach 2, this version is asymptotically faster whenever k is large (log k versus k per node) at the cost of O(k) heap memory instead of O(k) stack. For two or three columns the recursive merge is simpler and just as fast; for thousands of columns the heap is the better tool.
Edge cases and pitfalls
- Empty input.
head == nullmust returnnull. The brute force collects nothing, the recursion returns immediately, and the heap loop never runs. - Single column. Already flat; it must be returned unchanged (Approach 2 does this via its base case).
- Columns of length one. The structure is just a top-level list; the output is those values sorted.
- Leftover
nextpointers. The result must use onlybottom. In Approach 2 every column head’snextis cleared before merging, andmergeBottomclearsnexton each attached node; in Approach 3 each popped node’snextis cleared. The test file’stoArrayasserts that nonextsurvives. - Duplicates. Equal values across columns are fine;
a.val <= b.valin the merge keeps the output stable and the heap does not care about ties. - Unsorted column heads. Approaches 1 and 3 do not require the top-level values to be sorted among themselves. Approach 2 does not either, since it merges rather than concatenates. Only the individual columns must be sorted.
- Recursion depth. Approach 2 recurses once per column; with
10^4columns that is deep but usually within Java’s default stack. An iterative right-to-left loop removes the risk. - Rebuilding versus reusing. The brute force allocates
Nnew nodes. If the problem checks that the original nodes appear in the output, only Approaches 2 and 3 qualify.
Related problems
- Merge two Sorted Lists is the
mergeBottomstep on its own. - Sort LL uses the same merge in a divide-and-conquer sort.
- Flattening of LL is the SDE sheet entry for this problem.
- Reverse LL in group of given size K is another hard list problem solved by processing one segment at a time.