Concepts / Choosing Between Sequential and Associative Containers

Choosing Between Sequential and Associative Containers

A useful way to think about any container is that its behavior follows from the data structure underneath it.

  • Programming

Why the Container Choice Matters

When a program has to keep track of many elements, the container you choose decides three things: how quickly you can look an element up, what order you see the elements in when you loop over them, and how the data is laid out internally. These are not small details. Two containers can hold exactly the same data and still behave very differently in your program.

This article focuses on associative containers, the family that includes map and unordered_map, and on the practical guidelines that help you pick one. The guidelines are rules you can apply directly, but the goal is to understand the reasoning behind each rule, so that you can apply it to a requirement the guidelines never mention explicitly. We will trace what happens inside each container when an element is inserted or looked up, then use those traces to make decisions.

A useful way to think about any container is that its behavior follows from the data structure underneath it. Once you know the underlying structure, most of the container's properties follow naturally.

Tracing an Insertion into a Map's Tree

A map is usually implemented as a red-black tree. A tree is made of nodes, and each node holds a key and a value. The arrangement of the nodes is what matters: keys are placed so that moving through the tree in a fixed direction visits the keys in sorted order. This is why a map is the choice when elements must be iterated over in order.

Follow one insertion step by step. Suppose the tree already holds the keys 40, 20, 60, 10 and 30, arranged as shown in the diagram without the key 25. To place 25, you start at the root, 40. Since 25 is less than 40, you move to the left child, 20. Since 25 is greater than 20, you move to the right child, 30. Since 25 is less than 30, the new key lands as the left child of 30. The key is placed by comparing it against nodes on the path down from the root, and the position it lands in is determined by those comparisons.

The tree's shape is what makes ordered iteration possible. Because every key is placed according to its comparison with the keys above it, an in-order walk through the tree (left subtree, then the node, then the right subtree) returns the keys from smallest to largest. You never sort anything during iteration; the sorted order is already present in the arrangement of the nodes.

cpp
Output
10 Ann
20 Bo
30 Cy
Notes on the code
  • The keys are inserted in the order 30, 10, 20.
  • The loop walks the map in its stored order, not the insertion order.
  • The output is sorted because the map keeps its keys arranged in order.

What do you think happens?

Keys 30, 10, and 20 are inserted into a map in that order. In what order does a range-based loop print them?

  • Insertion order: 30, 10, 20

  • Sorted order: 10, 20, 30

  • The order is unpredictable

Reveal answer
Answer:

Sorted order: 10, 20, 30

A map is usually a red-black tree, and iterating over it in order yields the keys sorted. The order in which the keys were inserted does not determine the iteration order.

Tracing a Lookup in an unordered_map

An unordered_map is usually implemented as a hash table, and it is the choice for fast lookup on large amounts of data. Instead of arranging keys relative to one another, a hash table uses a hash of the key to decide where to look. The steps below trace a lookup for the key apple, as shown in the diagram.

Step one: the key apple is passed to a hash function, which produces a number from the key. Step two: that number is mapped to a bucket index, here 3. Step three: the search happens inside bucket 3, where apple is compared with the keys stored there. The other buckets are never examined for this lookup. This is why the hash table structure supports fast lookup: the work is concentrated in one small place rather than spread across the whole collection.

The consequence for ordering is direct. Keys are placed according to their hashes, not according to any relationship between them, so a hash table does not offer the sorted walk that a tree-based map offers. If your program needs to visit elements in order, the hash table arrangement is the wrong tool, however fast its lookups are.

Ordered Versus Unordered Side by Side

The two containers pair their usual implementations with different strengths. Neither one claims the other's strength, so the choice depends on which property your program needs most. The table below puts the source's points about each container next to each other.

Propertymapunordered_map
Usual implementationRed-black treeHash table
Typical reason to choose itElements must be iterated over in orderFast lookup on large amounts of data
Iteration orderSorted, as the tree arrangement providesNot ordered by key; the hash arrangement does not provide sorted order
Key requirement from the guidelinesKeys need a natural order (a less-than comparison)Works for element types that have no natural order

Ordered and unordered associative containers, as described by the source guidelines

Choosing a Container from Requirements

The source gives three guidelines for this decision. Use unordered containers when you need fast lookup for large amounts of data. Use ordered associative containers such as map and set when you need to iterate over their elements in order. Use unordered containers for element types that have no natural order, meaning types for which no reasonable less-than comparison exists.

Read the flowchart from the top. The ordering question comes first because it is the one hard constraint the source names: if the output must come out in order, an unordered container does not satisfy it, no matter how fast its lookups are. If ordering is not required, the next question is whether the element type has a natural order. If it does not, unordered_map is the choice. If it does, the question becomes whether you need fast lookup on large data, which also points to unordered_map. When neither condition applies, the source offers no default, and you must decide from your other requirements.

Parentheses Versus Braces at Creation

Container creation uses two initializer forms, and the source assigns each one a specific job. The parentheses form is used for sizes. The braces form is used for lists of elements. The same pair of symbols can therefore mean very different things depending on which form you use, and mixing them up is one of the most common sources of surprise with containers.

cpp
Output
3 1 3
Notes on the code
  • sized(3) uses the parentheses form, which gives a size: three elements.
  • listed{3} uses the braces form, which gives a list: one element whose value is 3.
  • Both declarations use the same number, but they create different containers.

What do you think happens?

After the declaration std::vector<int> listed{3};, how many elements does listed hold?

  • Three elements, each set to 0

  • One element holding 3

  • Three elements, each set to 3

Reveal answer
Answer:

One element holding 3

The braces form is for lists of elements, so {3} is a list containing the single value 3. The parentheses form, sized(3), is the one that would create three elements.

A practical habit follows from this split. When you want a container with a given number of elements, write the count in parentheses. When you want a container that starts with specific values, write those values inside braces. Reading the symbols as a statement of intent, size or list, makes the meaning of each declaration clear before you run the program.

Passing Containers and Returning Them

Once a container exists, it often moves between functions. The source gives a direct guideline for this: pass a container by reference, and return a container by value. Passing by reference means the function receives the caller's container itself rather than a separate parameter, so the function works on the same container the caller holds. Returning by value means the function hands the container back as its result, and the caller receives it as the function's output.

cpp
Output
2
Notes on the code
  • make_scores returns its map by value, and roster receives that result.
  • add_score takes the map by reference, so it modifies roster itself rather than a copy.
  • After add_score runs, roster holds two entries, Ann and Bo.

Read the two directions separately. Inputs travel into a function by reference, so the function changes the caller's container. Results travel back out by value, so the caller ends up holding the container the function produced. In the example, add_score adds Bo to the same roster that main owns, and make_scores hands its new map back to main as its result.

The source also gives a general preference for data structures: prefer compact and contiguous ones. It warns that a list is relatively expensive to traverse. These two points tell you what to avoid when a container will be walked through repeatedly. A structure whose elements are scattered and costly to step through is a weaker default than one whose elements sit close together. These preferences sit alongside the ordered and unordered rules rather than replacing them. When you choose a structure, ask whether it is compact and contiguous, and whether you will traverse it often enough for traversal cost to matter.

Mistakes Beginners Make

  • Common mistake

    Assuming an unordered_map will print its keys in sorted order

    What it looks like

    Inserting 30, 10, 20 into an unordered_map and expecting the loop to print 10, 20, 30.

    Why it fails

    An unordered_map is usually a hash table, and its arrangement follows hashes rather than any relationship between keys, so it does not provide the sorted walk a map provides.

    Do this instead

    When the output must be in order, choose map or set. The ordering requirement comes first in the decision flowchart.

  • Common mistake

    Choosing map for an element type that has no natural order

    What it looks like

    Using a map keyed by a custom type that defines no less-than comparison.

    Why it fails

    Ordered associative containers rely on a natural ordering of their keys, and the source says unordered containers are the guideline for element types with no natural order.

    Do this instead

    Use unordered_map for such types, provided the element type can be used as a key in the unordered container.

  • Common mistake

    Writing parentheses when a list of values was intended

    What it looks like

    std::vector<int> v(10); expecting a vector holding the single value 10.

    Why it fails

    Parentheses give a size. The parentheses form creates ten elements, not a list containing one element whose value is 10.

    Do this instead

    Use braces, as in std::vector<int> v{10}, for a list of elements, and parentheses only when you want that number of elements.

  • Common mistake

    Passing a container by value into a function that should modify it

    What it looks like

    A function declared as taking a map by value, then adding an entry to its parameter and expecting the caller's map to change.

    Why it fails

    Passing by value gives the function its own separate container, so changes made inside the function do not reach the caller's container. The source guideline is to pass containers by reference.

    Do this instead

    Take the container by reference, as in std::map<std::string, int>& scores, when the function must work on the caller's container.

Applying the Choice to Two Requirements

EASY

A program stores player records keyed by player name. Requirement A: it must print every name in alphabetical order when a report is generated. Requirement B: a separate lookup table stores a very large number of records keyed by a numeric ID, and it must answer lookups quickly with no ordered output needed. For each requirement, choose map or unordered_map, and name the guideline from the source that justifies your choice.

Hints
  • Start with the ordering question. Does Requirement A need sorted output?
  • For Requirement B, identify the two properties the source ties to unordered_map: fast lookup on large data.
  • Check the flowchart order: sorted iteration first, then the element-type question, then fast lookup.

Key Takeaways

  • A map is usually implemented as a red-black tree, so an in-order walk visits its keys in sorted order; choose it when elements must be iterated over in order.
  • An unordered_map is usually a hash table, which uses a hash of the key to find its bucket; choose it for fast lookup on large amounts of data or for element types with no natural order.
  • Check the ordering requirement first, because an unordered container cannot provide sorted output; the source gives no default beyond its three guidelines.
  • Use parentheses for sizes and braces for lists of elements, since the same symbols mean different things in each form.
  • Pass containers by reference and return them by value, and prefer compact, contiguous structures because lists are relatively expensive to traverse.

Related concepts

More concepts starting with C