Given a set of rectangular buildings, output the outline of the city skyline as a list of key points. Sweep a vertical line left to right over the building edges, keeping a max-heap of active heights; emit a point whenever the tallest live building changes.
Each building is [left, right, height]. We want the silhouette of the whole city as a sequence of key points [x, height], where each point marks the left endpoint of a horizontal segment of the outline; the last point always drops to height 0. For buildings = [[2,9,10],[3,7,15],[5,12,12]] the answer is [[2,10],[3,15],[7,12],[12,0]]: the outline rises to 10 at x=2, jumps to 15 at x=3, falls to 12 at x=7 (the tall middle building ends), and drops to 0 at x=12 when the last building ends.
[x, newMax]exactly when the heap's top differs from the previous max.That turns "what is the tallest building over every x" into a handful of updates at the 2n edges instead of scanning the ground inch by inch.
left and an end event at right. Encode a start as -height and an end as +height so a single sort key handles ties.x ascending; break ties by the signed height. This makes starts come before ends at the same x, and among starts the taller one first.push(height) onto the max-heap; at an end, mark that height for lazy deletion.heap.top() (0 if empty). If it differs from prevMax, append [x, top] and update prevMax.-h / +h encoding gives for free), makes the "emit only when max changes" rule produce the correct, duplicate-free outline.A binary heap has no O(log n) "remove arbitrary element". Instead we mark an ended height as pending and only discard it once it bubbles to the top — amortized O(log n) per event. A balanced multiset / TreeMap of counts is an equivalent clean alternative; divide-and-conquer (merge two skylines) also runs in O(n log n).
6 edge events from 3 buildings. Each start is stored as -height, each end as +height; sorted by x then signed height. The heap is empty and prevMax = 0.1type Building = [left: number, right: number, height: number];2type KeyPoint = [x: number, height: number];34// A tiny max-heap of heights with lazy deletion (no decrease-key needed).5class MaxHeap {6 private a: number[] = [];7 private toDelete = new Map<number, number>(); // height -> pending removals89 push(h: number): void {10 this.a.push(h);11 let i = this.a.length - 1;12 while (i > 0) {13 const parent = (i - 1) >> 1;14 if (this.a[parent] >= this.a[i]) break;15 [this.a[parent], this.a[i]] = [this.a[i], this.a[parent]];16 i = parent;17 }18 }1920 // Mark a height for removal; it is purged once it reaches the top.21 remove(h: number): void {22 this.toDelete.set(h, (this.toDelete.get(h) ?? 0) + 1);23 }2425 // Lazily discard stale tops, then report the live max (0 = ground).26 top(): number {27 while (this.a.length > 0) {28 const h = this.a[0];29 const pending = this.toDelete.get(h) ?? 0;30 if (pending === 0) return h;31 if (pending === 1) this.toDelete.delete(h);32 else this.toDelete.set(h, pending - 1);33 this.popRoot();34 }35 return 0;36 }3738 private popRoot(): void {39 const last = this.a.pop()!;40 if (this.a.length === 0) return;41 this.a[0] = last;42 let i = 0;43 const n = this.a.length;44 for (;;) {45 const l = 2 * i + 1;46 const r = 2 * i + 2;47 let big = i;48 if (l < n && this.a[l] > this.a[big]) big = l;49 if (r < n && this.a[r] > this.a[big]) big = r;50 if (big === i) break;51 [this.a[big], this.a[i]] = [this.a[i], this.a[big]];52 i = big;53 }54 }55}5657function getSkyline(buildings: Building[]): KeyPoint[] {58 // Build sweep events: a start lowers x first, ties broken to add tall first.59 const events: Array<[x: number, h: number]> = [];60 for (const [l, r, h] of buildings) {61▶ events.push([l, -h]); // start: negative height marks an "add"62▶ events.push([r, h]); // end: positive height marks a "remove"63 }64 // Sort by x; at equal x, process by signed height so adds beat removes and65 // a taller start is handled before a shorter one.66▶ events.sort((a, b) => (a[0] !== b[0] ? a[0] - b[0] : a[1] - b[1]));6768 const result: KeyPoint[] = [];69 const heap = new MaxHeap();70 let prevMax = 0; // current skyline height to the left of this x7172 for (const [x, signedH] of events) {73 if (signedH < 0) heap.push(-signedH); // a building begins74 else heap.remove(signedH); // a building ends7576 const curMax = heap.top();77 if (curMax !== prevMax) {78 // The running max changed here -> this x is a key point.79 result.push([x, curMax]);80 prevMax = curMax;81 }82 }8384 return result;85}
type Building = [left: number, right: number, height: number];
type KeyPoint = [x: number, height: number];
// A tiny max-heap of heights with lazy deletion (no decrease-key needed).
class MaxHeap {
private a: number[] = [];
private toDelete = new Map<number, number>(); // height -> pending removals
push(h: number): void {
this.a.push(h);
let i = this.a.length - 1;
while (i > 0) {
const parent = (i - 1) >> 1;
if (this.a[parent] >= this.a[i]) break;
[this.a[parent], this.a[i]] = [this.a[i], this.a[parent]];
i = parent;
}
}
// Mark a height for removal; it is purged once it reaches the top.
remove(h: number): void {
this.toDelete.set(h, (this.toDelete.get(h) ?? 0) + 1);
}
// Lazily discard stale tops, then report the live max (0 = ground).
top(): number {
while (this.a.length > 0) {
const h = this.a[0];
const pending = this.toDelete.get(h) ?? 0;
if (pending === 0) return h;
if (pending === 1) this.toDelete.delete(h);
else this.toDelete.set(h, pending - 1);
this.popRoot();
}
return 0;
}
private popRoot(): void {
const last = this.a.pop()!;
if (this.a.length === 0) return;
this.a[0] = last;
let i = 0;
const n = this.a.length;
for (;;) {
const l = 2 * i + 1;
const r = 2 * i + 2;
let big = i;
if (l < n && this.a[l] > this.a[big]) big = l;
if (r < n && this.a[r] > this.a[big]) big = r;
if (big === i) break;
[this.a[big], this.a[i]] = [this.a[i], this.a[big]];
i = big;
}
}
}
function getSkyline(buildings: Building[]): KeyPoint[] {
// Build sweep events: a start lowers x first, ties broken to add tall first.
const events: Array<[x: number, h: number]> = [];
for (const [l, r, h] of buildings) {
events.push([l, -h]); // start: negative height marks an "add"
events.push([r, h]); // end: positive height marks a "remove"
}
// Sort by x; at equal x, process by signed height so adds beat removes and
// a taller start is handled before a shorter one.
events.sort((a, b) => (a[0] !== b[0] ? a[0] - b[0] : a[1] - b[1]));
const result: KeyPoint[] = [];
const heap = new MaxHeap();
let prevMax = 0; // current skyline height to the left of this x
for (const [x, signedH] of events) {
if (signedH < 0) heap.push(-signedH); // a building begins
else heap.remove(signedH); // a building ends
const curMax = heap.top();
if (curMax !== prevMax) {
// The running max changed here -> this x is a key point.
result.push([x, curMax]);
prevMax = curMax;
}
}
return result;
}remove(h) just records a pending deletion in a count map; top() discards any pending heights that have surfaced to the root before returning the real maximum (or 0 for the ground when empty).left stored as -height, and an end at right stored as +height. The sign distinguishes "add" from "remove" and doubles as the tie-breaker in the sort.x; at equal x order by signed height. Because starts are negative, they sort before ends at the same x; among two starts the taller (more negative) comes first; among two ends the shorter comes first. This single comparator removes every tie ambiguity.push its height; a positive one means it ends, so remove that height (lazily).x, read heap.top(). If the live maximum differs from prevMax, the silhouette changes here, so append [x, curMax] and advance prevMax. When the last building ends the top becomes 0, producing the final ground-level key point.2n events is O(n log n). Each event does O(1) heap pushes/removes plus an amortized O(log n) cleanup in top(), so the sweep is O(n log n) overall. Space is O(n) for the events and the heap.type Building = [left: number, right: number, height: number];
type KeyPoint = [x: number, height: number];
// A tiny max-heap of heights with lazy deletion (no decrease-key needed).
class MaxHeap {
private a: number[] = [];
private toDelete = new Map<number, number>(); // height -> pending removals
push(h: number): void {
this.a.push(h);
let i = this.a.length - 1;
while (i > 0) {
const parent = (i - 1) >> 1;
if (this.a[parent] >= this.a[i]) break;
[this.a[parent], this.a[i]] = [this.a[i], this.a[parent]];
i = parent;
}
}
// Mark a height for removal; it is purged once it reaches the top.
remove(h: number): void {
this.toDelete.set(h, (this.toDelete.get(h) ?? 0) + 1);
}
// Lazily discard stale tops, then report the live max (0 = ground).
top(): number {
while (this.a.length > 0) {
const h = this.a[0];
const pending = this.toDelete.get(h) ?? 0;
if (pending === 0) return h;
if (pending === 1) this.toDelete.delete(h);
else this.toDelete.set(h, pending - 1);
this.popRoot();
}
return 0;
}
private popRoot(): void {
const last = this.a.pop()!;
if (this.a.length === 0) return;
this.a[0] = last;
let i = 0;
const n = this.a.length;
for (;;) {
const l = 2 * i + 1;
const r = 2 * i + 2;
let big = i;
if (l < n && this.a[l] > this.a[big]) big = l;
if (r < n && this.a[r] > this.a[big]) big = r;
if (big === i) break;
[this.a[big], this.a[i]] = [this.a[i], this.a[big]];
i = big;
}
}
}
function getSkyline(buildings: Building[]): KeyPoint[] {
// Build sweep events: a start lowers x first, ties broken to add tall first.
const events: Array<[x: number, h: number]> = [];
for (const [l, r, h] of buildings) {
events.push([l, -h]); // start: negative height marks an "add"
events.push([r, h]); // end: positive height marks a "remove"
}
// Sort by x; at equal x, process by signed height so adds beat removes and
// a taller start is handled before a shorter one.
events.sort((a, b) => (a[0] !== b[0] ? a[0] - b[0] : a[1] - b[1]));
const result: KeyPoint[] = [];
const heap = new MaxHeap();
let prevMax = 0; // current skyline height to the left of this x
for (const [x, signedH] of events) {
if (signedH < 0) heap.push(-signedH); // a building begins
else heap.remove(signedH); // a building ends
const curMax = heap.top();
if (curMax !== prevMax) {
// The running max changed here -> this x is a key point.
result.push([x, curMax]);
prevMax = curMax;
}
}
return result;
}2n events is O(n log n). Each event does O(1) heap pushes/removes plus an amortized O(log n) cleanup in top(), so the sweep is O(n log n) overall. Space is O(n) for the events and the heap.Split the buildings in half, recursively skyline each half, then merge the two outlines like merge-sort— walking both by x and tracking each side's current height, emitting a point only when the combined max changes.
type Building = [left: number, right: number, height: number];
type KeyPoint = [x: number, height: number];
// Merge two already-computed skylines into one. We walk both lists by x,
// keeping each side's current height; the combined height is their max, and
// we emit a point only when that combined max actually changes.
function mergeSkylines(left: KeyPoint[], right: KeyPoint[]): KeyPoint[] {
const merged: KeyPoint[] = [];
let i = 0;
let j = 0;
let leftH = 0; // current height contributed by the left skyline
let rightH = 0; // current height contributed by the right skyline
let prevMax = 0;
while (i < left.length && j < right.length) {
let x: number;
// Advance whichever side has the smaller x (or both, on a tie).
if (left[i][0] < right[j][0]) {
x = left[i][0];
leftH = left[i][1];
i++;
} else if (right[j][0] < left[i][0]) {
x = right[j][0];
rightH = right[j][1];
j++;
} else {
x = left[i][0];
leftH = left[i][1];
rightH = right[j][1];
i++;
j++;
}
const curMax = Math.max(leftH, rightH);
if (curMax !== prevMax) {
merged.push([x, curMax]);
prevMax = curMax;
}
}
// Drain whichever list still has points; the other side is at height 0 now,
// so its contribution can't change the running max again.
while (i < left.length) {
const [x, h] = left[i++];
if (h !== prevMax) {
merged.push([x, h]);
prevMax = h;
}
}
while (j < right.length) {
const [x, h] = right[j++];
if (h !== prevMax) {
merged.push([x, h]);
prevMax = h;
}
}
return merged;
}
function getSkyline(buildings: Building[]): KeyPoint[] {
// Base cases: empty -> no outline; single building -> rise then drop.
if (buildings.length === 0) return [];
if (buildings.length === 1) {
const [l, r, h] = buildings[0];
return [[l, h], [r, 0]];
}
// Split in half and recurse, then merge — exactly like merge-sort.
const mid = buildings.length >> 1;
const left = getSkyline(buildings.slice(0, mid));
const right = getSkyline(buildings.slice(mid));
return mergeSkylines(left, right);
}log nlevels of recursion and each level merges a total of O(n) key points. Merging two skylines is the same two-pointer walk as merge-sort's combine step.Collect every distinct building edge as a candidate x; at each one compute the max height among buildings covering it, and emit a point whenever that height differs from the previous boundary.
type Building = [left: number, right: number, height: number];
type KeyPoint = [x: number, height: number];
function getSkyline(buildings: Building[]): KeyPoint[] {
if (buildings.length === 0) return [];
// Every place the outline can change is a left or right edge of some building.
const xs: number[] = [];
for (const [l, r] of buildings) {
xs.push(l, r);
}
xs.sort((a, b) => a - b);
const result: KeyPoint[] = [];
let prevMax = 0;
let prevX = Number.NaN;
for (const x of xs) {
if (x === prevX) continue; // skip duplicate boundaries
prevX = x;
// Tallest building whose half-open span [l, r) covers x. Using a half-open
// interval means a building stops contributing exactly at its right edge,
// which is what lets the outline fall there.
let curMax = 0;
for (const [l, r, h] of buildings) {
if (l <= x && x < r && h > curMax) curMax = h;
}
if (curMax !== prevMax) {
result.push([x, curMax]);
prevMax = curMax;
}
}
return result;
}l <= x && x < r is what produces the final drop to 0 at the rightmost edge.| outline / silhouette of overlapping rectangles | sweep line over edges |
| answer changes only at interval endpoints | event sort + active set |
| need the tallest among a changing set | max-heap (priority queue) |
| remove arbitrary element from a binary heap | lazy deletion via count map |
const events = [];
for (const [l, r, h] of buildings) { events.push([l, -h]); events.push([r, h]); }
events.sort((a, b) => a[0] - b[0] || a[1] - b[1]); // x, then signed height
let prevMax = 0; const result = [];
for (const [x, sh] of events) {
sh < 0 ? heap.push(-sh) : heap.remove(sh); // start adds, end removes
const cur = heap.top(); // 0 = ground
if (cur !== prevMax) { result.push([x, cur]); prevMax = cur; }
}[x, height] marking the left end of a horizontal segment of the outline — emitted whenever the running max height changes (the last drops to 0).buildings = [[2,9,10],[3,7,15],[5,12,12]], the correct skyline is: