6 min read

Mastering the Two Heaps Technique in Algorithm Design

The Two Heaps technique is a powerful approach to solving problems requiring the maintenance of both minimum and maximum values within a dataset. This technique leverages two heaps – a min-heap and a max-heap – to efficiently track these extremes and often provides elegant solutions to otherwise complex problems.

Understanding the Mechanics

The core idea is simple: one heap (e.g., a min-heap) stores the smaller elements, while the other (a max-heap) stores the larger ones. This allows for quick access to both the smallest and largest elements in the data set. The balance between the two heaps can be dynamically adjusted as new elements are added or removed.

Example: Finding the Median in a Stream of Numbers

Imagine a continuous stream of numbers. You need to maintain the median at any given time. Using two heaps, we can do this efficiently:

  • Min-heap: Stores elements larger than the current median.
  • Max-heap: Stores elements smaller than or equal to the current median.

When a new number arrives:

  1. If the min-heap size is greater than the max-heap size, and the new number is larger than the root of the min-heap, we add it to the min-heap. Otherwise, it goes to the max-heap.

  2. If the max-heap size exceeds the min-heap size, the logic is reversed.

  3. After each insertion, we ensure the size difference between the heaps is at most one, balancing them by moving the root of the larger heap to the smaller one.

The median is then easily found by comparing the root elements of both heaps.


flowchart LR
    subgraph LowerHalf["Max-Heap (Lower Half of Numbers)"]
        direction TB
        MaxRoot["MaxRoot: Largest of smaller half"]
    end

    subgraph MedianCalc["Median Balancing Engine"]
        Cond{"len(maxHeap) == len(minHeap) ?"}
        Cond -- Equal --> Avg["Median = (MaxRoot + MinRoot) / 2.0"]
        Cond -- Odd --> OddMed["Median = MaxRoot"]
    end

    subgraph UpperHalf["Min-Heap (Upper Half of Numbers)"]
        direction TB
        MinRoot["MinRoot: Smallest of larger half"]
    end

    LowerHalf --> MedianCalc
    UpperHalf --> MedianCalc

Code Example (Python):

import heapq

class MedianFinder:
    def __init__(self):
        self.min_heap = []
        self.max_heap = []

    def addNum(self, num):
        heapq.heappush(self.max_heap, -num)  # Use negative for max-heap
        heapq.heappush(self.min_heap, -heapq.heappop(self.max_heap))

        if len(self.min_heap) > len(self.max_heap):
            heapq.heappush(self.max_heap, -heapq.heappop(self.min_heap))

    def findMedian(self):
        if len(self.min_heap) == len(self.max_heap):
            return (-self.max_heap[0] + self.min_heap[0]) / 2
        else:
            return -self.max_heap[0]

Java Example (Illustrative Snippet for addNum):

PriorityQueue<Integer> minHeap = new PriorityQueue<>();
PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Collections.reverseOrder());

public void addNum(int num) {
    maxHeap.offer(num);
    minHeap.offer(maxHeap.poll()); // Maintain balance
    if (maxHeap.size() < minHeap.size()) {
        maxHeap.offer(minHeap.poll());
    }
}

JavaScript Example (Illustrative Snippet for findMedian):

function findMedian() {
  if (minHeap.length === maxHeap.length) {
    return (minHeap[0] + maxHeap[0]) / 2;
  } else {
    return maxHeap[0];
  }
}

Identifying the Two Heaps Pattern

Recognizing when the Two Heaps technique is applicable is crucial. Look for problems involving:

  • Maintaining extremes: Finding medians, kth smallest/largest elements, or ranges.
  • Continuous data streams: Problems dealing with incoming data where efficient updates are required.
  • Finding optimal values within constraints: Scenarios requiring balancing between two competing aspects (e.g., minimizing a cost while maximizing a benefit).

Tips and Tricks for Identification:

  • Keywords: Look for words like "median," "kth smallest/largest," "running median," or phrases implying maintaining both minimum and maximum values.
  • Data flow: Identify if the problem involves a stream of data where you need to update results dynamically.
  • Problem structure: If the problem involves balancing two competing criteria, consider this technique.

Mastering Algorithms: Unveiling the Power of Two Heaps

This blog post delves into the Two Heaps technique, a powerful algorithmic approach used to efficiently solve various problems involving finding medians, kth largest/smallest elements, and more. We'll explore its mechanics, provide step-by-step problem-solving strategies, and illustrate its application across multiple programming languages.

Understanding the Two Heaps Technique

The Two Heaps technique typically involves employing two heaps: a min-heap and a max-heap. The min-heap stores the larger half of the elements, while the max-heap stores the smaller half. This setup allows for efficient retrieval of the median, which is either the top element of the min-heap or the top element of the max-heap (or the average of both if the total number of elements is even). Maintaining a balanced structure between the two heaps is crucial for optimal performance.

Example Problem: Finding the Running Median

Let's illustrate the Two Heaps technique with a classic example: finding the running median of a stream of numbers.

Problem: Given a continuous stream of numbers, efficiently calculate the running median at each step.

Step-by-Step Solution:

  1. Initialization: Create an empty min-heap (minHeap) and an empty max-heap (maxHeap).
  2. Adding an element: For each incoming number x:
    • If maxHeap is empty or x <= maxHeap.peek(), add x to maxHeap.
    • Otherwise, add x to minHeap.
  3. Balancing: Ensure the size difference between minHeap and maxHeap is at most 1. If the difference is greater than 1, transfer the appropriate element between the heaps to maintain balance.
  4. Median Calculation: The median is either the top element of maxHeap (if sizes are equal or maxHeap is larger) or the average of the top elements of maxHeap and minHeap (if minHeap is larger).

Code Example (Python):

import heapq

def running_median(stream):
    max_heap = []  # Smaller half
    min_heap = []  # Larger half
    medians = []

    for num in stream:
        heapq.heappush(max_heap, -num)  # Use negative for max-heap

        if len(max_heap) > len(min_heap) + 1:
            heapq.heappush(min_heap, -heapq.heappop(max_heap))

        if len(min_heap) > len(max_heap):
            heapq.heappush(max_heap, -heapq.heappop(min_heap))

        if len(max_heap) == len(min_heap):
            median = (-max_heap[0] + min_heap[0]) / 2
        else:
            median = -max_heap[0]

        medians.append(median)
    return medians


stream = [2, 1, 5, 7, 2, 0, 5]
print(running_median(stream)) # Output: [2, 1.5, 2, 3.5, 2, 1.5, 2]

Diagram: (Illustrative; cannot be displayed here. Imagine two heaps, one max and one min, with elements flowing into them and being rebalanced.)

More Example Problems

  1. Find the kth smallest element in an array.
  2. Find the kth largest element in a stream.
  3. Design a data structure that supports efficient insertion, deletion, and finding the median.
  4. Calculate the moving average of a data stream.
  5. Find the closest k points to the origin.
  6. Implement a priority queue using two heaps.
  7. Implement a self-balancing binary search tree using two heaps (advanced).
  8. Find the median of two sorted arrays (merge-sort approach is usually better but two heaps can be used).

Code Examples in Other Languages: (Illustrative Snippets - Full implementations require more code)

Java (Finding kth smallest):

// ... (Requires PriorityQueue implementation) ...
PriorityQueue<Integer> minHeap = new PriorityQueue<>();
// ... (Logic to populate and manage minHeap to find kth smallest) ...

JavaScript (Finding running median):

// ... (Requires custom min-heap and max-heap implementations using arrays or a library) ...
let maxHeap = [];
let minHeap = [];
// ... (Logic for adding elements, balancing, and calculating median) ...

Mastering Algorithms: The Two Heaps Technique

Understanding the Two Heaps Technique

The Two Heaps technique typically employs a min-heap and a max-heap to maintain a sorted structure of data. This approach excels in problems that require finding elements within a specific range, or finding medians and similar statistics dynamically as data streams in. The key is that one heap tracks the smaller half of the data, and the other tracks the larger half, allowing for quick access to the median or other relevant statistics.

Identifying the Two Heaps Pattern

Problems suitable for this technique often involve:

  • Finding the median of a data stream: This is a classic application. One heap stores the smaller half, the other the larger half, ensuring the median is readily accessible.
  • Finding the kth smallest/largest element: Two heaps can efficiently track the k smallest or largest elements, discarding irrelevant data.
  • Problems involving a sliding window: Two heaps can assist in efficiently tracking min/max values within a sliding window.

Simple Problems (Easy)

1. Finding the Median of a Data Stream:

  • Problem: Given a stream of integers, find the median at any point in the stream.

  • Solution: Use a min-heap to store the larger half and a max-heap to store the smaller half. Maintain balance by ensuring the heaps' size difference is at most 1.

  • Java Example:

import java.util.PriorityQueue;

class MedianFinder {
    PriorityQueue<Integer> minHeap;
    PriorityQueue<Integer> maxHeap;

    public MedianFinder() {
        minHeap = new PriorityQueue<>();
        maxHeap = new PriorityQueue<>((a, b) -> b - a); // Max heap comparator
    }

    public void addNum(int num) {
        maxHeap.offer(num);
        minHeap.offer(maxHeap.poll());
        if (maxHeap.size() < minHeap.size()) {
            maxHeap.offer(minHeap.poll());
        }
    }

    public double findMedian() {
        if (maxHeap.size() == minHeap.size()) {
            return (maxHeap.peek() + minHeap.peek()) / 2.0;
        } else {
            return maxHeap.peek();
        }
    }
}
  • Python Example:
import heapq

class MedianFinder:
    def __init__(self):
        self.min_heap = []
        self.max_heap = []

    def addNum(self, num):
        heapq.heappush(self.max_heap, -num)
        heapq.heappush(self.min_heap, -heapq.heappop(self.max_heap))
        if len(self.max_heap) < len(self.min_heap):
            heapq.heappush(self.max_heap, -heapq.heappop(self.min_heap))

    def findMedian(self):
        if len(self.max_heap) == len(self.min_heap):
            return (-self.max_heap[0] + self.min_heap[0]) / 2.0
        else:
            return -self.max_heap[0]

Medium Problems

2. Find the kth largest element in a stream: This is a variation of the median problem where k is a predefined value. The solution structure remains similar, but you focus on maintaining the k largest elements.

3. Sliding Window Maximum: Find the maximum value within a sliding window of size 'k' as it moves through an array. Use a max-heap to track the maximum within the window.

Hard Problems

4. Find the kth smallest element in an unsorted array: Adapt the Two Heaps approach to efficiently find this element without fully sorting the array.

5. Merge k Sorted Streams: Efficiently merge multiple sorted streams using heaps for optimized selection.

Example Problem Diagrams (Illustrative)

(For Median Finder):

Imagine a stream of numbers: 2, 5, 1, 8, 3.

  • Initially: Max-heap: [2] , Min-heap: []
  • After 5: Max-heap: [5], Min-heap: [2]
  • After 1: Max-heap: [2,1], Min-heap: [5]
  • After 8: Max-heap: [5, 2, 1], Min-heap: [8]
  • After 3: Max-heap: [5,3,1], Min-heap: [8,2]


Related Guides:
← PreviousTree Depth-First Search (DFS)Next →Two Pointers Pattern: Opposing, Fast-Slow & In-Place Moves