Hard#71511 min readIntervalsOrdered MapDesignLeetCode

Range Module

Ordered map of disjoint half-open intervals

Ordered map interval tracking. Relevant to order book modeling.

Published Apr 17, 2026

Problem

Design a data structure tracking a set of half-open integer intervals with three operations:

  • addRange(left, right) — add [left, right) to the tracked set (merging with anything it touches).
  • queryRange(left, right) — return true iff every number in [left, right) is currently tracked.
  • removeRange(left, right) — remove [left, right) from the tracked set (splitting any interval it cuts through).

All three operations should run in O(log n) amortized where n is the current number of stored disjoint intervals.

Input
["addRange", 10, 20], ["removeRange", 14, 16], ["queryRange", 10, 14], ["queryRange", 13, 15], ["queryRange", 16, 17]
Output
[null, null, true, false, true]
Explanation
After adding [10,20) and removing [14,16), the stored set is [10,14) ∪ [16,20). [10,14) is fully covered; [13,15) straddles the gap; [16,17) is fully covered.

Intuition

The structure we want is a set of disjoint, non-adjacent, half-open intervals sorted by start. addRange merges anything that touches or overlaps into a single interval. queryRange answers by finding the closest-starting interval at or before left and checking that it extends to right. removeRange splits straddling intervals.

An ordered map keyed by start with value end gives us O(log n) predecessor/successor queries — exactly what we need.

Half-open [l, r) is not a cosmetic choice: it makes adjacency trivial. [0, 3) and [3, 5) are adjacent iff the end of one equals the start of the other; no +1 tricks.

Approach

Boolean Array / Naive Simulation

TimeO(R) per op
SpaceO(R)

Maintain a Vec<bool> of size 10^9 + 1. Each op flips or reads a range. Correct but space-prohibitive at LC's scale (R = 10^9). Useful for testing correctness on small inputs.

struct RangeModule {
    covered: std::collections::BTreeSet<i32>,
}

impl RangeModule {
    fn new() -> Self { Self { covered: std::collections::BTreeSet::new() } }
    fn add_range(&mut self, left: i32, right: i32) {
        for x in left..right { self.covered.insert(x); }
    }
    fn query_range(&self, left: i32, right: i32) -> bool {
        (left..right).all(|x| self.covered.contains(&x))
    }
    fn remove_range(&mut self, left: i32, right: i32) {
        for x in left..right { self.covered.remove(&x); }
    }
}
Rust · runnable

Ordered Map of Intervals

TimeO(log n) amortized
SpaceO(n)
Recommended

Store BTreeMap<start, end> (Rust) / sorted entries in a plain Map<number, number> (TS). Invariant: intervals are disjoint and non-adjacent, sorted by start.

addRange(L, R). Collect every stored interval [s, e) with s R and e L (overlap or touch). Delete them; insert [min(L, s_min), max(R, e_max)).

queryRange(L, R). Look up the predecessor interval (largest s L). Return true iff e R.

removeRange(L, R). Collect every stored interval [s, e) with s < R and e > L (strict — adjacency doesn't count for removal). Delete each; re-insert [s, L) if s < L and [R, e) if e > R.

Each op touches k intervals and does O(k log n) work; k is amortized O(1) because every op either adds ≤ 1 interval and destroys k, or removes ≤ 2 per input interval.

use std::collections::BTreeMap;

struct RangeModule {
    intervals: BTreeMap<i32, i32>,
}

impl RangeModule {
    fn new() -> Self { Self { intervals: BTreeMap::new() } }

    fn add_range(&mut self, left: i32, right: i32) {
        let (mut lo, mut hi) = (left, right);
        let touching: Vec<i32> = self
            .intervals
            .range(..=hi)
            .filter(|(_, &e)| e >= lo)
            .map(|(&s, _)| s)
            .collect();
        for s in touching {
            let e = self.intervals.remove(&s).unwrap();
            if s < lo { lo= s; }
            if e > hi { hi = e; }
        }
        self.intervals.insert(lo, hi);
    }

    fn query_range(&self, left: i32, right: i32) -> bool {
        self.intervals
            .range(..=left)
            .next_back()
            .map_or(false, |(_, &e)| e >= right)
    }

    fn remove_range(&mut self, left: i32, right: i32) {
        let crossing: Vec<i32> = self
            .intervals
            .range(..right)
            .filter(|(_, &e)| e > left)
            .map(|(&s, _)| s)
            .collect();
        let mut to_insert: Vec<(i32, i32)> = Vec::new();
        for s in crossing {
            let e = self.intervals.remove(&s).unwrap();
            if s < left { to_insert.push((s, left)); }
            if e > right { to_insert.push((right, e)); }
        }
        for (s, e) in to_insert { self.intervals.insert(s, e); }
    }
}
Rust · runnable

Trace

Starting empty, running add(10,20); remove(14,16); query(10,14); query(13,15); query(16,17):

opintervals afterreturn
add(10, 20){[10, 20)}
remove(14, 16){[10, 14), [16, 20)}
query(10, 14)unchangedtrue (predecessor [10, 14) covers)
query(13, 15)unchangedfalse (predecessor [10, 14) ends at 14 less than 15)
query(16, 17)unchangedtrue (predecessor [16, 20) covers)

Edge cases

  • Adjacent adds (addRange(0, 3); addRange(3, 5)) — merged thanks to the (not strict) overlap check with half-open intervals.
  • Remove adjacent, not overlapping (stored [0, 3), removeRange(3, 5)) — strict > on the end check prevents a spurious split; nothing changes.
  • Query on untouched region — predecessor lookup returns None/negative index → false immediately.
  • Degenerate intervals (addRange(x, x)) — the caller is expected not to send these, but our code treats them as a no-op: lo == hi, nothing to merge into.
⚠️
Warning

Mixing strict vs. non-strict comparisons between addRange (touch counts) and removeRange (only strict overlap) is the single most common bug. Half-open intervals make the right answer crisp: for add, > for remove.

Takeaway

Ordered-map interval management is the workhorse of any "track ranges over time" problem. Order books, time-series presence, kernel page allocators, rate-limit windows — same structure. In the interview, reach for it the moment you see "half-open" or "interval add / query / remove".

If the language lacks a native ordered map (TypeScript), a sorted starts/ends pair with binary search is clean enough to write in 10 minutes. Resist the urge to implement a balanced BST from scratch.

More in Timed Random Hards