
LeetCode: Heaps Priority Queue
Heaps intro
- What is a Heap
- Heap Characteristics
- Heap Representation
- Heap IRL
- Heap Application: Top K Element Extraction
- Heap Application: Dual Heap Balancing Representing Secondary Property
- Heap Application: K Way Merge for Sorted Streams
- Heap Application: Best First Search or Breadth First Search with Priority
- Heap Application: Kth element Within Iterating Sliding Window
- Heap Application: Interval Scheduling Optimization
347. Top K Elements in List ::3:: - Medium
- Intro
- Abstraction
- Pseudocode
- Solution 1: [MinHeap] Min Heap Of Size K Tracks The K Most Frequent Elements - HashMap/Algorithm
- Solution 2: [Quick Select] Partition Unique Elements By Frequency Until Index K Minus 1 Is Placed - HashMap/Algorithm
- Solution 3: [Bucket Sort] Bucket Elements By Frequency Then Walk From The Highest Bucket Down - HashMap/Algorithm
378. Kth Smallest Element in a Sorted Matrix ::1:: - Medium
- Intro
- Abstraction
- Pseudocode
- Solution 1: [MinHeap] K Way Merge Of Sorted Rows With One Candidate Per Row - Heap/Heap
- Solution 2: [Binary Search] Binary Search On Value With A Staircase Count Of Elements At Or Below - Heap/Binary Search
- Solution 3: [Binary Search] Sampled Median Pivot With A Binary Search Fallback (Educational, Not True Frederickson Johnson) - Heap/Algorithm
480. Sliding Window Median ::2:: - Hard
- Intro
- Abstraction
- Pseudocode
- Solution 1: [Two Heaps] MaxHeap And MinHeap Of The Window Halves With Lazy Deletion And Tracked Sizes - Heap/Heap
- Solution 2: [SortedList] Sorted Window Reads The Middle Elements By Index - Heap/Heap
- Solution 3: [Treap] Randomized BST With Subtree Sizes Gives Insert, Delete, And Kth Smallest In Log K - Heap/Heap
Heaps intro
LeetCode problems with heap solutions.
What is a Heap
Heaps are specialized complete binary tree structures used to prioritize data.
A heap maintains a partial order property:
- MinHeap: Parent nodes are less than or equal to their children (smallest element at root)
- MaxHeap: Parent nodes are greater than or equal to their children (largest element at root)
The tree is complete: all levels are fully filled except possible the last, which is filled left to right
Peek(): efficient access to min/max element in O(1) time Insert()/Pop(): Heap edit in O(log n) time
Heap Characteristics
Heaps are characterized by:
- Nodes: Contain values arranged by heap property
- Complete Tree: Shape property ensures tree is always balanced
- Heap Property: Parent value ≤ (minHeap) or Parent value ≥ children values
- Nothing beyond heap property: No ordering between siblings or subtrees maintained, other than parent child from heap property
- Array: Implemented as array for space and cache efficiency
Heap Representation
Tree heap represented as an array:
Array Form
Index: 0 1 2 3 4 5 6
Value: [3, 5, 8, 9, 10, 12, 15]
Tree Form
(0)3
/ \
(1)5 (2)8
/ \ / \
(3)9 (4)10 (5)12 (6)15Heap IRL
Priority Queues: scheduling, bandwidth management Graph algorithms: Dijkstra's shortest path, Prim's MST Event simulation: process events by priority/time
Heap Application: Top K Element Extraction
We can maintain a heap to quickly retrieve the top K largest or smallest elements without fully sorting the input.
Ex: Find the kth largest element in array
def kthLargest(nums, k):
heap = []
for num in nums:
heapq.heappush(heap, num)
if len(heap) > k:
heapq.heappop(heap) # Keep only top K elements
return heap[0]
# Example: kthLargest([3, 2, 1, 5, 6, 4], 2) -> 5Heap Application: Dual Heap Balancing Representing Secondary Property
We can use two heaps, maxHeap for lower half of values and a minHeap for the upper half, to maintain a property while adding and removing elements. Pattern supports quick lookups of medians, balance points, or range constraints.
Ex: Maintain median of stream.
class MedianFinder:
def __init__(self):
self.low = [] # Max-heap (invert values)
self.high = [] # Min-heap
def addNum(self, num: int) -> None:
heapq.heappush(self.low, -num)
heapq.heappush(self.high, -heapq.heappop(self.low))
if len(self.low) < len(self.high):
heapq.heappush(self.low, -heapq.heappop(self.high))
def findMedian(self) -> float:
if len(self.low) > len(self.high):
return -self.low[0]
return (-self.low[0] + self.high[0]) / 2 Heap Application: K Way Merge for Sorted Streams
A minHeap can efficiently merge multiple sorted lists or streams by always extracting the next smallest element across all inputs.
Ex: Merge k sorted lists into one sorted lists
def mergeKSorted(lists):
heap = []
for i, lst in enumerate(lists):
if lst:
heapq.heappush(heap, (lst[0], i, 0))
result = []
while heap:
val, list_idx, elem_idx = heapq.heappop(heap)
result.append(val)
if elem_idx + 1 < len(lists[list_idx]):
heapq.heappush(heap, (lists[list_idx][elem_idx+1], list_idx, elem_idx+1))
return result
# mergeKSorted([[1,4,5],[1,3,4],[2,6]]) -> [1,1,2,3,4,4,5,6] Heap Application: Best First Search or Breadth First Search with Priority
A heap can drive a search algorithm where you can expand the 'best' candidate first: A*, Dijkstra's, Prims, etc. In a non negative weighted graph context: a minHeap can be used to expand on the closest node first in a This always ensures that the next node popped from the heap has the smallest known distance from the source.
Ex: Dijkstra's Algorithm MinHeap
def dijkstra(graph, start):
# Graph: Node -> list of (neighbor, weight) tuples
# start: Starting node for shortest path search
# Result:
# dist: shortest distance from start to all other nodes
# Init all distances to infinity,
# except start node which has distance 0
dist = {node: float('inf') for node in graph}
dist[start] = 0
# minHeap priority queue stores tuples of (distance, node)
# iterate from start node and commence exploring neighbors
heap = [(0, start)]
# while there are nodes to process in heap
while heap:
# grab node with smallest known distance from start
d, node = heapq.heappop(heap)
# Skip if we already have already explored a shorter
# path to the current node
if d > dist[node]:
continue
# Check each neighbor and see if we have found
# a shorter path through the current node
for nei, w in graph[node]:
# grab new distance
nd = d + w
# if shorter path found, update and push to heap
if nd < dist[nei]:
dist[nei] = nd
heapq.heappush(heap, (nd, nei))
# overall: time complexity
# overall: space complexity
return distHeap Application: Kth element Within Iterating Sliding Window
A heap can track max/min within a sliding window efficiently when combined with lazy deletion or index tracking.
Ex: Find maximum in each sliding windows
def max_sliding_window(nums, k):
# maxHeap via minHeap with negative values
# initialize with first k elements
heap = [(-nums[i], i) for i in range(k)]
heapq.heapify(heap)
# root of heap is the largest in the current window
res = [-heap[0][0]]
# iterate sliding window forward
for i in range(k, len(nums)):
# push new element to heap
heapq.heappush(heap, (-nums[i], i))
# remove top element if they are outside current window
while heap[0][1] <= i - k:
heapq.heappop(heap)
# after removing out of window elements, heap root is max
# within the current window
res.append(-heap[0][0])
return resHeap Application: Interval Scheduling Optimization
A minHeap can track the earliest finishing times among active intervals, enabling optimal scheduling of jobs or meetings without conflicts.
Ex: Find the minimum number of meeting rooms required
def minMeetingRooms(intervals):
# Sort intervals by their start time,
# ensures we process meetings in chronological order
intervals.sort(key=lambda x: x[0])
# minHeap to track end time of ongoing meetings,
# storing the earliest ending meeting at the top
heap = []
# iterate over rooms in earliest starting order
for start, end in intervals:
# if heap is not empty, and the earliest ending meeting the earliest meeting ends
# ends before or exactly when the new meeting starts,
# then we can remove the earliest ending meeting
# (essentially freeing up a room)
if heap and heap[0] <= start:
# pop meeting that ended the earliest
heapq.heappop(heap)
# 'start' the current meeting by pushing it to the heap
heapq.heappush(heap, end)
# last heap size is the number meetings running
# concurrently after all meetings have started
return len(heap)
# Example:
# minMeetingRooms([[0,30],[5,10],[15,20]]) -> 2215. Kth Largest Element in an Array ::2:: - Medium
Topics: Array, Math, Divide and Conquer, Geometry, Sorting, Heap (Priority Queue), Quickselect
Intro
Given an integer array nums and an integer k, return the kth largest element in the array. Note that it is the kth largest element in the sorted order, not the kth distinct element. Can you solve it without sorting?
| Example Input | Output |
|---|---|
| nums = [3,2,1,5,6,4], k = 2 | 5 |
| nums = [3,2,3,1,2,4,5,5,6], k = 4 | 4 |
Constraints:
1 ≤ k ≤ nums.length ≤ 105
-104 ≤ nums[i] ≤ 104
Abstraction
Given an array and a fixed size heap of size k, return the kth largest element in the array.
(keep only the k largest elements in a minHeap, so the root is the kth largest) (push while the heap has space, once full, replace the root only if the new value beats it)
(Monotonic: the root only ever increases or stays the same, since the heap only keeps values that beat the current kth largest, so smaller values can be ignored for good)
(Partition: after splitting around a pivot, the kth largest lives in exactly one of bigger, equal, or smaller, so recurse into that one side and discard the other two)
(Count what the heap holds) len(minHeap) = "how many of the largest elements have I kept so far? (at most k)"
(Read the answer from the root) minHeap[0] = "what is the smallest of my k largest, which is exactly the kth largest?"
(Count what bigger holds, for quick select) len(bigger) = "how many elements rank above the pivot? if k lte this, the answer is in bigger"
(Count what equal holds, for quick select) len(bigger) + len(equal) = "how many elements rank at or above the pivot? if k lte this, the answer is the pivot"
Pseudocode
oh! pseudocode hasn't been written yet, try another card! :)Solution 1: [Heap] Min Heap Of Size K Tracks The Kth Largest Element - Heap/Heap
def findKthLargest(self, nums: list[int], k: int) -> int:
# MinHeap (Fixed Size K)
# Heap Representation:
# - Maintain a minHeap holding the k largest elements seen so far
# - Root (minHeap[0]) is the smallest of those k elements,
# which is exactly the kth largest element overall
# - Heap is valid when it holds at most k elements
# Idea:
# - The kth largest element is the smallest of the top k elements
# - Keep only the top k elements in a minHeap, so the root is always the answer
# - If the heap has fewer than k elements, push the new num
# - If the heap is full, the new num only matters if it beats the root,
# since anything smaller can never be in the top k
# - Replace the root with the new num to evict the old kth largest
# - After every num has been seen, the root is the answer
# MinHeap of the k largest elements seen so far
minHeap = []
# Array Pointer
index = 0
# tc: O(n log k)
while index < len(nums):
# Current num
num = nums[index]
# Heap still has space, push num
# k = 2, minHeap = [3], num = 2 -> [2, 3]
# tc: O(log k)
if len(minHeap) < k:
heapq.heappush(minHeap, num)
# Heap is full, only keep num if it beats the current kth largest
else:
# Replace root if num is larger:
# k = 2, minHeap = [2, 3]
# num = 1 -> 1 < 2, ignored, heap stays [2, 3]
# num = 5 -> 5 > 2, replace root -> [3, 5]
# tc: O(log k)
if num > minHeap[0]:
heapq.heapreplace(minHeap, num)
# Iterate array
index += 1
# Kth largest element (root)
kthLargest = minHeap[0]
# overall: tc O(n log k)
# overall: sc O(k)
return kthLargestSolution 2: [Quick Select] Three Way Partition That Ignores Duplicates (Recurse Into One Side) - Heap/Heap
def findKthLargest(self, nums: list[int], k: int) -> int:
# Quick Select (Three Way Partition)
# Search Representation:
# - nums is the current search space
# - k is the rank (1 = largest) of the target inside the current nums
# - Search space is valid while it still contains the kth largest
# Idea:
# - Pick a random pivot and split nums into bigger, equal, and smaller
# - If k <= len(bigger), the kth largest is inside bigger, recurse there
# - If k <= len(bigger) + len(equal), the kth largest is the pivot itself,
# so every duplicate of the pivot is resolved in one step
# - Otherwise the kth largest is inside smaller, recurse there with
# k reduced by the number of elements ranked above it
# - Only one side is ever searched, so the work shrinks every call
# Why Three Way (Duplicates):
# - A two way partition (<= pivot vs > pivot) leaves every copy of the pivot
# on one side, so with many duplicates the pivot barely shrinks the search space
# - Random pivots don't help, since every pivot has the same value
# - Each call then removes almost nothing, so time degrades to O(n^2) (TLE)
# and recursion depth can hit the limit
# - The equal bucket is never recursed into: if k lands inside it, the answer
# is the pivot, so all copies of that value cost one pass and then disappear
# - With no duplicates it costs nothing extra, still O(n) per pass
# Helper that searches one partition for the kth largest
def quickSelect(nums: list[int], k: int) -> int:
# Random pivot
pivot = random.choice(nums)
# Partitions
bigger = []
equal = []
smaller = []
# Array Pointer
index = 0
# Partition nums around pivot
# tc: O(n)
while index < len(nums):
# Current num
num = nums[index]
# Sort num into its partition
if num > pivot:
bigger.append(num)
elif num == pivot:
equal.append(num)
else:
smaller.append(num)
# Iterate array
index += 1
# Kth largest is inside bigger, recurse
# nums = [3, 2, 1, 5, 6, 4], k = 2, pivot = 4
# bigger = [5, 6], k <= 2 -> search [5, 6] for 2nd largest
if k <= len(bigger):
return quickSelect(bigger, k)
# Kth largest is the pivot (duplicates resolved in one step):
# bigger = [6], equal = [5], k = 2
# k <= 1 + 1 -> the 2nd largest is 5
if k <= len(bigger) + len(equal):
return pivot
# Kth largest is inside smaller, recurse with k adjusted:
# nums = [3, 2, 1, 5, 6, 4], k = 5, pivot = 4
# bigger = [5, 6], equal = [4], smaller = [3, 2, 1]
# 5 > 2 + 1 -> search [3, 2, 1] for k = 5 - 2 - 1 = 2nd largest
newK = k - len(bigger) - len(equal)
return quickSelect(smaller, newK)
# Kth largest element
kthLargest = quickSelect(nums, k)
# overall: tc O(n) average (n + n/2 + n/4 + ...), O(n^2) worst case with bad pivots
# overall: sc O(n) for the partition lists, plus O(log n) expected recursion depth
return kthLargest703. Kth Largest Element in a Stream ::1:: - Easy
Topics: Tree, Design, Binary Search Tree, Heap (Priority Queue), Binary Tree, Data Stream
Intro
You are part of a university admissions office and need to keep
track of the kth highest test score from applicants in real-time. This helps to determine cut-off marks for interviews and admissions dynamically as new applicants submit their scores. You are tasked to implement a class which, for a given integer k, maintains a stream of test scores and continuously returns the kth highest test score after a new score has been submitted. More specifically, we are looking for the kth highest score in the sorted list of all scores. Implement the KthLargest class: KthLargest(int k, int[] nums) Initializes the object with the integer k and the stream of test scores nums. int add(int val) Adds a new test score val to the stream and returns the element representing the kth largest element in the pool of test scores so far.
| Example Input | Output |
|---|---|
| ["KthLargest", "add", "add", "add", "add", "add"] [[3, [4, 5, 8, 2]], [3], [5], [10], [9], [4]] | [null, 4, 5, 5, 8, 8] |
| ["KthLargest", "add", "add", "add", "add"] [[4, [7, 7, 7, 7, 8, 3]], [2], [10], [9], [9]] | [null, 7, 7, 7, 8] |
Constraints:
0 ≤ nums.length ≤ 104
1 ≤ k ≤ nums.length + 1
-104 ≤ nums[i] ≤ 104
-104 ≤ val ≤ 104
At most 104 calls will be make to add.
Abstraction
Given a stream of numbers, use a fixed size heap of size k, return the kth largest element seen so far after each new number is added.
(MinHeap)
(keep only the k largest elements in a minHeap, so the root is the kth largest) (push while the heap has space, once full, replace the root only if the new value beats it)
(Monotonic: the root only ever increases or stays the same, since the heap only keeps values that beat the current kth largest, so smaller values can be ignored for good)
(Count what the heap holds) len(minHeap) = "how many of the largest elements have I kept so far? (at most k)"
(Read the answer from the root) minHeap[0] = "what is the smallest of my k largest, which is exactly the kth largest?"
Pseudocode
oh! pseudocode hasn't been written yet, try another card! :)Solution 1: [Heap] Min Heap Of Size K Tracks The Kth Largest Element - Heap/Heap
class KthLargest:
# MinHeap (Fixed Size K)
# Heap Representation:
# - Maintain a minHeap holding the k largest elements seen so far
# - Root (minHeap[0]) is the smallest of those k elements,
# which is exactly the kth largest element overall
# - Heap is valid when it holds at most k elements
# Idea:
# - The kth largest element is the smallest of the top k elements
# - Keep only the top k elements in a minHeap, so the root is always the answer
# - If the heap has fewer than k elements, push the new value
# - If the heap is full, the new value only matters if it beats the root,
# since anything smaller can never be in the top k
# - Replace the root with the new value to evict the old kth largest
# - Return the root after every add
# KthLargest():
# - Initializes the minHeap
# - Streams nums into the heap one at a time through add
# add():
# - Adds a number to the minHeap if it belongs in the top k
# - Returns the kth largest element so far
def __init__(self, k: int, nums: list[int]):
# Heap Size Limit
self.k = k
# MinHeap of the k largest elements seen so far
self.minHeap = []
# Stream Pointer
index = 0
# Mimic 'streaming' input data into minHeap
# tc: O(n log k) - n calls to add, each O(log k)
while index < len(nums):
# Add num to heap
self.add(nums[index])
# Iterate stream
index += 1
# overall: tc O(n log k)
# overall: sc O(k)
def add(self, val: int) -> int:
# Heap still has space, push new value
# k = 3, minHeap = [4, 5], add(8) -> [4, 5, 8]
# tc: O(log k)
if len(self.minHeap) < self.k:
heapq.heappush(self.minHeap, val)
# Heap is full, only keep val if it beats the current kth largest
else:
# Current kth largest element
kthLargest = self.minHeap[0]
# Replace root if val is larger:
# k = 3, minHeap = [4, 5, 8]
# add(3) -> 3 < 4, ignored, heap stays [4, 5, 8]
# add(10) -> 10 > 4, replace root -> [5, 8, 10]
# tc: O(log k)
if val > kthLargest:
heapq.heapreplace(self.minHeap, val)
# Return kth largest element (root), tc: O(1)
# overall: tc O(log k)
# overall: sc O(1) extra, O(k) for the heap itself
return self.minHeap[0]2357. Make Array Zero by Subtracting Equal Amounts ::3:: - Easy
Topics: Array, Hash Table, Greedy, Sorting, Heap (Priority Queue), Simulation
Intro
You are given a non-negative integer array nums. In one operation, you must: Choose a positive integer x such that x is less than or equal to the smallest non-zero element in nums. Subtract x from every positive element in nums. Return the minimum number of operations to make every element in nums equal to 0.
| Example Input | Output |
|---|---|
| nums = [1,5,0,3,5] | 3 |
| nums = [0] | 0 |
Constraints:
1 ≤ nums.length ≤ 100
0 ≤ nums[i] ≤ 100
Abstraction
Given an array of non-negative integers, use a heap to of size n, to track the current min number in the array, to calculate the number of operations needed to decrease all numbers to 0.
(MinHeap)
(keep only the n largest elements in a minHeap, so the root is the smallest element) (keep popping the root and track the running total of all the numbers popped, until the heap is empty) (This is the min number of operations needed to decrease all numbers to 0)
Pseudocode
oh! pseudocode hasn't been written yet, try another card! :)Solution 1: [Sorting] Lazy Subtraction Simulation Over Sorted Values - Heap/Heap
def minimumOperations(self, nums: List[int]) -> int:
# Lazy Subtraction:
# Sort so values are processed in increasing order, then simulate
# each operation's effect without touching every array slot on
# every operation. Instead, track a running "alreadySubtracted"
# total; each element's TRUE current value is (original value -
# alreadySubtracted), which is exactly what repeatedly
# subtracting x from every positive element produces in aggregate.
# nums_sorted = [0, 1, 3, 5, 5]
# alreadySubtracted starts at 0, grows by each new tier's value
# as we sweep left to right
# sc: O(n), space used by sorted() to produce nums_sorted
numsSorted = sorted(nums)
n = len(numsSorted)
# Running count of total amount subtracted by completed operations
alreadySubtracted = 0
operationsCount = 0
# tc: O(n)
for i in range(n):
# Note:
# TRUE current value after all prior operations have been
# applied -- the real result of every earlier subtraction,
# computed lazily instead of mutating the whole array.
currentValue = numsSorted[i] - alreadySubtracted
# skip: already at zero (started at 0, or zeroed by a
# previous operation)
if currentValue <= 0:
continue
# New operation:
# new distinct positive value not yet zeroed. Perform one
# operation: x = currentValue (guaranteed smallest
# remaining nonzero value, since array is sorted).
# tc: O(1) per operation
alreadySubtracted += currentValue
operationsCount += 1
# overall: tc O(n log n)
# overall: sc O(n)
return operationsCountSolution 2: [Heap] Pop Smallest Remaining Value And Add To Running Subtraction Count - Heap/Heap
def minimumOperations(self, nums: List[int]) -> int:
# MinHeap:
# A min-heap always pops the current smallest remaining value in
# O(log n), without needing a full sort upfront. Same lazy
# subtraction idea as the sorting solution -- a running
# "alreadySubtracted" total is applied only when a value is
# popped, rather than mutating every array slot on every
# operation. This simulates the same tier-by-tier subtraction
# process, just driven by heap pops instead of a sorted pass.
# heap = [0, 1, 3, 5, 5] (heap-ordered, not necessarily sorted)
# each pop returns the current global minimum remaining value
# heap holds all elements initially
# sc: O(n)
heap = list(nums)
# heapify in linear time
# tc: O(n)
heapq.heapify(heap)
# Running count of total amount subtracted by completed operations
alreadySubtracted = 0
operationsCount = 0
# tc: O(n) pops each O(log n) for total O(n log n)
while heap:
# Pop the current smallest remaining value
# tc: O(log n) per pop
smallest = heapq.heappop(heap)
# Note:
# TRUE current value after all prior operations have been
# applied -- same lazy-subtraction idea as the sorting
# solution, just computed at pop time instead of during a
# linear pass over a sorted array.
currentValue = smallest - alreadySubtracted
# skip: already at zero (started at 0, or zeroed by a
# previous operation)
if currentValue <= 0:
continue
# New operation:
# new distinct positive value not yet zeroed. Perform one
# operation: x = currentValue (guaranteed smallest
# remaining nonzero value, since heap always pops the min).
alreadySubtracted += currentValue
operationsCount += 1
# overall: tc O(n log n)
# overall: sc O(n)
return operationsCountSolution 3: [Greedy] Distinct Positive Values Determine Operation Count - Heap/Heap
def minimumOperations(self, nums: List[int]) -> int:
# Distinct Count:
# Every operation subtracts the same x from ALL positive elements
# at once. This means elements sharing the same value always hit
# zero together, in the same single operation -- duplicates of a
# value never require extra operations. Also, x is capped at the
# current smallest nonzero value, so an operation can never skip
# past a distinct value to clear two tiers at once. The answer
# reduces to: how many DISTINCT positive values exist in nums.
# nums = [1, 5, 0, 3, 5]
# distinct positive values -> {1, 3, 5}
# each distinct value needs exactly 1 dedicated operation,
# duplicates (the two 5's) ride along for free
# sc: O(n) worst case, one entry per distinct positive value
distinct_positive = set()
# tc: O(n), one pass over nums
for num in nums:
# skip zeros: already at the target, no operation needed
if num > 0:
distinct_positive.add(num)
operationsCount = len(distinct_positive)
# overall: tc O(n)
# overall: sc O(n)
return operationsCount347. Top K Elements in List ::3:: - Medium
Topics: Array, Hash Table, Divide and Conquer, Sorting, Heap (Priority Queue), Bucket Sort, Counting, Quickselect
Intro
Given an integer array nums and an integer k, return the k most frequent element within the array. Test cases are generated such that the answer is always unique. You may return the output in any order
| Input | k | Output |
|---|---|---|
| [1,2,2,3,3,3,3] | 2 | [2,3] |
| [7,7] | 1 | [7] |
Constraints:
1 ≤ k ≤ number of distinct elements in nums
-1000 ≤ nums[i] ≤ 1000
Abstraction
Given an array, count how often each element occurs, then return the k elements with the highest counts.
(count first with a frequency map, so the problem becomes "pick the k largest counts" among the unique elements) (rank by count, not by value: the heap, the partition, and the buckets all compare frequencies)
(MinHeap: keep only the k most frequent in a minHeap, so the root is the least frequent of the k and the first one to evict)
(Quick Select: partition unique elements by frequency with high on the left, once a pivot lands at index k - 1 the first k elements are the top k, so the rest never needs sorting)
(Bucket Sort: a frequency is at most n, so index buckets by count and walk from the highest bucket down, no comparisons needed)
(Count what the heap holds) len(minHeap) = "how many top elements have I kept so far? (at most k, pop the root when it hits k + 1)"
(Read the pivot position) resultIndex = "where did the pivot land? if it equals k - 1, then unique[:k] holds the top k"
(Read a bucket) freqBuckets[count] = "which elements occur exactly count times?"
Pseudocode
oh! pseudocode hasn't been written yet, try another card! :)Solution 1: [MinHeap] Min Heap Of Size K Tracks The K Most Frequent Elements - HashMap/Algorithm
def topKFrequent(self, nums: list[int], k: int) -> list[int]:
# MinHeap (Fixed Size K)
# Heap Representation:
# - Maintain a minHeap of (frequency, num) pairs for the k most frequent elements so far
# - Root (minHeap[0]) is the least frequent of those k elements,
# which is the first one to evict
# - Heap is valid when it holds at most k pairs
# Idea:
# - Count the frequency of each element
# - Push each unique (frequency, num) pair into the minHeap
# - If the heap grows to size k + 1, pop the root, since the least frequent
# of k + 1 elements can never be in the top k
# - A heap only guarantees the root is the smallest, not the order of the rest,
# which is fine because the answer can be returned in any order
# - After every unique element has been seen, the heap holds the k most frequent
# Frequency of each element
frequency = defaultdict(int)
# Array Pointer
index = 0
# Count each num
# tc: O(n)
while index < len(nums):
# Add num to count
frequency[nums[index]] += 1
# Iterate array
index += 1
# MinHeap of (frequency, num) pairs for the k most frequent elements
minHeap = []
# Unique (num, count) pairs
uniqueItems = list(frequency.items())
# Unique Pointer
uniqueIndex = 0
# tc: O(m log k), m unique elements, each push and pop is O(log k)
while uniqueIndex < len(uniqueItems):
# Current pair
num, count = uniqueItems[uniqueIndex]
# Push pair
# k = 2, minHeap = [(1, 5), (2, 7)], pair = (3, 9) -> [(1, 5), (2, 7), (3, 9)]
# tc: O(log k)
heapq.heappush(minHeap, (count, num))
# Heap exceeds k, pop the least frequent root
# [(1, 5), (2, 7), (3, 9)] -> pop (1, 5) -> [(2, 7), (3, 9)]
# tc: O(log k)
if len(minHeap) > k:
heapq.heappop(minHeap)
# Iterate unique pairs
uniqueIndex += 1
# Top k elements
result = []
# Heap Pointer
heapIndex = 0
# Collect nums from the heap
# tc: O(k)
while heapIndex < len(minHeap):
# Add num (second item of the pair)
result.append(minHeap[heapIndex][1])
# Iterate heap
heapIndex += 1
# overall: tc O(n + m log k), worst case m = n so O(n log k)
# overall: sc O(n) for the frequency map, O(k) for the heap
return resultSolution 2: [Quick Select] Partition Unique Elements By Frequency Until Index K Minus 1 Is Placed - HashMap/Algorithm
def topKFrequent(self, nums: list[int], k: int) -> list[int]:
# Quick Select (Partition By Frequency, High To Low)
# Search Representation:
# - unique is the list of unique elements, partitioned in place
# - [left, right] is the current search section of unique
# - targetIndex = k - 1 is the final position we need to place correctly
# - Search section is valid while it still contains targetIndex
# Idea:
# - Count the frequency of each element, then work only on the unique elements
# - Pick a random pivot and partition the section so more frequent elements
# sit on the left and less frequent elements sit on the right:
# [more frequent ... pivot ... less frequent]
# - If the pivot lands at targetIndex, everything to its left is more frequent,
# so unique[:k] is the top k
# - If targetIndex is left of the pivot, search the left section
# - If targetIndex is right of the pivot, search the right section
# - Only one side is ever searched, so we never sort the whole array
# - A random pivot avoids the O(n^2) worst case of a fixed pivot
# Frequency of each element
frequency = defaultdict(int)
# Array Pointer
index = 0
# Count each num
# tc: O(n)
while index < len(nums):
# Add num to count
frequency[nums[index]] += 1
# Iterate array
index += 1
# Unique elements to partition by frequency
unique = list(frequency.keys())
# Helper that partitions [left, right] around the pivot, returns the pivot's final index
def partitionSection(left: int, right: int, pivotIndex: int) -> int:
# Frequency of pivot element
pivotFrequency = frequency[unique[pivotIndex]]
# Move pivot to the end
unique[pivotIndex], unique[right] = unique[right], unique[pivotIndex]
# Next slot for a more frequent element
partitionIndex = left
# Scan Pointer
scanIndex = left
# Partition section around pivot
# tc: O(m) for a section of m elements
while scanIndex < right:
# Move more frequent elements to the left:
# unique = [4, 1, 2] with frequency 4 -> 1, 1 -> 3, 2 -> 2, pivot = 2 (at right)
# scan 4 -> frequency 1 < 2, stays
# scan 1 -> frequency 3 > 2, swap with partitionIndex 0 -> [1, 4, 2], partitionIndex = 1
if frequency[unique[scanIndex]] > pivotFrequency:
unique[scanIndex], unique[partitionIndex] = unique[partitionIndex], unique[scanIndex]
partitionIndex += 1
# Iterate section
scanIndex += 1
# Place pivot at its final index:
# [1, 4, 2] -> swap partitionIndex 1 with right -> [1, 2, 4], pivot lands at 1
unique[partitionIndex], unique[right] = unique[right], unique[partitionIndex]
return partitionIndex
# Helper that narrows [left, right] until targetIndex holds its final element
def quickSelect(left: int, right: int, targetIndex: int) -> None:
# One element left, it is already in place
if left == right:
return
# Random pivot
pivotIndex = random.randint(left, right)
# Partition section
resultIndex = partitionSection(left, right, pivotIndex)
# Pivot landed on target, unique[:k] is the top k
if targetIndex == resultIndex:
return
# Target is left of the pivot, search the left section
# k = 2 -> targetIndex = 1, resultIndex = 3 -> search [left, 2]
elif targetIndex < resultIndex:
quickSelect(left, resultIndex - 1, targetIndex)
# Target is right of the pivot, search the right section
# k = 4 -> targetIndex = 3, resultIndex = 1 -> search [2, right]
else:
quickSelect(resultIndex + 1, right, targetIndex)
# Final index of the kth most frequent element
# k = 2 -> top 2 are indexes [0, 1], so targetIndex = 1
targetIndex = k - 1
# Place targetIndex, everything left of it is at least as frequent
# tc: O(m) average, O(m^2) worst case with bad pivots
quickSelect(0, len(unique) - 1, targetIndex)
# overall: tc O(n) average, n for counting + m for quick select, O(n + m^2) worst case
# overall: sc O(n) for the frequency map and unique list, plus O(log m) expected recursion depth
return unique[:k]Solution 3: [Bucket Sort] Bucket Elements By Frequency Then Walk From The Highest Bucket Down - HashMap/Algorithm
def topKFrequent(self, nums: list[int], k: int) -> list[int]:
# Bucket Sort (Index = Frequency)
# Bucket Representation:
# - freqBuckets[count] holds every element that occurs exactly count times
# - Buckets run from index 0 to n, since an element can occur at most n times
# - Higher index = more frequent, so walking from the end visits the top elements first
# Idea:
# - Count the frequency of each element
# - Drop each unique element into the bucket for its frequency
# - Walk the buckets from highest to lowest, collecting elements
# - Stop as soon as k elements are collected
# - No comparison sort is needed, since the frequency itself is the bucket index
# Frequency of each element
frequency = defaultdict(int)
# Array Pointer
index = 0
# Count each num
# tc: O(n)
while index < len(nums):
# Add num to count
frequency[nums[index]] += 1
# Iterate array
index += 1
# Number of buckets, +1 so index n exists (one element repeated n times)
# nums length 5 -> buckets [0, 1, 2, 3, 4, 5], index 0 stays empty
numBuckets = len(nums) + 1
# Empty bucket for every possible frequency
freqBuckets = []
# Bucket Pointer
bucketIndex = 0
# Create buckets
# tc: O(n)
while bucketIndex < numBuckets:
# Add empty bucket
freqBuckets.append([])
# Iterate buckets
bucketIndex += 1
# Unique (num, count) pairs
uniqueItems = list(frequency.items())
# Unique Pointer
uniqueIndex = 0
# Place each unique num into its frequency bucket
# nums = [1, 1, 1, 2, 2, 3] -> freqBuckets = [[], [3], [2], [1], [], [], []]
# tc: O(m)
while uniqueIndex < len(uniqueItems):
# Current pair
num, count = uniqueItems[uniqueIndex]
# Add num to its frequency bucket
freqBuckets[count].append(num)
# Iterate unique pairs
uniqueIndex += 1
# Top k elements
result = []
# Bucket Pointer (highest frequency first, index 0 is always empty)
bucketIndex = len(freqBuckets) - 1
# Walk buckets from high to low until k elements are collected
# k = 2 -> bucket 3 adds [1], bucket 2 adds [2], result = [1, 2], loop stops
# tc: O(n) buckets, O(m) elements across all buckets
while bucketIndex > 0 and len(result) < k:
# Bucket Element Pointer
bucketItemIndex = 0
# Collect elements from the current bucket
while bucketItemIndex < len(freqBuckets[bucketIndex]) and len(result) < k:
# Add num to result
result.append(freqBuckets[bucketIndex][bucketItemIndex])
# Iterate bucket
bucketItemIndex += 1
# Iterate buckets (toward lower frequency)
bucketIndex -= 1
# overall: tc O(n)
# overall: sc O(n) for the frequency map and buckets
return result973. K Closest Points to Origin ::2:: - Medium
Topics: Array, Math, Divide and Conquer, Geometry, Sorting, Heap (Priority Queue), Quickselect
Intro
Given an array of points where points[i] = [x_i, y_i] represents a point on the X-Y plane and an integer k, return the k closest points to the origin (0, 0). The distance between two points on the X-Y plane is the Euclidean distance: sqrt((x_1, x-2)^2 + (y_1 + y_2)^2) You may return the answer in any order. The answer is guaranteed to be unique (except for the order that it is in).
| Example Input | Output |
|---|---|
| points = [[1,3],[-2,2]], k = 1 | [[-2, 2]] |
| points = [[3,3],[5,-1],[-2,4]], k = 2 | [[3,3],[-2,4]] |
Constraints:
1 ≤ k ≤ points.length ≤ 104
-104 ≤ x_1, y_i ≤ 104
Abstraction
Given an array of points, return the k points closest to the origin (any order).
(squared distance: sqrt never changes the order, so compare x * x + y * y and skip the sqrt, and the problem becomes "pick the k smallest values" among the points)
(MaxHeap: keep only the k closest in a maxHeap, so the root is the farthest of the k and the first one to evict, push while the heap has space, once full, replace the root only if the new point is closer)
(Monotonic: the root distance only ever decreases or stays the same once the heap is full, since the heap only keeps points closer than the current farthest, so farther points can be ignored for good)
(Quick Select: partition points by squared distance with closer points on the left, once a pivot lands at index k - 1 the first k points are the k closest, so the rest never needs sorting)
(Count what the heap holds) len(maxHeap) = "how many of the closest points have I kept so far? (at most k)"
(Read the farthest kept point) -maxHeap[0][0] = "what is the squared distance of the farthest of my k closest points?"
(Read the pivot position) resultIndex = "where did the pivot land? if it equals k - 1, then points[:k] holds the k closest"
Pseudocode
oh! pseudocode hasn't been written yet, try another card! :)Solution 1: [MaxHeap] Max Heap Of Size K Tracks The K Closest Points - Heap/Heap
def kClosest(self, points: list[list[int]], k: int) -> list[list[int]]:
# MaxHeap (Fixed Size K)
# Heap Representation:
# - Maintain a maxHeap of (-squaredDistance, point) for the k closest points seen so far
# - heapq is a minHeap, so distances are negated to make the largest distance the root
# - Root (maxHeap[0]) is the farthest of those k points,
# which is the first one to evict
# - Heap is valid when it holds at most k points
# Idea:
# - sqrt is monotonic, so comparing x * x + y * y orders points the same
# as comparing true distances, and avoids floats
# - The k closest points are the k smallest distances, so keep only those k
# in a maxHeap, where the root is the farthest of the k
# - If the heap has fewer than k points, push the new point
# - If the heap is full, the new point only matters if it is closer than the root,
# since anything farther can never be in the top k
# - Replace the root with the new point to evict the old farthest
# - After every point has been seen, the heap holds the k closest, in any order
# MaxHeap of (-squaredDistance, point) for the k closest points
maxHeap = []
# Array Pointer
index = 0
# tc: O(n log k)
while index < len(points):
# Current point and its squared distance
x, y = points[index]
squaredDistance = x * x + y * y
# Heap still has space, push point
# k = 2, maxHeap = [(-18, [3, 3])], point = [5, -1] -> [(-26, [5, -1]), (-18, [3, 3])]
# tc: O(log k)
if len(maxHeap) < k:
heapq.heappush(maxHeap, (-squaredDistance, [x, y]))
# Heap is full, only keep point if it is closer than the current farthest
else:
# Replace root if point is closer:
# k = 2, maxHeap = [(-26, [5, -1]), (-18, [3, 3])]
# point = [-2, 4] -> 20 < 26, replace root -> [(-20, [-2, 4]), (-18, [3, 3])]
# a point with distance 30 -> 30 > 20, ignored, heap stays the same
# tc: O(log k)
if squaredDistance < -maxHeap[0][0]:
heapq.heapreplace(maxHeap, (-squaredDistance, [x, y]))
# Iterate array
index += 1
# K closest points
result = []
# Heap Pointer
heapIndex = 0
# Collect points from the heap
# tc: O(k)
while heapIndex < len(maxHeap):
# Add point (second item of the pair)
result.append(maxHeap[heapIndex][1])
# Iterate heap
heapIndex += 1
# overall: tc O(n log k)
# overall: sc O(k) for the heap
return resultSolution 2: [Quick Select] Partition Points By Squared Distance Until Index K Minus 1 Is Placed - Heap/Heap
def kClosest(self, points: list[list[int]], k: int) -> list[list[int]]:
# Quick Select (Partition By Squared Distance, Closest To Farthest)
# Search Representation:
# - points is partitioned in place
# - [left, right] is the current search section of points
# - targetIndex = k - 1 is the final position we need to place correctly
# - Search section is valid while it still contains targetIndex
# Idea:
# - sqrt is monotonic, so partition by x * x + y * y instead of true distance
# - Pick a random pivot and partition the section so closer points
# sit on the left and farther points sit on the right:
# [closer ... pivot ... farther]
# - If the pivot lands at targetIndex, everything to its left is closer,
# so points[:k] is the k closest
# - If targetIndex is left of the pivot, search the left section
# - If targetIndex is right of the pivot, search the right section
# - Only one side is ever searched, so we never sort the whole array
# - A random pivot avoids the O(n^2) worst case of a fixed pivot
# Helper that returns the squared distance of the point at index
def squaredDistance(index: int) -> int:
return points[index][0] ** 2 + points[index][1] ** 2
# Helper that partitions [left, right] around the pivot, returns the pivot's final index
def partitionSection(left: int, right: int, pivotIndex: int) -> int:
# Squared distance of pivot
pivotDistance = squaredDistance(pivotIndex)
# Move pivot to the end
points[pivotIndex], points[right] = points[right], points[pivotIndex]
# Next slot for a closer point
partitionIndex = left
# Scan Pointer
scanIndex = left
# Partition section around pivot
# tc: O(m) for a section of m points
while scanIndex < right:
# Move closer points to the left:
# points = [[3, 3], [5, -1], [-2, 4]] with distances 18, 26, 20, pivot = [-2, 4] (at right)
# scan [3, 3] -> 18 < 20, swap with partitionIndex 0 (itself), partitionIndex = 1
# scan [5, -1] -> 26 > 20, stays
if squaredDistance(scanIndex) < pivotDistance:
points[scanIndex], points[partitionIndex] = points[partitionIndex], points[scanIndex]
partitionIndex += 1
# Iterate section
scanIndex += 1
# Place pivot at its final index:
# [[3, 3], [5, -1], [-2, 4]] -> swap partitionIndex 1 with right -> [[3, 3], [-2, 4], [5, -1]], pivot lands at 1
points[partitionIndex], points[right] = points[right], points[partitionIndex]
return partitionIndex
# Helper that narrows [left, right] until targetIndex holds its final point
def quickSelect(left: int, right: int, targetIndex: int) -> None:
# One point left, it is already in place
if left >= right:
return
# Random pivot
pivotIndex = random.randint(left, right)
# Partition section
resultIndex = partitionSection(left, right, pivotIndex)
# Pivot landed on target, points[:k] is the k closest
if targetIndex == resultIndex:
return
# Target is left of the pivot, search the left section
# k = 1 -> targetIndex = 0, resultIndex = 2 -> search [left, 1]
elif targetIndex < resultIndex:
quickSelect(left, resultIndex - 1, targetIndex)
# Target is right of the pivot, search the right section
# k = 3 -> targetIndex = 2, resultIndex = 1 -> search [2, right]
else:
quickSelect(resultIndex + 1, right, targetIndex)
# Final index of the kth closest point
# k = 2 -> k closest are indexes [0, 1], so targetIndex = 1
targetIndex = k - 1
# Place targetIndex, everything left of it is at least as close
# tc: O(n) average, O(n^2) worst case with bad pivots
quickSelect(0, len(points) - 1, targetIndex)
# K closest points
# tc: O(k)
closest = points[:k]
# overall: tc O(n) average, O(n^2) worst case with bad pivots
# overall: sc O(k) for the returned list, plus O(log n) expected recursion depth
return closest1985. Find the Kth Largest Integer in the Array ::1:: - Medium
Topics: Array, String, Divide and Conquer, Sorting, Heap (Priority Queue), Quickselect
Intro
You are given an array of strings nums and an integer k. Each string in nums represents an integer without leading zeros. Return the string that represents the kth largest integer in nums. Note: Duplicate numbers should be counted distinctly. For example, if nums is ["1","2","2"], "2" is the first largest integer, "2" is the second-largest integer, and "1" is the third-largest integer.
| Example Input | Output |
|---|---|
| nums = ["3","6","7","10"], k = 4 | "3" |
| nums = ["2","21","12","1"], k = 3 | "2" |
| nums = ["0","0"], k = 2 | "0" |
Constraints:
1 ≤ k ≤ nums.length ≤ 10^4
1 ≤ nums[i].length ≤ 100
nums[i] consists of only digits.
nums[i] will not have any leading zeros.
Abstraction
Given an array of strings representing integers (too large to compare as plain numbers), return the kth largest element in sorted numeric order.
(compare by (length, string): a longer digit string is always the bigger number, and equal length strings compare the same lexicographically as numerically, so raw string order like "10" < "9" is avoided)
(keep only the k largest in a minHeap, so the root is the kth largest) (push while the heap has space, once full, replace the root only if the new key beats it)
(Monotonic: the root only ever increases or stays the same, since the heap only keeps keys that beat the current kth largest, so smaller keys can be ignored for good)
(Partition: after splitting around a pivot, the kth largest lives in exactly one of bigger, equal, or smaller, so recurse into that one side and discard the other two)
(Compare by key) numKey(numStr) = "what (length, string) pair makes this string sort in true numeric order?"
(Read the answer from the root) minHeap[0] = "what is the smallest of my k largest keys, which is exactly the kth largest?"
(Count what bigger holds, for quick select) len(bigger) = "how many elements rank above the pivot? if k lte this, the answer is in bigger"
(Count what equal holds, for quick select) len(bigger) + len(equal) = "how many elements rank at or above the pivot? if k lte this, the answer is the pivot"
Pseudocode
oh! pseudocode hasn't been written yet, try another card! :)Solution 1: [Heap] MinHeap Of Size K With Numeric Key Tracks The Kth Largest Element - Heap/Heap
def kthLargestNumber(self, nums: list[str], k: int) -> str:
# MinHeap (Fixed Size K)
# Heap Representation:
# - Maintain a minHeap holding the keys of the k largest numbers seen so far
# - Each key is (length, string), which sorts in true numeric order
# - Root (minHeap[0]) is the smallest of those k keys,
# which is exactly the kth largest number overall
# - Heap is valid when it holds at most k keys
# Idea:
# - nums are strings of huge integers, so raw string comparison is wrong ("10" < "9")
# - A longer string is always the bigger number (no leading zeros),
# and equal length strings compare correctly lexicographically,
# so the key (length, string) orders them numerically
# - The kth largest is the smallest of the top k, so keep only the top k keys in a minHeap
# - If the heap has fewer than k keys, push the new key
# - If the heap is full, the new key only matters if it beats the root,
# since anything smaller can never be in the top k
# - Replace the root with the new key to evict the old kth largest
# - After every string has been seen, the root's string is the answer
# Helper that builds a numerically ordered key
def numKey(numStr: str) -> tuple[int, str]:
return (len(numStr), numStr)
# MinHeap of the keys of the k largest numbers seen so far
minHeap = []
# Array Pointer
index = 0
# tc: O(n log k * d), n calls with an O(log k) heap op, each comparing keys up to d digits
while index < len(nums):
# Current key
key = numKey(nums[index])
# Heap still has space, push key
# k = 2, minHeap = [(1, "3")], key = (2, "10") -> [(1, "3"), (2, "10")]
# tc: O(log k * d)
if len(minHeap) < k:
heapq.heappush(minHeap, key)
# Heap is full, only keep key if it beats the current kth largest
else:
# Replace root if key is larger:
# k = 2, minHeap = [(1, "3"), (2, "10")]
# "2" -> (1, "2") < (1, "3"), ignored, heap stays the same
# "6" -> (1, "6") > (1, "3"), replace root -> [(1, "6"), (2, "10")]
# tc: O(log k * d)
if key > minHeap[0]:
heapq.heapreplace(minHeap, key)
# Iterate array
index += 1
# Kth largest number (string stored in the root key)
kthLargest = minHeap[0][1]
# overall: tc O(n log k * d), d = max digits in a string
# overall: sc O(k * d), for the heap keys
return kthLargestSolution 2: [Quick Select] Three Way Partition With Numeric Key That Ignores Duplicates (Recurse Into One Side) - Heap/Heap
def kthLargestNumber(self, nums: list[str], k: int) -> str:
# Quick Select (Three Way Partition)
# Search Representation:
# - nums is the current search space
# - k is the rank (1 = largest) of the target inside the current nums
# - Search space is valid while it still contains the kth largest
# Idea:
# - nums are strings of huge integers, so compare by key (length, string)
# instead of raw string order, which avoids int() conversion on 100 digit strings
# - Pick a random pivot and split nums into bigger, equal, and smaller by key
# - If k <= len(bigger), the kth largest is inside bigger, recurse there
# - If k <= len(bigger) + len(equal), the kth largest is the pivot itself,
# so every duplicate of the pivot is resolved in one step
# - Otherwise the kth largest is inside smaller, recurse there with
# k reduced by the number of elements ranked above it
# - Only one side is ever searched, so the work shrinks every call
# Why Three Way (Duplicates):
# - A two way partition (<= pivot vs > pivot) leaves every copy of the pivot
# on one side, so with many duplicates the pivot barely shrinks the search space
# - Random pivots don't help, since every pivot has the same value
# - Each call then removes almost nothing, so time degrades to O(n^2) (TLE)
# and recursion depth can hit the limit
# - The equal bucket is never recursed into: if k lands inside it, the answer
# is the pivot, so all copies of that value cost one pass and then disappear
# - With no duplicates it costs nothing extra, still O(n) per pass
# Helper that builds a numerically ordered key
def numKey(numStr: str) -> tuple[int, str]:
return (len(numStr), numStr)
# Helper that searches one partition for the kth largest
def quickSelect(nums: list[str], k: int) -> str:
# Random pivot
pivot = random.choice(nums)
pivotKey = numKey(pivot)
# Partitions
bigger = []
equal = []
smaller = []
# Array Pointer
index = 0
# Partition nums around pivot
# tc: O(n * d)
while index < len(nums):
# Current num and key
num = nums[index]
key = numKey(num)
# Sort num into its partition
if key > pivotKey:
bigger.append(num)
elif key == pivotKey:
equal.append(num)
else:
smaller.append(num)
# Iterate array
index += 1
# Kth largest is inside bigger, recurse
# nums = ["3", "6", "7", "10"], k = 2, pivot = "6"
# bigger = ["7", "10"], k <= 2 -> search ["7", "10"] for 2nd largest
if k <= len(bigger):
return quickSelect(bigger, k)
# Kth largest is the pivot (duplicates resolved in one step):
# nums = ["3", "6", "7", "10"], k = 3, pivot = "6"
# bigger = ["7", "10"], equal = ["6"]
# k <= 2 + 1 -> the 3rd largest is "6"
if k <= len(bigger) + len(equal):
return pivot
# Kth largest is inside smaller, recurse with k adjusted:
# nums = ["3", "6", "7", "10"], k = 4, pivot = "6"
# bigger = ["7", "10"], equal = ["6"], smaller = ["3"]
# 4 > 2 + 1 -> search ["3"] for k = 4 - 2 - 1 = 1st largest
newK = k - len(bigger) - len(equal)
return quickSelect(smaller, newK)
# Kth largest number
kthLargest = quickSelect(nums, k)
# overall: tc O(n * d) average (n + n/2 + n/4 + ...), O(n^2 * d) worst case with bad pivots
# overall: sc O(n) for the partition lists, plus O(log n) expected recursion depth
return kthLargest23. Merge K Sorted Lists ::2:: - Hard
Topics: Linked List, Divide and Conquer, Heap (Priority Queue), Merge Sort
Intro
You are given an array of k linked-lists lists, each linked-list is sorted in ascending order. Merge all the linked-lists into one sorted linked-list and return it.
| Example Input | Output |
|---|---|
| lists = [[1,4,5],[1,3,4],[2,6]] | [1,1,2,3,4,4,5,6] |
| lists = [] | [] |
| lists = [[]] | [] |
Constraints:
k == lists.length
0 ≤ k ≤ 104
0 ≤ lists[i].length ≤ 500
-104 ≤ lists[i][j] ≤ 104
lists[i] is sorted in ascending order.
The sum of lists[i].length will not exceed 104
Abstraction
Given a list of k sorted linked lists, merge them into one sorted linked list.
(sorted lists: each list's smallest unmerged node is its head, so the next node of the merged list is always the smallest of the k heads, and the problem becomes "repeatedly pick the smallest head")
(MinHeap: keep one candidate per list in a minHeap, so the root is the smallest node not yet taken, pop it onto the merged list, then push the next node from the same list)
(Monotonic: a list's 2nd node can never matter before its 1st is taken, so one candidate per list is enough, and the heap never holds more than k nodes instead of all N)
(Divide And Conquer: merging two sorted lists is a simple two pointer walk, so merge the lists in pairs, each round halves the number of lists, so there are O(log k) rounds and each round touches every node once)
(Count what the heap holds) len(minHeap) = "how many lists still have a candidate in play? (at most k, one per list)"
(Read the next node from the root) minHeap[0] = "what is the smallest node among all the list heads not yet taken?"
(Count what the lists hold) len(lists) = "how many lists are left to merge? stop when it reaches 1"
(Read the end of the merged list) tail = "where does the next node attach to the merged list?"
Pseudocode
oh! pseudocode hasn't been written yet, try another card! :)Solution 1: [MinHeap] K Way Merge Of List Heads With A List Index Tiebreaker - Linked List/Simple Traversal
def mergeKLists(self, lists: list[Optional[ListNode]]) -> Optional[ListNode]:
# MinHeap (K Way Merge)
# Heap Representation:
# - Maintain a minHeap of (value, listIndex, node) candidates, one per non empty list
# - Each list is sorted, so a list's candidate is its smallest node not yet taken
# - Root (minHeap[0]) is the smallest value among all candidates not yet taken
# - Heap is valid when it holds at most one candidate per list
# Idea:
# - Every list is sorted, so the next node of the merged list is always
# the smallest of the k current heads
# - Seed the heap with the head of every non empty list
# - Pop the root, attach it to the merged list, then push the next node
# in the same list, so each list always has exactly one candidate in the heap
# - A list's 2nd node can't matter before its 1st is popped,
# so the heap holds at most k nodes instead of all N
# - listIndex is the tiebreaker: when two values are equal, heapq would compare
# the ListNode objects next, which raises a TypeError, so the unique index
# decides before it ever gets that far
# - Stop when the heap is empty, every list has been fully merged
# MinHeap of (value, listIndex, node) candidates, one per non empty list
minHeap = []
# List Pointer
seedIndex = 0
# Seed each non empty list with its head
# tc: O(k)
while seedIndex < len(lists):
# Add head of the list, skipping empty lists
if lists[seedIndex]:
minHeap.append((lists[seedIndex].val, seedIndex, lists[seedIndex]))
# Iterate lists
seedIndex += 1
# Order the seeds into a heap
# tc: O(k)
heapq.heapify(minHeap)
# Dummy head so the first attach needs no special case
dummyHead = ListNode(-1)
# End of the merged list
tail = dummyHead
# tc: O(N log k), N pops, each heap op is O(log k)
while minHeap:
# Smallest candidate
value, listIndex, node = heapq.heappop(minHeap)
# Attach node to merged list
tail.next = node
tail = tail.next
# List has a next node, push it:
# lists = [[1, 4, 5], [1, 3, 4], [2, 6]]
# seeds -> [(1, 0, 1), (1, 1, 1), (2, 2, 2)]
# pop (1, 0, 1) -> merged [1], push (4, 0, 4)
# pop (1, 1, 1) -> merged [1, 1], push (3, 1, 3)
# pop (2, 2, 2) -> merged [1, 1, 2], push (6, 2, 6)
# ... until the heap is empty -> [1, 1, 2, 3, 4, 4, 5, 6]
# tc: O(log k)
if node.next:
heapq.heappush(minHeap, (node.next.val, listIndex, node.next))
# Merged list (after the dummy)
mergedHead = dummyHead.next
# overall: tc O(N log k), N = total nodes, k = number of lists
# overall: sc O(k) for the heap, nodes are relinked in place
return mergedHeadSolution 2: [Divide And Conquer] Merge Lists In Pairs Each Round Until One List Remains - Linked List/Simple Traversal
def mergeKLists(self, lists: list[Optional[ListNode]]) -> Optional[ListNode]:
# Divide And Conquer (Pairwise Merge)
# Merge Representation:
# - lists holds the sorted lists still to be merged
# - Each round merges lists[0] with lists[1], lists[2] with lists[3], and so on
# - Merge is done when lists holds exactly one list
# Idea:
# - Merging two sorted lists is a simple two pointer walk, O(length of both)
# - Merge the k lists in pairs, so each round halves the number of lists,
# which takes O(log k) rounds
# - Every round touches each node once across all of its merges, O(N) per round
# - An odd list out is merged with None, which just carries it to the next round
# - Merging one list at a time into a growing result is O(N * k),
# because the early nodes get re walked every merge, pairing keeps the lists balanced
# Empty check, k can be 0
if not lists:
return None
# Helper that merges two sorted lists
def mergeTwo(first: Optional[ListNode], second: Optional[ListNode]) -> Optional[ListNode]:
# Dummy head so the first attach needs no special case
dummyHead = ListNode(-1)
# End of the merged list
tail = dummyHead
# Merge while both lists have nodes
# first = [1, 4], second = [2, 3]
# 1 < 2 -> attach 1, then 2 <= 4 -> attach 2, then 3 < 4 -> attach 3
# second is empty, attach the rest of first -> [1, 2, 3, 4]
# tc: O(length of both lists)
while first and second:
# Attach the smaller node
if first.val < second.val:
tail.next = first
first = first.next
else:
tail.next = second
second = second.next
# Iterate merged list
tail = tail.next
# Attach the remaining list, at most one is non empty
if first:
tail.next = first
else:
tail.next = second
return dummyHead.next
# tc: O(log k) rounds, each round is O(N)
while len(lists) > 1:
# Lists produced by this round
merged = []
# Pair Pointer
pairIndex = 0
# Merge lists in pairs
# lists = [A, B, C] -> [merge(A, B), merge(C, None)] -> [merge(merge(A, B), C)]
# tc: O(N)
while pairIndex < len(lists):
# Pair of lists, second is None for an odd list out
first = lists[pairIndex]
second = lists[pairIndex + 1] if pairIndex + 1 < len(lists) else None
# Add merged pair
merged.append(mergeTwo(first, second))
# Iterate pairs
pairIndex += 2
# Next round works on the merged lists
lists = merged
# overall: tc O(N log k), N = total nodes, k = number of lists
# overall: sc O(k) for the merged list of heads each round, nodes are relinked in place
return lists[0]373. Find K Pairs with Smallest Sums ::1:: - Medium
Topics: Array, Heap (Priority Queue)
Intro
You are given two integer arrays nums1 and nums2 sorted in non-decreasing order and an integer k. Define a pair (u, v) which consists of one element from the first array and one element from the second array. Return the k pairs (u1, v1), (u2, v2), ..., (uk, vk) with the smallest sums.
| Example Input | Output |
|---|---|
| nums1 = [1,7,11], nums2 = [2,4,6], k = 3 | [[1,2],[1,4],[1,6]] |
| nums1 = [1,1,2], nums2 = [1,2,3], k = 2 | [[1,1],[1,1]] |
Constraints:
1 ≤ nums1.length, nums2.length ≤ 10^5
-10^9 ≤ nums1[i], nums2[i] ≤ 10^9
nums1 and nums2 both are sorted in non-decreasing order.
1 ≤ k ≤ 10^4
k ≤ nums1.length * nums2.length
Abstraction
Given two sorted arrays, return the k pairs (one element from each) with the smallest sums.
(sorted arrays: fix a num in nums1 and the sums with nums2[0], nums2[1], ... are non-decreasing, so every nums1 index owns its own sorted list of candidate sums, and the problem becomes "merge k sorted lists")
(MinHeap: keep one candidate per row in a minHeap, so the root is the smallest sum not yet taken, pop it, then push that row's next candidate)
(Monotonic: row i's best pair (i, 0) is never smaller than row i - 1's best pair, so row i can only matter after row i - 1 has been used, which means only the first k rows can ever contribute)
(Count what the heap holds) len(minHeap) = "how many rows still have a candidate in play? (at most min(n1, k) seeded, one per row)"
(Read the next pair from the root) minHeap[0] = "what is the smallest sum among all candidates not yet taken?"
(Count what the result holds) len(result) = "how many of the k smallest pairs have I collected so far?"
Pseudocode
oh! pseudocode hasn't been written yet, try another card! :)Solution 1: [Heap] MinHeap K Way Merge Of Sorted Rows With Bounded Seeding - Heap/Heap
def kSmallestPairs(self, nums1: list[int], nums2: list[int], k: int) -> list[list[int]]:
# MinHeap (K Way Merge)
# Heap Representation:
# - Maintain a minHeap of (sum, i, j) candidates, one per row i of nums1
# - Row i is the sorted list nums1[i] + nums2[0], nums1[i] + nums2[1], ...
# - Root (minHeap[0]) is the smallest sum among all candidates not yet taken
# - Heap is valid when it holds at most one candidate per row
# Idea:
# - nums2 is sorted, so for a fixed i the sums grow as j grows,
# which makes every row its own sorted list
# - Finding the k smallest across these lists is the same k way merge as
# Merge K Sorted Lists and Kth Smallest In A Sorted Matrix
# - Pop the root, add its pair to the result, then push the next pair in the same row
# (i, j + 1), so each row always has exactly one candidate in the heap
# - Only seed min(n1, k) rows: pair (i, 0) is never smaller than pairs (0..i-1, 0),
# so a row at index k or beyond can't reach the first k results
# - Seeding fewer rows keeps the heap size, and each heap op, bounded by min(n1, k)
# - Stop once k pairs are collected
# Lengths
n1 = len(nums1)
n2 = len(nums2)
# Rows to seed
seedCount = min(n1, k)
# MinHeap of (sum, i, j) candidates, one per seeded row
minHeap = []
# Row Pointer
row = 0
# Seed each row with nums2[0], the smallest partner for that row
# tc: O(min(n1, k))
while row < seedCount:
# Add first candidate of the row
minHeap.append((nums1[row] + nums2[0], row, 0))
# Iterate rows
row += 1
# Order the seeds into a heap
# tc: O(min(n1, k))
heapq.heapify(minHeap)
# K smallest pairs
result = []
# tc: O(k log min(n1, k)), k pops, each heap op is O(log min(n1, k))
while minHeap and len(result) < k:
# Smallest candidate
total, i, j = heapq.heappop(minHeap)
# Add pair to result
result.append([nums1[i], nums2[j]])
# Row has a next candidate, push it:
# nums1 = [1, 7, 11], nums2 = [2, 4, 6], k = 3
# seeds -> [(3, 0, 0), (9, 1, 0), (13, 2, 0)]
# pop (3, 0, 0) -> [1, 2], push (5, 0, 1)
# pop (5, 0, 1) -> [1, 4], push (7, 0, 2)
# pop (7, 0, 2) -> [1, 6], j + 1 = 3 is out of range, nothing to push
# tc: O(log min(n1, k))
if j + 1 < n2:
heapq.heappush(minHeap, (nums1[i] + nums2[j + 1], i, j + 1))
# overall: tc O(k log min(n1, k))
# overall: sc O(min(n1, k)) for the heap, O(k) for the result
return result378. Kth Smallest Element in a Sorted Matrix ::1:: - Medium
Topics: String, Greedy, Sorting, Heap (Priority Queue), Counting
Intro
Given an n x n matrix where each of the rows and columns is sorted in ascending order, return the kth smallest element in the matrix. Note that it is the kth smallest element in the sorted order, not the kth distinct element. You must find a solution with a memory complexity better than O(n^2). Follow up: Could you solve the problem with a constant memory (i.e., O(1) memory complexity)? Could you solve the problem in O(n) time complexity? The solution may be too advanced for an interview but you may find reading this paper fun.
| Example Input | Output |
|---|---|
| matrix = [[1,5,9],[10,11,13],[12,13,15]], k = 8 | 13 |
| matrix = [[-5]], k = 1 | -5 |
Constraints:
n == matrix.length == matrix[i].length
1 ≤ n ≤ 300
-10^9 ≤ matrix[i][j] ≤ 10^9
All the rows and columns of matrix are guaranteed to be sorted in non-decreasing order.
1 ≤ k ≤ n^2
Abstraction
Given an n x n matrix with sorted rows and columns, return the kth smallest element (duplicates count).
(sorted rows: every row is already a sorted list, so the matrix is really n sorted lists, and the problem becomes "merge sorted lists until the kth element")
(MinHeap: keep one candidate per row in a minHeap, so the root is the smallest element not yet taken, pop it, then push that row's next element, the kth pop is the answer)
(Monotonic: a row's 2nd element can never matter before its 1st is popped, so one candidate per row is enough, and the heap never holds more than n entries instead of n^2)
(Binary Search On Value: the answer is a value inside the matrix, so search the value range [matrix[0][0], matrix[n - 1][n - 1]], for a candidate x count how many elements are lte x, if the k lte count the answer is lte x, otherwise it is > x)
(Staircase Count: start at the bottom left, if the element is lte x the whole column above it is lte x so count it and move right, otherwise the whole row to its right is > x so move up, each step moves right or up, so a count is O(n))
(Count what the heap holds) len(minHeap) = "how many rows still have a candidate in play? (at most n, one per row)"
(Count the discarded pops) popCount = "how many of the smaller elements have I thrown away? stop at k - 1"
(Read the answer from the root) minHeap[0][0] = "after k - 1 pops, what is the smallest element left, which is exactly the kth smallest?"
(Count elements at or below a candidate) countLessEqual(x) = "how many elements in the matrix are lte x? if k lte this count, the answer is at most x"
Pseudocode
oh! pseudocode hasn't been written yet, try another card! :)Solution 1: [MinHeap] K Way Merge Of Sorted Rows With One Candidate Per Row - Heap/Heap
def kthSmallest(self, matrix: list[list[int]], k: int) -> int:
# MinHeap (K Way Merge)
# Heap Representation:
# - Maintain a minHeap of (value, row, col) candidates, one per row
# - Each row is already a sorted list, so a row's candidate is its smallest element not yet taken
# - Root (minHeap[0]) is the smallest value among all candidates not yet taken
# - Heap is valid when it holds at most one candidate per row
# Idea:
# - Every row is sorted, so the matrix is n separate sorted lists
# - Finding the kth smallest across sorted lists is the same k way merge as
# Merge K Sorted Lists
# - Seed every row with its first (smallest) element
# - Pop the root k - 1 times to discard the k - 1 smallest values,
# and after each pop push the next element in the same row (row, col + 1),
# so each row always has exactly one candidate in the heap
# - A row's 2nd element can't matter before its 1st is popped,
# so the heap holds at most n entries instead of n^2
# - After the k - 1 pops, the root is the kth smallest
# Number of rows
n = len(matrix)
# MinHeap of (value, row, col) candidates, one per row
minHeap = []
# Row Pointer
row = 0
# Seed each row with its first element
# tc: O(n)
while row < n:
# Add first candidate of the row
minHeap.append((matrix[row][0], row, 0))
# Iterate rows
row += 1
# Order the seeds into a heap
# tc: O(n)
heapq.heapify(minHeap)
# Discarded Pops
popCount = 0
# Discard the k - 1 smallest values
# tc: O(k log n), k - 1 pops and pushes, each heap op is O(log n)
while popCount < k - 1:
# Smallest candidate
value, row, col = heapq.heappop(minHeap)
# Row has a next candidate, push it:
# matrix = [[1, 5, 9], [10, 11, 13], [12, 13, 15]], k = 8
# seeds -> [(1, 0, 0), (10, 1, 0), (12, 2, 0)]
# pop (1, 0, 0), col + 1 = 1 < 3 -> push (5, 0, 1)
# pop (5, 0, 1), col + 1 = 2 < 3 -> push (9, 0, 2)
# pop (9, 0, 2), col + 1 = 3 is out of range, nothing to push
# ... after 7 pops the root is the second 13, the 8th smallest
# tc: O(log n)
if col + 1 < n:
heapq.heappush(minHeap, (matrix[row][col + 1], row, col + 1))
# Iterate pops
popCount += 1
# Kth smallest value (root)
kthSmallest = minHeap[0][0]
# overall: tc O(n + k log n), seeding with heapify is O(n), k - 1 pops and pushes are O(k log n)
# overall: sc O(n) for the heap
return kthSmallestSolution 2: [Binary Search] Binary Search On Value With A Staircase Count Of Elements At Or Below - Heap/Binary Search
def kthSmallest(self, matrix: list[list[int]], k: int) -> int:
# Binary Search (On Value)
# Search Representation:
# - [low, high] is the range of possible values for the answer
# - Starts as [matrix[0][0], matrix[n - 1][n - 1]], the smallest and largest values
# - Search is done when low == high, one candidate value remains
# Idea:
# - We don't binary search positions, we binary search over VALUES
# - For a candidate value x, count how many elements are <= x
# - If the count is >= k, the kth smallest is at most x, so high = mid
# - Otherwise the kth smallest is greater than x, so low = mid + 1
# - The range collapses onto the smallest value whose count is >= k,
# and that value is guaranteed to be inside the matrix
# Staircase Count (Saddleback Search):
# - Start at the bottom left corner
# - If the element is <= x, every element above it in that column is also <= x,
# so count the whole column at once and move right
# - If the element is > x, every element to its right in that row is also > x,
# so move up
# - Each step moves right or up, so at most 2n steps, O(n) per count
# Number of rows and columns
n = len(matrix)
# Helper that counts elements <= x
def countLessEqual(x: int) -> int:
# Elements <= x
count = 0
# Staircase Pointers (bottom left)
row = n - 1
col = 0
# tc: O(n), at most n moves right + n moves up
while row >= 0 and col < n:
# Element <= x, count the whole column above it and move right:
# matrix = [[1, 5, 9], [10, 11, 13], [12, 13, 15]], x = 13
# (2, 0) = 12 <= 13 -> count += 3, move right
# (2, 1) = 13 <= 13 -> count += 3, move right
# (2, 2) = 15 > 13 -> move up
# (1, 2) = 13 <= 13 -> count += 2, move right, out of range
# count = 8
if matrix[row][col] <= x:
count += row + 1
col += 1
# Element > x, the rest of the row is also > x, move up
else:
row -= 1
return count
# Binary Search Boundaries (possible answer values)
low = matrix[0][0]
high = matrix[n - 1][n - 1]
# tc: O(log(high - low)), each iteration does an O(n) count
while low < high:
# Middle value
mid = low + (high - low) // 2
# Enough elements <= mid, the answer is at most mid, keep mid as a candidate:
# k = 8, matrix above
# low = 1, high = 15, mid = 8 -> countLessEqual(8) = 2 < 8 -> low = 9
# low = 9, high = 15, mid = 12 -> countLessEqual(12) = 6 < 8 -> low = 13
# low = 13, high = 15, mid = 14 -> countLessEqual(14) = 8 >= 8 -> high = 14
# low = 13, high = 14, mid = 13 -> countLessEqual(13) = 8 >= 8 -> high = 13
# low == high == 13
if countLessEqual(mid) >= k:
high = mid
# Too few elements <= mid, the answer is greater than mid
else:
low = mid + 1
# low == high, the smallest value whose count is >= k
kthSmallest = low
# overall: tc O(n log(max - min)), max and min are the largest and smallest values in the matrix
# overall: sc O(1) for the pointers and counters
return kthSmallestSolution 3: [Binary Search] Sampled Median Pivot With A Binary Search Fallback (Educational, Not True Frederickson Johnson) - Heap/Algorithm
def kthSmallest(self, matrix: list[list[int]], k: int) -> int:
# Binary Search (Sampled Median Pivot)
# NOTE: This is a simplified, educational sketch inspired by Frederickson Johnson selection.
# It is NOT a verified implementation of the paper, and it does NOT achieve true O(n).
# Search Representation:
# - [low, high] is the range of possible values for the answer
# - Search is done when low == high, one candidate value remains
# Idea:
# - Same value search as Solution 2, but the pivot is chosen from the matrix
# instead of always taking the middle of the value range
# - Pivot: take the middle element of every row (that row's median) and
# use the median of those medians, analogous to median of medians in 1D selection
# - Count elements <= pivot with the staircase count from Solution 2
# - If the count is >= k, high = pivot, otherwise low = pivot + 1
# - The pivot must land inside [low, high - 1] to shrink the range,
# otherwise fall back to the middle value, as in Solution 2
# Number of rows and columns
n = len(matrix)
# Helper that counts elements <= x (same staircase count as Solution 2)
def countLessEqual(x: int) -> int:
# Elements <= x
count = 0
# Staircase Pointers (bottom left)
row = n - 1
col = 0
# tc: O(n)
while row >= 0 and col < n:
# Element <= x, count the whole column above it and move right
if matrix[row][col] <= x:
count += row + 1
col += 1
# Element > x, move up
else:
row -= 1
return count
# Helper that returns the median of the row medians
def medianOfRowMedians() -> int:
# Middle element of every row
rowMedians = []
# Row Pointer
row = 0
# tc: O(n)
while row < n:
# Add row median
rowMedians.append(matrix[row][n // 2])
# Iterate rows
row += 1
# Median of the row medians
# tc: O(n log n)
rowMedians.sort()
return rowMedians[n // 2]
# Binary Search Boundaries (possible answer values)
low = matrix[0][0]
high = matrix[n - 1][n - 1]
# tc: O(n log(max - min)) worst case, each iteration does an O(n) count
while low < high:
# Sampled pivot
pivot = medianOfRowMedians()
# Pivot can't shrink the range, fall back to the middle value:
# matrix = [[1, 2], [3, 4]], k = 1
# low = 1, high = 4, pivot = 4 -> pivot >= high, so mid = 2
# without this check, count(4) = 4 >= 1 sets high = 4 forever (infinite loop)
if pivot < low or pivot >= high:
pivot = low + (high - low) // 2
# Enough elements <= pivot, the answer is at most pivot
if countLessEqual(pivot) >= k:
high = pivot
# Too few elements <= pivot, the answer is greater than pivot
else:
low = pivot + 1
# low == high, the smallest value whose count is >= k
kthSmallest = low
# overall: tc O(n log(max - min)) worst case, the sampled pivot only helps on the first round
# because it is computed from the whole matrix, not the remaining range
# overall: sc O(n) for the rowMedians list
return kthSmallest786. Kth Smallest Prime Fraction ::1:: - Medium
Topics: Array, Heap (Priority Queue)
Intro
You are given a sorted integer array arr containing 1 and prime numbers, where all the integers of arr are unique. You are also given an integer k. For every i and j where 0 lte i lt j lt arr.length, we consider the fraction arr[i] / arr[j]. Return the kth smallest fraction considered. Return your answer as an array of integers of size 2, where answer[0] == arr[i] and answer[1] == arr[j].
| Example Input | Output |
|---|---|
| arr = [1,2,3,5], k = 3 | [2,5] |
| arr = [1,7], k = 1 | [1,7] |
Constraints:
2 ≤ arr.length ≤ 1000
1 ≤ arr1[i] ≤ 3 * 10^4
arr[0] == 1
arr[i] is a prime number for i > 0
All the numbers of arr are unique and sorted in strictly increasing order.
1 ≤ k ≤ arr.length * (arr.length - 1) / 2
Abstraction
Given a sorted array of 1 and primes, return the kth smallest fraction arr[i] / arr[j] where i < j.
(sorted array: fix a numerator index i and shrink the denominator index j from the end toward i + 1, arr[j] gets smaller while arr[i] stays fixed, so the fractions strictly increase, which means every numerator index owns its own sorted row, and the problem becomes "merge sorted rows")
(MinHeap: keep one candidate per row in a minHeap, so the root is the smallest fraction not yet taken, pop it, then push that row's next candidate by moving j one step left)
(Monotonic: a row's first candidate, with j = n - 1, is its smallest fraction, so seeding every row there guarantees the root is always the true global minimum, and a row's next fraction can never beat its current one, so it only needs to enter the heap after the current one is popped)
(Count what the heap holds) len(minHeap) = "how many rows still have a candidate in play? (n - 1 seeded, one per numerator index)"
(Count the discarded pops) popCount = "how many of the smaller fractions have I thrown away? stop at k - 1"
(Read the answer from the root) minHeap[0] = "after k - 1 pops, what is the smallest fraction left, which is exactly the kth smallest?"
Pseudocode
oh! pseudocode hasn't been written yet, try another card! :)Solution 1: [Heap] MinHeap K Way Merge Of Sorted Rows Over Shrinking Denominators - Heap/Heap
def kthSmallestPrimeFraction(self, arr: list[int], k: int) -> list[int]:
# MinHeap (K Way Merge)
# Heap Representation:
# - Maintain a minHeap of (fraction, i, j) candidates, one per numerator index i
# - Row i is the sorted list arr[i] / arr[n - 1], arr[i] / arr[n - 2], ..., arr[i] / arr[i + 1]
# - Root (minHeap[0]) is the smallest fraction among all candidates not yet taken
# - Heap is valid when it holds at most one candidate per row
# Idea:
# - For a fixed i, arr[i] stays the same and arr[j] shrinks as j moves left,
# so the fraction strictly increases, which makes every row its own sorted list
# - Finding the kth smallest across these lists is the same k way merge as
# Kth Smallest In A Sorted Matrix, just walking denominators backward
# - Seed every row at j = n - 1, since the largest denominator gives
# the smallest fraction for that row
# - Pop the root k - 1 times to discard the k - 1 smallest fractions,
# and after each pop push the next candidate in the same row (i, j - 1),
# so each row always has exactly one candidate in the heap
# - After the k - 1 pops, the root is the kth smallest
# - A row is exhausted once j - 1 reaches i, since j must stay greater than i
# Number of elements
n = len(arr)
# Index of the largest denominator
lastIndex = n - 1
# MinHeap of (fraction, i, j) candidates, one per row
minHeap = []
# Row Pointer
row = 0
# Seed each row with its smallest fraction, arr[row] / arr[lastIndex]
# tc: O(n)
while row < lastIndex:
# Add first candidate of the row
minHeap.append((arr[row] / arr[lastIndex], row, lastIndex))
# Iterate rows
row += 1
# Order the seeds into a heap
# tc: O(n)
heapq.heapify(minHeap)
# Discarded Pops
popCount = 0
# Discard the k - 1 smallest fractions
# tc: O(k log n), k - 1 pops and pushes, each heap op is O(log n)
while popCount < k - 1:
# Smallest candidate
fraction, i, j = heapq.heappop(minHeap)
# Row has a next candidate, push it:
# arr = [1, 2, 3, 5], k = 3
# seeds -> [(0.2, 0, 3), (0.4, 1, 3), (0.6, 2, 3)]
# pop (0.2, 0, 3), j - 1 = 2 > 0 -> push (1/3, 0, 2)
# pop (1/3, 0, 2), j - 1 = 1 > 0 -> push (1/2, 0, 1)
# k - 1 = 2 pops done, root is (0.4, 1, 3) -> [2, 5]
# tc: O(log n)
if j - 1 > i:
heapq.heappush(minHeap, (arr[i] / arr[j - 1], i, j - 1))
# Iterate pops
popCount += 1
# Kth smallest fraction (root)
_, i, j = minHeap[0]
# overall: tc O(n + k log n), seeding with heapify is O(n), k - 1 pops and pushes are O(k log n)
# overall: sc O(n) for the heap
return [arr[i], arr[j]]1046. Last Stone Weight ::1:: - Easy
Topics: Array, Heap (Priority Queue)
Intro
You are given an array of integers stones where stones[i] is the weight of the ith stone. We are playing a game with the stones. On each turn, we choose the heaviest two stones and smash them together. Suppose the heaviest two stones have weights x and y with x ≤ y. The result of this smash is: If x == y, both stones are destroyed, and If x != y, the stone of weight x is destroyed, and the stone of weight y has new weight y - x. At the end of the game, there is at most one stone left. Return the weight of the last remaining stone. If there are no stones left, return 0.
| Example Input | Output |
|---|---|
| stones = [2,7,4,1,8,1] | 1 |
| stones = [1] | 1 |
Constraints:
1 ≤ stones.length ≤ 30
1 ≤ stones[i] ≤ 1000
Abstraction
Given an array of stone weights, repeatedly smash the two heaviest stones together until at most one remains, and return its weight (0 if none).
(smash rule: equal stones both vanish, unequal stones leave one stone of weight y - x, so each round removes two stones and adds back at most one)
(MaxHeap: the only stones that matter each round are the two heaviest, so keep every stone in a maxHeap, where the root is the heaviest, pop two, smash them, then push the remainder if there is one)
(Monotonic: a smash never creates a stone heavier than the heaviest one used, since y - x lte y, so the heaviest stone never grows, and the heap shrinks by at least one stone every round, so the game always ends)
(Python heapq is a minHeap, so store negated weights to make the heaviest stone the root)
(Count what the heap holds) len(maxHeap) = "how many stones are left? stop when this is 1 or less"
(Read the heaviest stone) -maxHeap[0] = "what is the weight of the heaviest stone still in play?"
(Read the remainder) heaviest - secondHeaviest = "what weight is left after smashing the two heaviest? if 0, both are destroyed"
Pseudocode
oh! pseudocode hasn't been written yet, try another card! :)Solution 1: [MaxHeap] Negated MinHeap Always Yields The Two Heaviest Stones - Heap/Heap
def lastStoneWeight(self, stones: list[int]) -> int:
# MaxHeap (Negated MinHeap)
# Heap Representation:
# - Maintain a maxHeap of every stone still in play, stored as negated weights
# - heapq is a minHeap, so negating weights makes the heaviest stone the root
# - Root (maxHeap[0]) is the negated weight of the heaviest stone
# - Heap is valid when it holds exactly the stones still in play
# Idea:
# - Every round only the two heaviest stones matter, so a maxHeap gives them in O(log n)
# - Pop the two heaviest, smash them, and if they differ push the remainder back
# - Equal stones both vanish, so nothing is pushed
# - Each round removes two stones and adds back at most one,
# so the heap shrinks every round and the game ends
# - Stop when at most one stone remains, the root is the answer, or 0 if the heap is empty
# - heapify builds the heap from the negated list in O(n), cheaper than n pushes
# MaxHeap of negated stone weights
maxHeap = []
# Stone Pointer
index = 0
# Negate each stone
# tc: O(n)
while index < len(stones):
# Add negated weight
maxHeap.append(-stones[index])
# Iterate stones
index += 1
# Order the negated weights into a heap
# tc: O(n)
heapq.heapify(maxHeap)
# tc: O(n log n), at most n - 1 rounds, each round is a constant number of O(log n) heap ops
while len(maxHeap) > 1:
# Two heaviest stones (undo the negation)
heaviest = -heapq.heappop(maxHeap)
secondHeaviest = -heapq.heappop(maxHeap)
# Stones differ, push the remainder back:
# stones = [2, 7, 4, 1, 8, 1]
# smash 8 and 7 -> 1, push -> [4, 2, 1, 1, 1]
# smash 4 and 2 -> 2, push -> [2, 1, 1, 1]
# smash 2 and 1 -> 1, push -> [1, 1, 1]
# smash 1 and 1 -> equal, both destroyed -> [1]
# one stone left -> 1
# tc: O(log n)
if heaviest != secondHeaviest:
remainder = heaviest - secondHeaviest
heapq.heappush(maxHeap, -remainder)
# Last stone weight, 0 if every stone was destroyed
lastStone = -maxHeap[0] if maxHeap else 0
# overall: tc O(n log n)
# overall: sc O(n) for the heap
return lastStone1834. Single Threaded CPU ::1:: - Medium
Topics: Array, Heap (Priority Queue)
Intro
You are given n tasks labeled from 0 to n - 1 represented by a 2D integer array tasks, where tasks[i] = [enqueueTimei, processingTimei] means that the ith task will be available to process at enqueueTimei and will take processingTimei to finish processing. You have a single-threaded CPU that can process at most one task at a time and will act in the following way: If the CPU is idle and there are no available tasks to process, the CPU remains idle. If the CPU is idle and there are available tasks, the CPU will choose the one with the shortest processing time. If multiple tasks have the same shortest processing time, it will choose the task with the smallest index. Once a task is started, the CPU will process the entire task without stopping. The CPU can finish a task then start a new one instantly. Return the order in which the CPU will process the tasks.
| Example Input | Output |
|---|---|
| tasks = [[1,2],[2,4],[3,2],[4,1]] | [0,2,3,1] |
| tasks = [[7,10],[7,12],[7,5],[7,4],[7,2]] | [4,3,2,0,1] |
Constraints:
1 ≤ tasks.length ≤ 10^5
tasks[i] = [enqueueTimei, processingTimei]
1 ≤ enqueueTimei, processingTimei ≤ 10^9
Abstraction
Given tasks with an enqueue time and a processing time, simulate a single threaded CPU that always runs the shortest available task (ties go to the smaller index), and return the order the tasks run in.
(Shortest Job First: at any moment the CPU only chooses among tasks whose enqueue time has already passed, so the problem becomes "which tasks are available right now, and which of them is shortest?")
(Sort first: sort task indexes by enqueue time, so tasks can be released into the heap left to right with a single pointer, and the original indexes are kept because the answer reports them)
(MinHeap: keep every available task in a minHeap keyed by (processingTime, index), so the root is the shortest task, with the smaller index winning ties for free, pop it, run it, and advance the clock by its processing time)
(Monotonic: the clock only moves forward, so a released task stays available until it runs, and every task is pushed once and popped once)
(Clock Jump: if the heap is empty, nothing happens until the next task arrives, so jump the clock straight to that task's enqueue time instead of idling one unit at a time)
(Count what the heap holds) len(minHeap) = "how many tasks are available and waiting right now?"
(Read the next task from the root) minHeap[0] = "which available task is shortest, with the smallest index on a tie?"
(Read the clock) clock = "what time is it? every task with enqueueTime lte clock has been released"
(Read the release pointer) taskPointer = "how many tasks have been released into the heap so far?"
Pseudocode
oh! pseudocode hasn't been written yet, try another card! :)Solution 1: [Heap] Sort By Enqueue Time Then MinHeap Of Available Tasks With Clock Jumping - Heap/Heap
def getOrder(self, tasks: list[list[int]]) -> list[int]:
# MinHeap (Shortest Job First Simulation)
# Heap Representation:
# - Maintain a minHeap of (processingTime, index) for every available task
# - A task is available once its enqueueTime <= clock
# - Root (minHeap[0]) is the shortest available task, and the smaller index
# wins ties because index is the second item of the tuple
# - Heap is valid when it holds exactly the released tasks that have not run yet
# Idea:
# - The CPU wants the shortest available task, which is exactly what a minHeap
# keyed on (processingTime, index) gives in O(log n)
# - The tricky part is knowing which tasks are available at the current time,
# so sort task indexes by enqueueTime once, then sweep left to right with a pointer
# - Sort the indexes, not the tasks, since the answer reports original indexes
# - Each round, release every task with enqueueTime <= clock into the heap,
# then pop the shortest, record it, and advance the clock by its processingTime
# - If the heap is empty, nothing happens until the next task arrives,
# so jump the clock to that task's enqueueTime instead of idling one unit at a time
# - Each task is pushed once and popped once, so the heap work is O(n log n)
# Number of tasks
n = len(tasks)
# Task indexes in order of enqueueTime
# tc: O(n log n), the sort
sortedIds = sorted(range(n), key=lambda taskId: tasks[taskId][0])
# MinHeap of (processingTime, index) for available tasks
minHeap = []
# Order tasks run in
result = []
# Current time of the CPU
clock = 0
# Release Pointer (next task not yet released)
taskPointer = 0
# tc: O(n log n), each task is pushed once and popped once, each heap op is O(log n)
while len(result) < n:
# Release every task that has arrived by the current time:
# tasks = [[1, 2], [2, 4], [3, 2], [4, 1]], clock = 3, taskPointer = 1
# task 1 (enqueue 2) -> push (4, 1)
# task 2 (enqueue 3) -> push (2, 2)
# task 3 (enqueue 4) -> 4 > 3, not yet released
# tc: O(log n) per push
while taskPointer < n and tasks[sortedIds[taskPointer]][0] <= clock:
# Add available task
taskId = sortedIds[taskPointer]
heapq.heappush(minHeap, (tasks[taskId][1], taskId))
# Iterate release pointer
taskPointer += 1
# Task available, run the shortest one
if minHeap:
# Shortest available task
processingTime, taskId = heapq.heappop(minHeap)
# Record task and advance the clock:
# tasks = [[1, 2], [2, 4], [3, 2], [4, 1]]
# clock = 0, heap empty -> jump to 1
# run task 0 -> clock = 3
# run task 2 (shortest of (4, 1), (2, 2)) -> clock = 5
# run task 3 (shortest of (4, 1), (1, 3)) -> clock = 6
# run task 1 -> clock = 10, result = [0, 2, 3, 1]
# tc: O(log n)
result.append(taskId)
clock += processingTime
# Nothing available, jump the clock to the next arrival
else:
clock = tasks[sortedIds[taskPointer]][0]
# overall: tc O(n log n), dominated by the sort and the heap operations
# overall: sc O(n) for the sorted indexes, the heap, and the result
return result621. Task Scheduler ::3:: - Medium
Topics: Array, Hash Table, Greedy, Sorting, Heap (Priority Queue), Counting, Computer Architecture Scheduling
Intro
You are given an array of CPU tasks, each labeled with a letter from A to Z, and a number n. Each CPU interval can be idle or allow the completion of one task. Tasks can be completed in any order, but there's a constraint: there has to be a gap of at least n intervals between two tasks with the same label. Return the minimum number of CPU intervals required to complete all tasks.
| Example Input | Output |
|---|---|
| tasks = ["A","A","A","B","B","B"], n = 2 | 8 |
| ["A","C","A","B","D","B"], n = 1 | 6 |
| tasks = ["A","A","A", "B","B","B"], n = 3 | 10 |
Constraints:
1 ≤ tasks.length ≤ 105
tasks[i] is an uppercase English letter
0 ≤ n ≤ 100
Abstraction
Given a list of tasks labeled by letter and a cooldown n between identical tasks, return the minimum number of CPU intervals needed to finish every task (idle intervals count).
(most frequent first: the task with the most copies sets the schedule, since its copies need n other intervals between them, so lay it out as maxFrequency - 1 full groups of length n + 1, plus a last group with no cooldown after it)
(Formula: (maxFrequency - 1) * (n + 1) + maxFrequencyCount, where the last group only holds the tasks tied for the max frequency, the other tasks fill the idle slots, and if there are more tasks than slots no idle is needed, so take the max with len(tasks))
(MaxHeap: simulate the CPU one interval at a time, run the task type with the most copies left, then hold it in a cooldown queue until it is allowed to run again)
(Monotonic: one task runs per interval, so ready times in the cooldown queue are strictly increasing, which means the front of the queue is always the next task to become ready)
(Clock Jump: if the heap is empty but the cooldown queue is not, nothing can run until the front becomes ready, so jump the clock straight to its ready time instead of idling one interval at a time)
(Count what the heap holds) len(maxHeap) = "how many task types are ready to run right now?"
(Read when the next task is ready) cooldown[0][0] = "at what interval does the next waiting task become ready again?"
(Read the clock) clock = "how many intervals have been used so far, idle included?"
(Count the intervals the most frequent tasks force) (maxFrequency - 1) * (n + 1) + maxFrequencyCount = "how many intervals do the most frequent tasks need, idle slots included?"
Pseudocode
oh! pseudocode hasn't been written yet, try another card! :)Solution 1: [Greedy] Math Formula From The Most Frequent Tasks And Idle Gaps - Heap/Heap
def leastInterval(self, tasks: list[str], n: int) -> int:
# Greedy (Math Formula)
# Schedule Representation:
# - Lay the most frequent task out in maxFrequency groups
# - Each group is n + 1 intervals long: the task, then n intervals before it can repeat
# - The last group only holds the tasks tied for the max frequency,
# since nothing has to cool down after it
# - Other tasks fill the idle slots inside the groups
# Idea:
# - The most frequent task decides the minimum schedule length, since its copies
# must be spaced n intervals apart
# - maxFrequency - 1 full groups of length n + 1, then one last group
# of maxFrequencyCount tasks
# - Other tasks slot into the idle gaps, so they cost no extra intervals
# - If there are more tasks than slots, the gaps overflow and no idle is needed,
# so the answer is just len(tasks), the lower bound
# - Answer is the max of the two
# Frequency of each task
frequency = defaultdict(int)
# Task Pointer
index = 0
# Count each task
# tc: O(N), N = len(tasks)
while index < len(tasks):
# Add task to count
frequency[tasks[index]] += 1
# Iterate tasks
index += 1
# Count of each task type
counts = list(frequency.values())
# Most copies of any one task
maxFrequency = max(counts)
# Number of task types tied for the most copies
maxFrequencyCount = 0
# Count Pointer
countIndex = 0
# tc: O(k), k = distinct tasks (at most 26)
while countIndex < len(counts):
# Task type is tied for the most copies
if counts[countIndex] == maxFrequency:
maxFrequencyCount += 1
# Iterate counts
countIndex += 1
# Intervals forced by the most frequent tasks:
# A, B share the max frequency 3, n = 2
# [A B idle] [A B idle] [A B] -> (3 - 1) * (2 + 1) + 2 = 8
forcedIntervals = (maxFrequency - 1) * (n + 1) + maxFrequencyCount
# More tasks than slots means no idle is needed, len(tasks) is the lower bound:
# tasks = [A, C, A, B, D, B], n = 1
# forcedIntervals = (2 - 1) * (1 + 1) + 2 = 4, but there are 6 tasks -> answer 6
minIntervals = max(forcedIntervals, len(tasks))
# overall: tc O(N), N = len(tasks)
# overall: sc O(k) for the frequency map, k <= 26 so effectively O(1)
return minIntervalsSolution 2: [MaxHeap] Tick By Tick Simulation With A Cooldown Queue - Heap/Heap
def leastInterval(self, tasks: list[str], n: int) -> int:
# MaxHeap (Greedy Simulation With Cooldown)
# Heap Representation:
# - Maintain a maxHeap of negated remaining counts, one per task type ready to run
# - heapq is a minHeap, so counts are negated to make the largest count the root
# - Root (maxHeap[0]) is the task type with the most copies left
# - Labels aren't stored, since only the counts decide the schedule
# - A task that just ran waits in a cooldown queue of (readyTime, remaining)
# Idea:
# - Each loop is one CPU interval, so clock counts total intervals including idle
# - Run the task type with the most copies left, since it is the most at risk
# of forcing idle time later
# - If it has copies left, put it in the cooldown queue with readyTime = clock + n
# - A task ran at interval t can run again at t + n + 1, so it goes back into
# the heap at the end of interval t + n
# - One task runs per interval, so ready times are strictly increasing,
# and a FIFO queue is enough: at most one task becomes ready per interval
# - If the heap is empty but the queue is not, the CPU idles this interval
# - Stop when both the heap and the queue are empty
# Frequency of each task
frequency = defaultdict(int)
# Task Pointer
index = 0
# Count each task
# tc: O(N), N = len(tasks)
while index < len(tasks):
# Add task to count
frequency[tasks[index]] += 1
# Iterate tasks
index += 1
# MaxHeap of negated counts, one per task type
maxHeap = []
# Count of each task type
counts = list(frequency.values())
# Count Pointer
countIndex = 0
# Seed each task type with its negated count
# tc: O(k), k = distinct tasks (at most 26)
while countIndex < len(counts):
# Add negated count
maxHeap.append(-counts[countIndex])
# Iterate counts
countIndex += 1
# Order the seeds into a heap
# tc: O(k)
heapq.heapify(maxHeap)
# Tasks waiting out their cooldown as (readyTime, remaining)
cooldown = deque()
# Intervals used so far
clock = 0
# tc: O(T log k), T = intervals used (the answer), each interval does at most 2 heap ops
while maxHeap or cooldown:
# One CPU interval
clock += 1
# A task is ready, run the one with the most copies left
if maxHeap:
# Remove most frequent task, use one copy (counts are negated, so + 1)
remaining = heapq.heappop(maxHeap) + 1
# Copies left, wait out the cooldown
if remaining != 0:
cooldown.append((clock + n, remaining))
# Front of the queue finished its cooldown, push it back:
# tasks = [A, A, A, B, B, B], n = 2
# clock 1: run A -> cooldown [(3, -2)]
# clock 2: run B -> cooldown [(3, -2), (4, -2)]
# clock 3: heap empty, idle, A ready -> push -2
# clock 4: run A -> cooldown [(4, -2), (6, -1)], B ready -> push -2
# clock 5: run B -> cooldown [(6, -1), (7, -1)]
# clock 6: idle, A ready -> push -1
# clock 7: run A, B ready -> push -1
# clock 8: run B, both empty -> 8
# n = 0: readyTime = clock, so it is pushed back the same interval
# tc: O(log k)
if cooldown and cooldown[0][0] == clock:
# Pop FIFO
_, remaining = cooldown.popleft()
# Add task type back to the heap
heapq.heappush(maxHeap, remaining)
# overall: tc O(T log k), T = answer, k <= 26 so effectively O(T)
# overall: sc O(k) for the heap and cooldown queue
return clockSolution 3: [MaxHeap] Cooldown Queue Simulation That Jumps The Clock Over Idle Intervals - Heap/Heap
def leastInterval(self, tasks: list[str], n: int) -> int:
# MaxHeap (Greedy Simulation With Clock Jump)
# Heap Representation:
# - Same as Solution 2: a maxHeap of negated remaining counts for task types
# ready to run, and a cooldown queue of (readyTime, remaining)
# - Root (maxHeap[0]) is the task type with the most copies left
# Idea:
# - Same simulation as Solution 2, but idle intervals are skipped
# - If the heap is empty, nothing can run until the front of the queue is ready,
# so jump the clock straight to its readyTime instead of idling one interval at a time
# - The queue is never empty when the heap is, since the loop only runs
# while one of them has something
# - Ready times only increase, so the jump never moves the clock backward
# - Every loop now either runs a task or jumps to a ready task,
# so loops are at most 2N instead of T, where T includes all idle intervals
# Frequency of each task
frequency = defaultdict(int)
# Task Pointer
index = 0
# Count each task
# tc: O(N), N = len(tasks)
while index < len(tasks):
# Add task to count
frequency[tasks[index]] += 1
# Iterate tasks
index += 1
# MaxHeap of negated counts, one per task type
maxHeap = []
# Count of each task type
counts = list(frequency.values())
# Count Pointer
countIndex = 0
# Seed each task type with its negated count
# tc: O(k), k = distinct tasks (at most 26)
while countIndex < len(counts):
# Add negated count
maxHeap.append(-counts[countIndex])
# Iterate counts
countIndex += 1
# Order the seeds into a heap
# tc: O(k)
heapq.heapify(maxHeap)
# Tasks waiting out their cooldown as (readyTime, remaining)
cooldown = deque()
# Intervals used so far
clock = 0
# tc: O(N log k), each loop runs a task or jumps to a ready task, so at most 2N loops
while maxHeap or cooldown:
# A task is ready, run the one with the most copies left
if maxHeap:
# One CPU interval
clock += 1
# Remove most frequent task, use one copy (counts are negated, so + 1)
remaining = heapq.heappop(maxHeap) + 1
# Copies left, wait out the cooldown
if remaining != 0:
cooldown.append((clock + n, remaining))
# Nothing ready, jump the clock to the next ready task:
# tasks = [A, A], n = 5
# clock 1: run A -> cooldown [(6, -1)]
# heap empty -> jump clock to 6, skipping the idle intervals 2 to 5
# A ready -> push -1
# clock 7: run A -> 7
else:
clock = cooldown[0][0]
# Front of the queue finished its cooldown, push it back
# tc: O(log k)
if cooldown and cooldown[0][0] == clock:
# Pop FIFO
_, remaining = cooldown.popleft()
# Add task type back to the heap
heapq.heappush(maxHeap, remaining)
# overall: tc O(N log k), k <= 26 so effectively O(N)
# overall: sc O(k) for the heap and cooldown queue
return clock767. Reorganize String ::1:: - Medium
Topics: String, Greedy, Sorting, Heap (Priority Queue), Counting
Intro
Given a string s, rearrange the characters of s so that any two adjacent characters are not the same. Return any possible rearrangement of s or return "" if not possible.
| Example Input | Output |
|---|---|
| s = "aab" | "aba" |
| s = "aaab" | "" |
Constraints:
1 ≤ s.length ≤ 500
s consists of lowercase English letters.
Abstraction
Given a string, rearrange its characters so no two adjacent characters match, and return any valid rearrangement ("" if impossible).
(most frequent first: the character with the most copies is the most at risk of landing next to itself, so always place whichever character has the most copies left)
(Impossible: a character with count > ceil(n / 2) can't be spaced out, since there aren't enough other characters to fill the gaps between its copies, so return "" up front)
(MaxHeap: keep every character in a maxHeap keyed by remaining count, so the root is the character with the most copies left, pop it, place it, and it can't be placed again right away)
(Cooldown: after placing a character it only has to wait one round, so hold just that one character aside, and push it back into the heap after the next, different character is placed)
(Count what the heap holds) len(maxHeap) = "how many characters still have copies waiting to be placed? (at most 26)"
(Read the next character from the root) maxHeap[0] = "which character has the most copies left, and is it safe to place?"
(Read the cooldown slot) prevChar = "which character was just placed, and has to sit out this round?"
Pseudocode
oh! pseudocode hasn't been written yet, try another card! :)Solution 1: [Heap] Greedy MaxHeap By Count With A One Round Cooldown - Heap/Heap
def reorganizeString(self, s: str) -> str:
# MaxHeap (Greedy With Cooldown)
# Heap Representation:
# - Maintain a maxHeap of (-count, char) for every character with copies left
# - heapq is a minHeap, so counts are negated to make the largest count the root
# - Root (maxHeap[0]) is the character with the most copies left
# - The character just placed is held outside the heap for one round (cooldown)
# Idea:
# - The character with the most copies is the most at risk of ending up
# next to itself, so always place the one with the most copies left
# - If any count > (n + 1) // 2, there aren't enough other characters
# to separate every copy, so return "" immediately
# - After placing a character, it can't go next to itself, but it can go
# after the next different character, so a one round cooldown is enough:
# hold only the last placed character aside, and push it back
# after the next one is placed
# - Only one character is ever on cooldown, so no queue of history is needed
# - Heap holds at most 26 characters, so each heap op is O(log 26), effectively O(1)
# - With the impossibility check passed, the greedy always finishes placing
# all n characters
# Number of characters
n = len(s)
# Frequency of each character
counts = defaultdict(int)
# String Pointer
index = 0
# Count each character
# tc: O(n)
while index < n:
# Add character to count
counts[s[index]] += 1
# Iterate string
index += 1
# Most copies of any one character
maxCount = max(counts.values())
# Impossible, the most frequent character can't be spaced out:
# s = "aaab" -> n = 4, 'a' appears 3 times, 3 > (4 + 1) // 2 = 2 -> return ""
# s = "aab" -> n = 3, 'a' appears 2 times, 2 <= (3 + 1) // 2 = 2 -> possible
if maxCount > (n + 1) // 2:
return ""
# MaxHeap of (-count, char) for every character
maxHeap = []
# Unique (char, count) pairs
uniqueItems = list(counts.items())
# Unique Pointer
uniqueIndex = 0
# Seed each character with its negated count
# tc: O(k), k = distinct characters (at most 26)
while uniqueIndex < len(uniqueItems):
# Current pair
char, count = uniqueItems[uniqueIndex]
# Add negated count
maxHeap.append((-count, char))
# Iterate unique pairs
uniqueIndex += 1
# Order the seeds into a heap
# tc: O(k)
heapq.heapify(maxHeap)
# Placed characters
result = []
# Cooldown slot (negated count, char), empty at the start
prevCount = 0
prevChar = ""
# tc: O(n log k), n pops, each pop and push is O(log k)
while maxHeap:
# Character with the most copies left
count, char = heapq.heappop(maxHeap)
# Place character
result.append(char)
# Previous character finished its cooldown and still has copies, push it back
# s = "aab", maxHeap = [(-2, 'a'), (-1, 'b')]
# pop (-2, 'a') -> "a", cooldown ('a', -1), nothing to push back
# pop (-1, 'b') -> "ab", push back (-1, 'a'), cooldown ('b', 0)
# pop (-1, 'a') -> "aba", 'b' has no copies left so it is not pushed
# heap empty -> "aba"
# tc: O(log k)
if prevCount < 0:
heapq.heappush(maxHeap, (prevCount, prevChar))
# Use one copy of this character, then put it on cooldown for one round
prevCount = count + 1
prevChar = char
# Rearranged string
reorganized = "".join(result)
# overall: tc O(n log k), k <= 26 so effectively O(n)
# overall: sc O(k) for the heap, O(n) for the result
return reorganized295. Find Median from Data Stream ::2:: - Hard
Topics: Two Pointers, Design, Sorting, Heap (Priority Queue), Data Stream
Intro
The median is the middle value in an ordered integer list. If the size of the list is even, there is no middle value, and the median is the mean of the two middle values. For example, for arr = [2,3,4], the median is 3. For example, for arr = [2,3], the median is (2 + 3) / 2 = 2.5. Implement the MedianFinder class: MedianFinder() initializes the MedianFinder object. void addNum(int num) adds the integer num from the data stream to the data structure. double findMedian() returns the median of all elements so far. Answers within 10-5 of the actual answer will be accepted.
| Example Input | Output |
|---|---|
| ["MedianFinder", "addNum", "addNum", "findMedian", "addNum", "findMedian"] [[], [1], [2], [], [3], []] | [null, null, null, 1.5, null, 2.0] |
Constraints:
-10^5 ≤ num ≤ 10^5
There will be at least one element in the data structure before calling findMedian.
At most 5 * 10^4 calls will be made to addNum and findMedian
Abstraction
Given a stream of numbers, support adding a number and returning the median of everything added so far.
(the median only depends on the middle one or two values, so split the numbers into a lower half and an upper half, and the problem becomes "keep track of the largest of the lower half and the smallest of the upper half")
(Two Heaps: keep the lower half in a maxHeap and the upper half in a minHeap, so the roots are the two middle values, every number in the lower half is lte every number in the upper half)
(Balance: the lower half holds either the same number of elements as the upper half or exactly one more, so for an odd count the median is the lower root, and for an even count it is the average of both roots)
(Monotonic: the lower root can only increase or stay the same as numbers arrive, since a number that would raise it is pushed up to the upper half the moment it passes the upper root, so a number never has to be re-sorted against the whole stream, only against the two roots)
(Sorted Container: if the numbers are stored in sorted order, the median is just the middle index, so the cost moves from reading the median to inserting a number)
(Count the size difference) len(maxHeap) - len(minHeap) = "is the lower half balanced? it must be 0 or 1"
(Read the lower middle) -maxHeap[0] = "what is the largest value of the lower half, which is the median when the count is odd?"
(Read the upper middle) minHeap[0] = "what is the smallest value of the upper half, which pairs with the lower root when the count is even?"
Pseudocode
oh! pseudocode hasn't been written yet, try another card! :)Solution 1: [Two Heaps] MaxHeap Of The Lower Half And MinHeap Of The Upper Half Track The Median - Heap/Heap
class MedianFinder:
# Two Heaps (Lower Half MaxHeap, Upper Half MinHeap)
# Heap Representation:
# - maxHeap holds the lower half of the numbers, stored as negated values
# - minHeap holds the upper half of the numbers
# - heapq is a minHeap, so negating values makes the largest of the lower half the root
# - Roots (-maxHeap[0] and minHeap[0]) are the two middle values
# - Heaps are valid when every lower number is <= every upper number,
# and len(maxHeap) is len(minHeap) or len(minHeap) + 1
# Idea:
# - The median only depends on the middle one or two values
# - Split the numbers into a lower half and an upper half,
# so the middle values are the two roots
# - Push every new number into the lower half first,
# then fix the two ways the heaps can go wrong
# - Out of order: if the lower root is bigger than the upper root, move it up
# - Out of balance: if the lower half is smaller than the upper half, or bigger by more than 1,
# move one root across
# - Odd count: the lower half has the extra element, so the median is its root
# - Even count: the halves are equal, so the median is the average of both roots
# - Reading the median is O(1), the cost is paid when adding
# MedianFinder():
# - Initializes the two empty heaps
# addNum():
# - Adds a number to the correct half
# - Rebalances so the lower half is equal or one larger
# findMedian():
# - Returns the middle value, or the average of the two middle values
def __init__(self):
# MaxHeap of the lower half (negated values)
self.maxHeap = []
# MinHeap of the upper half
self.minHeap = []
# overall: tc O(1)
# overall: sc O(1)
def addNum(self, num: int) -> None:
# Add num to the lower half
# tc: O(log n)
heapq.heappush(self.maxHeap, -num)
# Lower root is bigger than upper root, out of order, move it up:
# adding 1, 2, 3
# add 3 -> maxHeap [3, 1], minHeap [2], 3 > 2 -> move 3 up
# maxHeap [1], minHeap [2, 3]
# tc: O(log n)
if self.minHeap and -self.maxHeap[0] > self.minHeap[0]:
# Largest of the lower half
largestLower = -heapq.heappop(self.maxHeap)
# Move it to the upper half
heapq.heappush(self.minHeap, largestLower)
# Upper half is bigger, move its root down:
# maxHeap [1], minHeap [2, 3] -> move 2 down -> maxHeap [2, 1], minHeap [3]
# tc: O(log n)
if len(self.maxHeap) < len(self.minHeap):
# Smallest of the upper half
smallestUpper = heapq.heappop(self.minHeap)
# Move it to the lower half
heapq.heappush(self.maxHeap, -smallestUpper)
# Lower half is bigger by more than 1, move its root up:
# adding 1, 2 -> add 2 -> maxHeap [2, 1], minHeap [] -> move 2 up
# maxHeap [1], minHeap [2]
# tc: O(log n)
elif len(self.maxHeap) > len(self.minHeap) + 1:
# Largest of the lower half
largestLower = -heapq.heappop(self.maxHeap)
# Move it to the upper half
heapq.heappush(self.minHeap, largestLower)
# overall: tc O(log n), at most 3 heap operations
# overall: sc O(1) extra, the heaps themselves hold the n numbers
def findMedian(self) -> float:
# Odd count, the lower half has the extra element:
# maxHeap [2, 1], minHeap [3] -> median 2
if len(self.maxHeap) > len(self.minHeap):
median = -self.maxHeap[0]
# Even count, average of both roots:
# maxHeap [1], minHeap [2] -> (1 + 2) / 2 = 1.5
else:
median = (-self.maxHeap[0] + self.minHeap[0]) / 2
# overall: tc O(1)
# overall: sc O(1)
return medianSolution 2: [Sort] Sorted Container Reads The Middle Elements By Index - Heap/Heap
class MedianFinder:
# SortedList (Sorted Container)
# Container Representation:
# - sortedNums holds every number seen so far, always in sorted order
# - The median sits at the middle index, or the two middle indexes
# Idea:
# - If the numbers are always sorted, the median is just the middle element
# - SortedList (from sortedcontainers) inserts in sorted position and supports indexing,
# so no heaps or balancing are needed
# - Odd count: the median is sortedNums[n // 2]
# - Even count: the median is the average of sortedNums[n // 2 - 1] and sortedNums[n // 2]
# - The cost moves to inserting, and the container uses more memory than two heaps
# MedianFinder():
# - Initializes an empty SortedList
# addNum():
# - Inserts the number in sorted position
# findMedian():
# - Returns the middle value, or the average of the two middle values
def __init__(self):
# Numbers seen so far, in sorted order
self.sortedNums = SortedList()
# overall: tc O(1)
# overall: sc O(1)
def addNum(self, num: int) -> None:
# Insert num in sorted position
# tc: O(log n)
self.sortedNums.add(num)
# overall: tc O(log n)
# overall: sc O(1) extra, the container itself holds the n numbers
def findMedian(self) -> float:
# Number of elements
n = len(self.sortedNums)
# Odd count, middle element:
# sortedNums = [1, 2, 3] -> n = 3, index 1 -> 2
# tc: O(log n)
if n % 2 == 1:
median = self.sortedNums[n // 2]
# Even count, average of the two middle elements:
# sortedNums = [1, 2] -> n = 2, indexes 0 and 1 -> (1 + 2) / 2 = 1.5
# tc: O(log n)
else:
median = (self.sortedNums[n // 2 - 1] + self.sortedNums[n // 2]) / 2
# overall: tc O(log n)
# overall: sc O(1)
return median480. Sliding Window Median ::2:: - Hard
Topics: Array, Hash Table, Sliding Window, Heap (Priority Queue), Treap
Intro
The median is the middle value in an ordered integer list. If the size of the list is even, there is no middle value. So the median is the mean of the two middle values. For examples, if arr = [2,3,4], the median is 3. For examples, if arr = [1,2,3,4], the median is (2 + 3) / 2 = 2.5. You are given an integer array nums and an integer k. There is a sliding window of size k which is moving from the very left of the array to the very right. You can only see the k numbers in the window. Each time the sliding window moves right by one position. Return the median array for each window in the original array. Answers within 10-5 of the actual value will be accepted.
| Example Input | Output |
|---|---|
| nums = [1,3,-1,-3,5,3,6,7], k = 3 | [1.00000,-1.00000,-1.00000,3.00000,5.00000,6.00000] |
Constraints:
1 ≤ k ≤ nums.length &leq 10^5
-2^31 ≤ nums[i] ≤ 2^31 - 1
Abstraction
Given an array and a window size k, return the median of every window as it slides from left to right.
(the median only depends on the middle one or two values of the window, so this is 295 with one new twist: numbers leave the window as well as enter it)
(Two Heaps: keep the lower half of the window in a maxHeap and the upper half in a minHeap, so the roots are the two middle values, slide by adding the new number, removing the old number, then rebalancing)
(Lazy Deletion: a heap can only remove its root efficiently, so don't dig out the old number, just record it in a delayed count, and discard it for real only when it surfaces at a root)
(Size Tracking: stale numbers still sit inside the heaps, so len(heap) lies, track the real size of each half in lowerSize and upperSize instead)
(Monotonic: a stale number is never read, since the roots are pruned right after every removal or move, so the roots are always real window values and the median reads are always correct)
(Sorted Container: if the window is stored in sorted order, the median is just the middle index, and removing the old number is a direct remove instead of a lazy one)
(Count the real sizes) lowerSize - upperSize = "is the lower half balanced? it must be 0 or 1"
(Read the lower middle) -lowerHeap[0] = "what is the largest real value of the lower half, which is the median when k is odd?"
(Count the stale numbers) delayed[num] = "how many copies of num have left the window but still sit inside a heap?"
Pseudocode
oh! pseudocode hasn't been written yet, try another card! :)Solution 1: [Two Heaps] MaxHeap And MinHeap Of The Window Halves With Lazy Deletion And Tracked Sizes - Heap/Heap
def medianSlidingWindow(self, nums: list[int], k: int) -> list[float]:
# Two Heaps (Lower Half MaxHeap, Upper Half MinHeap, Lazy Deletion)
# Heap Representation:
# - lowerHeap holds the lower half of the window, stored as negated values
# - upperHeap holds the upper half of the window
# - heapq is a minHeap, so negating values makes the largest of the lower half the root
# - delayed[num] counts copies of num that left the window but are still inside a heap
# - lowerSize and upperSize are the real sizes of each half, stale numbers not counted
# - Heaps are valid when every lower number is <= every upper number,
# lowerSize is upperSize or upperSize + 1, and both roots are real (not stale)
# Idea:
# - Same split as 295, but numbers also leave the window
# - A heap can only remove its root cheaply, so removing an arbitrary number is lazy:
# count it in delayed, and discard it when it reaches a root
# - Which half a number belongs to is decided by comparing with the lower root,
# since every lower number is <= the lower root and every upper number is >= it
# - Stale numbers still sit inside the heaps, so len(heap) can't be used,
# the real sizes are tracked by hand and drive the rebalancing
# - After every pop or removal, prune the root: while it is stale, pop it for real
# so both roots are always real window values
# - Slide: add the new number, remove the old number, then record the median
# - Odd k: the median is the lower root, even k: the average of both roots
# Number of elements
n = len(nums)
# MaxHeap of the lower half (negated values)
lowerHeap = []
# MinHeap of the upper half
upperHeap = []
# Copies that left the window but are still inside a heap
delayed = defaultdict(int)
# Real size of each half
lowerSize = 0
upperSize = 0
# Helper that pops stale numbers off the root of a heap
# tc: O(log n) per stale number, each number is discarded at most once
def prune(heap: list[int], isLower: bool) -> None:
# Root is stale, pop it for real
while heap:
# Real value of the root
value = -heap[0] if isLower else heap[0]
# Root is a real window value, done
if delayed[value] == 0:
break
# Discard stale copy
delayed[value] -= 1
heapq.heappop(heap)
# Helper that moves one root across so the halves are balanced
# tc: O(log n)
def rebalance() -> None:
nonlocal lowerSize, upperSize
# Lower half too big, move its root up
if lowerSize > upperSize + 1:
heapq.heappush(upperHeap, -heapq.heappop(lowerHeap))
lowerSize -= 1
upperSize += 1
prune(lowerHeap, True)
# Upper half too big, move its root down
elif lowerSize < upperSize:
heapq.heappush(lowerHeap, -heapq.heappop(upperHeap))
lowerSize += 1
upperSize -= 1
prune(upperHeap, False)
# Helper that adds a number to the correct half
# tc: O(log n)
def addNum(num: int) -> None:
nonlocal lowerSize, upperSize
# Number belongs to the lower half
if not lowerHeap or num <= -lowerHeap[0]:
heapq.heappush(lowerHeap, -num)
lowerSize += 1
# Number belongs to the upper half
else:
heapq.heappush(upperHeap, num)
upperSize += 1
rebalance()
# Helper that lazily removes a number that left the window
# tc: O(log n)
def removeNum(num: int) -> None:
nonlocal lowerSize, upperSize
# Mark the copy as stale
delayed[num] += 1
# Number is in the lower half
if num <= -lowerHeap[0]:
lowerSize -= 1
# Stale copy is the root, discard it now
if num == -lowerHeap[0]:
prune(lowerHeap, True)
# Number is in the upper half
else:
upperSize -= 1
# Stale copy is the root, discard it now
if num == upperHeap[0]:
prune(upperHeap, False)
rebalance()
# Medians of each window
result = []
# Fill the first window
# tc: O(k log k)
index = 0
while index < k:
# Add num to the window
addNum(nums[index])
# Iterate array
index += 1
# Median of the first window
if k % 2 == 1:
result.append(float(-lowerHeap[0]))
else:
result.append((-lowerHeap[0] + upperHeap[0]) / 2)
# Slide the window one step at a time
# tc: O(n log k), each slide is a constant number of O(log k) heap operations
while index < n:
# Add the entering number, remove the leaving number:
# nums = [1, 3, -1, -3, 5, 3, 6, 7], k = 3
# first window [1, 3, -1] -> lower [1, -1], upper [3], median 1
# add -3 -> lower [1, -1, -3] too big, move 1 up -> lower [-1, -3], upper [1, 3]
# remove 1 -> 1 > -1 so it is in the upper half, it is the root, prune it -> upper [3]
# window [3, -1, -3] -> median -1
addNum(nums[index])
removeNum(nums[index - k])
# Record the median
if k % 2 == 1:
result.append(float(-lowerHeap[0]))
else:
result.append((-lowerHeap[0] + upperHeap[0]) / 2)
# Iterate array
index += 1
# overall: tc O(n log k), each number is added, removed, and discarded at most once
# overall: sc O(n) worst case, stale numbers can wait inside the heaps before they surface
return resultSolution 2: [SortedList] Sorted Window Reads The Middle Elements By Index - Heap/Heap
def medianSlidingWindow(self, nums: list[int], k: int) -> list[float]:
# SortedList (Sorted Container)
# Container Representation:
# - window holds exactly the current k numbers, always in sorted order
# - The median sits at the middle index, or the two middle indexes
# - Both middle indexes are fixed, since k never changes
# Idea:
# - If the window is always sorted, the median is just the middle element
# - SortedList (from sortedcontainers) inserts and removes in sorted position
# and supports indexing, so no heaps, balancing, or lazy deletion are needed
# - Slide: remove the leaving number, add the entering number
# - Odd k: the median is window[k // 2]
# - Even k: the median is the average of window[k // 2 - 1] and window[k // 2]
# - Simpler than two heaps, but it needs the third party sortedcontainers package
# Number of elements
n = len(nums)
# Numbers of the current window, in sorted order
window = SortedList()
# Fill the first window
# tc: O(k log k)
index = 0
while index < k:
# Add num to the window
window.add(nums[index])
# Iterate array
index += 1
# Medians of each window
result = []
# Window Start Pointer
windowStart = 0
# tc: O(n log k), each slide is one remove and one add
while windowStart + k <= n:
# Record the median:
# window = [-1, 1, 3], k = 3 -> window[1] = 1
# window = [1, 2], k = 2 -> (window[0] + window[1]) / 2 = 1.5
# tc: O(log k)
if k % 2 == 1:
result.append(float(window[k // 2]))
else:
result.append((window[k // 2 - 1] + window[k // 2]) / 2)
# Slide the window, unless this was the last one
if windowStart + k < n:
# Remove the leaving number, add the entering number
# tc: O(log k)
window.remove(nums[windowStart])
window.add(nums[windowStart + k])
# Iterate window
windowStart += 1
# overall: tc O(n log k)
# overall: sc O(k) for the window, O(n - k + 1) for the returned list
return resultSolution 3: [Treap] Randomized BST With Subtree Sizes Gives Insert, Delete, And Kth Smallest In Log K - Heap/Heap
def medianSlidingWindow(self, nums: list[int], k: int) -> list[float]:
# Treap (Randomized BST With Subtree Sizes)
# Tree Representation:
# - Each node holds val, a random priority, the size of its subtree, and left and right children
# - BST order on val: everything in the left subtree is < val, everything in the right is >= val
# - Heap order on priority: a parent's priority is higher than its children's
# - Random priorities keep the expected height O(log k), so no rotations are needed
# - The tree holds exactly the k numbers of the current window
# Idea:
# - The median only depends on the middle one or two values, so we need
# insert, delete, and "kth smallest" on the window
# - split(node, key) cuts a treap into values < key and values >= key
# - merge(a, b) joins two treaps where every value in a is <= every value in b
# - Insert: split at num, then merge left + new node + right
# - Delete one copy: split at num, split the right part at num + 1 to isolate the copies of num,
# drop the root of that middle part, then merge everything back
# - Kth smallest: walk down, comparing the index with the left subtree size
# - Deleting removes the real node, so there are no stale numbers and no size tracking by hand
# - Odd k: the median is the kth at index k // 2
# - Even k: the average of the kth at k // 2 - 1 and k // 2
# Treap node
class TreapNode:
def __init__(self, val: int):
self.val = val
self.priority = random.random()
self.size = 1
self.left = None
self.right = None
# Helper that returns the size of a subtree
def sizeOf(node) -> int:
return node.size if node else 0
# Helper that recomputes a node's size from its children
def update(node) -> None:
node.size = 1 + sizeOf(node.left) + sizeOf(node.right)
# Helper that splits a treap into values < key and values >= key
# tc: O(log k) expected
def split(node, key: int):
# Empty treap
if not node:
return None, None
# Node belongs to the left part, split its right child
if node.val < key:
smaller, larger = split(node.right, key)
node.right = smaller
update(node)
return node, larger
# Node belongs to the right part, split its left child
smaller, larger = split(node.left, key)
node.left = larger
update(node)
return smaller, node
# Helper that merges two treaps, every value in first is <= every value in second
# tc: O(log k) expected
def merge(first, second):
# One side is empty
if not first:
return second
if not second:
return first
# Higher priority becomes the root
if first.priority > second.priority:
first.right = merge(first.right, second)
update(first)
return first
second.left = merge(first, second.left)
update(second)
return second
# Helper that inserts one copy of num
# tc: O(log k) expected
def insert(root, num: int):
# Cut at num, then slot the new node between the two parts
smaller, larger = split(root, num)
return merge(merge(smaller, TreapNode(num)), larger)
# Helper that deletes one copy of num
# tc: O(log k) expected
def delete(root, num: int):
# Values < num, and values >= num
smaller, rest = split(root, num)
# Values == num, and values > num
equal, larger = split(rest, num + 1)
# Drop the root of the equal part, which removes exactly one copy
equal = merge(equal.left, equal.right)
return merge(merge(smaller, equal), larger)
# Helper that returns the kth smallest value, 0 indexed
# tc: O(log k) expected
def kthSmallest(root, index: int) -> int:
# Current node
node = root
while node:
# Size of the left subtree
leftSize = sizeOf(node.left)
# Target is in the left subtree
if index < leftSize:
node = node.left
# Target is this node
elif index == leftSize:
return node.val
# Target is in the right subtree, skip the left subtree and this node
else:
index -= leftSize + 1
node = node.right
# Number of elements
n = len(nums)
# Root of the treap holding the current window
root = None
# Fill the first window
# tc: O(k log k)
index = 0
while index < k:
# Add num to the window
root = insert(root, nums[index])
# Iterate array
index += 1
# Medians of each window
result = []
# Window Start Pointer
windowStart = 0
# tc: O(n log k), each slide is one delete, one insert, and one or two kth lookups
while windowStart + k <= n:
# Record the median:
# window = [-1, 1, 3], k = 3 -> kth at index 1 -> 1
# window = [1, 2], k = 2 -> (kth at 0 + kth at 1) / 2 = 1.5
# tc: O(log k)
if k % 2 == 1:
result.append(float(kthSmallest(root, k // 2)))
else:
result.append((kthSmallest(root, k // 2 - 1) + kthSmallest(root, k // 2)) / 2)
# Slide the window, unless this was the last one
if windowStart + k < n:
# Remove the leaving number, add the entering number:
# nums = [1, 3, -1, -3, 5, 3, 6, 7], k = 3
# window [1, 3, -1] -> remove 1, add -3 -> window [3, -1, -3]
# tc: O(log k)
root = delete(root, nums[windowStart])
root = insert(root, nums[windowStart + k])
# Iterate window
windowStart += 1
# overall: tc O(n log k) expected, random priorities keep the height O(log k)
# overall: sc O(k) for the treap, O(n - k + 1) for the returned list
return result502. IPO ::2:: - Hard
Topics: Array, Greedy, Sorting, Heap (Priority Queue)
Intro
Suppose LeetCode will start its IPO soon. In order to sell a good price of its shares to Venture Capital, LeetCode would like to work on some projects to increase its capital before the IPO. Since it has limited resources, it can only finish at most k distinct projects before the IPO. Help LeetCode design the best way to maximize its total capital after finishing at most k distinct projects. You are given n projects where the ith project has a pure profit profits[i] and a minimum capital of capital[i] is needed to start it. Initially, you have w capital. When you finish a project, you will obtain its pure profit and the profit will be added to your total capital. Pick a list of at most k distinct projects from given projects to maximize your final capital, and return the final maximized capital. The answer is guaranteed to fit in a 32-bit signed integer.
| Example Input | Output |
|---|---|
| k = 2, w = 0, profits = [1,2,3], capital = [0,1,1] | 4 |
Constraints:
1 ≤ k ≤ 10^5
0 ≤ w ≤ 10^9
n == profits.length
n == capital.length
1 ≤ n ≤ 10^5
0 ≤ profits[i] ≤ 10^4
0 ≤ capital[i] ≤ 10^9
Abstraction
Given starting capital w and up to k projects, each with a capital requirement and a profit, pick at most k projects (each once) to maximize final capital.
(greedy: finishing a project only adds profit and never removes capital, so among the projects you can afford right now, always take the one with the biggest profit, since it grows your capital the most and unlocks the most future projects)
(Two Heaps: split the projects by whether you can afford them, a minHeap of the projects you can't afford yet, keyed by capital, so the root is the next one to unlock, and a maxHeap of the projects you can afford, keyed by profit, so the root is the best one to take)
(Monotonic: w never decreases, so a project that is affordable stays affordable until it is taken, which means every project moves from the waiting heap to the ready heap at most once, and never moves back)
(Sort Variant: sort the projects by capital once, then a pointer replaces the waiting heap, since the next project to unlock is always the one at the pointer)
(Count the rounds) rounds = "how many projects have I taken so far? stop at k, or when nothing is affordable"
(Read the best affordable project) -maxHeap[0] = "what is the biggest profit among the projects I can afford right now?"
(Read the next project to unlock) minHeap[0][0] = "what is the smallest capital I still can't afford? once w reaches it, that project moves to the maxHeap"
Pseudocode
oh! pseudocode hasn't been written yet, try another card! :)Solution 1: [Two Heaps] MinHeap Of Unaffordable Projects By Capital And MaxHeap Of Affordable Projects By Profit - Heap/Heap
def findMaximizedCapital(self, k: int, w: int, profits: list[int], capital: list[int]) -> int:
# Two Heaps (Waiting MinHeap, Ready MaxHeap)
# Heap Representation:
# - minHeap holds (capital, profit) for projects we can't afford yet
# - Root (minHeap[0]) is the cheapest project we still can't afford
# - maxHeap holds negated profits for projects we can afford right now
# - heapq is a minHeap, so negating profits makes the biggest profit the root
# - Heaps are valid when every project in minHeap needs more capital than w,
# and every project in maxHeap needs at most w
# Idea:
# - Taking a project only adds capital, so among affordable projects
# the best choice is always the one with the biggest profit
# - Each round, move every project we can now afford (capital <= w)
# from minHeap to maxHeap, then take the root of maxHeap
# - Taking a project raises w, which may unlock more projects next round
# - w never decreases, so a project that is affordable stays affordable,
# and each project moves from minHeap to maxHeap at most once
# - If maxHeap is empty, nothing is affordable and w can't grow, so stop early
# - Stop after k projects
# Number of projects
n = len(profits)
# MinHeap of (capital, profit) for projects not yet affordable
minHeap = []
# Project Pointer
index = 0
# Add every project to the waiting heap
# tc: O(n)
while index < n:
# Add project
minHeap.append((capital[index], profits[index]))
# Iterate projects
index += 1
# Order the projects into a heap
# tc: O(n)
heapq.heapify(minHeap)
# MaxHeap of negated profits for affordable projects
maxHeap = []
# Projects Taken
rounds = 0
# tc: O((n + k) log n), each project moves heaps at most once, and there are at most k takes
while rounds < k:
# Move every newly affordable project to the ready heap:
# k = 2, w = 0, profits = [1, 2, 3], capital = [0, 1, 1]
# round 1: w = 0 -> (0, 1) affordable -> maxHeap [-1], take 1 -> w = 1
# round 2: w = 1 -> (1, 2) and (1, 3) affordable -> maxHeap [-3, -2], take 3 -> w = 4
# tc: O(log n) per move
while minHeap and minHeap[0][0] <= w:
# Cheapest waiting project
projectCapital, projectProfit = heapq.heappop(minHeap)
# Add negated profit to the ready heap
heapq.heappush(maxHeap, -projectProfit)
# Nothing affordable, w can't grow, stop early
if not maxHeap:
break
# Take the most profitable affordable project
# tc: O(log n)
w += -heapq.heappop(maxHeap)
# Iterate rounds
rounds += 1
# overall: tc O((n + k) log n), heapify is O(n), each project moves at most once, at most k takes
# overall: sc O(n) for the two heaps combined
return wSolution 2: [Sort] [Heap] MaxHeap Sort Projects By Capital Then Pointer Unlocks Projects Into A MaxHeap Of Profits - Heap/Heap
def findMaximizedCapital(self, k: int, w: int, profits: list[int], capital: list[int]) -> int:
# Sort + MaxHeap (Pointer Replaces The Waiting Heap)
# Heap Representation:
# - projects is sorted by capital, so the next project to unlock is always at projectPointer
# - maxHeap holds negated profits for projects we can afford right now
# - Root (maxHeap[0]) is the negated biggest affordable profit
# - Everything before projectPointer has been unlocked, everything after needs more than w
# Idea:
# - Same greedy as Solution 1: take the biggest profit among affordable projects
# - The waiting heap only exists to hand out projects in order of capital,
# so sorting by capital once does the same job
# - Sweep a pointer left to right, pushing each project with capital <= w into maxHeap
# - w never decreases, so the pointer never moves backward
# - Sorting costs the same O(n log n) as building the waiting heap,
# but the sweep is a plain pointer instead of heap pops, so it is simpler and has a smaller constant
# - If maxHeap is empty, nothing is affordable, so stop early
# Number of projects
n = len(profits)
# Projects as (capital, profit) pairs in order of capital
projects = []
# Project Pointer
index = 0
# Pair up each project
# tc: O(n)
while index < n:
# Add project
projects.append((capital[index], profits[index]))
# Iterate projects
index += 1
# Sort by capital
# tc: O(n log n)
projects.sort()
# MaxHeap of negated profits for affordable projects
maxHeap = []
# Release Pointer (next project not yet unlocked)
projectPointer = 0
# Projects Taken
rounds = 0
# tc: O(n log n + k log n), n pushes total, at most k pops
while rounds < k:
# Unlock every project we can now afford:
# projects = [(0, 1), (1, 2), (1, 3)], w = 1, projectPointer = 1
# (1, 2) -> push -2
# (1, 3) -> push -3, pointer reaches n
# tc: O(log n) per push
while projectPointer < n and projects[projectPointer][0] <= w:
# Add negated profit to the ready heap
heapq.heappush(maxHeap, -projects[projectPointer][1])
# Iterate release pointer
projectPointer += 1
# Nothing affordable, w can't grow, stop early
if not maxHeap:
break
# Take the most profitable affordable project
# tc: O(log n)
w += -heapq.heappop(maxHeap)
# Iterate rounds
rounds += 1
# overall: tc O(n log n + k log n), dominated by the sort and the heap operations
# overall: sc O(n) for the sorted projects and the heap
return w