Part IV · Search, Selection and Optimisation Pattern 9 3 problems

Top ‘K’ Elements (Heaps)

You almost never need the data sorted. You need its extremes. A heap of size K gives you those in O(n log K) instead of O(n log n). It also works on a stream you cannot hold in memory.

Sorting a million items to find the top three does far more work than the question asks for. A heap answers “what is the smallest thing I am keeping?” in O(1). It lets you replace that item in O(log K). That one move is the whole pattern. In C#, the heap is PriorityQueue<TElement, TPriority>, built into .NET since version 6.

Contents

  1. When to use
  2. Core idea
  3. Working with PriorityQueue
  4. The templates
  5. Common mistakes
  6. Kth Largest Element in an Array
  7. Top K Frequent Elements
  8. K Closest Points to Origin
  9. Recap

When to use

The trigger. The question asks for the K largest, K smallest, K most frequent, or K closest items. Or it asks for a running median or a k-th value. Extra hints are “from a stream”, “the data does not fit in memory”, or a huge n with a small k.

When a heap is not the answer

Core idea

The surprise: to keep the K largest items, use a min-heap. The heap holds the K best seen so far. Its root is the weakest of them. That root is the bar to get in. A new item gets a place only if it beats the weakest survivor. Letting it in evicts exactly that root. The heap never grows past K.
stream 9 2 7 8 8 > root? min-heap, K = 3 2 9 7 root = weakest kept = 2 yes, replace 7 9 8 2 evicted, new root = 7 the root is both the answer to “K-th largest” and the admission gate
Figure 9.1 — A min-heap of size K used as a filter. The root doubles as the threshold and, at the end, as the K-th largest value.

The cost comparison worth quoting

Working with PriorityQueue

PriorityQueue<TElement, TPriority> lives in System.Collections.Generic. Each entry is an element plus a separate priority. The queue only ever compares priorities. Six facts cover almost every interview.

Ties have no fixed order. Python’s heapq compares whole tuples, so a tie can crash on an unorderable payload. C# never compares elements, so nothing crashes. But two equal priorities come out in no promised order. When the order matters, make the priority a value tuple such as (score, index). Value tuples compare left to right, so the index settles every tie.

The class below shows the max-heap tricks, an O(n) build, an in-order drain, and the Remove workaround for decrease-key.

public static class PqFacts
{
    /// <summary>Largest value, read from a max-heap built with a reversed comparer.</summary>
    /// <param name="nums">Values to scan. Must not be empty.</param>
    /// <returns>The maximum of <paramref name="nums"/>.</returns>
    /// <example><c>PqFacts.MaxWithReverseComparer([4, 9, 2])</c> returns 9.</example>
    public static int MaxWithReverseComparer(int[] nums)
    {
        // b.CompareTo(a) flips the order, so the biggest priority sits at the root.
        var reverse = Comparer<int>.Create((a, b) => b.CompareTo(a));
        var maxHeap = new PriorityQueue<int, int>(reverse);

        // value is the next number. Invariant: the root is the max of all enqueued so far.
        foreach (int value in nums)
        {
            // The value is both the element and its priority.
            maxHeap.Enqueue(value, value);
        }
        return maxHeap.Peek();
    }

    /// <summary>Largest value, read from a min-heap keyed on the negated value.</summary>
    /// <param name="nums">Values to scan. Must not be empty or hold int.MinValue.</param>
    /// <returns>The maximum of <paramref name="nums"/>.</returns>
    /// <example><c>PqFacts.MaxWithNegation([4, 9, 2])</c> returns 9.</example>
    public static int MaxWithNegation(int[] nums)
    {
        var heap = new PriorityQueue<int, int>();

        // value is the next number. Invariant: the root is the max of all enqueued so far.
        foreach (int value in nums)
        {
            // Minus sign: the biggest value gets the smallest priority, so it is the root.
            // -int.MinValue overflows back to int.MinValue, which is why the comparer is safer.
            heap.Enqueue(value, -value);
        }
        return heap.Peek();
    }

    /// <summary>Heapify all values in O(n), then dequeue until empty.</summary>
    /// <param name="nums">Values to sort.</param>
    /// <returns>The values in ascending order.</returns>
    /// <example><c>PqFacts.DrainInOrder([5, 1, 4])</c> returns [1, 4, 5].</example>
    public static List<int> DrainInOrder(int[] nums)
    {
        // The constructor that takes (element, priority) pairs heapifies them in one O(n) pass.
        var heap = new PriorityQueue<int, int>(nums.Select(value => (value, value)));
        // nums.Length: final size, so the list never has to grow.
        var sorted = new List<int>(nums.Length);

        // Invariant: sorted holds the smallest values dequeued so far, in ascending order.
        while (heap.TryDequeue(out int value, out _))
        {
            sorted.Add(value);
        }
        return sorted;
    }

    /// <summary>Changes one task's priority. No decrease-key, so remove and re-add.</summary>
    /// <returns>The task at the root after the change.</returns>
    /// <example><c>PqFacts.Reprioritize()</c> returns "email".</example>
    public static string Reprioritize()
    {
        var tasks = new PriorityQueue<string, int>();
        tasks.Enqueue("email", 5);    // 5: low urgency, a small number means more urgent
        tasks.Enqueue("deploy", 2);   // 2: smaller than 5, so deploy is the root for now

        // Remove (.NET 9+) does a linear search for "email". O(n), so use it sparingly.
        tasks.Remove("email", out _, out _);
        // 1: smaller than 2, so email becomes the new root.
        tasks.Enqueue("email", 1);
        return tasks.Peek();
    }
}
LINQ has no nlargest. The production one-liner is nums.OrderDescending().Take(k). It is short and clear, and it is fine for most real data. In an interview, write the heap loop first. Then say “in real code I would reach for OrderDescending().Take(k) unless the data is a stream.” Both halves of that sentence score.

The templates

Template A — keep the K largest
public static class KLargestTemplate
{
    /// <summary>The k largest values, in no particular order.</summary>
    /// <param name="nums">Values to scan.</param>
    /// <param name="k">How many to keep. 0 gives an empty list.</param>
    /// <returns>The k largest values in heap order, not sorted.</returns>
    /// <example><c>KLargestTemplate.KLargest([5, 1, 9, 3, 7], 3)</c> holds 5, 7 and 9.</example>
    public static List<int> KLargest(int[] nums, int k)
    {
        // Min-heap: the root is the weakest value kept. k: initial capacity, so no regrowth.
        var heap = new PriorityQueue<int, int>(k);

        // value is the next number.
        // Invariant: heap holds the k largest values seen so far (fewer at the start).
        foreach (int value in nums)
        {
            if (heap.Count < k)
            {
                // Still filling up: every value earns a seat until there are k.
                heap.Enqueue(value, value);
            }
            // TryPeek is false only when k is 0. Then nothing is ever admitted.
            else if (heap.TryPeek(out _, out int weakest) && value > weakest)
            {
                // Beats the weakest survivor: drop the root and add value in one sift.
                heap.DequeueEnqueue(value, value);
            }
        }

        // UnorderedItems walks the internal array. Only the root is in order.
        return heap.UnorderedItems.Select(pair => pair.Element).ToList();
    }
}

For the K smallest, flip both. Build the queue with a reversed comparer and admit when value < weakest.

next value heap.Count < k ? yes Enqueue(value, value) still filling: every value gets a seat no value > heap.Peek() ? yes DequeueEnqueue(value, value) old root leaves, size stays k no skip the value not better than the weakest kept
Figure 9.2 — Every value takes one of three exits, so the heap never holds more than k items.

Reading the figure. Amber is the incoming value. Blue boxes change the heap. Red means the value is dropped. Notice that once the heap is full, only one test runs: value > heap.Peek().

Once the heap is full, EnqueueDequeue can replace the test and the call together. It hands back whichever is smallest: the new value or the old root. If the new value loses, it bounces straight back out and the heap does not change.

// Heap already holds k values. One call does the compare and the swap.
// The return value is whatever left: the old root, or value itself if it lost.
int leaving = heap.EnqueueDequeue(value, value);
Template B — K best by a computed key
public static class KBestTemplate
{
    /// <summary>The k items with the largest score, in no particular order.</summary>
    /// <typeparam name="TItem">Item type. Items are never compared, only scores.</typeparam>
    /// <param name="items">Items to rank.</param>
    /// <param name="k">How many to keep.</param>
    /// <param name="score">Ranking key. Bigger is better.</param>
    /// <returns>The k best items in heap order.</returns>
    /// <example><c>KBestTemplate.KBest(["aa", "b", "cccc"], 2, s => s.Length)</c>
    /// holds "aa" and "cccc".</example>
    public static List<TItem> KBest<TItem>(IEnumerable<TItem> items, int k, Func<TItem, long> score)
    {
        // Priority is (score, index). Value tuples compare left to right,
        // so a tie on score falls back to index and the order is fixed.
        var heap = new PriorityQueue<TItem, (long Score, int Index)>();
        // 0: the first item gets index 0.
        int index = 0;

        // item is the next candidate. Invariant: heap holds the k best items seen so far.
        foreach (TItem item in items)
        {
            // score(item) is the placeholder ranking key, bigger is better.
            // Top K Frequent: the count. K Closest: -(x * x + y * y).
            var priority = (score(item), index);
            index++;   // +1: the next item gets the next index

            if (heap.Count < k)
            {
                // Fewer than k kept: admit without a contest.
                heap.Enqueue(item, priority);
            }
            // weakest is the root's priority. CompareTo above 0 means priority beats it.
            else if (heap.TryPeek(out _, out var weakest) && priority.CompareTo(weakest) > 0)
            {
                // Swap out the weakest for the new item. Size stays at k.
                heap.DequeueEnqueue(item, priority);
            }
        }
        return heap.UnorderedItems.Select(pair => pair.Element).ToList();
    }
}
Two entries from Top K Frequent: priority (score, index), element item score index item entry 1 3 0 "a" entry 2 3 1 "b" 3 = 3: tie, move right 0 < 1: decided never compared Without index: priorities 3 and 3 tie, so the dequeue order is not defined
Figure 9.3 — The index in second place settles every tie, and the element is never compared.

Reading the figure. A value tuple compares one slot at a time, left to right. Amber shows the tie on score. Green shows the index that settles it. The grey item is the element, and the queue never looks at it. The red bar shows what goes wrong with no index.

Common mistakes

The problems

1. Kth Largest Element in an Array Medium

Problem

Return the k-th largest element in an unsorted array. This is the k-th largest in sorted order, not the k-th distinct value.

Approach

Solution: heap

public static class KthLargest
{
    /// <summary>The k-th largest value in nums, counting duplicates.</summary>
    /// <param name="nums">Integers, unsorted.</param>
    /// <param name="k">1-based rank from the top, with 1 &lt;= k &lt;= nums.Length.</param>
    /// <returns>The k-th largest value.</returns>
    /// <exception cref="ArgumentOutOfRangeException">k is outside 1..nums.Length.</exception>
    /// <example><c>KthLargest.WithHeap([3, 2, 1, 5, 6, 4], 2)</c> returns 5.</example>
    public static int WithHeap(int[] nums, int k)
    {
        // 1: k is a 1-based rank, so 1 is the smallest legal k. nums.Length is the largest.
        if (k < 1 || k > nums.Length)
        {
            throw new ArgumentOutOfRangeException(nameof(k), $"k must be in 1..{nums.Length}");
        }

        // nums[..k]: indexes 0..k-1, exactly k values. The constructor heapifies in O(k).
        var heap = new PriorityQueue<int, int>(nums[..k].Select(value => (value, value)));

        // AsSpan(k): a view from index k to the end, no copy. These were not seeded.
        // Invariant: heap holds the k largest of the values scanned so far.
        foreach (int value in nums.AsSpan(k))
        {
            // Peek() is the root, the smallest of the k kept.
            if (value > heap.Peek())
            {
                // Evict the root, insert value. Size stays at k.
                heap.DequeueEnqueue(value, value);
            }
        }

        // The root is the weakest of the k largest, which is the k-th largest.
        return heap.Peek();
    }
}

Walkthrough

nums = [3, 2, 1, 5, 6, 4], k = 2. Seed the heap with the first two values. Then scan the other four.

seed [3, 2] 2 3 built in O(k), root 2 see 1 1 2 3 1 <= 2, skip see 5 5 3 5 5 > 2, evict 2 out: 2 see 6 6 5 6 6 > 3, evict 3 out: 3 see 4 4 5 6 4 <= 5, skip After the scan the root is 5, the 2nd largest. Answer: 5.
Figure 9.4 — With k = 2 the root climbs from 2 to 3 to 5, and 5 is the answer.

Reading the figure. Each frame is one step of the loop. The top circle is the root, the weakest value kept. Amber is a value that gets in. Red is a value that is skipped or evicted. The green root in the last frame is the answer.

Solution: quickselect

public static class KthLargestQuickselect
{
    /// <summary>The k-th largest value in O(n) expected time. Reorders nums in place.</summary>
    /// <param name="nums">Integers, unsorted. This array is mutated.</param>
    /// <param name="k">1-based rank from the top, with 1 &lt;= k &lt;= nums.Length.</param>
    /// <returns>The k-th largest value.</returns>
    /// <exception cref="ArgumentOutOfRangeException">k is outside 1..nums.Length.</exception>
    /// <example><c>KthLargestQuickselect.Find([3, 2, 1, 5, 6, 4], 2)</c> returns 5.</example>
    public static int Find(int[] nums, int k)
    {
        // 1: the smallest legal rank. nums.Length: the largest.
        if (k < 1 || k > nums.Length)
        {
            throw new ArgumentOutOfRangeException(nameof(k), $"k must be in 1..{nums.Length}");
        }

        // In ascending order the k-th largest sits k slots from the end.
        int target = nums.Length - k;
        // 0 is the first index. nums.Length - 1 is the last index.
        int left = 0, right = nums.Length - 1;

        // Invariant: target lies inside nums[left..right], the unsettled window.
        while (left <= right)
        {
            // A random pivot makes the O(n^2) worst case vanishingly unlikely.
            // Next's upper bound is exclusive, so right + 1 lets right be picked.
            int pivotIndex = Partition(nums, left, right, Random.Shared.Next(left, right + 1));

            if (pivotIndex == target)
            {
                // The pivot landed exactly on the rank we want.
                return nums[pivotIndex];
            }
            if (pivotIndex < target)
            {
                left = pivotIndex + 1;    // + 1: target is right of the settled pivot
            }
            else
            {
                right = pivotIndex - 1;   // - 1: target is left of the settled pivot
            }
        }

        // Unreachable: k was checked, so target is always inside the window.
        throw new InvalidOperationException("k out of range");
    }

    /// <summary>Lomuto partition. Returns the pivot's final, sorted index.</summary>
    private static int Partition(int[] nums, int left, int right, int pivotIndex)
    {
        int pivot = nums[pivotIndex];
        // Park the pivot at the right end so the scan below never moves it.
        (nums[pivotIndex], nums[right]) = (nums[right], nums[pivotIndex]);

        // store is the next slot for a value smaller than the pivot.
        int store = left;
        // i stops before right: right holds the parked pivot.
        // Invariant: nums[left..store-1] are all less than pivot.
        for (int i = left; i < right; i++)
        {
            if (nums[i] < pivot)
            {
                // Move the small value into the "less than" zone.
                (nums[store], nums[i]) = (nums[i], nums[store]);
                store++;   // +1: the zone grew by one slot
            }
        }

        // Drop the pivot between the two zones. It is now in its sorted position.
        (nums[store], nums[right]) = (nums[right], nums[store]);
        return store;
    }
}
Why quickselect is O(n) on average. Quicksort recurses into both halves: T(n) = 2T(n/2) + n, which is O(n log n). Quickselect recurses into one half: T(n) = T(n/2) + n. The series n + n/2 + n/4 + … sums to 2n. That one comparison is the answer the interviewer wants.
HeapO(n log k) time, O(k) spaceQuickselectO(n) average, O(1) space

Which to offer

Edge cases to raise

Say this out loud: “A min-heap, not a max-heap. I need constant-time access to the weakest item I am keeping, so I know what to evict.”

2. Top K Frequent Elements Medium

Problem

Given an array nums and an integer k, return the k most frequent elements, in any order.

Approach

Solution: heap

using System.Runtime.InteropServices;

public static class TopKFrequent
{
    /// <summary>The k most frequent values in nums, in no particular order.</summary>
    /// <param name="nums">Input values.</param>
    /// <param name="k">How many to return, with 0 &lt;= k &lt;= number of distinct values.</param>
    /// <returns>The k values with the highest counts.</returns>
    /// <example><c>TopKFrequent.WithHeap([1, 1, 1, 2, 2, 3], 2)</c> holds 1 and 2.</example>
    public static int[] WithHeap(int[] nums, int k)
    {
        // Phase 1: count each value in one O(n) pass.
        var counts = new Dictionary<int, int>();
        foreach (int value in nums)
        {
            // A ref to the slot: one hash lookup instead of a read plus a write.
            ref int slot = ref CollectionsMarshal.GetValueRefOrAddDefault(counts, value, out _);
            slot++;   // +1: one more sighting. A new slot starts at 0, the int default.
        }

        // Priority is the frequency, so the root is the least frequent value kept.
        var heap = new PriorityQueue<int, int>();

        // Invariant: heap holds the k highest-count values seen so far.
        foreach (var (value, frequency) in counts)
        {
            if (heap.Count < k)
            {
                // Fewer than k kept: admit without a contest.
                heap.Enqueue(value, frequency);
            }
            // weakest is the root's frequency. TryPeek is false only when k is 0.
            else if (heap.TryPeek(out _, out int weakest) && frequency > weakest)
            {
                // More frequent than the weakest kept: swap it in. Size stays at k.
                heap.DequeueEnqueue(value, frequency);
            }
        }

        // Drop the priorities and keep only the values.
        return [.. heap.UnorderedItems.Select(pair => pair.Element)];
    }
}

Solution: bucket by count, O(n)

public static class TopKFrequentBuckets
{
    /// <summary>Same answer in linear time, by using each count as an array index.</summary>
    /// <param name="nums">Input values.</param>
    /// <param name="k">How many to return.</param>
    /// <returns>Up to k values, highest count first. Ties come out in no fixed order.</returns>
    /// <example><c>TopKFrequentBuckets.Find([1, 1, 1, 2, 2, 3], 2)</c> returns [1, 2].</example>
    public static List<int> Find(int[] nums, int k)
    {
        var counts = new Dictionary<int, int>();
        foreach (int value in nums)
        {
            // GetValueOrDefault gives 0 for a new value. + 1 counts this sighting.
            counts[value] = counts.GetValueOrDefault(value) + 1;
        }

        // buckets[f] holds every value that occurs exactly f times.
        // nums.Length + 1 slots so index nums.Length exists: counts run 1..nums.Length.
        var buckets = new List<int>?[nums.Length + 1];
        foreach (var (value, frequency) in counts)
        {
            // ??= makes the list on first use. Empty buckets stay null and cost nothing.
            (buckets[frequency] ??= []).Add(value);
        }

        var result = new List<int>();
        // frequency walks from the largest possible count, nums.Length, down to 1.
        // Stop at 0: no value occurs 0 times. -- steps downward.
        // Invariant: result holds values whose counts are above frequency.
        for (int frequency = nums.Length; frequency > 0; frequency--)
        {
            // Most buckets are empty. Skip them.
            if (buckets[frequency] is not { } bucket)
            {
                continue;
            }
            foreach (int value in bucket)
            {
                // Checked before adding, so k = 0 returns an empty list.
                if (result.Count == k)
                {
                    return result;
                }
                result.Add(value);
            }
        }

        // Reached when k is at least the number of distinct values.
        return result;
    }
}

Walkthrough of the bucket version

nums = [1, 1, 1, 2, 2, 3], k = 2. Counts are {1: 3, 2: 2, 3: 1}. Bucket 1 holds [3]. Bucket 2 holds [2]. Bucket 3 holds [1]. Walk down from index 6. Buckets 6, 5 and 4 are empty. Bucket 3 gives 1. Bucket 2 gives 2. Now we have two, so return [1, 2].

1. counts 1 → 3 times 2 → 2 times 3 → 1 times 2. buckets[count] index 0 [] index 1 [3] index 2 [2] index 3 [1] index 4 [] index 5 [] index 6 [] 3. walk from index 6 down to 1, collecting values 6, 5, 4 empty. Index 3 gives 1. Index 2 gives 2. Now k = 2, stop. Answer: [1, 2]. Index 1 is never reached.
Figure 9.5 — The count is the array index, so a downward walk finds the top k with no heap.

Reading the figure. Blue cells hold counts or values not yet taken. Grey cells are empty buckets. The amber arrow is the walk from high counts to low. Green buckets are the ones the walk takes before it reaches k.

HeapO(n + m log k)BucketsO(n)SpaceO(n)

The production one-liner

public static class TopKFrequentLinq
{
    /// <summary>What you would actually ship: count, sort by count, take k.</summary>
    /// <param name="nums">Input values.</param>
    /// <param name="k">How many to return.</param>
    /// <returns>The k most frequent values, highest count first.</returns>
    /// <example><c>TopKFrequentLinq.Find([1, 1, 1, 2, 2, 3], 2)</c> returns [1, 2].</example>
    public static int[] Find(int[] nums, int k) =>
        // CountBy (.NET 9+) yields one (Key, Value) pair per distinct value, Value = count.
        [.. nums.CountBy(value => value)
                .OrderByDescending(pair => pair.Value)
                .Take(k)
                .Select(pair => pair.Key)];
}

CountBy arrived in .NET 9. On older targets, use GroupBy(v => v) and g.Count(). The sort makes this O(m log m), not O(m log k). Write your own version first. Then show this one.

Edge cases to raise

Say this out loud: “Counts are bounded by n. So I can use the count itself as an array index and drop the log factor. That is the linear-time version.”

3. K Closest Points to Origin Medium

Problem

Given a list of points on a plane and an integer k, return the k points closest to the origin. Distance is the usual Euclidean distance.

Three observations before any code

Solution

public static class KClosest
{
    /// <summary>The k points nearest the origin, in no particular order.</summary>
    /// <param name="points">Each point is [x, y] in integer coordinates.</param>
    /// <param name="k">How many to return, with 0 &lt;= k &lt;= points.Length.</param>
    /// <returns>The k closest points.</returns>
    /// <example><c>KClosest.Find([[1, 3], [-2, 2], [5, 8], [0, 1]], 2)</c>
    /// holds [-2, 2] and [0, 1].</example>
    public static int[][] Find(int[][] points, int k)
    {
        // Reversed comparer: the biggest squared distance sits at the root.
        // So the root is the furthest point we are keeping.
        var furthestFirst = Comparer<long>.Create((a, b) => b.CompareTo(a));
        var heap = new PriorityQueue<int[], long>(furthestFirst);

        // point is the next candidate. Invariant: heap holds the k closest points seen so far.
        foreach (int[] point in points)
        {
            // point[0] is x and point[1] is y. long first, so squaring cannot overflow.
            long x = point[0], y = point[1];
            // Squared distance: same order as the true distance, and no Math.Sqrt.
            long distance = x * x + y * y;

            if (heap.Count < k)
            {
                // Fewer than k kept: admit without a contest.
                heap.Enqueue(point, distance);
            }
            // worst is the root's distance. TryPeek is false only when k is 0.
            else if (heap.TryPeek(out _, out long worst) && distance < worst)
            {
                // Closer than the furthest kept: evict it. Size stays at k.
                heap.DequeueEnqueue(point, distance);
            }
        }

        // Drop the distances and keep only the points.
        return [.. heap.UnorderedItems.Select(pair => pair.Element)];
    }
}

Python fakes a max-heap by negating the distance. That works in C# too: enqueue with priority -distance on a default queue. The reversed comparer reads more plainly, and it has no edge case at long.MinValue.

Walkthrough

points = [[1,3],[-2,2],[5,8],[0,1]], k = 2. Squared distances are 10, 8, 89 and 1.

Result: [[-2, 2], [0, 1]].

[1,3] d²=10 [1,3] [1,3] d²=10 not full, push [-2,2] d²=8 [-2,2] [1,3] d²=10 [-2,2] d²=8 not full, push [5,8] d²=89 [5,8] [1,3] d²=10 [-2,2] d²=8 89 > 10, skip [0,1] d²=1 [0,1] [-2,2] d²=8 [0,1] d²=1 1 < 10, evict out: [1,3] Thick border = root = furthest point kept. Answer: [[-2,2], [0,1]].
Figure 9.6 — The max-heap root is the furthest kept point, so it is the one a closer point evicts.

Reading the figure. Each frame handles one point. Boxes inside a frame are the heap, with the root on top and a thick border. Amber is a point that gets in. Red is a point that is skipped or evicted. Green marks the final two points.

TimeO(n log k)SpaceO(k)

The alternatives, ranked

Edge cases to raise

Say this out loud: “I compare squared distances in long. Square root keeps the order, so skipping it keeps everything in exact integers. I want the K smallest, so the size-K heap is a max-heap. In C# that means a reversed comparer.”

Recap

The six things to carry forward

Where this goes next

Pattern 10, Subsets and Backtracking, goes the other way. It does not shrink the problem to K items. It walks an exponential space on purpose. The skill is pruning that space before it eats the clock.


← 08 — Tree and Graph Depth-First Search 10 — Subsets (Backtracking) →