13.1. ADT Basics#
This section introduces the central design idea of the chapter: separate what a collection does from how it is implemented.
13.1.1. Abstract Types and Implementations#
An abstract data type defines behavior. It says what operations are supported and what each operation means. A concrete implementation says how values are stored and how each operation is performed.
Abstract idea |
Required behavior |
Possible Python implementation |
|---|---|---|
Stack |
Add and remove from the same end |
|
Queue |
Add at the back, remove from the front |
|
Set |
Store unique values and test membership |
|
Map |
Associate keys with values |
|
Graph |
Store nodes and relationships |
|
The important design question is not which container looks familiar. The important question is which operations the problem needs.
names_list = ["alice", "bob", "charlie"]
names_set = {"alice", "bob", "charlie"}
print("bob" in names_list)
print("bob" in names_set)
True
True
The next example implements one behavior, a stack, in two ways. The second version uses collections.deque (short for double-ended queue), a list-like container from the standard library that you import with from collections import deque. A deque supports append() to add an item at the right end and pop() to remove the item at the right end, just like a list. The Stacks and Queues section covers deques in depth.
from collections import deque
# Two implementations of the same LIFO (last-in, first-out) behavior
# list-backed
stack_list = []
stack_list.append("first")
stack_list.append("second")
stack_list.append("third")
print("list pop:", stack_list.pop()) # third
# deque-backed
stack_deque = deque()
stack_deque.append("first")
stack_deque.append("second")
stack_deque.append("third")
print("deque pop:", stack_deque.pop()) # third
# The caller sees identical behavior.
# The choice of implementation affects performance, not correctness.
list pop: third
deque pop: third
### Exercise: One Behavior, Two Implementations
# A task manager needs to support undo. The last task added is the first one undone.
# Implement this LIFO behavior twice: once using a list, once using a deque.
#
# For each: push "draft report", then "send email", then "schedule meeting".
# Pop and print the most recent task from each.
### Your code starts here.
### Your code ends here.
list undo: schedule meeting
deque undo: schedule meeting
13.1.2. Operation Tradeoffs#
A data structure is a tradeoff. One structure may make lookup easy but insertion expensive. Another may make insertion easy but searching slower.
The four operations to reason about are access, search, insert, and delete.
Operation |
|
|
|
|
|---|---|---|---|---|
Access by index |
O(1) |
— |
— |
O(n) |
Look up by key |
— |
— |
O(1) avg |
— |
Search for value |
O(n) |
O(1) avg |
O(n) |
O(n) |
Insert / add |
O(1) amortized (back) |
O(1) avg |
O(1) avg |
O(1) (either end) |
Insert at front |
O(n) |
— |
— |
O(1) |
Delete by key |
— |
— |
O(1) avg |
— |
Delete by value |
O(n) |
O(1) avg |
O(n) |
O(n) |
A dict is fast only when you work through its keys. Finding which key holds a given value means scanning every entry, which is O(n), just like a list. A set stores only keys, so membership tests are O(1) on average.
Use this table to match the dominant operation in your workload to the right structure:
Dominant need |
Best choice |
Why |
|---|---|---|
Ordered history, access by position |
|
O(1) index access |
Fast membership or deduplication |
|
O(1) avg search |
Key-value lookup |
|
O(1) avg lookup |
Add or remove from both ends |
|
O(1) at front and back |
Hierarchical containment |
tree |
O(height) search; O(log n) when balanced |
Many-to-many relationships |
graph |
Depends on traversal |
Before coding, write down the operations your program will perform most often.
import time
n = 100_000
data_list = list(range(n))
data_set = set(data_list)
target = n - 1 # worst case for list: last element
start = time.perf_counter()
_ = target in data_list
list_time = time.perf_counter() - start
start = time.perf_counter()
_ = target in data_set
set_time = time.perf_counter() - start
print(f"list (O(n)): {list_time:.6f} s — scans up to {n:,} items")
print(f"set (O(1) avg): {set_time:.6f} s — jumps via hash")
list (O(n)): 0.000439 s — scans up to 100,000 items
set (O(1) avg): 0.000018 s — jumps via hash
Note
Operation Costs and Big O
You have seen O(1) and O(n) in Chapter 7, where dictionary and set lookups were compared with list searches. Here is a recap, plus O(log n), which appears with trees.
Notation |
Name |
Meaning |
ADT example |
|---|---|---|---|
O(1) |
Constant time |
Same speed regardless of size |
|
O(n) |
Linear time |
Scales with collection size |
|
O(log n) |
Logarithmic time |
Halves the search space each step |
Balanced binary search tree lookup |
A binary search tree lookup starts at the root, goes left if the target is smaller and right if larger, and repeats until it finds the value or runs out of nodes. Each step moves down one level, so the cost is O(h), where h is the height of the tree.
In a balanced tree, each step eliminates about half of the remaining nodes, so h ≈ log₂ n and lookup is O(log n) — the same principle behind binary search on a sorted list.
In an unbalanced tree, such as one built by inserting values that are already sorted, every node has only one child. The tree degenerates into a chain with h = n, and lookup becomes O(n).
Self-balancing trees (such as AVL and red-black trees) restructure themselves on each insert to keep the height close to log n.
### Exercise: Match Workload to Structure
# For each workload description, write the best Python structure.
# Choose from: list, set, dict, deque.
#
# 1. Store chat messages in the order sent; retrieve by position.
# 2. Track which email addresses have already received a newsletter.
# 3. Map each student name to their grade.
# 4. Process print jobs: add new jobs at the back, serve from the front.
workload = {
"ordered messages, access by position": "",
"track seen email addresses": "",
"name to grade mapping": "",
"job queue: add at back, remove from front": "",
}
### Your code starts here.
### Your code ends here.
{'ordered messages, access by position': 'list', 'track seen email addresses': 'set', 'name to grade mapping': 'dict', 'job queue: add at back, remove from front': 'deque'}
13.1.3. Summary#
Abstract data types focus on behavior. Concrete implementations focus on storage and execution. Good data-structure choices start with the operations the program needs most.