Skip to content
Work Practice Products Coding interviews Machine learning coding Quant developer HFT course Blog Method Why me About Enquire Book a call

Quant developer HFT course · Module 3: Containers and memory

map, list and deque: when nodes and blocks earn their place

Lesson · 9 min

The question

Give me one situation where std::map is the right choice over std::unordered_map, one for std::deque, and one for std::list.

std::map

std::map is a balanced binary search tree, a red-black tree in all mainstream libraries. Each element is a node with the key, the value, three pointers (left, right, parent) and a colour bit. Lookups and inserts are O(log n), and iteration visits keys in order.

It is the right choice when you need ordering as well as lookup and the data changes often enough that a sorted vector would spend too long shifting:

  • range queries: all orders with price between two values;
  • iteration in key order: dumping levels best to worst;
  • lower_bound and upper_bound: the first level at or beyond a price;
  • stable references across inserts and erases.

Its costs are the node allocations and the pointer chase: a tree of a million elements is twenty levels deep, and each level can be a cache miss. A sorted vector with binary search makes the same number of comparisons on contiguous memory, which is much faster when updates are rare.

std::unordered_map gives no order at all. If you need "the next price up", it cannot help.

std::deque

A deque stores elements in fixed-size blocks and keeps a small array of pointers to those blocks.

block map···block 0: q0 … q15block 1: q16 … q31block 2: q32 … q40
A deque: fixed-size blocks plus a small array of pointers to them. Pushing at either end fills or adds a block; elements never move, but each access goes through the map.

Pushing or popping at either end never moves existing elements: a full end block gets a new neighbour. Indexed access is constant time but costs an extra indirection through the block map. Iteration is contiguous within each block.

It is the right choice for a queue that grows and shrinks at both ends where you need references to stay valid, such as a FIFO of pending requests. Two caveats: block sizes are small in some implementations (libc++ uses 4 KB, libstdc++ 512 bytes, and MSVC's tiny 16-byte blocks make it nearly a list of elements), and a deque still allocates a block when it grows. For a fixed-size FIFO on the hot path, a ring buffer over a preallocated array is usually better (Module 4).

std::list

A doubly linked list of nodes. The previous module covered why scanning one is slow. It earns its place when you need:

  • O(1) insertion and removal anywhere given an iterator, with no element moving;
  • splice, moving nodes between lists without copying or allocating;
  • references that survive every insert and erase of other elements.

An LRU cache is the textbook example: a list in recency order plus a hash map from key to list iterator, so that touching an entry splices its node to the front in constant time.

In trading code the same need (stable, O(1) removal from the middle of a queue) arises in price levels, where an order can be cancelled from anywhere in the time-priority queue. The usual implementation is an intrusive list: the previous and next links are fields of the order itself, and orders live in a pool. You get the list's operations without its separate allocations.

struct Order {
    std::uint32_t prev = NIL, next = NIL;   // links as pool indices, not pointers
    std::int64_t price; std::int32_t qty;
    // ...
};

The answer

std::map when I need ordered iteration or range queries over data that changes often, such as the far levels of a book. std::deque for a queue with pushes and pops at both ends where elements must not move. std::list for O(1) removal from the middle with stable references, such as an LRU cache, though on a hot path I would make it intrusive over a pool. In each case I would say what I would measure before replacing it with something flat.

Check yourself

Question 1 of 3

Which operation can std::map do that std::unordered_map cannot?

Question 2 of 3

What happens to references to existing elements when you push_front on a std::deque?

Question 3 of 3

Why are intrusive lists popular in order books?

In the interview

Say your own answer out loud first, then open the one we would give.

When would you use std::map instead of std::unordered_map?

When I need order: range queries, iteration in key order, or lower_bound to find the next key. Also when I need references that are stable and never invalidated by rehashing. If the data is small or rarely updated, a sorted vector does the same faster.

How is std::deque implemented?

As fixed-size blocks plus an array of pointers to them. Pushing at either end fills or adds a block, so elements never move, but access goes through the block map. It suits double-ended queues where references must stay valid.

Is std::list ever the right choice?

For O(1) removal or splicing anywhere with stable references, such as an LRU cache. On a hot path I would use an intrusive list over a preallocated pool to get the same operations without an allocation per node.