Problem Walkthrough

The Skyline Problem LeetCode 218 Guide

Solve LeetCode 218 The Skyline Problem using a line sweep that processes building edge events left to right, maintaining a max-heap of active building heights to detect height changes at critical x-coordinates and emit the minimal set of skyline key points.

10 min read|

The Skyline Problem

LeetCode 218

Problem Overview — Buildings, Edges, and Key Points

LeetCode 218 The Skyline Problem takes a list of buildings, each described as [left, right, height], and asks you to output the skyline silhouette as a list of key points. Each key point [x, h] marks an x-coordinate where the visible skyline height changes: h is the new height after the change. The output is the minimal list of such points that fully describes the silhouette outline when looking at all buildings from the front.

For example, given buildings [[2,9,10],[3,7,15],[5,12,12],[15,20,10],[19,24,8]], the skyline is [[2,10],[3,15],[7,12],[12,0],[15,10],[20,8],[24,0]]. Each key point records the x where the outline profile transitions to a new height. The final key point in any valid output must end with height 0, signaling that the skyline returns to ground level.

The key challenge is that buildings can overlap arbitrarily. A tall building might be fully contained within a wide short one, two buildings might share only one edge, or many buildings might all start at the same x. An efficient solution must identify exactly those x-positions where the maximum visible height changes — skipping positions where overlapping buildings maintain a constant peak — and do so in O(n log n) time.

  • Given: list of buildings, each [left, right, height], 1 <= buildings.length <= 10^4
  • Return: skyline as list of key points [x, height] where height changes
  • Key point [x, h] means: at x-coordinate x, the skyline height becomes h
  • Buildings can overlap, be nested, or share edges
  • Output must end with [rightmost_x, 0] — skyline returns to ground
  • Example: [[2,9,10]] → [[2,10],[9,0]]

The Line Sweep Idea — Events at Building Edges

The line sweep approach transforms each building into two events: a start event at the left edge and an end event at the right edge. Instead of reasoning about all x-coordinates continuously, the algorithm only visits the finite set of x-values where something changes — the building edges. Between consecutive events, the set of active buildings and therefore the skyline height remains constant.

At each x-coordinate, the skyline height equals the maximum height among all currently active buildings — those whose left edge has been passed but whose right edge has not yet been reached. By sweeping events left to right and maintaining a data structure of active heights, the algorithm can detect each height change in O(log n) per event, giving O(n log n) total for n buildings generating 2n events.

The event list is built by iterating over buildings: each building [l, r, h] produces a start event at x=l with height h and an end event at x=r with height h. After sorting all events by x, the sweep processes them in order, updating the active heights set and checking whether the current maximum changed. If it did, a new key point is added to the output.

💡

Start Events Add Height, End Events Remove Height

The core mental model for the line sweep is simple: treat left edges as "add height h" events and right edges as "remove height h" events. At any x during the sweep, the skyline height is the maximum of all heights that have been added but not yet removed. This transforms the problem from geometric reasoning about overlapping rectangles into a straightforward event-driven maximum-tracking problem over a sorted sequence of updates.

Event Processing with Max-Heap

A max-heap efficiently tracks the current maximum active height. As events are processed left to right, start events push a height onto the heap and end events mark that height for removal. After processing all events at a given x-coordinate, the heap top gives the current maximum height. If this maximum differs from the previous step's maximum, a key point [x, new_max] is appended to the result.

The algorithm initializes with an empty heap and prev_max = 0 (ground level). For each group of events at the same x, all starts are processed first (pushing their heights) and then all ends are processed (marking heights for removal). After the group, the heap top is queried: if it differs from prev_max, emit [x, heap_top] and update prev_max. This ensures that positions where starts and ends at the same x cancel out do not generate spurious key points.

The max-heap guarantees O(log n) push and O(log n) for extracting the maximum. With 2n events and O(log n) work per event, total time is O(n log n). The result list grows only when height changes occur, so its size is at most 2n but typically much smaller for non-degenerate inputs.

  1. 1Create events: for each [l, r, h], add (l, -h, "start") and (r, h, "end")
  2. 2Sort events by x; for same x: starts before ends; taller starts first; shorter ends first
  3. 3Initialize max-heap, prev_max = 0, result = []
  4. 4Group events by x-coordinate
  5. 5For each group: push heights from start events onto heap
  6. 6For each group: mark heights from end events for removal (lazy deletion)
  7. 7After group: clean heap top by removing any marked heights
  8. 8current_max = heap top (or 0 if empty)
  9. 9If current_max != prev_max: append [x, current_max] to result, update prev_max
  10. 10Return result

Handling Ties and Edge Cases at the Same X

When multiple buildings share the same left or right edge, the order of event processing at that x-coordinate critically determines correctness. If a building starts and another ends at the same x, processing the end before the start would temporarily show the ground height between them and generate a spurious key point for a dip that does not exist in the actual skyline. The rule is: always process start events before end events at the same x.

Among multiple start events at the same x, process taller buildings first. This ensures the heap top reflects the tallest new building immediately and avoids recording an intermediate lower height. Among multiple end events at the same x, process shorter buildings first (or equivalently, process them all before querying the heap, since lazy deletion defers the actual removal to the heap top query). The critical check only happens once per x-group after all events at that x are processed.

A subtle edge case is a building of zero height or a point building where left equals right. Such buildings contribute no visible area to the skyline, so their events cancel exactly at the same x-coordinate and produce no key point as long as start-before-end ordering is maintained. Another edge case is buildings that perfectly share a boundary: building A ends at x=5 and building B starts at x=5. If A is taller than B, the skyline at x=5 transitions from A's height to B's height with a single key point.

ℹ️

Sort Events with Negative Heights for Correct Ordering

A clean implementation trick is to encode start events as (x, -height) and end events as (x, height). When sorting event tuples, Python's default tuple sort handles all the tie-breaking automatically: at the same x, negative heights (starts) sort before positive heights (ends); among starts, more negative (taller) heights sort first; among ends, smaller positive (shorter) heights sort first. This single sort call correctly orders all events without any custom comparator logic.

Lazy Deletion from the Heap

Python's heapq module implements a min-heap with no built-in support for arbitrary element removal. To remove a specific height when a building ends, the standard technique is lazy deletion: instead of removing the height immediately, record it in a "to-remove" multiset (a Counter or dict). When querying the heap top, pop elements from the heap until the top is not in the to-remove set (or its to-remove count is zero). This amortizes removal work across future queries.

Lazy deletion is correct because a height in the to-remove set has logically been removed even if it still sits in the heap. The heap top represents the current maximum only when it is not in the to-remove set. Each element is pushed at most once and popped at most once, so the total heap operations across all events are O(n), and each operation is O(log n), preserving the overall O(n log n) bound despite the deferred removal.

Java offers a cleaner alternative: TreeMap<Integer, Integer> where keys are heights and values are counts. Adding a building increments count at its height; removing a building decrements count and removes the key if count reaches zero. The current maximum is simply treeMap.lastKey(), which is O(log n). This avoids lazy deletion entirely and makes the implementation more readable, at the cost of slightly higher constant factors due to TreeMap's balanced BST overhead compared to a heap.

  1. 1Python lazy deletion: maintain a Counter for heights to remove
  2. 2When building ends: to_remove[height] += 1
  3. 3When querying heap top: while heap and heap[0] in to_remove and to_remove[heap[0]] > 0: pop and decrement
  4. 4Current max = -heap[0] if heap else 0 (using negated min-heap for max-heap)
  5. 5Java TreeMap approach: Map<Integer, Integer> active = new TreeMap<>()
  6. 6Start event: active.merge(height, 1, Integer::sum)
  7. 7End event: active.merge(height, -1, Integer::sum); if count == 0: active.remove(height)
  8. 8Current max = active.isEmpty() ? 0 : active.lastKey()

Code Walkthrough — Python and Java

In Python, the solution builds an events list with (x, -h) for starts and (x, h) for ends, sorts it, and iterates with a heap and Counter. A negated min-heap simulates a max-heap: push -h for each start. For lazy deletion, push -h into to_remove for each end. After processing each group of same-x events, clean the heap top. Emit a key point when -heap[0] != prev_max. The total code is around 20 lines. Time is O(n log n); space is O(n) for the heap, events list, and to_remove counter.

In Java, the cleanest approach uses a TreeMap<Integer, Integer> for active heights. Build a list of int[] events where [x, -h] means start and [x, h] means end. Sort with Arrays.sort using a lambda: compare by x first, then by height (so negative heights come first). Process each event: if height < 0, add -height to treemap; else decrement and possibly remove height from treemap. After each event, check if treemap.lastKey() changed. Emit [x, newMax] when it does. Java's TreeMap handles all tie-breaking and removal cleanly without lazy deletion.

The most subtle correctness point in both implementations is the interaction between sorting and grouping. Sorting ensures events at the same x are processed in the correct order, but the height-change check must happen after all events at a given x are processed — not after each individual event. In the Java TreeMap approach, check after each event whether the current max differs from the previous x's max, and only emit if x has changed or all events at x are processed. Python implementations often process all same-x events in a batch using itertools.groupby.

⚠️

Most Common Bug: Same X-Coordinate Handling

The most common bug is not handling overlapping buildings at the same x-coordinate correctly. If you check for a height change after every single event rather than after all events at a given x, you will emit spurious key points for transient intermediate states. Similarly, processing end events before start events at the same x will produce a phantom dip to ground level even when a new building starts right where another ends. Always batch-process all events sharing an x-coordinate before checking if the skyline height changed.

Ready to master algorithm patterns?

YeetCode flashcards help you build pattern recognition through active recall and spaced repetition.

Start practicing now