Skip to content

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 bisect gives you, and the precise reason it is not a TreeMap.
  • The three real options, and the one question that decides between them.
  • Why sortedcontainers is 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.

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”:

searchbisect_left: the first index where arr[i] >= targetO(log n)
search space
103132337495
target3
setupThe exclusive-upper-bound convention. It looks like a small change from the exact-match version, but it is what makes this variant able to answer "where would it go" rather than "is it there" — and it never returns −1, so callers need no special case.
1/5

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.

sorted list + bisectSortedListheapq
search / rankO(logn)O(\log n)O(logn)O(\log n)O(n)O(n)
insertO(n)O(n)O(logn)O(\log n)O(logn)O(\log n)
deleteO(n)O(n)O(logn)O(\log n)O(logn)O(\log n) arbitrary: O(n)O(n)
min / maxO(1)O(1)O(1)O(1)O(1)O(1) min only
k-th smallestO(1)O(1)O(logn)O(\log n)O(nlogn)O(n \log n)
predecessor / successorO(logn)O(\log n)O(logn)O(\log n)not supported
in the standard libraryyesnoyes

Four queries, all built from two functions:

ordered_queries.py
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.

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:

ivwindow (sorted)look for successor of v - tfoundin range?
01[]−2none
15[1]2none
29[1, 5]6none
31[5, 9]−255 − 1 = 4 > 3, no
45[9, 1][1, 9]299 − 5 = 4 > 3, no
59[1, 5]6none

Answer False.

Two things worth reading off that table:

  • One successor query answers the whole question. The smallest value that is at least v - t is the only candidate worth checking: anything smaller is out of range below, and if this one exceeds v + t then so does everything above it. That reduces a pairwise scan to one O(logk)O(\log k) lookup.
  • The window must support deletion, not just insertion, because values leave as the window slides. That is what rules out a heap: heapq has no efficient removal of an arbitrary element.

With bisect on a list this is O(nk)O(n \cdot k) because of the shifts; with SortedList it is O(nlogk)O(n \log k). State which you are writing and why.

ProblemWith bisect on a listWith SortedList
LC 220 Contains Duplicate IIIO(nk)O(n \cdot k)O(nlogk)O(n \log k)
LC 480 Sliding Window MedianO(nk)O(n \cdot k)O(nlogk)O(n \log k)
LC 493 Reverse PairsO(n2)O(n^2)O(nlogn)O(n \log n)
Static: count in range, closest valueO(logn)O(\log n) per query after an O(nlogn)O(n \log n) sortsame

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.

NeedApproach
Count elements < x (rank)bisect_left(arr, x)
Count occurrences of xbisect_right - bisect_left
Closest element to xcompare 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 mediantwo heaps, or SortedList and index the middle
Insert keeping orderbisect.insortO(n)O(n), so beware
Sort by a keybisect with a key= argument (Python 3.10+), or keep a parallel key list
  • Assuming bisect.insort is O(logn)O(\log n). The search is; the insert is O(n)O(n). This is the defining trap of this page.
  • Using bisect on 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_left gives the insertion point, so the predecessor is at i - 1 and you must guard i > 0.
  • Confusing bisect_left and bisect_right on duplicates. left gives the start of a run of equal values, right gives one past its end. For a predecessor query with duplicates, left is what you want.
  • Assuming sortedcontainers is importable. It is third-party. LeetCode has it; a bare CoderPad or a company’s own judge may not. Have the bisect fallback 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.
They askWhat they’re checkingThe answer
“Python has no TreeMap. What do you use?”Whether you know the gap existsA 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 docsO(logn)O(\log n) to find the position, O(n)O(n) to insert because of the shift. So the operation is O(n)O(n)
“Then how is your solution O(nlogn)O(n \log n)?”HonestyIt is not, with bisect — it is O(nk)O(n \cdot k). 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 fallbackbisect and accept the O(n)O(n) 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?”Precisionleft 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 toolA Fenwick tree over compressed values: O(logn)O(\log n) for both update and prefix count. That is what SortedList is doing internally
5 problems
1 easy1 medium3 hard

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.

pch.quizTag Ordered structures in Python — self-check
  1. What is the time complexity of bisect.insort?

    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.

  2. Python has no TreeMap. What do you tell an interviewer?

    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.

  3. bisect_left versus bisect_right — what is the difference, and what is the gap between them?

    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.

  4. Why can two heaps not directly handle a sliding-window median?

    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.

  5. You need to count elements less than x in an array that keeps changing. What is the right tool?

    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.

  • The gap — Python has no TreeMap or std::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 where arr[i] >= x. Never returns −1; returns the insertion point.
  • insort is O(n)O(n) — the search is logarithmic, the shift is not. This is the trap.
  • Idioms — predecessor arr[i-1] guarding i > 0; successor arr[i] guarding i < len; count in range bisect_right(hi) - bisect_left(lo).
  • Both frequent? Fenwick tree over compressed values — O(logn)O(\log n) either way.
  • The missing TreeMap is a real interview problem in Python, and the answer is judgement rather than an algorithm.
  • bisect answers rank, count, predecessor, successor and range-count in O(logn)O(\log n) — but insertion is O(n)O(n), and saying so unprompted is the mark of someone who has actually used it.
  • sortedcontainers.SortedList is 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.coffeeCta

pch.feedbackHeading

pch.feedbackSubheading