Why is indexing the middle of a `collections.deque` O(n) when `d[0]` is O(1)?
answer
- No formula from index to address
- Blocks chained together, not one array
- The walk starts from the nearer end
- Ends are cheap, the midpoint is worst
- Slicing a deque raises TypeError
basics
~20 sA deque is a doubly linked chain of fixed-size blocks, not one array, so there is no formula from index to address. Reading an index means walking blocks from whichever end is nearer, which is worst at the middle.
solid answer
~50 sCPython stores a `collections.deque` as a doubly linked list of blocks, each holding a fixed number of item pointers. The deque object knows only where the left and right ends are, so `d[0]` and `d[-1]` are direct hits, but `d[i]` has to walk the block chain from the nearer end — O(1) near the ends, O(n) in the middle. The same layout explains the rest of the API's costs: `d.insert`, `d.remove`, `d.index` and `x in d` are all linear, and slicing is not supported at all (`d[1:3]` raises `TypeError`; use `itertools.islice`). So a deque is the right structure when items are pushed and popped at the ends and read by iterating, and the wrong one when the code does random access or membership tests in a hot path — there, keep an index beside it, typically a dict.
code
python · 12 linesfrom collections import deque
from itertools import islice
d = deque(range(1_000_000))
print(d[0], d[-1]) # O(1) at both ends
print(d[len(d) // 2]) # O(n): walks the block chain
print(list(islice(d, 10, 15))) # the way to take a window
try:
d[1:3]
except TypeError as exc:
print(type(exc).__name__, exc)go deeper
Remember that a deque is fast at its two ends and not designed for indexing into the middle, and that d[1:3] is not even allowed. Reach for a list when the code indexes or slices.
Explain the block-chain layout and why it removes the index-to-address formula, and be able to classify the API: ends and len O(1); insert, remove, index and in O(n); rotate O(k); slicing unsupported.
Show that you would catch this in review and diagnose it in production: latency that scales with buffer size rather than request rate, a linear scan hidden behind an in, and the fix of pairing the deque with a dict or using functools.lru_cache.
Own the container-choice guidance for the codebase: state the access pattern first, treat a deque as a specialised structure rather than a faster list, and decide where a hand-rolled recency cache is justified versus a ready-made one.
## The layout dictates the cost table A `list` is one contiguous array of pointers, so element i lives at a computable address and `lst[i]` is a single arithmetic step. A `collections.deque` is not: CPython implements it as a **doubly linked list of fixed-length blocks** (64 item slots per block in current CPython), with the deque object holding pointers to the leftmost and rightmost blocks and the used-slot indices inside them. There is no address formula, so `__getitem__` divides the index by the block size and walks that many block links — starting from whichever end is closer to the requested index. Near either end that walk is a couple of hops and reads as constant time; at the midpoint it traverses roughly half the blocks, which is O(n). Everything else in the API follows from the same layout: * `append`, `appendleft`, `pop`, `popleft` — O(1), the reason the type exists. * `d[0]`, `d[-1]`, `len(d)` — O(1). * `d[i]` for a middle i, `d.insert(i, x)`, `d.remove(x)`, `d.index(x)`, `x in d`, `d.count(x)` — O(n). * `d.rotate(k)` — O(k), because rotation relinks blocks rather than moving every element. * Slicing — **unsupported**. `d[1:3]` raises `TypeError`, and there is no `__setitem__` for slices either. Take a window with `itertools.islice(d, start, stop)`, which iterates and therefore costs O(stop). ## Where this actually bites The realistic failure is a deque chosen correctly for its ends and then quietly used for lookups. Picture a route-optimisation job whose worker keeps the last 5,000 solved legs in `deque(maxlen=5000)` so it can answer repeat requests without recomputing. The bound is the right call — without it the buffer grows for the whole run and the process shows unbounded memory growth. But if the hit path is `for key, value in cache: if key == wanted`, or worse `if wanted in [k for k, _ in cache]`, then every lookup is a linear scan of 5,000 entries. At an 83% cache-hit rate the scan runs on nearly every request, and the "optimisation" costs more than the recompute it avoids. The profile looks flat and boring — no single slow call, just a lot of iteration — which is what makes it survive. The fix is not to abandon the deque. Recency ordering and O(1) eviction are exactly what it is good at; membership is what it is bad at. Keep both: a `dict` mapping key to value for O(1) lookup, and the deque holding keys in recency order for eviction, deleting the evicted key from the dict when you push. When the cached thing is a pure function of its arguments, `functools.lru_cache` already implements that pairing and you should reach for it before hand-rolling one. ## Diagnosing it The symptom of a middle-index or membership misuse is throughput that degrades with buffer size rather than with request rate — double `maxlen` and latency roughly doubles. Two checks settle it quickly: look for `in`, `.index(`, `.remove(` or a non-zero integer subscript applied to a deque, and time the operation against a small and a large instance of the same structure. Because the deque never raises for these operations, static review is the only place they get caught cheaply. ## Choosing deliberately State the access pattern before choosing the container. Pushes and pops at both ends, iteration front to back, an eviction bound: `collections.deque`. Random access, slicing, sorting, in-place index assignment: `list`. Membership and keyed lookup: a `dict` or a `set`, possibly alongside the deque. A deque is not a general-purpose sequence with better ends — it is a specialised structure that trades random access away, and the trade only pays when you are honest about which operations the code really performs. ## Iterate, do not index The practical rule that falls out of the layout is: **iterate a deque, never index into it in a loop.** Iteration is genuinely fast — the iterator walks the block chain once, and because each block holds its item pointers contiguously the traversal is cache-friendly, comparable to iterating a list. What is slow is *repeated* index lookups, because each one restarts the walk from an end. So `for x in d: ...` over a million items is fine, while `for i in range(len(d)): use(d[i])` is quadratic and looks almost identical in review. The same trap dressed differently is a membership test inside a loop: `if key in d` over a large deque turns an O(1)-looking line into a full scan on every iteration, which is why the fix is a `set` or `dict` beside the deque rather than a cleverer way to search it.
- How do you take a slice of a `collections.deque`?You cannot subscript it with a slice — `d[1:3]` raises `TypeError`. Use `itertools.islice(d, start, stop)`, which iterates the deque and yields the window, costing O(stop) rather than O(stop - start). If the code slices often, that is a signal the data wants to be in a list, or that you should keep a separate index.
- If a deque is bad at membership tests, how do you build a recency cache with it?Pair it with a dict: the dict maps key to value for O(1) lookup, the deque holds keys in recency order for O(1) eviction, and when you push past capacity you `popleft` a key and delete it from the dict. For caching a pure function's results, `functools.lru_cache` already implements exactly that pairing.
- Is `deque.rotate` also linear in the length of the deque?No — it is O(k) in the rotation distance, bounded by the length. Rotation relinks the block chain and moves the end pointers rather than shifting every element, which is why a small rotation on a huge deque is cheap. That makes round-robin ordering and window alignment idiomatic one-liners.
saying these in an interview costs you the question
- Thinks a deque supports slicing like a list
- Assumes every deque index is O(1)
- Runs `x in d` over a large deque per request
- Calls deque.insert in the middle of a hot loop
- Treats deque as a drop-in faster list
- Cannot name a structure to pair with it for lookup