5.2.19 · D4C++ Programming

Exercises — STL containers — vector, list, deque, array, set, multiset, map, multimap, unordered_map, unordered_set

2,259 words10 min readBack to topic

Throughout, is the number of elements currently in the container. "Cost" always means how the running time grows with — see Big-O notation if that phrase is new.


Level 1 — Recognition

(Can you name the right tool and its headline cost?)

Exercise 1.1

For each need below, name the single best STL container: (a) 30-element lookup table whose size is a fixed compile-time constant. (b) A growable list of pixels you only ever push_back and index by position. (c) A dictionary of string → int where you must be able to print entries in alphabetical order. (d) A collection where you push_front and push_back equally often.

Recall Solution

(a) ==std::array<T, 30> — fixed size known at compile time, lives on the stack, zero overhead. (b) std::vector== — contiguous, index v[i], amortised push_back. (c) ==std::map<string,int>== — tree-based, so iteration is in sorted key order for free. (d) ==std::deque== — the only sequence container with amortised at both ends.

Why not swap them? A vector for (d) pays per push_front because every element must shift right one slot. An unordered_map for (c) would print in arbitrary bucket order — it has no ordering to give you.

Exercise 1.2

Fill the blanks with , , or : vector random access = ____; list random access = ____; set search = ____; unordered_set search (average) = ____.

Recall Solution
  • vector random access: ==== — one multiply-and-add on the base pointer.
  • list random access: ==== — nodes are scattered, you must walk next pointers.
  • set search: ==== — a balanced red-black tree halves the search space each level.
  • unordered_set search (average): ==== — a hash jumps straight to the bucket.

Level 2 — Application

(Plug the rules in and compute costs.)

Exercise 2.1

A vector<int> starts empty. You call push_back exactly times. Using the doubling strategy (capacity ), how many element copies happen in total during resizes, and is this ?

Recall Solution

Resizes trigger just before sizes (the powers of two ). At a resize from capacity you copy the existing elements. Total copies: Why this sum? A geometric series . Here , so . , so copy work is . Spread over pushes ⇒ , i.e. amortised ==== per push. See Amortised analysis.

Exercise 2.2

You will do insertions into a container and then one full iteration in sorted order. Compare total asymptotic cost of map vs unordered_map (assume you'd have to sort the unordered_map's keys separately, at ).

Recall Solution
  • map: each insert total; iteration already sorted, . Grand total ====.
  • unordered_map: inserts average, but to get sorted output you copy the keys and sort them: . Grand total ==== too.

Conclusion: same asymptotic class. The map wins on simplicity (order is free), while unordered_map might win on raw insert speed but then pays the sort. Neither is "always faster."


Level 3 — Analysis

(Diagnose why a design is slow or wrong.)

Exercise 3.1

A student keeps a vector<int> sorted and, for each of incoming numbers, does an insert at the correct sorted position (found by binary search). What is the total cost, and which container fixes it?

Figure — STL containers — vector, list, deque, array, set, multiset, map, multimap, unordered_map, unordered_set
Recall Solution

Binary search to find the slot is — cheap. But inserting into the middle of a vector shifts every element after it right by one: up to copies per insert (see the orange shifted block in the figure). Over inserts: The binary search does not save you — the shift dominates. Fix: a ==std::multiset== (tree-based). Each insert is including placement, total , and it stays sorted with duplicates allowed.

Exercise 3.2

Why can unordered_map degrade to per lookup in the worst case, while map never does?

Recall Solution

An unordered_map scatters keys into buckets via a hash function. If many keys collide into one bucket, that bucket becomes a linked chain of length up to ; a lookup must scan the whole chain — . This happens with a bad hash or an adversarial input set. A map is a red-black tree whose height is guaranteed by the balancing rules, so search is always — there is no worst case to fear. The tradeoff: average is slower than the hash's average when the hash behaves.


Level 4 — Synthesis

(Build a full solution, choosing containers deliberately.)

Exercise 4.1

Given a stream of words, print each distinct word together with its count, in order of decreasing frequency (ties broken alphabetically). Which containers, and what total cost?

Recall Solution

Two phases:

  1. Count with an ==unordered_map<string,int>== — freq[w]++ is average, total .
  2. Rank by moving the pairs into a vector<pair<string,int>> and sorting with a custom comparator: higher count first, alphabetical on ties. Sorting distinct words is .
unordered_map<string,int> freq;
for (auto& w : words) freq[w]++;
vector<pair<string,int>> v(freq.begin(), freq.end());
sort(v.begin(), v.end(), [](auto& a, auto& b){
    if (a.second != b.second) return a.second > b.second; // count desc
    return a.first < b.first;                             // word asc
});
for (auto& [w,c] : v) cout << w << " " << c << "\n";

Why this split? Counting needs raw lookup speed (no order) → hash map. Ranking needs a custom order the hash map can't provide → dump to a vector and sort. Total ====, and since this is . Uses std::sort.

Exercise 4.2

Maintain a running collection of integers supporting: insert(x), erase one copy of x, and query the current median. Which container(s)?

Recall Solution

Use two multiset<int> halves: lo (the smaller half, whose largest is *lo.rbegin()) and hi (the larger half, whose smallest is *hi.begin()). Keep sizes balanced so lo has the same count as hi or exactly one more.

  • Insert: put x into lo or hi by comparing to the current median, then rebalance by moving one boundary element across if sizes differ by 2. Each step is .
  • Erase one copy: s.erase(s.find(x)) — note find gives an iterator so only one copy is removed; erase(x) would wipe all copies (that's the L5 trap below). .
  • Median: if sizes equal, average *lo.rbegin() and *hi.begin(); else *lo.rbegin(). .

Why multiset not set? Duplicates must be storable — the same integer can appear many times. All ops are ====.


Level 5 — Mastery

(Subtle semantics and invalidation traps.)

Exercise 5.1

multiset<int> ms = {5,5,5,7}; Predict the size after (a) ms.erase(5); and (b) ms.erase(ms.find(5));.

Recall Solution

Starting size = 4. (a) ms.erase(5) passes a value ⇒ removes every 5. Three fives gone. Size = ==== (just the 7). (b) ms.erase(ms.find(5)) passes an iterator to one 5 ⇒ removes exactly one element. Size = ==== (two 5s and a 7).

The rule: value-erase deletes all matches; iterator-erase deletes exactly one.

Exercise 5.2

vector<int> v = {1,2,3};
v.reserve(3);                 // capacity now 3, size 3
int* p = &v[0];
v.push_back(4);               // triggers reallocation
cout << *p;                   // ??

Is reading *p safe? Explain.

Recall Solution

No — it is undefined behaviour. The vector was full (capacity 3, size 3), so push_back(4) allocates a new, larger block, copies 1,2,3,4 into it, and frees the old block. p still points at the freed old memory — it is a dangling pointer. Reading *p may print 1, garbage, or crash. Fix: re-fetch after any insertion that may reallocate, or reserve enough capacity up front so no reallocation occurs while you hold p. See Iterators in C++ — the same invalidation applies to iterators and references.

Exercise 5.3

You need average lookup by key and the ability to iterate keys in sorted order, on the same live data. Can a single standard container do both? What's the correct design?

Recall Solution

No single standard container gives both. unordered_map gives lookup but has no order; map gives sorted iteration but lookup. You must pick one primary structure:

  • If lookups vastly outnumber ordered scans → keep an unordered_map, and when you occasionally need sorted output, copy keys to a vector and sort (, done rarely).
  • If ordered iteration is frequent → use a map and accept lookups.

There is no free lunch: the parent note's ONE idea — every container is a chosen tradeoff — is exactly what forbids a container that is simultaneously unordered-fast and order-preserving.


Recall Self-test checklist

I can name the right container for a given access pattern ::: L1 done I can amortise doubling and get copies for pushes ::: L2 done I can explain why sorted-vector insert is and multiset fixes it ::: L3 done I can design a two-multiset median tracker at ::: L4 done I know value-erase vs iterator-erase and pointer invalidation on reallocation ::: L5 done