Choosing a Structure
Data Structures in Practice ๐
Chapter 1 ยท Choosing a Structure
Two programs can give exactly the same answers and still differ by a factor of ten thousand in speed. The usual cause is not clever code but the data structure underneath: the way the values are arranged in memory and the operations that arrangement makes cheap or expensive. This course is practical. Each chapter takes one family of structures, shows what it costs, and gives you a rule for when to reach for it. All examples are Python 3.
Cost Is a Property of the Operation
A structure is never simply "fast" or "slow". It is fast at some operations and slow at others. The honest question is always: what will my code do most often? Append at the end? Look up by key? Check "is this already here?" Remove from the front? The answer picks the structure.
| Operation | list | set | dict | deque |
|---|---|---|---|---|
| x in c | scans every item | one hash lookup | one hash lookup (keys) | scans every item |
| add at the end | cheap (amortised) | cheap | cheap | cheap |
| add at the front | moves every item | no front | no front | cheap |
| item by position | direct | no positions | no positions | slow in the middle |
| keeps order | yes | no | yes (insertion order) | yes |
Counting Work: Linear Versus Binary Search
Instead of timing, count. The function below walks a list from the start and counts how many times it compares an item with the target. That count is the work, and it does not depend on the machine.
If the list is sorted, you can do far better. Look at the middle item. If it is too small, the target can only be in the upper half; if too large, in the lower half. Each comparison throws away half of what is left.
Output, for a target that is not present (the worst case):
A thousand times more data costs linear search a thousand times more work, but binary search only a few more steps. That growth pattern is what "Big-O" describes: linear search is O(n), binary search is O(log n). You do not need the maths yet, only the habit of asking how the cost grows when the data does.
The Hash Shortcut: set and dict
A set and a dict use a hash table: the value is turned into a number that points almost straight at where it is stored, so the lookup does not scan. No sorting is required either. The rule of thumb that follows is one of the most useful in everyday Python: if you ask "is x in here?" more than a few times, use a set or dict, not a list.
On the author's machine the gap was roughly 100 times at a thousand items and roughly ten thousand times at a hundred thousand items. Your numbers will differ; what will not differ is that the gap widens as the data grows.
The Front of a Queue: list Versus deque
Removing from the front of a list forces every other item to shuffle along one place, so it costs work proportional to the length. collections.deque (pronounced "deck", a double-ended queue) is built for both ends. For first-in-first-out queues, use it.
Challenges
Run linear_count and binary_count on list(range(n)) for n = 100 and n = 100000, searching for a target that is present at the very end (n - 1). Predict the counts first, then compare them with the results.
Write dedupe(items) that removes duplicates but keeps the first occurrence of each value in its original order. Make it fast for long inputs, and say which two structures you used and why.
A program keeps a queue of waiting jobs held in a list and removes the oldest with pop(0). Rewrite it with deque, then time both for 100,000 jobs. State what you expect before you run it.
Quick Reference
- Count work rather than relying on raw timings
- Linear search: O(n); binary search: O(log n), needs sorted data
- Membership test: use set or dict, not list
- Queue: use deque, not list.pop(0)
- Ask first: which operation will my code do most often?