Facebook Pixel

218. The Skyline Problem

Problem Description

The Skyline Problem asks you to find the outline of a city's skyline formed by a collection of rectangular buildings when viewed from a distance.

You're given an array buildings where each building is represented as [left, right, height]:

  • left: the x-coordinate of the building's left edge
  • right: the x-coordinate of the building's right edge
  • height: the building's height

All buildings sit on flat ground at height 0.

The output should be a list of "key points" [[x1, y1], [x2, y2], ...] that define the skyline, where:

  • Each point represents a location where the skyline height changes
  • Points must be sorted by x-coordinate
  • The last point always has y-coordinate 0 to mark where the rightmost building ends
  • Consecutive horizontal lines at the same height must be merged (no redundant points)

For example, if multiple buildings create a continuous horizontal line at height 5 from x=4 to x=12, you should output [4, 5] and [12, next_height], not separate points for each building edge.

The solution uses a sweep line algorithm with a priority queue. It processes all building edges (both left and right) in sorted order. At each x-coordinate:

  1. Add any buildings that start at or before this position to the priority queue
  2. Remove any buildings that have ended from the queue
  3. The current maximum height in the queue determines the skyline height at this position
  4. Only add a key point if the height changes from the previous point

This approach efficiently tracks the tallest building at each x-coordinate to construct the skyline outline.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The key insight is to think about what causes the skyline to change. The skyline only changes height at specific x-coordinates - namely, where buildings start or end. Between these critical points, the skyline remains at a constant height determined by the tallest building present at that location.

Imagine walking along the x-axis from left to right. At any point, the skyline height equals the maximum height among all buildings that "cover" that x-coordinate. This height changes only when:

  1. A new building starts (potentially raising the skyline if it's taller than current buildings)
  2. A building ends (potentially lowering the skyline if it was the tallest)

This observation leads us to focus on the building edges rather than entire buildings. We can collect all left and right edges, sort them by x-coordinate, and process them in order.

At each edge position, we need to know: "What's the tallest building currently active?" This naturally suggests using a data structure that can:

  • Add new buildings as we encounter their left edges
  • Remove buildings as we pass their right edges
  • Quickly tell us the maximum height among active buildings

A max heap (priority queue) fits perfectly. As we sweep through the x-coordinates:

  • When we reach a building's left edge, add it to our heap
  • When we pass a building's right edge, remove it from the heap
  • The top of the heap always gives us the current skyline height

The skyline changes only when the maximum height changes. By comparing the current maximum with the previous one, we know when to add a new key point to our result.

This "sweep line" approach transforms a complex 2D problem into a simpler 1D problem of tracking maximum values over intervals, making it much more manageable to solve.

Solution Approach

The implementation uses a sweep line algorithm with a priority queue to efficiently track the maximum height at each critical point.

Step 1: Collect and Sort Critical Points

lines = []
for build in buildings:
    lines.extend([build[0], build[1]])
lines.sort()

We extract all x-coordinates where buildings start or end. These are the only positions where the skyline can change height. Sorting them allows us to process events from left to right.

Step 2: Initialize Data Structures

  • pq: A priority queue (max heap) to track active buildings. Python's PriorityQueue is a min heap, so we negate heights to simulate a max heap.
  • skys: The result list to store skyline key points
  • city: A pointer to track which buildings we've processed

Step 3: Process Each Critical Point

for line in lines:
    # Add buildings that start at or before current position
    while city < n and buildings[city][0] <= line:
        pq.put([-buildings[city][2], buildings[city][0], buildings[city][1]])
        city += 1

For each x-coordinate, we first add all buildings that start at or before this position to the priority queue. The queue entry is [-height, left, right] where height is negated for max heap behavior.

Step 4: Remove Expired Buildings

while not pq.empty() and pq.queue[0][2] <= line:
    pq.get()

Remove buildings whose right edge is at or before the current position, as they no longer contribute to the skyline.

Step 5: Determine Current Height

high = 0
if not pq.empty():
    high = -pq.queue[0][0]

The current skyline height is the maximum height among active buildings (top of the heap), or 0 if no buildings are active.

Step 6: Add Key Point if Height Changes

if len(skys) > 0 and skys[-1][1] == high:
    continue
skys.append([line, high])

Only add a new key point if the height differs from the previous one. This ensures no consecutive horizontal lines at the same height, satisfying the problem's requirement for a minimal representation.

The algorithm processes each building edge once and performs heap operations, resulting in O(n log n) time complexity where n is the number of buildings. The space complexity is O(n) for storing the edges and heap.

Ready to land your dream job?

Unlock your dream job with a 3-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through a small example with three buildings: [[1,3,3], [2,4,4], [5,7,1]]

Initial Setup:

  • Building 1: left=1, right=3, height=3 (covers x from 1 to 3)
  • Building 2: left=2, right=4, height=4 (covers x from 2 to 4)
  • Building 3: left=5, right=7, height=1 (covers x from 5 to 7)

Step 1: Collect critical points Extract all x-coordinates where buildings start or end: [1, 3, 2, 4, 5, 7] After sorting: [1, 2, 3, 4, 5, 7]

Step 2: Process each critical point

At x=1:

  • Add Building 1 (starts at 1) to heap: [[-3, 1, 3]]
  • Current max height: 3
  • Skyline changes from 0 to 3
  • Add key point: [1, 3]

At x=2:

  • Add Building 2 (starts at 2) to heap: [[-4, 2, 4], [-3, 1, 3]]
  • Current max height: 4
  • Skyline changes from 3 to 4
  • Add key point: [2, 4]

At x=3:

  • No new buildings start
  • Remove Building 1 (ends at 3) from heap: [[-4, 2, 4]]
  • Current max height: 4
  • Skyline stays at 4 (no change)
  • No key point added

At x=4:

  • No new buildings start
  • Remove Building 2 (ends at 4) from heap: []
  • Current max height: 0 (no active buildings)
  • Skyline changes from 4 to 0
  • Add key point: [4, 0]

At x=5:

  • Add Building 3 (starts at 5) to heap: [[-1, 5, 7]]
  • Current max height: 1
  • Skyline changes from 0 to 1
  • Add key point: [5, 1]

At x=7:

  • No new buildings start
  • Remove Building 3 (ends at 7) from heap: []
  • Current max height: 0
  • Skyline changes from 1 to 0
  • Add key point: [7, 0]

Final Result: [[1,3], [2,4], [4,0], [5,1], [7,0]]

This represents the skyline that:

  • Rises to height 3 at x=1
  • Rises to height 4 at x=2
  • Drops to ground at x=4
  • Rises to height 1 at x=5
  • Returns to ground at x=7

Solution Implementation

1from queue import PriorityQueue
2from typing import List
3
4
5class Solution:
6    def getSkyline(self, buildings: List[List[int]]) -> List[List[int]]:
7        """
8        Find the skyline formed by buildings.
9      
10        Args:
11            buildings: List of [left, right, height] representing each building
12          
13        Returns:
14            List of key points [x, height] representing the skyline
15        """
16        # Result list to store skyline key points
17        skyline_points = []
18      
19        # Collect all x-coordinates (both start and end of buildings)
20        x_coordinates = []
21        for building in buildings:
22            x_coordinates.extend([building[0], building[1]])
23        x_coordinates.sort()
24      
25        # Priority queue to store active buildings (max heap by height)
26        # Format: [-height, left, right] (negative for max heap behavior)
27        active_buildings = PriorityQueue()
28      
29        # Index to track which building to process next
30        building_index = 0
31        total_buildings = len(buildings)
32      
33        # Process each x-coordinate
34        for current_x in x_coordinates:
35            # Add all buildings that start at or before current x-coordinate
36            while building_index < total_buildings and buildings[building_index][0] <= current_x:
37                building = buildings[building_index]
38                # Store as negative height for max heap behavior
39                active_buildings.put([-building[2], building[0], building[1]])
40                building_index += 1
41          
42            # Remove all buildings that have ended before or at current x-coordinate
43            while not active_buildings.empty() and active_buildings.queue[0][2] <= current_x:
44                active_buildings.get()
45          
46            # Get the current maximum height
47            current_height = 0
48            if not active_buildings.empty():
49                # Convert back from negative to get actual height
50                current_height = -active_buildings.queue[0][0]
51          
52            # Only add point if height changes (avoid duplicate heights)
53            if skyline_points and skyline_points[-1][1] == current_height:
54                continue
55              
56            skyline_points.append([current_x, current_height])
57      
58        return skyline_points
59
1class Solution {
2    public List<List<Integer>> getSkyline(int[][] buildings) {
3        // Collect all unique x-coordinates (building starts and ends)
4        Set<Integer> uniquePositions = new TreeSet<>();
5        for (int[] building : buildings) {
6            uniquePositions.add(building[0]);  // left edge
7            uniquePositions.add(building[1]);  // right edge
8        }
9      
10        // Create a mapping from x-coordinate to compressed index
11        Map<Integer, Integer> positionToIndex = new HashMap<>();
12        int index = 0;
13        for (int position : uniquePositions) {
14            positionToIndex.put(position, index++);
15        }
16      
17        // Track the maximum height at each compressed position
18        int[] maxHeights = new int[positionToIndex.size()];
19        for (int[] building : buildings) {
20            int leftIndex = positionToIndex.get(building[0]);
21            int rightIndex = positionToIndex.get(building[1]);
22            int height = building[2];
23          
24            // Update heights for all positions covered by this building
25            for (int i = leftIndex; i < rightIndex; i++) {
26                maxHeights[i] = Math.max(maxHeights[i], height);
27            }
28        }
29      
30        // Convert compressed positions back to actual x-coordinates
31        List<Integer> sortedPositions = new ArrayList<>(uniquePositions);
32      
33        // Build the skyline by finding height changes
34        List<List<Integer>> skyline = new ArrayList<>();
35        int previousHeight = 0;
36      
37        for (int i = 0; i < maxHeights.length; i++) {
38            // Check if height changes from previous
39            if (maxHeights[i] != previousHeight) {
40                // Add key point when height changes
41                skyline.add(Arrays.asList(sortedPositions.get(i), maxHeights[i]));
42                previousHeight = maxHeights[i];
43            }
44        }
45      
46        // Add final point if the last building doesn't extend to the end
47        if (previousHeight != 0) {
48            skyline.add(Arrays.asList(sortedPositions.get(sortedPositions.size() - 1), 0));
49        }
50      
51        return skyline;
52    }
53}
54
1class Solution {
2public:
3    vector<pair<int, int>> getSkyline(vector<vector<int>>& buildings) {
4        // Collect all unique x-coordinates (building starts and ends)
5        set<int> uniquePositions;
6        for (const auto& building : buildings) {
7            uniquePositions.insert(building[0]);  // left edge
8            uniquePositions.insert(building[1]);  // right edge
9        }
10      
11        // Create a mapping from x-coordinate to compressed index
12        map<int, int> positionToIndex;
13        int index = 0;
14        for (int position : uniquePositions) {
15            positionToIndex[position] = index++;
16        }
17      
18        // Track the maximum height at each compressed position
19        vector<int> maxHeights(positionToIndex.size(), 0);
20        for (const auto& building : buildings) {
21            int leftIndex = positionToIndex[building[0]];
22            int rightIndex = positionToIndex[building[1]];
23            int height = building[2];
24          
25            // Update heights for all positions covered by this building
26            for (int i = leftIndex; i < rightIndex; ++i) {
27                maxHeights[i] = max(maxHeights[i], height);
28            }
29        }
30      
31        // Convert compressed positions back to actual x-coordinates
32        vector<int> sortedPositions(uniquePositions.begin(), uniquePositions.end());
33      
34        // Build the skyline by finding height changes
35        vector<pair<int, int>> skyline;
36        for (int i = 0; i < maxHeights.size(); ++i) {
37            // Check if height changes at next position
38            if (i + 1 >= maxHeights.size() || maxHeights[i] != maxHeights[i + 1]) {
39                skyline.push_back({sortedPositions[i], maxHeights[i]});
40            } else {
41                // Handle consecutive positions with same height
42                int startIndex = i;
43                skyline.push_back({sortedPositions[startIndex], maxHeights[i]});
44              
45                // Skip positions with same height
46                while (i + 1 < maxHeights.size() && maxHeights[i] == maxHeights[i + 1]) {
47                    ++i;
48                }
49            }
50        }
51      
52        return skyline;
53    }
54};
55
1function getSkyline(buildings: number[][]): number[][] {
2    // Collect all unique x-coordinates (building starts and ends)
3    const uniquePositions = new Set<number>();
4    for (const building of buildings) {
5        uniquePositions.add(building[0]); // left edge
6        uniquePositions.add(building[1]); // right edge
7    }
8  
9    // Convert set to sorted array and create position to index mapping
10    const sortedPositions = Array.from(uniquePositions).sort((a, b) => a - b);
11    const positionToIndex = new Map<number, number>();
12    sortedPositions.forEach((position, index) => {
13        positionToIndex.set(position, index);
14    });
15  
16    // Track the maximum height at each compressed position
17    const maxHeights = new Array(sortedPositions.length).fill(0);
18    for (const building of buildings) {
19        const leftIndex = positionToIndex.get(building[0])!;
20        const rightIndex = positionToIndex.get(building[1])!;
21        const height = building[2];
22      
23        // Update heights for all positions covered by this building
24        for (let i = leftIndex; i < rightIndex; i++) {
25            maxHeights[i] = Math.max(maxHeights[i], height);
26        }
27    }
28  
29    // Build the skyline by finding height changes
30    const skyline: number[][] = [];
31    let i = 0;
32  
33    while (i < maxHeights.length) {
34        const currentHeight = maxHeights[i];
35        const currentPosition = sortedPositions[i];
36      
37        // Add key point when height changes
38        if (i === 0 || maxHeights[i - 1] !== currentHeight) {
39            skyline.push([currentPosition, currentHeight]);
40        }
41      
42        // Skip consecutive positions with the same height
43        while (i < maxHeights.length && maxHeights[i] === currentHeight) {
44            i++;
45        }
46      
47        // Add ending key point if we've reached a height change
48        if (i < maxHeights.length) {
49            skyline.push([sortedPositions[i], maxHeights[i]]);
50        } else if (currentHeight > 0) {
51            // Add final key point to bring height back to 0
52            skyline.push([sortedPositions[sortedPositions.length - 1], 0]);
53        }
54    }
55  
56    return skyline;
57}
58

Time and Space Complexity

Time Complexity: O(nΒ² log n)

The algorithm processes each building and creates critical points (start and end positions):

  • Creating the lines array by extracting start and end points from all buildings: O(n)
  • Sorting the lines array containing 2n points: O(n log n)
  • Main loop iterating through each line position: O(n) iterations
    • Inner while loop for adding buildings to priority queue: Each building is added once across all iterations, contributing O(n log n) total
    • Inner while loop for removing expired buildings: In worst case, we might check all buildings multiple times. Since we're directly accessing pq.queue[0] without popping unless needed, and each building might be checked against multiple line positions, this contributes O(nΒ²) in worst case
    • Priority queue operations (put/get): O(log n) per operation

The bottleneck is the second inner while loop that checks expired buildings, which can lead to O(nΒ²) comparisons in worst case when buildings have overlapping ranges.

Space Complexity: O(n)

  • lines array stores 2n points: O(n)
  • Priority queue can contain at most n buildings: O(n)
  • skys result array can have at most 2n points: O(n)
  • Other variables use constant space: O(1)

Total space complexity is O(n).

Common Pitfalls

Pitfall: Incorrect Handling of Buildings with Same X-Coordinates

One of the most common pitfalls in the skyline problem occurs when multiple buildings share the same x-coordinate for their edges. The current implementation processes all events at the same x-coordinate sequentially, which can lead to incorrect intermediate states and spurious key points.

Problematic Scenarios:

  1. Multiple buildings starting at the same x-coordinate: If buildings A (height 5) and B (height 10) both start at x=2, the algorithm might add A first, record height 5, then add B and record height 10, creating an incorrect intermediate point [2, 5].

  2. Building ending while another starts at the same x: If building A ends at x=5 and building B starts at x=5, processing the removal before the addition could create a false gap [5, 0] when the skyline should continue seamlessly.

  3. Multiple buildings ending at the same x-coordinate: Similar issues arise when several buildings end simultaneously.

Example of the Problem:

buildings = [[0, 2, 3], [2, 5, 3]]  # Two adjacent buildings of same height
# Current approach might produce: [[0, 3], [2, 0], [2, 3], [5, 0]]
# Correct output should be: [[0, 3], [5, 0]]

Solution: Process All Events at Same X-Coordinate Together

Instead of processing events one by one, collect all events at the same x-coordinate and handle them as a batch:

from queue import PriorityQueue
from typing import List

class Solution:
    def getSkyline(self, buildings: List[List[int]]) -> List[List[int]]:
        # Create events: (x, event_type, height)
        # event_type: 0 for start, 1 for end
        events = []
        for left, right, height in buildings:
            events.append((left, 0, height))  # Building start
            events.append((right, 1, height)) # Building end
      
        # Sort events: by x-coordinate, then by type (starts before ends),
        # then by height (taller first for starts, shorter first for ends)
        events.sort(key=lambda x: (x[0], x[1], -x[2] if x[1] == 0 else x[2]))
      
        result = []
        active_heights = PriorityQueue()
        active_heights.put(0)  # Ground level
      
        # Track active buildings with their end positions
        active_buildings = {}  # height -> count
      
        i = 0
        while i < len(events):
            current_x = events[i][0]
          
            # Get previous max height
            prev_max = self._get_max_height(active_buildings)
          
            # Process all events at current x-coordinate
            while i < len(events) and events[i][0] == current_x:
                x, event_type, height = events[i]
              
                if event_type == 0:  # Building starts
                    active_buildings[height] = active_buildings.get(height, 0) + 1
                else:  # Building ends
                    active_buildings[height] -= 1
                    if active_buildings[height] == 0:
                        del active_buildings[height]
              
                i += 1
          
            # Get new max height after processing all events at this x
            new_max = self._get_max_height(active_buildings)
          
            # Only add point if height changed
            if new_max != prev_max:
                result.append([current_x, new_max])
      
        return result
  
    def _get_max_height(self, active_buildings):
        return max(active_buildings.keys()) if active_buildings else 0

Alternative Solution Using Multiset/TreeMap Pattern:

For better performance with frequent max queries, use a data structure that maintains sorted order:

from sortedcontainers import SortedList

class Solution:
    def getSkyline(self, buildings: List[List[int]]) -> List[List[int]]:
        events = []
        for left, right, height in buildings:
            # Negative height for start events ensures they're processed first
            # when at same x-coordinate
            events.append((left, -height))  # Start event
            events.append((right, height))   # End event
        events.sort()
      
        result = []
        heights = SortedList([0])  # Multiset of active heights
      
        i = 0
        while i < len(events):
            current_x = events[i][0]
          
            # Process all events at same x-coordinate
            while i < len(events) and events[i][0] == current_x:
                x, h = events[i]
                if h < 0:  # Start event
                    heights.add(-h)
                else:  # End event
                    heights.remove(h)
                i += 1
          
            # Check if max height changed
            max_height = heights[-1]
            if not result or result[-1][1] != max_height:
                result.append([current_x, max_height])
      
        return result

This approach ensures that all changes at the same x-coordinate are processed atomically, preventing intermediate incorrect states from appearing in the output.

Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

What's the output of running the following function using input [30, 20, 10, 100, 33, 12]?

1def fun(arr: List[int]) -> List[int]:
2    import heapq
3    heapq.heapify(arr)
4    res = []
5    for i in range(3):
6        res.append(heapq.heappop(arr))
7    return res
8
1public static int[] fun(int[] arr) {
2    int[] res = new int[3];
3    PriorityQueue<Integer> heap = new PriorityQueue<>();
4    for (int i = 0; i < arr.length; i++) {
5        heap.add(arr[i]);
6    }
7    for (int i = 0; i < 3; i++) {
8        res[i] = heap.poll();
9    }
10    return res;
11}
12
1class HeapItem {
2    constructor(item, priority = item) {
3        this.item = item;
4        this.priority = priority;
5    }
6}
7
8class MinHeap {
9    constructor() {
10        this.heap = [];
11    }
12
13    push(node) {
14        // insert the new node at the end of the heap array
15        this.heap.push(node);
16        // find the correct position for the new node
17        this.bubble_up();
18    }
19
20    bubble_up() {
21        let index = this.heap.length - 1;
22
23        while (index > 0) {
24            const element = this.heap[index];
25            const parentIndex = Math.floor((index - 1) / 2);
26            const parent = this.heap[parentIndex];
27
28            if (parent.priority <= element.priority) break;
29            // if the parent is bigger than the child then swap the parent and child
30            this.heap[index] = parent;
31            this.heap[parentIndex] = element;
32            index = parentIndex;
33        }
34    }
35
36    pop() {
37        const min = this.heap[0];
38        this.heap[0] = this.heap[this.size() - 1];
39        this.heap.pop();
40        this.bubble_down();
41        return min;
42    }
43
44    bubble_down() {
45        let index = 0;
46        let min = index;
47        const n = this.heap.length;
48
49        while (index < n) {
50            const left = 2 * index + 1;
51            const right = left + 1;
52
53            if (left < n && this.heap[left].priority < this.heap[min].priority) {
54                min = left;
55            }
56            if (right < n && this.heap[right].priority < this.heap[min].priority) {
57                min = right;
58            }
59            if (min === index) break;
60            [this.heap[min], this.heap[index]] = [this.heap[index], this.heap[min]];
61            index = min;
62        }
63    }
64
65    peek() {
66        return this.heap[0];
67    }
68
69    size() {
70        return this.heap.length;
71    }
72}
73
74function fun(arr) {
75    const heap = new MinHeap();
76    for (const x of arr) {
77        heap.push(new HeapItem(x));
78    }
79    const res = [];
80    for (let i = 0; i < 3; i++) {
81        res.push(heap.pop().item);
82    }
83    return res;
84}
85

Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More