Ordered Structures in Python
Java has TreeMap. C++ has std::map and std::set. Python has neither, and
that gap is a genuine interview problem: a question that a Java candidate answers
with one data structure requires you to know three fallbacks and to justify your
choice out loud.
This page is that justification. It is short on algorithms and long on judgement, because judgement is what is actually being tested — “I would use a TreeMap” is not an available answer, and “I would sort it” is often the wrong one.
What you’ll learn
Section titled “What you’ll learn”- What
bisectgives you, and the precise reason it is not a TreeMap. - The three real options, and the one question that decides between them.
- Why
sortedcontainersis the competitive-Python answer and why you must say it is third-party. - The specific problems where this gap shows up: sliding-window medians, “find the closest element”, interval overlap checks, and LC 220.
The cue
Section titled “The cue”Visual intuition
Section titled “Visual intuition”bisect_left is the primitive everything here is built on. It answers “where
would this go”, which is strictly more useful than “is it present”:
Note it never returns -1. For an absent value it returns the insertion point, which is exactly what makes it usable for predecessor and successor queries. bisect_right is the same loop with <= — and the gap between the two is the count of occurrences.
The three options, honestly
Section titled “The three options, honestly”sorted list + bisect | SortedList | heapq | |
|---|---|---|---|
| search / rank | |||
| insert | |||
| delete | arbitrary: | ||
| min / max | min only | ||
| k-th smallest | |||
| predecessor / successor | not supported | ||
| in the standard library | yes | no | yes |
What bisect actually gives you
Section titled “What bisect actually gives you”Four queries, all built from two functions:
import bisect
arr = [1, 3, 5, 7, 9] # must already be sorted
# 1. Insertion point / rank
bisect.bisect_left(arr, 5) # 2 -> first index where arr[i] >= 5
bisect.bisect_right(arr, 5) # 3 -> first index where arr[i] > 5
# 2. Count of a value = the gap between them
bisect.bisect_right(arr, 5) - bisect.bisect_left(arr, 5) # 1
# 3. Predecessor: largest element strictly less than x
i = bisect.bisect_left(arr, 6)
pred = arr[i - 1] if i > 0 else None # 5
# 4. Successor: smallest element >= x
j = bisect.bisect_left(arr, 6)
succ = arr[j] if j < len(arr) else None # 7
# 5. Count in a range [lo, hi] inclusive
bisect.bisect_right(arr, 7) - bisect.bisect_left(arr, 3) # 3
# Insertion keeps it sorted -- but note the cost.
bisect.insort(arr, 4) # O(n): binary search, then SHIFT
print(arr) # [1, 3, 4, 5, 7, 9]Everything an ordered structure is asked for reduces to those two calls plus an
index adjustment. Memorise the predecessor and successor idioms — the i - 1
and the bounds guards are where mistakes happen under pressure.
Dry run
Section titled “Dry run”LC 220 Contains Duplicate III — the canonical problem for this gap. Given
nums, is there a pair within index distance k whose values differ by at most
t?
nums = [1, 5, 9, 1, 5, 9], k = 2, t = 3. Maintain a sorted window of the
last k values and, for each new value v, ask for the successor of v - t:
i | v | window (sorted) | look for successor of v - t | found | in range? |
|---|---|---|---|---|---|
| 0 | 1 | [] | −2 | none | — |
| 1 | 5 | [1] | 2 | none | — |
| 2 | 9 | [1, 5] | 6 | none | — |
| 3 | 1 | [5, 9] | −2 | 5 | 5 − 1 = 4 > 3, no |
| 4 | 5 | [9, 1] → [1, 9] | 2 | 9 | 9 − 5 = 4 > 3, no |
| 5 | 9 | [1, 5] | 6 | none | — |
Answer False.
Two things worth reading off that table:
- One successor query answers the whole question. The smallest value that is
at least
v - tis the only candidate worth checking: anything smaller is out of range below, and if this one exceedsv + tthen so does everything above it. That reduces a pairwise scan to one lookup. - The window must support deletion, not just insertion, because values leave
as the window slides. That is what rules out a heap:
heapqhas no efficient removal of an arbitrary element.
With bisect on a list this is because of the shifts; with
SortedList it is . State which you are writing and why.
Complexity
Section titled “Complexity”| Problem | With bisect on a list | With SortedList |
|---|---|---|
| LC 220 Contains Duplicate III | ||
| LC 480 Sliding Window Median | ||
| LC 493 Reverse Pairs | ||
| Static: count in range, closest value | per query after an sort | same |
The pattern is consistent: bisect costs a factor of the window size on writes,
and buys you standard-library availability. Whether that trade is acceptable
depends entirely on the constraints, which is why reading them first matters.
The variant map
Section titled “The variant map”| Need | Approach |
|---|---|
Count elements < x (rank) | bisect_left(arr, x) |
Count occurrences of x | bisect_right - bisect_left |
Closest element to x | compare arr[i-1] and arr[i] at i = bisect_left(arr, x) |
| Predecessor (strictly less) | arr[bisect_left(arr, x) - 1], guarding i > 0 |
| Successor (greater or equal) | arr[bisect_left(arr, x)], guarding i < len(arr) |
Count in [lo, hi] | bisect_right(arr, hi) - bisect_left(arr, lo) |
| Sliding-window median | two heaps, or SortedList and index the middle |
| Insert keeping order | bisect.insort — , so beware |
| Sort by a key | bisect with a key= argument (Python 3.10+), or keep a parallel key list |
Pitfalls
Section titled “Pitfalls”- Assuming
bisect.insortis . The search is; the insert is . This is the defining trap of this page. - Using
bisecton an unsorted list. It returns nonsense silently — no error, no warning. The precondition is yours to maintain. - Off-by-one in the predecessor idiom.
bisect_leftgives the insertion point, so the predecessor is ati - 1and you must guardi > 0. - Confusing
bisect_leftandbisect_righton duplicates.leftgives the start of a run of equal values,rightgives one past its end. For a predecessor query with duplicates,leftis what you want. - Assuming
sortedcontainersis importable. It is third-party. LeetCode has it; a bare CoderPad or a company’s own judge may not. Have thebisectfallback ready and say which you are relying on. - Reaching for an ordered structure when a heap would do. If you only need the
minimum, use
heapq— it is simpler and in the standard library.
Interview follow-ups
Section titled “Interview follow-ups”| They ask | What they’re checking | The answer |
|---|---|---|
| “Python has no TreeMap. What do you use?” | Whether you know the gap exists | A sorted list plus bisect for read-heavy work, sortedcontainers.SortedList for balanced read/write. Name that SortedList is third-party |
“What is the complexity of insort?” | Whether you read the docs | to find the position, to insert because of the shift. So the operation is |
| “Then how is your solution ?” | Honesty | It is not, with bisect — it is . Either accept that given the constraints, or use SortedList, or implement a Fenwick tree over compressed values |
“sortedcontainers is not available” | Whether you have a fallback | bisect and accept the insert; or two heaps with lazy deletion; or a Fenwick tree over coordinate-compressed values for rank queries |
“Difference between bisect_left and bisect_right?” | Precision | left returns the first index where arr[i] >= x, right the first where arr[i] > x. On duplicates they bracket the run, and the gap is the count |
“Count elements less than x in a changing array” | Whether you know the real tool | A Fenwick tree over compressed values: for both update and prefix count. That is what SortedList is doing internally |
Practice
Section titled “Practice”Work down the ladder. Tick each problem off as you go — progress is saved in this browser, and the Export button in the filter bar writes it to a file you can keep.
- 35Search Insert Positioneasy
- 981Time Based Key-Value Storemedium
- 220Contains Duplicate IIIhard
- 480Sliding Window Medianhard
- 493Reverse Pairshard
Exercises
Section titled “Exercises”Predecessor and successor with bisect
Section titled “Predecessor and successor with bisect”Count in a range, and count occurrences
Section titled “Count in a range, and count occurrences”LC 220 — Contains Duplicate III · Hard
Section titled “LC 220 — Contains Duplicate III · Hard”Self-check
Section titled “Self-check”-
What is the time complexity of bisect.insort?
This is the defining trap of the pattern: the binary search makes the code LOOK logarithmic. In a loop it is O(n squared) — fine at n=1000, TLE at n=100000.
pch.quizShowAnswer
B — O(n) — the search is O(log n) but inserting shifts every later element — This is the defining trap of the pattern: the binary search makes the code LOOK logarithmic. In a loop it is O(n squared) — fine at n=1000, TLE at n=100000.
-
Python has no TreeMap. What do you tell an interviewer?
A dict preserves INSERTION order, not sorted order, so it answers none of these queries. Naming the third-party caveat matters because a bare CoderPad may not have sortedcontainers.
pch.quizShowAnswer
B — Sorted list plus bisect for read-heavy work; sortedcontainers.SortedList for balanced read/write — and note that SortedList is third-party — A dict preserves INSERTION order, not sorted order, so it answers none of these queries. Naming the third-party caveat matters because a bare CoderPad may not have sortedcontainers.
-
bisect_left versus bisect_right — what is the difference, and what is the gap between them?
That gap is how you count occurrences in O(log n). For a predecessor query with duplicates you want left, since it lands at the start of the run.
pch.quizShowAnswer
B — left gives the first index where arr[i] >= x, right the first where arr[i] > x — so on duplicates they bracket the run and the gap is the count — That gap is how you count occurrences in O(log n). For a predecessor query with duplicates you want left, since it lands at the start of the run.
-
Why can two heaps not directly handle a sliding-window median?
Insertion is fine; deletion is the problem. The workaround is lazy deletion with a map of pending removals purged at the roots — fiddly, which is why SortedList makes LC 480 almost trivial.
pch.quizShowAnswer
B — Because a heap has no efficient removal of an arbitrary element, and a sliding window must delete the value that just left — Insertion is fine; deletion is the problem. The workaround is lazy deletion with a map of pending removals purged at the roots — fiddly, which is why SortedList makes LC 480 almost trivial.
-
You need to count elements less than x in an array that keeps changing. What is the right tool?
bisect gives O(log n) queries but O(n) updates. Once both are frequent you need a structure logarithmic in both, which is what SortedList uses internally and what a Fenwick tree gives you in the standard library.
pch.quizShowAnswer
B — A Fenwick tree over coordinate-compressed values — O(log n) for both update and prefix count — bisect gives O(log n) queries but O(n) updates. Once both are frequent you need a structure logarithmic in both, which is what SortedList uses internally and what a Fenwick tree gives you in the standard library.
Recall card
Section titled “Recall card”- The gap — Python has no
TreeMaporstd::map. Know the three fallbacks and say which you are using. - The deciding question — are writes frequent? Rare writes → sorted list plus
bisect. Frequent →SortedList(third-party). Only extremes →heapq. bisect_left— first index wherearr[i] >= x. Never returns −1; returns the insertion point.insortis — the search is logarithmic, the shift is not. This is the trap.- Idioms — predecessor
arr[i-1]guardingi > 0; successorarr[i]guardingi < len; count in rangebisect_right(hi) - bisect_left(lo). - Both frequent? Fenwick tree over compressed values — either way.
- The missing
TreeMapis a real interview problem in Python, and the answer is judgement rather than an algorithm. bisectanswers rank, count, predecessor, successor and range-count in — but insertion is , and saying so unprompted is the mark of someone who has actually used it.sortedcontainers.SortedListis the competitive-Python answer; name it as third-party and keep a fallback.- When reads and writes are both hot, the real tool is a Fenwick tree over coordinate-compressed values.
Next: Graph Representations — choosing storage by density and by the operation you repeat.
pch.coffeeTagline
pch.coffeeCtapch.feedbackHeading
pch.feedbackSubheading