../

Problem patterns

The recurring shapes behind most interview problems: how to spot each one from the prompt, a template in TypeScript, JavaScript and Python, its cost and the classic problems that use it. Structures are in Linear structures and Trees & graphs (including the MinHeap used below), binary search in Sorting & searching, BFS and friends in Graph algorithms, backtracking and DP in Recursion & DP. How to use all this under a clock is in Coding interviews.

Signal → pattern

Read the prompt for these signals first; the constraints (n ≤ 20, n ≤ 10⁵) confirm the target cost.

Signal in the promptTryTypical cost
sorted array + a pair / triple hitting a targettwo pointers, opposite endsO(n) per pass
in-place remove / dedupe / partitiontwo pointers, same directionO(n), O(1) space
contiguous subarray / substring with a limit ("at most k", "no repeats")sliding windowO(n)
subarray sums, range sum queries, "sum equals k" with negativesprefix sums + hash mapO(n)
next greater / smaller, span, "days until"monotonic stackO(n)
max / min of every windowmonotonic dequeO(n)
top k, k-th largest, k closest, merge k sortedheapO(n log k)
linked list cycle, middle, "find the duplicate"fast & slow pointersO(n), O(1) space
overlapping ranges, meetings, calendarssort + sweepO(n log n)
values in 1..n in an array of size n, "missing / duplicate"cyclic sortO(n), O(1) space
XOR, parity, "appears once", subsets of ≤ 20 itemsbitsO(n) or O(2ⁿ)
all combinations / permutations / placementsbacktracking, Recursion & DPexponential
min / max / count over choices, subproblems repeatDP, Recursion & DPstates × transitions
fewest steps, nearest, unweighted gridBFS, Graph algorithmsO(V + E)
prerequisites, build ordertopological sort, Graph algorithmsO(V + E)
"are these connected", merging groups onlineunion-find≈ O(1) per op
"minimum capacity / speed such that…" (monotonic yes/no)binary search on the answer, Sorting & searchingO(n log range)
prefix / word lookups, autocompletetrie, Trees & graphsO(word length)
Constraint on nTarget (≈ 10⁸ simple ops / s)Likely approach
≤ 10O(n!)permutations
≤ 20O(2ⁿ · n)subsets, bitmask DP
≤ 500O(n³)triple loop, interval DP
≤ 5 000O(n²)2-D DP, all pairs
≤ 10⁵ – 10⁶O(n log n)sort, heap, binary search
≤ 10⁸O(n)window, hashing, prefix sums
above 10⁸O(log n) or O(1)binary search, math

Full table and the reasoning: Big-O.

Two pointers

Two indexes walk the array instead of a nested loop. Opposite ends (sorted input): compare the pair, move the side that can only improve the answer. Same direction: fast reads, slow marks where the next kept item goes.

1 0 3 1 4 2 6 3 8 4 11 5 lo hi sorted xs, target 10 1 + 11 = 12 too big hi moves left 1 + 8 = 9 too small lo moves right 3 + 8 = 11 too big hi moves left 3 + 6 = 9 too small lo moves right 4 + 6 = 10 found return [2, 3] each step discards a whole row or column of pairs, so n − 1 steps at most
Opposite ends on a sorted array: too big moves hi left, too small moves lo right.

type Pair = [number, number];
 
// opposite ends: indexes of a pair summing to t
function pairSum(xs: number[], t: number): Pair | null {
  let lo = 0;
  let hi = xs.length - 1;
  while (lo < hi) {
    const s = xs[lo] + xs[hi];
    if (s === t) return [lo, hi];
    if (s < t) lo++; // too small: raise the low side
    else hi--; // too big: lower the high side
  }
  return null;
}
 
// same direction: fast reads, slow is the write index
function dedupe(xs: number[]): number {
  let slow = 0;
  for (let fast = 0; fast < xs.length; fast++) {
    if (fast === 0 || xs[fast] !== xs[fast - 1]) {
      xs[slow++] = xs[fast];
    }
  }
  return slow; // xs[0..slow) holds the unique values
}

Time O(n), space O(1) (plus O(n log n) if you have to sort first).

ProblemVariantKey move
Two Sum II (sorted input)opposite endsthe template above
Container With Most Wateropposite endsmove the shorter wall; the taller one can't do better with less width
3Sumsort + fix one + opposite endsskip equal neighbors to avoid duplicate triples (recipe)
Valid Palindromeopposite endsskip non-alphanumerics, compare case-folded
Remove Duplicates from Sorted Array, Move Zeroessame directionslow = next write slot
Sort Colors (Dutch flag)three pointerslo, mid, hi partitions

Sliding window

A window [l, r] over a contiguous range. r always moves right (expand, add xs[r] to the window's state); l moves right only to restore the rule (shrink, remove xs[l]). Each index enters and leaves once, so O(n) total even with the inner while.

a 0 b 1 c 2 a 3 b 4 c 5 b 6 b 7 l r a 0 b 1 c 2 a 3 b 4 c 5 b 6 b 7 l r s = "abcabcbb", rule: no letter twice in the window [l, r] 1. expand add s[r] 2. shrink while invalid r = 3 adds a second "a": window abca is invalid l++ drop s[0]: bca is valid, record r − l + 1 = 3
Expand with r until the window breaks the rule, then shrink with l until it holds again.
state = empty; l = 0
for r in 0 .. n-1:
    add xs[r] to state                 (expand)
    while state breaks the rule:       (shrink while invalid)
        remove xs[l] from state; l += 1
    answer = best(answer, r - l + 1)   (longest valid window)
Asked forShrink loop runs whileRecord the answer
longest valid windowwindow is invalidafter the loop (window is valid again)
shortest valid windowwindow is validinside the loop, before each shrink
count of valid windows (at most k)window is invalidadd r - l + 1 after the loop (windows ending at r)
exactly katMost(k) - atMost(k - 1)
fixed size knever; drop xs[r - k] as xs[r] entersonce r ≥ k - 1

// fixed size k (k ≤ n): add the new item, drop the old
function maxSumK(xs: number[], k: number): number {
  let sum = 0;
  for (let i = 0; i < k; i++) sum += xs[i];
  let best = sum;
  for (let r = k; r < xs.length; r++) {
    sum += xs[r] - xs[r - k];
    best = Math.max(best, sum);
  }
  return best;
}
 
// variable: longest substring without repeats
function longestUnique(s: string): number {
  const count = new Map<string, number>();
  let best = 0;
  let l = 0;
  for (let r = 0; r < s.length; r++) {
    const c = s[r];
    count.set(c, (count.get(c) ?? 0) + 1);
    while (count.get(c)! > 1) { // invalid: shrink
      const d = s[l++];
      count.set(d, count.get(d)! - 1);
    }
    best = Math.max(best, r - l + 1); // valid here
  }
  return best;
}

Time O(n), space O(size of the alphabet or window). The window needs the rule to be monotonic: growing a valid window can break it, shrinking an invalid one can fix it. With negative numbers "sum ≤ k" is not monotonic, so use prefix sums instead.

ProblemWindow stateRule
Maximum Average Subarray Irunning sumfixed size k
Longest Substring Without Repeating Characterschar countsevery count ≤ 1
Longest Repeating Character Replacementcounts + top countlen - maxCount ≤ k (recipe)
Minimum Window Substringcounts still neededshortest window covering t (recipe)
Permutation in String, Find All Anagramscounts of the last len(p) charsfixed size, counts equal
Minimum Size Subarray Sum (positives)running sumshortest with sum ≥ target
Subarrays with K Different IntegerscountsatMost(k) - atMost(k - 1)

Prefix sums

pre[i] is the sum of the first i items, so any range sum is one subtraction. Pair it with a hash map of prefix values seen so far to count or find subarrays with a target sum in one pass, negatives included.

xs   =    3   -1    4    2
pre  = 0  3    2    6    8
sum(xs[l..r]) = pre[r+1] - pre[l]
xs[l..r) sums to k  ⇔  pre[r] - pre[l] = k
                    ⇔  pre[r] - k was seen before

// pre[i] = sum of xs[0..i)
function prefix(xs: number[]): number[] {
  const pre = [0];
  for (const x of xs) pre.push(pre.at(-1)! + x);
  return pre;
}
 
// Subarray Sum Equals K: count, negatives allowed
function subarraySum(xs: number[], k: number): number {
  const seen = new Map<number, number>([[0, 1]]);
  let sum = 0;
  let count = 0;
  for (const x of xs) {
    sum += x;
    count += seen.get(sum - k) ?? 0;
    seen.set(sum, (seen.get(sum) ?? 0) + 1);
  }
  return count;
}

Build O(n), query O(1), space O(n). The {0: 1} seed counts subarrays that start at index 0.

ProblemTwist
Range Sum Query - Immutableplain prefix array
Subarray Sum Equals Kmap of prefix → count
Contiguous Array (equal 0s and 1s)treat 0 as −1, map prefix → first index, maximize length
Continuous Subarray Sum (multiple of k)store pre % k → first index
Product of Array Except Selfprefix and suffix products (recipe)
Range Sum Query 2DP[r][c] over a grid, inclusion–exclusion
Range updates (add v to l..r, many times)difference array: d[l] += v, d[r+1] -= v, prefix-sum once at the end

Monotonic stack & deque

A stack whose values stay sorted (decreasing for "next greater"). Each new item pops everything it beats, and each pop is an answer: the popped index just found its next greater element. A deque version keeps the window maximum at the front.

// Daily Temperatures: days until a warmer day
function dailyTemps(ts: number[]): number[] {
  const ans = new Array<number>(ts.length).fill(0);
  const stack: number[] = []; // indexes, temps falling
  ts.forEach((t, i) => {
    while (stack.length && ts[stack.at(-1)!] < t) {
      const j = stack.pop()!;
      ans[j] = i - j; // i is j's next warmer day
    }
    stack.push(i);
  });
  return ans;
}
 
// Sliding Window Maximum. JS has no deque: an array
// plus a head index gives O(1) pops at both ends
function windowMax(xs: number[], k: number): number[] {
  const dq: number[] = []; // indexes, values falling
  let head = 0; // dq[head..] is the live deque
  const out: number[] = [];
  for (let i = 0; i < xs.length; i++) {
    while (dq.length > head && xs[dq.at(-1)!] <= xs[i]) {
      dq.pop();
    }
    dq.push(i);
    if (dq[head] <= i - k) head++; // left the window
    if (i >= k - 1) out.push(xs[dq[head]]);
  }
  return out;
}

Time O(n) (each index is pushed and popped once), space O(n). Store indexes, not values: you need distances and window bounds.

WantStack order (bottom → top)Pop while
next greaterdecreasingtop < x
next smallerincreasingtop > x
previous greaterdecreasingtop ≤ x; the top after popping is the answer
window max / mindeque decreasing / increasingback loses to x; front leaves the window

Classics: Daily Temperatures, Next Greater Element I and II (circular: loop 2n with i % n), Online Stock Span, Largest Rectangle in Histogram (increasing stack; on pop, width runs from the new top to i), Trapping Rain Water, Sliding Window Maximum, Remove K Digits.

Fast & slow pointers

Two pointers on a linked list (or any next function), one moving two steps per turn. In a cycle the fast one gains one node per turn, so they meet; on a straight list, fast hits the end when slow is halfway.

type ListNode = { val: number; next: Link };
type Link = ListNode | null;
 
function hasCycle(head: Link): boolean {
  let slow = head;
  let fast = head;
  while (fast && fast.next) {
    slow = slow!.next;
    fast = fast.next.next;
    if (slow === fast) return true;
  }
  return false;
}
 
// Floyd: after they meet, restart one at the head;
// stepping both by one, they meet at the cycle start
function cycleStart(head: Link): Link {
  let slow = head;
  let fast = head;
  while (fast && fast.next) {
    slow = slow!.next;
    fast = fast.next.next;
    if (slow === fast) {
      let p = head;
      while (p !== slow) {
        p = p!.next;
        slow = slow!.next;
      }
      return p;
    }
  }
  return null;
}
 
// middle node (the second one for even lengths)
function middle(head: ListNode): ListNode {
  let slow = head;
  let fast: Link = head;
  while (fast && fast.next) {
    slow = slow.next!;
    fast = fast.next.next;
  }
  return slow;
}

Time O(n), space O(1) (a Set of visited nodes also works, in O(n) space).

ProblemUse
Linked List Cycle, Linked List Cycle IIhasCycle, cycleStart
Middle of the Linked Listmiddle
Palindrome Linked Listfind the middle, reverse the second half, compare
Reorder Listmiddle + reverse + merge
Happy Numberthe "list" is n → sum of squared digits; a cycle without 1 means unhappy
Find the Duplicate Numberi → nums[i] is a list with a cycle; its start is the duplicate

Intervals

Sort by start, then sweep once, comparing each interval with the last one kept. Two intervals [a, b] and [c, d] overlap when a ≤ d and c ≤ b (use < if touching ends don't count). For "how many at once", keep a min-heap of end times: its top is the room that frees up first.

type Interval = [number, number];
 
function merge(xs: Interval[]): Interval[] {
  const sorted = [...xs].sort((a, b) => a[0] - b[0]);
  const out: Interval[] = [];
  for (const [s, e] of sorted) {
    const last = out.at(-1);
    if (last && s <= last[1]) {
      last[1] = Math.max(last[1], e); // overlap: extend
    } else {
      out.push([s, e]);
    }
  }
  return out;
}
 
// Insert Interval: xs is sorted and non-overlapping
function insert(xs: Interval[], add: Interval): Interval[] {
  const out: Interval[] = [];
  let [s, e] = add;
  let i = 0;
  while (i < xs.length && xs[i][1] < s) out.push(xs[i++]);
  while (i < xs.length && xs[i][0] <= e) { // overlaps
    s = Math.min(s, xs[i][0]);
    e = Math.max(e, xs[i][1]);
    i++;
  }
  return [...out, [s, e], ...xs.slice(i)];
}
 
// Meeting Rooms II: the most meetings at once
function minRooms(xs: Interval[]): number {
  const sorted = [...xs].sort((a, b) => a[0] - b[0]);
  const ends = new MinHeap<number>((a, b) => a - b);
  for (const [s, e] of sorted) {
    // earliest-ending meeting is over: reuse its room
    if (ends.size > 0 && ends.peek()! <= s) ends.pop();
    ends.push(e);
  }
  return ends.size;
}

merge and minRooms O(n log n) (the sort), insert O(n); space O(n). MinHeap is the class from Trees & graphs.

ProblemApproach
Merge Intervalsmerge
Insert Intervalinsert: before, overlapping, after
Meeting Rooms (can one person attend all?)sort by start; any start < previous end means no
Meeting Rooms II / Car Poolingheap of ends, or a sweep over +1 at starts and −1 at ends
Non-overlapping Intervals (fewest to remove)sort by end, greedily keep the earliest-ending
Minimum Number of Arrows to Burst Balloonssort by end, shoot at each kept end
Interval List Intersectionstwo pointers over two sorted lists; advance the one that ends first

Top-k & k-way merge

A heap answers "best k so far" in O(log k) per item. For the k largest, keep a min-heap of size k: the smallest of the k best sits on top, ready to be evicted. For k sorted lists, a heap of each list's current head yields the global order.

// k largest, largest first
function topK(xs: number[], k: number): number[] {
  const heap = new MinHeap<number>((a, b) => a - b);
  for (const x of xs) {
    heap.push(x);
    if (heap.size > k) heap.pop(); // drop the smallest
  }
  const out: number[] = [];
  while (heap.size > 0) out.push(heap.pop()!);
  return out.reverse();
}
 
// merge k sorted arrays; entry = [value, list, index]
type Entry = [number, number, number];
 
function mergeK(lists: number[][]): number[] {
  const heap = new MinHeap<Entry>((a, b) => a[0] - b[0]);
  lists.forEach((xs, i) => {
    if (xs.length) heap.push([xs[0], i, 0]);
  });
  const out: number[] = [];
  while (heap.size > 0) {
    const [v, i, j] = heap.pop()!;
    out.push(v);
    if (j + 1 < lists[i].length) {
      heap.push([lists[i][j + 1], i, j + 1]);
    }
  }
  return out;
}

topK O(n log k) time, O(k) space; mergeK O(N log k) for N total items, O(k) heap. Put a tiebreaker (the list index) second in each tuple so Python never compares payloads such as ListNodes, which raises TypeError.

ProblemHeap holdsAlternative
Kth Largest Element in an Arraymin-heap of size kquickselect, O(n) average
Top K Frequent Elements(count, value), size kbucket sort by count, O(n) (recipe)
K Closest Points to Originmax-heap of size k by distance (negate in Python)quickselect
Merge k Sorted Lists(val, i, node) headspairwise merging, same O(N log k)
Find Median from Data Streammax-heap of the low half + min-heap of the high halfkeep sizes within 1
Task Scheduler, Reorganize Stringmax-heap of remaining countsgreedy counting formula

Cyclic sort & index as hash

When values are in 1..n (or 0..n) and the array has n slots, the array can be its own hash table: swap each value to index v - 1, or flip the sign at |v| - 1 to mark "seen". O(n) time, O(1) extra space, but it rewrites the input.

// First Missing Positive (reorders xs in place)
function firstMissingPositive(xs: number[]): number {
  const n = xs.length;
  for (let i = 0; i < n; i++) {
    // swap xs[i] home until it's home or out of range
    while (xs[i] >= 1 && xs[i] <= n
        && xs[xs[i] - 1] !== xs[i]) {
      const j = xs[i] - 1;
      [xs[i], xs[j]] = [xs[j], xs[i]];
    }
  }
  for (let i = 0; i < n; i++) {
    if (xs[i] !== i + 1) return i + 1;
  }
  return n + 1;
}

Each swap puts one value home for good, so the inner loop runs at most n times in total: O(n). In Python, compute j first: xs[i], xs[xs[i] - 1] = … assigns xs[i] before evaluating the second target's index.

Classics: Missing Number (or XOR / Gauss sum), Find All Numbers Disappeared in an Array, Find All Duplicates in an Array (negate xs[|v| - 1]; already negative means seen), First Missing Positive, Set Mismatch.

Bit manipulation

TrickDoesUsed in
x & (x - 1)clears the lowest set bit; x > 0 && (x & (x - 1)) === 0 is a power of twoNumber of 1 Bits, Power of Two
x & -xisolates the lowest set bitFenwick trees, Single Number III
a ^ a = 0, a ^ 0 = apairs cancel, order doesn't matterSingle Number, Missing Number
(x >> i) & 1test bit isubsets, Counting Bits
x | (1 << i), x & ~(1 << i), x ^ (1 << i)set, clear, toggle bit ibitmask state
1 << n2ⁿ: number of subsets of n itemssubset enumeration, bitmask DP
mask & (1 << i)is item i in the subsetSubsets, Traveling Salesman DP
(sub - 1) & masknext smaller submask of maskDP over submasks, O(3ⁿ) total
popcount(a ^ b)Hamming distanceHamming Distance
x >> 1, x << 1halve (floor), doubleCounting Bits: bits[i] = bits[i >> 1] + (i & 1)

// Single Number: every other value appears twice
function single(xs: number[]): number {
  return xs.reduce((acc, x) => acc ^ x, 0);
}
 
// Kernighan: one iteration per set bit; works on the
// 32-bit pattern, so negatives count 32-bit style
function popcount(x: number): number {
  let n = 0;
  for (let v = x; v !== 0; v &= v - 1) n++;
  return n;
}
 
// all subsets via masks 0 .. 2^n - 1 (n ≤ 30 in JS)
function subsets<T>(items: T[]): T[][] {
  const out: T[][] = [];
  for (let m = 0; m < 1 << items.length; m++) {
    out.push(items.filter((_, i) => (m >> i) & 1));
  }
  return out;
}

single and popcount O(n) and O(bits); subsets O(n · 2ⁿ).

JavaScript / TypeScriptPython
Integer width for bit opsoperands become 32-bit signed; 1 << 31 is -2147483648, 2 ** 32 | 0 is 0unbounded; 1 << 31 is 2147483648
Shift counttaken mod 32: 1 << 32 is 1exact: 1 << 32 is 4294967296
Unsigned viewx >>> 0; >>> is the unsigned right shiftx & 0xFFFFFFFF (no >>>)
~x-x - 1 in 32 bits-x - 1; negatives have infinite leading 1s
Above 32 bitsBigInt: 1n << 40n, can't mix with numberjust works
Popcountno built-in; loop or tableint.bit_count() (3.10+)
Reverse Integer style overflow checksnumbers are doubles; compare with 2 ** 31 - 1ints never overflow: check the 32-bit range by hand

Recipes

Stuck? Try this order

When no signal jumps out, walk this list and ask "does this make the repeated work go away?"

1. Brute force out loud, with its cost. Where's the repeat?
2. Sort it?          → two pointers, binary search, greedy
3. Hash it?          → seen set, value → index, prefix counts
4. Contiguous range? → sliding window or prefix sums
5. Next bigger?      → monotonic stack
6. Best k / stream   → heap
7. Choices + overlap → DP (recurrence first, then memoize)
8. All answers       → backtracking with pruning
9. Graph in disguise → BFS / DFS / topo sort / union-find
10. Monotonic answer → binary search on the answer

Minimum Window Substring

The shortest-window template: record and shrink while the window is valid. missing counts characters of t not yet covered, so validity is O(1) to check.

function minWindow(s: string, t: string): string {
  const need = new Map<string, number>();
  for (const c of t) need.set(c, (need.get(c) ?? 0) + 1);
  let missing = t.length; // chars of t still uncovered
  let bestL = 0;
  let bestLen = s.length + 1;
  let l = 0;
  for (let r = 0; r < s.length; r++) {
    const n = need.get(s[r]) ?? 0;
    if (n > 0) missing--;
    need.set(s[r], n - 1); // negative = surplus
    while (missing === 0) { // valid: record, shrink
      if (r - l + 1 < bestLen) {
        bestL = l;
        bestLen = r - l + 1;
      }
      const m = need.get(s[l])! + 1;
      need.set(s[l++], m);
      if (m > 0) missing++;
    }
  }
  return bestLen > s.length
    ? ""
    : s.slice(bestL, bestL + bestLen);
}

O(|s| + |t|) time, O(alphabet) space.

Longest Repeating Character Replacement

The longest-window template: a window is fixable with k edits when length - count of its most common letter ≤ k. maxFreq never has to decrease: a stale (too high) value only lets the window slide at its current length, and the window grows only when some letter beats the old maxFreq, which is exactly when a longer answer exists.

function charReplacement(s: string, k: number): number {
  const count = new Map<string, number>();
  let maxFreq = 0; // top count seen in any window
  let best = 0;
  let l = 0;
  for (let r = 0; r < s.length; r++) {
    const n = (count.get(s[r]) ?? 0) + 1;
    count.set(s[r], n);
    maxFreq = Math.max(maxFreq, n);
    while (r - l + 1 - maxFreq > k) { // invalid
      count.set(s[l], count.get(s[l])! - 1);
      l++;
    }
    best = Math.max(best, r - l + 1);
  }
  return best;
}

O(n) time, O(alphabet) space.

3Sum: sort, then two pointers

Fix the smallest element, run opposite-ends two pointers on the rest, and skip equal neighbors so each triple appears once.

function threeSum(xs: number[]): number[][] {
  const a = [...xs].sort((x, y) => x - y);
  const out: number[][] = [];
  for (let i = 0; i < a.length - 2; i++) {
    if (a[i] > 0) break; // three positives can't sum to 0
    if (i > 0 && a[i] === a[i - 1]) continue; // dup
    let lo = i + 1;
    let hi = a.length - 1;
    while (lo < hi) {
      const s = a[i] + a[lo] + a[hi];
      if (s < 0) lo++;
      else if (s > 0) hi--;
      else {
        out.push([a[i], a[lo++], a[hi--]]);
        while (lo < hi && a[lo] === a[lo - 1]) lo++;
      }
    }
  }
  return out;
}

O(n²) time, O(n) for the sorted copy. 4Sum adds one more fixed loop: O(n³).

Product of Array Except Self

Prefix products from the left, then multiply by suffix products from the right, without division.

function productExceptSelf(xs: number[]): number[] {
  const n = xs.length;
  const out = new Array<number>(n).fill(1);
  let pre = 1;
  for (let i = 0; i < n; i++) {
    out[i] = pre; // product of everything left of i
    pre *= xs[i];
  }
  let suf = 1;
  for (let i = n - 1; i >= 0; i--) {
    out[i] *= suf; // times everything right of i
    suf *= xs[i];
  }
  return out;
}

O(n) time, O(1) extra space besides the output.

Top K Frequent Elements: bucket sort

Counts are between 1 and n, so bucket values by count and read buckets from the top: O(n), beating the O(n log k) heap.

function topKFrequent(xs: number[], k: number): number[] {
  const freq = new Map<number, number>();
  for (const x of xs) freq.set(x, (freq.get(x) ?? 0) + 1);
  // buckets[f] = values seen exactly f times
  const buckets: number[][] = Array.from(
    { length: xs.length + 1 },
    () => [],
  );
  for (const [x, f] of freq) buckets[f].push(x);
  const out: number[] = [];
  for (let f = xs.length; f > 0 && out.length < k; f--) {
    out.push(...buckets[f]);
  }
  return out.slice(0, k);
}

O(n) time and space.

References