Recommended resources
The freely available resources this lesson will direct you to.
In our previous lesson, you successfully implemented the two-heaps pattern to find the median of a data stream, mastering the addNum and findMedian operations. That model works perfectly when we only add numbers. However, many real-world scenarios, like analyzing recent stock prices or network traffic, require processing data in a sliding window, where old elements are removed as new ones are added.
This introduces a significant challenge: standard heap implementations, including Python's heapq, do not provide an efficient way to remove an arbitrary element. This lesson is dedicated to solving that exact problem. Your objective is to devise a strategy for handling element removal from the two heaps, focusing on an elegant and efficient technique called "lazy deletion." Understanding this is the final prerequisite before we can tackle the full Sliding Window Median problem.
The Problem: The Cost of Direct Removal
Let's first understand why removing an element from a heap is tricky. A heap's efficiency for insertions and top-element removals comes from its specific structure. Removing an element from the middle of the heap breaks this structure.
The standard textbook algorithm to remove an arbitrary element involves three steps:
- Find the element: This requires a linear scan through the heap's underlying array, which takes time for a heap of size .
- Swap and Remove: The found element is swapped with the last element in the heap, and the size of the heap is reduced by one. This is an operation.
- Restore the Heap: The swapped element, now in a new position, may violate the heap property. It must be "bubbled up" or "bubbled down" to its correct place, which takes time.
The bottleneck is clearly the linear scan. In a sliding window scenario where we perform this for each of the elements in an array, this leads to an overall complexity of , which is often too slow.
The following video demonstrates this exact process. While it also introduces an optimization using a hash table (which is a valid alternative), pay close attention to the fundamental mechanics and cost of removing an arbitrary node.
Priority Queue Removing Elements
This video from WilliamFiset's data structures series clearly illustrates heap removal operations.
Watch the segment explaining how to remove an arbitrary element. Note the initial linear scan to find the element and the subsequent steps to restore the heap. This will help you appreciate why the O(k) search is the part we need to optimize away.
As you can see, a more sophisticated approach is required to maintain the desired logarithmic time complexity.
The Solution: Lazy Deletion
The key insight is to change our perspective: what if we don't remove an element immediately? This is the core idea of lazy deletion. Instead of paying the cost to find and remove an element right away, we simply mark it for deletion and leave it in the heap. We only perform the actual removal if and when this "stale" element rises to the top of a heap and interferes with our median calculation.
This approach trades a small amount of space (to keep track of stale elements) for a significant gain in time complexity.
To implement this, we need one additional data structure:
- A hash map (dictionary in Python): We'll call it
delayed. This map will store the numbers that need to be removed and the count of how many times they need to be removed. Using a count is essential to correctly handle duplicate numbers within the window.
The logic is then split into two main parts: marking for removal and pruning.
A Very Thorough Solution to Sliding Window Median and Some ...
This article provides a concise and high-level explanation of the lazy deletion concept, contrasting it with other heap removal strategies.
Please read the part of the article that starts with 'The problem with this heapq library' and continue through the explanation of the lazy deletion technique. This will give you a strong conceptual foundation for the strategy.
The Algorithm: remove_num and prune
Let's detail how this strategy fits into our MedianFinder class from the previous lesson. We'll add a remove_num method and a helper prune method.
480. Sliding Window Median - In-Depth Explanation - AlgoMonster
This Algo.monster article breaks down the implementation details of lazy deletion within the two-heaps structure.
First, read the Intuition section, focusing on the concise explanation of lazy deletion. Next, in the "Key Methods Implementation" section, carefully study the logic for the remove_num and prune methods. This will show you exactly how the concept is translated into an algorithm.
Based on the readings, we can define the methods as follows:
-
remove_num(num): When an elementnumleaves the sliding window:- Add
numto thedelayedhash map, incrementing its count.delayed[num] += 1. This is an operation. - Decrement the "virtual" size of the heap that
numbelongs to. (We need to keep track of the actual number of valid elements, not the physical size of the heap which may contain stale items). - Crucially, if
numhappens to be at the top of either heap, we must immediately callpruneon that heap to remove it and any other stale elements below it that are now exposed.
- Add
-
prune(heap): This helper function is the cleanup crew.- It looks at the top element of the given
heap. - As long as the heap is not empty and its top element is present in the
delayedmap with a count greater than zero:- Decrement the element's count in the
delayedmap. - Pop the element from the
heap. This is the standard, efficient heap pop.
- Decrement the element's count in the
- This loop continues until the top of the heap is a "valid" element (i.e., not marked for deletion).
- It looks at the top element of the given
This prune function must also be called whenever we move elements between heaps during the rebalance step, as this could also expose a stale element at the top.
A Visual Walkthrough
Let's trace the lazy deletion process with an example. Consider the array [1, 3, 2, 3, 1, 3, 1] with a window size k=3. The two heaps are smalls (a max-heap) and larges (a min-heap).

Let's analyze the steps shown in the image:
i=3: The first window is[1, 3, 2]. After balancing,smallscontains{1, 2}andlargescontains{3}. All nodes are valid (green). The median is2.i=4: The window slides to[3, 2, 3].1leaves the window and3enters.remove(1): We mark1as stale (red) by adding it to ourdelayedmap. It remains in thesmallsheap for now.add(3): The new3is added to the heaps.
i=5: The window slides to[2, 3, 1].2leaves and1enters.remove(2): We mark2as stale.add(1): The new1is added.- At this point, the image shows a "Lazy Heap Removal!" The element at the top of the
smallsmax-heap was one of the stale elements. Theprunefunction is triggered, popping it from the heap.
i=6: The window becomes[3, 1, 3].3leaves, another3enters. The stale elements are now1and2.i=7: The window is[1, 3, 1].3leaves,1enters.- Now, stale elements are at the top of both heaps. The
prunefunction is called on both, clearing them out and exposing the new valid top elements.
- Now, stale elements are at the top of both heaps. The
This lazy approach ensures that the expensive work of removal is only done when absolutely necessary (when a stale element is at the top), and when it is done, it's an efficient pop.
Important Considerations
While powerful, this technique has nuances you must be aware of for a robust implementation, especially in an interview setting.
- Heap Bloat: Since we don't remove elements immediately, the heaps can grow larger than size
k, containing many stale elements. In the worst case, the heap size could approach , impacting performance. While this is a valid theoretical concern, for typical interview problems, the simple lazy deletion described is sufficient and expected. - Correctly Identifying the Target Heap for Removal: When calling
remove_num(num), how do we decide whether to decrement the size ofsmallsorlarges? A naive comparison likenum <= smalls.top()can fail ifsmalls.top()is itself a stale element! The robust solution is toprunethe heaps before making the comparison to ensure you are comparing against a valid element. This is a subtle but critical edge case.
Conclusion
In this lesson, you've bridged the gap between a static median finder and a dynamic one capable of handling a sliding window. You dissected the inefficiency of direct heap removal and mastered the logic of lazy deletion as a superior alternative.
Key Takeaways:
- Removing an arbitrary element from a heap is inefficiently due to the required linear scan.
- Lazy Deletion defers the removal cost. Elements are marked as "stale" in a hash map and only physically popped when they reach the top of a heap.
- This strategy relies on two key operations:
remove_numto mark elements for deletion () and aprunehelper function to clean stale elements from the heap tops (). - The overall time complexity for removing an element is amortized , a significant improvement over .
You now have all the necessary components. You know how to add elements and maintain balance from our previous lesson, and you now know how to efficiently remove elements. In our next lesson, we will put it all together and implement a complete solution for the Sliding Window Median problem.