🧠
Balanced Two Heaps with Lazy Deletion
💡 This approach uses two heaps to maintain the lower and upper halves of the window, allowing median retrieval in O(1) and updates in O(log k). Lazy deletion handles removals efficiently.
Intuition
Maintain two heaps: a max-heap for the smaller half and a min-heap for the larger half of the current window. Balance their sizes so median is accessible from the tops.
Algorithm
- Initialize two heaps: max-heap for smaller half, min-heap for larger half.
- Insert first k elements, balancing heaps after each insertion.
- For each window, the median is either the top of max-heap or average of tops if even-sized.
- When sliding the window, remove the outgoing element lazily and add the incoming element, rebalancing heaps.
💡 The challenge is managing removals efficiently without direct heap element deletion, solved by lazy deletion with a hash map.
import heapq
from collections import defaultdict
from typing import List
class DualHeap:
def __init__(self, k):
self.small = [] # max heap (invert values)
self.large = [] # min heap
self.delayed = defaultdict(int) # counts of elements to remove
self.k = k
self.small_size = 0
self.large_size = 0
def prune(self, heap):
while heap and self.delayed[heap[0][1]] > 0:
num = heap[0][1]
self.delayed[num] -= 1
if self.delayed[num] == 0:
del self.delayed[num]
heapq.heappop(heap)
def balance(self):
if self.small_size > self.large_size + 1:
val = heapq.heappop(self.small)[1]
heapq.heappush(self.large, (val, val))
self.small_size -= 1
self.large_size += 1
self.prune(self.small)
elif self.small_size < self.large_size:
val = heapq.heappop(self.large)[1]
heapq.heappush(self.small, (-val, val))
self.large_size -= 1
self.small_size += 1
self.prune(self.large)
def insert(self, num):
if not self.small or num <= self.small[0][1]:
heapq.heappush(self.small, (-num, num))
self.small_size += 1
else:
heapq.heappush(self.large, (num, num))
self.large_size += 1
self.balance()
def erase(self, num):
self.delayed[num] += 1
if num <= self.small[0][1]:
self.small_size -= 1
if num == self.small[0][1]:
self.prune(self.small)
else:
self.large_size -= 1
if self.large and num == self.large[0][1]:
self.prune(self.large)
self.balance()
def get_median(self):
if self.k % 2 == 1:
return float(self.small[0][1])
else:
return (self.small[0][1] + self.large[0][1]) / 2
def medianSlidingWindow(nums: List[int], k: int) -> List[float]:
dh = DualHeap(k)
for i in range(k):
dh.insert(nums[i])
result = [dh.get_median()]
for i in range(k, len(nums)):
dh.insert(nums[i])
dh.erase(nums[i - k])
result.append(dh.get_median())
return result
# Example usage
if __name__ == '__main__':
print(medianSlidingWindow([1,3,-1,-3,5,3,6,7], 3))
Line Notes
self.small = [] # max heap (invert values)Use negative values to simulate max heap in Python
self.delayed = defaultdict(int)Track elements to lazily remove from heaps
def prune(self, heap):Remove elements marked for deletion from heap top
def balance(self):Ensure heaps sizes differ by at most one
def insert(self, num):Add new number to appropriate heap and rebalance
def erase(self, num):Mark number for lazy deletion and rebalance
def get_median(self):Return median based on heap sizes and tops
import java.util.*;
public class SlidingWindowMedian {
class DualHeap {
PriorityQueue<Integer> small, large;
Map<Integer, Integer> delayed;
int k, smallSize, largeSize;
public DualHeap(int k) {
this.k = k;
small = new PriorityQueue<>(Collections.reverseOrder());
large = new PriorityQueue<>();
delayed = new HashMap<>();
smallSize = 0;
largeSize = 0;
}
private void prune(PriorityQueue<Integer> heap) {
while (!heap.isEmpty()) {
int num = heap.peek();
if (delayed.containsKey(num)) {
delayed.put(num, delayed.get(num) - 1);
if (delayed.get(num) == 0) delayed.remove(num);
heap.poll();
} else {
break;
}
}
}
private void balance() {
if (smallSize > largeSize + 1) {
int val = small.poll();
large.offer(val);
smallSize--;
largeSize++;
prune(small);
} else if (smallSize < largeSize) {
int val = large.poll();
small.offer(val);
largeSize--;
smallSize++;
prune(large);
}
}
public void insert(int num) {
if (small.isEmpty() || num <= small.peek()) {
small.offer(num);
smallSize++;
} else {
large.offer(num);
largeSize++;
}
balance();
}
public void erase(int num) {
delayed.put(num, delayed.getOrDefault(num, 0) + 1);
if (!small.isEmpty() && num <= small.peek()) {
smallSize--;
if (num == small.peek()) prune(small);
} else {
largeSize--;
if (!large.isEmpty() && num == large.peek()) prune(large);
}
balance();
}
public double getMedian() {
if (k % 2 == 1) return (double) small.peek();
else return ((double) small.peek() + large.peek()) / 2.0;
}
}
public static double[] medianSlidingWindow(int[] nums, int k) {
SlidingWindowMedian swm = new SlidingWindowMedian();
DualHeap dh = swm.new DualHeap(k);
for (int i = 0; i < k; i++) dh.insert(nums[i]);
double[] result = new double[nums.length - k + 1];
result[0] = dh.getMedian();
for (int i = k; i < nums.length; i++) {
dh.insert(nums[i]);
dh.erase(nums[i - k]);
result[i - k + 1] = dh.getMedian();
}
return result;
}
public static void main(String[] args) {
int[] nums = {1,3,-1,-3,5,3,6,7};
double[] medians = medianSlidingWindow(nums, 3);
System.out.println(Arrays.toString(medians));
}
}
Line Notes
small = new PriorityQueue<>(Collections.reverseOrder());Max heap for smaller half
delayed = new HashMap<>();Map to track lazy deletions
private void prune(PriorityQueue<Integer> heap) {Remove elements marked for deletion from heap top
private void balance() {Balance heaps sizes to maintain median property
public void insert(int num) {Insert new element into correct heap
public void erase(int num) {Mark element for lazy deletion and rebalance
public double getMedian() {Return median from heap tops
#include <iostream>
#include <vector>
#include <queue>
#include <unordered_map>
using namespace std;
class DualHeap {
priority_queue<int> small; // max heap
priority_queue<int, vector<int>, greater<int>> large; // min heap
unordered_map<int, int> delayed;
int k, smallSize, largeSize;
void prune(priority_queue<int>& heap) {
while (!heap.empty()) {
int num = heap.top();
if (delayed.count(num)) {
delayed[num]--;
if (delayed[num] == 0) delayed.erase(num);
heap.pop();
} else break;
}
}
void prune(priority_queue<int, vector<int>, greater<int>>& heap) {
while (!heap.empty()) {
int num = heap.top();
if (delayed.count(num)) {
delayed[num]--;
if (delayed[num] == 0) delayed.erase(num);
heap.pop();
} else break;
}
}
void balance() {
if (smallSize > largeSize + 1) {
int val = small.top(); small.pop();
large.push(val);
smallSize--;
largeSize++;
prune(small);
} else if (smallSize < largeSize) {
int val = large.top(); large.pop();
small.push(val);
largeSize--;
smallSize++;
prune(large);
}
}
public:
DualHeap(int k): k(k), smallSize(0), largeSize(0) {}
void insert(int num) {
if (small.empty() || num <= small.top()) {
small.push(num);
smallSize++;
} else {
large.push(num);
largeSize++;
}
balance();
}
void erase(int num) {
delayed[num]++;
if (!small.empty() && num <= small.top()) {
smallSize--;
if (num == small.top()) prune(small);
} else {
largeSize--;
if (!large.empty() && num == large.top()) prune(large);
}
balance();
}
double getMedian() {
if (k % 2 == 1) return (double)small.top();
else return ((double)small.top() + large.top()) / 2.0;
}
};
vector<double> medianSlidingWindow(vector<int>& nums, int k) {
DualHeap dh(k);
for (int i = 0; i < k; i++) dh.insert(nums[i]);
vector<double> result = {dh.getMedian()};
for (int i = k; i < (int)nums.size(); i++) {
dh.insert(nums[i]);
dh.erase(nums[i - k]);
result.push_back(dh.getMedian());
}
return result;
}
int main() {
vector<int> nums = {1,3,-1,-3,5,3,6,7};
vector<double> medians = medianSlidingWindow(nums, 3);
for (double m : medians) cout << m << " ";
cout << endl;
return 0;
}
Line Notes
priority_queue<int> small;Max heap for smaller half
unordered_map<int, int> delayed;Track lazy deletions
void prune(priority_queue<int>& heap) {Remove delayed elements from max heap
void prune(priority_queue<int, vector<int>, greater<int>>& heap) {Remove delayed elements from min heap
void balance() {Balance heap sizes to maintain median property
void insert(int num) {Insert element into correct heap
void erase(int num) {Mark element for lazy deletion and rebalance
double getMedian() {Return median from heap tops
class DualHeap {
constructor(k) {
this.small = new MaxHeap();
this.large = new MinHeap();
this.delayed = new Map();
this.k = k;
this.smallSize = 0;
this.largeSize = 0;
}
prune(heap) {
while (!heap.isEmpty()) {
let num = heap.peek();
if (this.delayed.has(num)) {
this.delayed.set(num, this.delayed.get(num) - 1);
if (this.delayed.get(num) === 0) this.delayed.delete(num);
heap.pop();
} else break;
}
}
balance() {
if (this.smallSize > this.largeSize + 1) {
let val = this.small.pop();
this.large.push(val);
this.smallSize--;
this.largeSize++;
this.prune(this.small);
} else if (this.smallSize < this.largeSize) {
let val = this.large.pop();
this.small.push(val);
this.largeSize--;
this.smallSize++;
this.prune(this.large);
}
}
insert(num) {
if (this.small.isEmpty() || num <= this.small.peek()) {
this.small.push(num);
this.smallSize++;
} else {
this.large.push(num);
this.largeSize++;
}
this.balance();
}
erase(num) {
this.delayed.set(num, (this.delayed.get(num) || 0) + 1);
if (!this.small.isEmpty() && num <= this.small.peek()) {
this.smallSize--;
if (num === this.small.peek()) this.prune(this.small);
} else {
this.largeSize--;
if (!this.large.isEmpty() && num === this.large.peek()) this.prune(this.large);
}
this.balance();
}
getMedian() {
if (this.k % 2 === 1) return this.small.peek();
else return (this.small.peek() + this.large.peek()) / 2;
}
}
// Simple heap implementations omitted for brevity; assume MaxHeap and MinHeap classes exist with push, pop, peek, isEmpty
function medianSlidingWindow(nums, k) {
const dh = new DualHeap(k);
for (let i = 0; i < k; i++) dh.insert(nums[i]);
const result = [dh.getMedian()];
for (let i = k; i < nums.length; i++) {
dh.insert(nums[i]);
dh.erase(nums[i - k]);
result.push(dh.getMedian());
}
return result;
}
// Example usage
console.log(medianSlidingWindow([1,3,-1,-3,5,3,6,7], 3));
Line Notes
this.small = new MaxHeap();Max heap for smaller half
this.delayed = new Map();Map to track lazy deletions
prune(heap) {Remove elements marked for deletion from heap top
balance() {Balance heaps sizes to maintain median property
insert(num) {Insert new number into correct heap
erase(num) {Mark number for lazy deletion and rebalance
getMedian() {Return median from heap tops
Each insertion and lazy deletion costs O(log k), done n times, total O(n log k).
💡 For n=100000 and k=1000, this is about 100000 * 10 = 1,000,000 operations, efficient enough for interviews.
Interview Verdict: Accepted
This is the standard optimal approach for sliding window median problems in interviews.