Skip to content
Blockchain Association of Cameroon
BAC-L0-FOUND-1023 h

Data structures and the cost of operations

Arrays, hash tables, trees: what each structure makes fast and what it makes expensive. The basis of every later argument about chain storage.

  • Compare the cost of lookup, insertion and traversal operations
  • Choose a structure from the expected access profile
  • Measure a real cost instead of estimating it

A program spends its life searching, adding and walking through data, and how that data is laid out decides how long it takes: finding one customer among a million can need a million comparisons or about twenty. This workshop teaches you to count what each operation costs on an array, a hash table and a tree, to choose the structure that fits the real access pattern, and above all to check your predictions with a measurement instead of trusting them blindly. It is the base for later reasoning: a blockchain is first of all a system that must find, add and verify huge amounts of data at a sensible cost.

Lessons 1/4 · 25 min

Counting steps: scanning and binary search

A notebook of customers

Awa runs a shop in Bonamoussadi and writes her customers in a notebook: one name and one phone number per line, in the order they came. To find a number she reads line after line. With 50 customers that is bearable. With 10,000 she will read 5,000 lines on average and, at worst, all 10,000. Had she kept the notebook in alphabetical order, she would open it in the middle, see whether the name sought comes before or after, and keep only the right half — the way you look up a word in a dictionary. Same notebook, same content: how it is laid out changes everything.

The vocabulary of cost

We measure an operation's cost in elementary steps (a comparison, a read) as a function of the number n of elements. Three operations come up everywhere:

  • lookup: finding a given element;
  • insertion: adding an element;
  • traversal: visiting every element, for instance to display them all. It costs at least n steps whatever the structure: you cannot visit n elements in fewer than n visits.

We write the order of growth with "big O" notation: O(n) means "cost grows in proportion to n", O(log n) "cost grows like the number of times you can halve n", O(1) "cost does not depend on n". The notation deliberately ignores constants ("3n" and "n" are both O(n)); we come back to this in the last lesson.

The linear scan

Go through in order, until you find it or run out of list. At worst (the element is absent or last) it takes n comparisons.

Binary search

It requires a sorted array. Compare the target with the middle element; depending on the result keep the left or right half, and repeat.

def binary_search(items, target):
    low, high = 0, len(items) - 1
    while low <= high:
        mid = (low + high) // 2
        if items[mid] == target:
            return mid
        if items[mid] < target:
            low = mid + 1
        else:
            high = mid - 1
    return None

Note: Python has no three-way comparison, and this code makes up to two per step (== then <). In this course we count one three-way comparison per step (like a sorting comparator in C, which returns a negative, zero or positive number): that is the convention of the table below.

Worked example. Sorted array [3, 8, 14, 21, 29, 35, 42], target 35. First middle: index 3, value 21; 21 < 35, keep the right (indices 4 to 6). New middle: index 5, value 35: found in 2 steps, instead of 6 for a scan.

Why so few? At each step at most half of the candidates remain: after k steps, at most n ÷ 2^k. We are done when one is left, hence after about log₂ n steps. In this course's counting convention we count one three-way comparison (equal, smaller, larger) per step, so at worst ⌊log₂ n⌋ + 1 steps:

nScan (worst case)Binary search (worst case)
1001007
1,0001,00010
5,0005,00013
100,000100,00017
1,000,0001,000,00020

Multiplying n by 1,000 multiplies the scan by 1,000, but adds only about ten steps to binary search. That is what "scaling" means.

What this does not guarantee

  • Binary search only works on sorted data. Sorting itself costs about n × log₂ n comparisons. For a single lookup in 50,000 unsorted items, sorting first costs far more than one scan: it only pays off if you search often.
  • Keeping an array sorted has a price at every insertion (lesson 2).
  • The "worst case" is not the average case: a scan finds the element around n ÷ 2 on average when it is present. And hidden constants matter for small n: with ten elements the simplest approach is often the fastest.

Recap

Count steps as a function of n: scan = n, binary search ≈ log₂ n (on sorted data). A full traversal always costs at least n. The "worst case" gives a guarantee, not a prediction.

Lessons 2/4 · 30 min

Arrays and hash tables

Finding a customer without reading the notebook

In the previous lesson, even with binary search, Awa still has to sort her notebook and keep it sorted. What if she could go straight to the right page, like a numbered locker where each customer has a drawer? That is the idea of the hash table. To understand it, start from the simpler structure it derives from: the array.

The array (Python list)

An array stores its elements side by side in memory. Element number i is found by a simple address calculation: access by index in O(1). On the other hand:

Operation on an array of n elementsCost
Read element number iO(1)
Search for a value (unsorted array)O(n)
Search for a value (sorted array, binary search)O(log n)
Append at the endO(1) on average (sometimes more, when the array must grow)
Insert at the start or in the middleO(n): all following elements must shift
Keep the array sorted while insertingO(n) per insertion

In Python, x in my_list is a scan. my_list.insert(0, x) shifts everything. For a queue where you remove on the left, the collections.deque structure is made for it.

The hash table (dict, set)

A hash table has a number of buckets. A hash function turns a key (a name, a number) into a number; take that number modulo the number of buckets to get the bucket where the key is stored. Finding the key means redoing the same calculation and going straight to the bucket — without walking through the others.

Example with a TOY function. A table of 8 buckets; the "function" adds up the letters' codes, then takes the remainder of division by 8. (Real functions are much better; this one is to see the mechanics.)

KeySum of codesRemainder mod 8 = bucket
Awa65 + 119 + 97 = 2811
Ngu78 + 103 + 117 = 2982
Mba77 + 98 + 97 = 2720
Aaw65 + 97 + 119 = 2811

"Aaw" lands in the same bucket as "Awa": a collision. Two different keys, one bucket. The classic fix, chaining, keeps a small list in each bucket: new keys are added at the end, and to search you compare, in order, the keys of the bucket. Searching "Aaw": compute bucket 1, compare with "Awa" (no), then "Aaw" (yes): 2 comparisons, instead of reading the whole notebook.

As long as the function spreads the keys well and the table is not too full, each bucket holds very few keys: lookup, insertion and deletion cost O(1) on average. When the table gets too full it is enlarged and all keys are placed again: one exceptional insertion costs O(n), but spread over all insertions it stays O(1) "amortised".

In Python: set(my_list) builds a table; "Mba" in my_set hashes "Mba" and looks at one bucket. On a million elements, the difference with in on a list is a million comparisons against a handful.

What this does not guarantee

  • The worst case is O(n). If all keys land in the same bucket, the table becomes a plain list. That is rare with a good hash function… unless someone causes it on purpose: see the security exercise.
  • No sorted order. A hash table cannot answer quickly "the customers between D and K" or "the smallest". (A Python dict remembers insertion order since Python 3.7, but that is not key order.)
  • It costs memory: empty buckets are kept in reserve.
  • Chaining is our teaching model, not Python's internal detail. CPython's dict and set use open addressing (on a collision they look for another free slot) rather than a list per bucket; the cost story (O(1) on average, O(n) worst case) is the same, but the picture of "a small list in each bucket" is not what Python does inside.
  • Keys must be immutable: if a key changes afterwards, its bucket is no longer the right one.
  • This hash function has nothing cryptographic about it: it only tries to spread. The functions that protect data are the subject of workshop 104.

Recap

Array: access by index in O(1), search in O(n) (or O(log n) if sorted, but insertion in O(n)). Hash table: lookup and insertion in O(1) on average, but no order, with an O(n) worst case when keys pile up.

Lessons 3/4 · 30 min

Search trees and order

A directory that accepts new customers

The sorted array searches fast but inserts badly; the hash table searches and inserts fast but ignores order. Is there a structure that does both — search in O(log n), insert in O(log n), and stay sorted? Yes: the binary search tree (BST).

The principle

Each node holds a key and up to two children: smaller keys to the left, larger keys to the right. To search, start at the root and, at each node, compare: go left or right, never both.

Insert 50, 30, 70, 20, 40, 60, 80 in that order into an empty tree:

50
  left : 30
    left : 20
    right: 40
  right: 70
    left : 60
    right: 80

Search for 60: compare with 50 (60 is larger, go right), with 70 (smaller, go left), with 60 (found): 3 comparisons, for 7 keys. A tree of height h (number of levels) is walked from the root to a leaf in at most h comparisons. If the tree is well balanced, h ≈ log₂ n: for a million keys, about 20 levels.

In-order traversal

The big advantage over the hash table: visiting the tree "left, node, right" (an in-order traversal) yields the keys sorted: 20, 30, 40, 50, 60, 70, 80. The cost is O(n), the minimum possible. And a range query ("all keys between 35 and 65") first descends towards 35 in O(log n), then walks in order up to 65: it visits only what is needed. A hash table would have to examine all n keys.

The trap: the unbalanced tree

Now insert 10, 20, 30, 40, 50 — keys already sorted — into an empty tree. Each new key is larger than all the previous ones, so it always goes right:

10
  right: 20
    right: 30
      right: 40
        right: 50

The tree has become a linked list: finding 50 takes 5 comparisons, and with n sorted keys, n comparisons. The O(log n) guarantee depends on balance. Self-balancing trees (red-black trees, AVL trees…) reorganise on insertion to keep the height close to log₂ n. Databases use variants with many children per node, B-trees: SQLite stores its tables and indexes in B-trees. You will meet them again in workshop 103.

Comparing the four structures

StructureSearchInsertSorted walkRange
Unsorted arrayO(n)O(1) at the endsort first: ≈ n log nO(n)
Sorted arrayO(log n)O(n)O(n)O(log n + k)
Hash tableO(1) average, O(n) worstO(1) averagesort firstO(n)
Balanced treeO(log n)O(log n)O(n)O(log n + k)

(k = number of results returned.)

What this does not guarantee

  • A plain BST does not balance itself: everything depends on the order in which keys arrive.
  • The tree is slower than the hash table for a pure exact lookup (log n against ≈ 1), more work to write, and its nodes scattered in memory suit the processor less well than a contiguous array.
  • The "O"s do not predict which will be fastest on 100 elements. Only a measurement says so (lesson 4).

Recap

The balanced tree offers search and insertion in O(log n) and a sorted walk: that is what you buy by giving up the table's O(1). Without balancing, the arrival order can degrade it into a list.

Lessons 4/4 · 30 min

Choosing by access profile, then measuring

Which structure for which job?

There is no "best" structure, only trade-offs. To choose, start by describing the access profile: what the program really does, and in what proportions.

  1. How big? How many elements today, and in a year?
  2. Which operations dominate? Exact lookup, insertion, deletion, full traversal, range query, "the smallest"?
  3. Does order matter? Do we need the data sorted?
  4. How often? Once a day or a thousand times a second?

A few cases:

ProfileReasonable choiceWhy
Check whether a number belongs to a list of 5 million blocked numbers, thousands of times a second, never listing themhash table (set)exact lookup in O(1) on average; order is useless
Find all transactions between two dates, among a million, oftensorted structure (balanced tree, or sorted array + binary search if additions are rare)range in O(log n + k)
Queue: append at the end, remove at the frontdequeboth ends in O(1)
10 elements, rarely consultedany one, the simplestconstants dominate; the writing complexity costs more than the gain

In a blockchain you will meet these profiles again: finding a balance by address (exact lookup, hash table), walking the blocks in order (ordered list), checking that a piece of data belongs to a set (later workshops).

Measure instead of estimating

O notation says how the cost grows, not how many milliseconds it is worth. Two O(n) programs can differ ten-fold. An O(log n) can lose to an O(n) on small arrays. The only way to know is to measure, with method.

import timeit

for n in (1_000, 2_000, 4_000, 8_000):
    items = list(range(n))
    # unfavourable case: the element sought is the last one
    t = min(timeit.repeat(lambda: (n - 1) in items, number=200, repeat=5)) / 200
    print(n, round(t * 1e6, 1), "µs per call")

Good practice for this measurement:

  • Time only the operation, not the building of the data.
  • Repeat (here 5 series of 200 calls, each series' time being divided by 200 to get the time of one call) and keep the minimum or the median: noise (other programs, caching) can only add time.
  • Vary n and watch how the time evolves, rather than trusting one point.
  • Do not conclude from very short durations: below a few microseconds you mostly measure the cost of the call.

Reading a doubling

Double n and look at the ratio of the times:

Time ratio when n doublesLikely growth
≈ 1 (almost no change)O(1)
slightly above 1 (+ a small constant)O(log n)
≈ 2O(n)
slightly above 2O(n log n)
≈ 4O(n²)

If your prediction ("the hash table should not vary") is not confirmed by the measurement, the measurement is right — then ask why (collisions? resizing? memory exhausted?). If it is confirmed, you have evidence, not an opinion.

What this does not guarantee

A measurement holds for this hardware, this data set and this version of the language. A random data set can hide an unfavourable case (sorted keys in an unbalanced tree); a data set that is too small can hide growth. So say what you measured on, and do not generalise beyond it.

Recap

Describe the access profile before choosing; do not confuse growth (O) with duration; measure by repeating, isolating the operation and varying n; read a doubling to guess the growth; state the limits of your measurement.

Guided lab

The browser does not run Python: this lab has you count comparisons, buckets and levels exactly, and read (fictitious) measurements as you would real ones. You will really write and time code in the independent lab. Each generated task changes on every attempt; the fixed questions check that you can reason, not copy.

A browser is no substitute for a real terminal: this lab trains reading and reasoning; the real hands-on practice is in the independent lab.

A certified account is required

Graded activities record results in your name and are the basis for a credential. They therefore need a complete certified file and a current consent. Filling in the file does not verify your identity; the Secretariat can verify it afterwards ("Verified" badge).

  1. 1.Count worst-case comparisons

    The task below gives a size n. Give the maximum number of comparisons to find an element (or conclude it is absent) with a linear scan, then with binary search on a sorted array, counting one comparison per step as in lesson 1. For binary search, halve n until you reach 1, or use ⌊log₂ n⌋ + 1; check against the lesson's table for known values.

  2. 2.Follow a collision in a chained table

    Take the TOY table from lesson 2: 8 buckets, bucket = (sum of the letters' codes) modulo 8, new keys appended at the end of their bucket's chain. These were inserted in order: Awa, Ngu, Mba, Aaw. Now insert "Kofi" (codes: K = 75, o = 111, f = 102, i = 105). Work out its bucket and say how many key comparisons a later search for "Kofi" will cost (keys in the chain are compared in order until it is found). Write the calculation on paper before answering.

  3. 3.Build a tree and measure a lookup

    Insert, in this exact order, the keys 40, 20, 60, 10, 30, 50, 70 into an empty binary search tree (smaller to the left, larger to the right). Draw it on paper. Then insert, into a NEW empty tree, the keys 10, 20, 30, 40, 50, 60, 70 in that order. Then compare the cost of searching for key 70 in the two trees, and answer the question asked about the second one.

  4. 4.Read a measurement

    An apprentice timed the search for an absent value in an unknown structure and got (FICTITIOUS measurements, median of 5 series): n = 50,000 → 1.8 ms; n = 100,000 → 3.6 ms; n = 200,000 → 7.3 ms; n = 400,000 → 14.5 ms. Apply the "reading a doubling" method from lesson 4 and answer the question asked: what growth do these measurements indicate?

  5. 5.Choose by profile

    Write down, in two lines, the access profile of the service described in the question below (size, dominant operations, whether order is needed, frequency), then pick the structure. Justify it by the row of lesson 3's comparison table that favours it and the column that penalises it.

Security exercise — Deliberate collision: an attacker who chooses the keys

The service that stores parameters in a table

A small web service stores each incoming request's parameters in a chained hash table (the one from lesson 2). To add a parameter, it first walks the whole chain of the target bucket, to see whether the key already exists: an insertion therefore costs as many comparisons as there are keys already in the chain.

To keep it simple, assume the service uses our TOY function from lesson 2 (sum of the letters' codes, modulo 8), and that the attacker knows it. So they also know this property: any word made of the same letters in a different order lands in the same bucket. With the six letters a, b, c, d, e, f there are 720 different orderings, hence 720 different keys that all aim at the same bucket.

Fictitious service log, one normal request then one hostile request:

request 1   parameters: city, district, page               3 keys, 0 extra comparisons
request 2   parameters: abcdef, abcdfe, abcedf, abcefd, ... (200 anagrams)
            processing time: about 60 times the usual (illustrative figure)

Each anagram is a distinct key, and all aim at the same bucket. The i-th key inserted must be compared with the i - 1 keys already in that chain. With 200 keys the attacker obtains work of the order of n squared, where a well-spread table would do the order of n. By sending thousands of such requests, they keep the server's processor busy to the point of saturation: this is an algorithmic complexity attack (or "hash flooding"), with no memory flaw and no stolen password.

To do

Answer the check question: how many key comparisons does inserting the 200 anagrams cause in total? (Use the sum of the integers from 0 to 199.)

Defences (to know, not graded)

  • Use a hash function with a secret random key per process: the attacker can no longer predict collisions. Python randomises the hash of its strings this way at every start (not that of integers, which hash to themselves).
  • Limit what one request can bring: maximum number of parameters, maximum body size.
  • Use a structure with a bounded worst case (a balanced tree per bucket, or a table whose buckets are trees).
  • Rate-limit per client, and watch for abnormal processing times.

Never try this kind of attack against a service you do not own: redo the experiment in your own program, as in the independent lab.

Workshop quiz

Eight questions drawn from a larger pool, 70% to pass. It opens once every lab checkpoint is passed. You can retake it: the draw changes.

A certified account is required

Graded activities record results in your name and are the basis for a credential. They therefore need a complete certified file and a current consent. Filling in the file does not verify your identity; the Secretariat can verify it afterwards ("Verified" badge).

Independent lab (outside the site)

Not graded online: do it on your own machine and publish it to your Git repository.

Independent lab: measure on a data set a hundred times larger

Do this on your machine with Python 3 (no external library).

Part 1: the baseline measurement. Write a script that, for n equal to 1,000, 10,000 and 100,000, builds the integers from 0 to n - 1 in four forms: a list, a sorted list (same content) queried with the bisect module, a set, and a dict. For each structure, measure with timeit.repeat (at least 5 series) the cost of looking up an absent element (the worst case for the list). Keep the minimum of each series. Print an n × structure table.

Part 2: a hundred times larger. Redo the measurement with n = 10,000,000, one structure at a time (delete it with del before building the next); if your machine lacks memory, use n = 1,000,000 for all structures and say so. For each structure, compute the ratio between the time at n and the time at n ÷ 100. Compare with what O notation predicts (≈ 100 for the list, ≈ 1 for the set, slightly above 1 for bisect) and explain the gaps: processor cache, memory, allocation, noise. If a gap surprises you, say which hypothesis you would test next.

Part 3: insertion. Measure 10,000 insertions at the start of a list (insert(0, x)) then in a collections.deque (appendleft). Repeat with 100,000. Explain the ratio observed by the shifting of elements.

Part 4: collision, in your own program. Implement lesson 2's TOY chained table (8 buckets, sum of codes modulo 8). Insert 200 anagrams of abcdef (use itertools.permutations), count the comparisons, find the total from the security exercise, then compare with 200 varied words. Never attack a program that is not your own.

Part 5: the tree. Implement a binary search tree (insertion, lookup, height computation; the iterative version avoids Python's recursion limit). Load n = 100,000 random keys, then the same keys sorted. In both cases note the height and time the lookup of an absent key, then compare with a dict and a set on the same data set. Explain the gap between the two insertion orders in the words of lesson 3.

To hand in. A one-to-two-page report: your table of measurements (parts 1 to 5), the machine used (processor, memory, Python version), your growth ratios, your explanations of the gaps, and a justified structure recommendation for two profiles of your choice from lesson 4. A measurement without the description of the machine and the method is not admissible.

Takeaways

  • Cost is counted in steps as a function of n: a scan costs n, binary search on sorted data about log₂ n (20 for a million).
  • Array: index access in O(1) but middle insertion in O(n). Hash table: lookup and insertion in O(1) on average, no order, O(n) worst case. Balanced tree: O(log n) everywhere and a sorted walk.
  • Choose a structure from the access profile (size, dominant operations, order, frequency), not from habit.
  • An unbalanced tree or a table whose keys pile up falls back to O(n): arrival order or an attacker can cause it.
  • O notation says how cost grows, not how much it is: measure by repeating, isolating the operation and doubling n.

Common misconceptions

  • Misconception : A hash table is always O(1), so it is always the best choice.

    In fact : O(1) holds on average and if keys are well spread. The worst case is O(n), the table gives no sorted order and it uses extra memory.

  • Misconception : An O(log n) algorithm is always faster than an O(n) one.

    In fact : O notation ignores constants and describes growth for large n. On small data, the simplest can win. Only a measurement settles it.

  • Misconception : To search faster, just sort the list first.

    In fact : Sorting costs about n × log₂ n comparisons. For a single lookup, a scan is cheaper; sorting pays off only if you search often and insert rarely.

  • Misconception : A binary search tree always has a height of about log₂ n.

    In fact : Only if it is balanced. Inserting already sorted keys into a plain tree produces a chain of height n, where a lookup costs n.

Glossary

Cost (complexity)
Number of elementary steps an operation needs, expressed as a function of the number n of elements.
Big-O notation
Way of writing the order of growth of cost: O(1), O(log n), O(n), O(n log n), O(n²). It ignores constants.
Binary search
Search in a sorted array that discards half of the candidates at each comparison: about log₂ n steps.
Hash table
Structure that places each key in a bucket computed by a hash function; lookup and insertion in O(1) on average.
Collision
Situation where two different keys give the same bucket. Chaining resolves it by keeping a list per bucket.
Binary search tree
Tree where each node has smaller keys to the left and larger keys to the right; its height decides the cost of a lookup.
Balanced tree
Tree that reorganises itself to keep a height close to log₂ n, guaranteeing O(log n) lookups.
Access profile
Description of what a program really does with its data: size, dominant operations, need for order, frequency.