C06: Data structures, representation, and amortized cost
On this page
- A. Prerequisites and observable goals
- B. The central question: which operations should this representation make easy?
- C. Abstract behavior is not the same as one implementation
- D. Why a costly resize can coexist with cheap amortized append
- E. Hashing maps keys to candidate locations, not unique identities
- F. Choosing under realistic constraints
- G. Independent practice
- H. Complete solutions: C06
- I. Retention and readiness
A. Prerequisites and observable goals#
C01–C05: state, indexing, basic proof, and counting.
Choose between contiguous and linked representations, trace stacks and queues, explain hash collisions, and distinguish worst-case, expected, and amortized costs.
Level: Degree core.
B. The central question: which operations should this representation make easy?#
A data structure is a representation together with supported operations and invariants. Start with a workload: indexed reads, append, search by key, insertion near a known element, minimum extraction, range queries, or sequential scanning. “Which structure is fastest?” has no answer until the operations, sizes, distributions, memory limits, and latency requirements are specified.
An array stores fixed-size elements contiguously. With base address and element size , element begins at within the valid range. This explains constant-time indexing in the usual machine model. It does not mean every indexed access has the same wall time: cache state, paging, and memory dependencies affect execution. Inserting in the middle of a packed array generally shifts later elements. A dynamic array adds a capacity policy to support growth while retaining contiguity.
A singly linked list stores nodes with a value and a next reference. Finding the th node requires traversal from an available starting node, usually . Inserting after a known node can be constant time, excluding allocation; finding that node is a separate cost. Removing an arbitrary singly linked node generally requires its predecessor. Doubly linked lists store previous links too, increasing bookkeeping and space but supporting more local operations. Their addresses may be scattered, which can make traversal expensive despite simple asymptotic counts.
C. Abstract behavior is not the same as one implementation#
A stack is last-in, first-out: push adds the next item, pop removes the most recently added remaining item. It supports nested work such as call processing, undo histories, and depth-first exploration. A queue is first-in, first-out and supports breadth-first exploration or ordered work arrival. Either abstraction can have several representations. A dynamic array often implements a stack well. A circular buffer or deque avoids repeatedly shifting every element when a queue removes from its front.
For a bounded circular queue with capacity , a logical position can map to physical slot . Read and write indices alone can make empty and full states ambiguous if both are equal; use an explicit count, a reserved slot, or another documented encoding. A wraparound arithmetic trick does not prove thread safety. C10 adds synchronization to a bounded queue, keeping the abstract FIFO contract distinct from the concurrent implementation.
D. Why a costly resize can coexist with cheap amortized append#
Suppose a dynamic array doubles capacity when full. Individual append operations without growth cost constant time in the simplified model; a growth operation copies all existing elements and may cost . Across appends from empty, copied element counts follow . Adding ordinary writes gives less than a constant multiple of total operations. The amortized cost is therefore per append.
This is not an average over a probability distribution. It bounds every allowed sequence of appends under the stated growth policy. It also does not promise low latency for each append: a single resize still causes a burst. A real-time system may preallocate or use another representation when the worst individual pause matters. If an array shrinks immediately whenever it becomes half full and grows at full capacity, alternating insertions and deletions can cause repeated large copies. Hysteresis, such as shrinking at one-quarter occupancy, separates the thresholds and avoids this simple thrashing pattern.
A potential-method proof makes the accounting explicit. One common dynamic-array analysis stores credit in the unused capacity or in a function of size and capacity; cheap operations accumulate credit that pays for a later copy. Credits are a mathematical accounting device, not a claim that the allocator literally saves CPU time for later.
E. Hashing maps keys to candidate locations, not unique identities#
A hash function maps a key to a hash value, which is mapped to a bucket. Equal keys must produce compatible hashes; different keys may collide. A collision is not a correctness failure if the structure compares actual keys and resolves it correctly. Separate chaining stores several key–value pairs in each bucket. Open addressing searches a sequence of slots inside a table. Deletion in open addressing often needs a tombstone or a carefully repaired probe sequence: replacing a removed slot with an ordinary empty marker can cause later searches to stop before a displaced key.
The load factor is for stored items and buckets. Under suitable uniform-hashing assumptions, chaining has expected lookup cost . A malicious or unlucky collection that all hashes to one bucket can require comparisons. Resizing controls load factor, not every possible collision pattern. A custom hash table handling untrusted keys may need additional defenses; the integer-key lab is not a hardened adversarial map.
With capacity four and keys , the simple bucket rule sends all three to bucket zero. Looking up 8 checks the stored key identities in that chain. Deleting 4 must retain 0 and 8; changing a value for an existing key must not increase the size. These are representation invariants that tests should target. A mutable key whose hash-relevant state changes while stored can become unreachable through its new hash; stable key semantics matter.
F. Choosing under realistic constraints#
For a small table scanned every frame, a contiguous vector may beat a hash table despite linear lookup, because the constant factors and locality favor it. For millions of independent key lookups, a suitable hash map may be preferable. For ordered range queries, a balanced search tree or sorted array may be more appropriate. For minimum-priority work, a heap supplies a different contract. C08 develops these alternatives.
Write an operation budget: item count, key size, read/write ratio, maximum burst, memory budget, and required ordering. Then compare correct implementations with the same workload. Distinguish construction time, steady-state lookup, resizing, and destruction. A benchmark that excludes all updates cannot justify a claim about an update-heavy engine system.
G. Independent practice#
F foundation C standard course work T transfer S optional stretch L runnable lab or project. Attempt the complete set before opening its solutions.
A structure needs frequent indexed reads and sequential scans, with occasional append but no middle insertion. Compare a dynamic array with a singly linked list, including indexing, traversal locality, and growth pauses.
For doubling capacities , bound the total number of copied elements across appends. Explain why the result is amortized rather than expected, and why one append may still cost linear time.
Trace inserting keys with values into a four-bucket chained table, updating key 4 to 99, and deleting key 0. Give the final size and chain contents.
An open-addressed table replaces a deleted occupied slot with the same empty marker used for never-occupied slots. Construct a collision sequence showing how a later lookup can incorrectly fail. Propose a repair and state its tradeoff.
Implement IntMap in hash_lab.py using separate chaining, integer keys, size tracking, replacement of existing values, deletion, and doubling when the load threshold is exceeded. Test deliberate collisions, negative keys, resizing, missing-key errors, and generated operations against a Python dictionary. State the hashing assumptions behind any expected-cost claim.
Optional small hints
C06.02: use the finite geometric sum. C06.04: two keys should begin at the same bucket but occupy successive probe positions. C06.05: reinserting during resize must not count the same key twice.
H. Complete solutions: C06#
Compare the first step where your work differs. A corrected answer is not yet an independent success.
C06.01Show solution
Match the dominant operations
A dynamic array offers direct indexing and contiguous scanning, both matching the workload. A linked list requires traversal for an indexed read and typically incurs pointer chasing during scans. Append is amortized constant time for a doubling array but occasionally triggers reallocation and copies or moves. A linked list can append in constant time when it retains a tail reference, excluding allocation, but allocates node overhead and lacks contiguity. Pre-reserving an expected capacity can reduce array growth pauses. The correct choice still depends on actual element size, invalidation constraints, and measurements; the asymptotic model explains the likely tradeoffs rather than guaranteeing a universal winner.
Back to C06.01C06.02Show solution
A deterministic sequence bound
If the largest copied capacity before the final append is , total copies are . There are also writes of newly appended elements, so total simplified work is across the sequence. No randomness was assumed, making this an amortized bound. The append that grows a nearly -element allocation still copies existing elements; amortization does not bound each operation’s latency by a constant.
Back to C06.02C06.03Show solution
Keys, not hashes, determine identity
All three keys enter bucket zero. Updating key 4 changes its value without inserting a fourth item. Deleting key 0 removes only that key–value pair. The final map has size two and bucket zero contains and in the preserved insertion order of this simple implementation. Other buckets are empty. A lookup must compare actual keys so colliding keys remain distinct.
Back to C06.03C06.04Show solution
A deleted slot must not break the search history
Let two keys hash initially to slot zero. Insert at zero and at one by linear probing. Delete and mark zero as never occupied. A lookup for starts at zero, sees the ordinary empty marker, and stops incorrectly before slot one. A tombstone says “continue searching, but this location may be reusable for insertion.” Tombstones require careful insertion logic and eventual cleanup or rehashing. Backward-shift deletion is another option for compatible probing schemes, but it requires proving the remaining probe paths stay valid.
Back to C06.04C06.05Show solution
Reference chained map
"""C06.05: separate-chaining map with integer keys; educational only."""
class IntMap:
def __init__(self, capacity: int = 4):
if type(capacity) is not int or capacity < 1:
raise ValueError("positive integer capacity required")
self._buckets: list[list[tuple[int, object]]] = [
[] for _ in range(capacity)]
self._size = 0
def __len__(self) -> int:
return self._size
def _bucket(self, key: int) -> list[tuple[int, object]]:
if type(key) is not int:
raise TypeError("integer keys only")
return self._buckets[key % len(self._buckets)]
def __setitem__(self, key: int, value: object) -> None:
bucket = self._bucket(key)
for i, (k, _) in enumerate(bucket):
if k == key:
bucket[i] = (key, value)
return
bucket.append((key, value))
self._size += 1
if self._size > 2 * len(self._buckets):
old = self._buckets
self._buckets = [[] for _ in range(2 * len(old))]
self._size = 0
for chain in old:
for k, v in chain:
self[k] = v
def __getitem__(self, key: int) -> object:
for k, value in self._bucket(key):
if k == key:
return value
raise KeyError(key)
def __delitem__(self, key: int) -> None:
bucket = self._bucket(key)
for i, (k, _) in enumerate(bucket):
if k == key:
del bucket[i]
self._size -= 1
return
raise KeyError(key)
if __name__ == "__main__":
m = IntMap()
m[2], m[6] = "a", "b"
del m[2]
print(m[6], len(m))Each key resides in exactly one chain determined by the current capacity. The stored size counts distinct keys, not write operations. Resizing creates new buckets and reassigns every pair using the new modulus; merely extending the old list of buckets would leave keys in the wrong locations. Generated comparisons with a dictionary check updates, deletion, and retrieval sequences against a separate implementation. Worst-case lookup remains linear if many keys collide. With a suitable distribution and bounded load factor, expected chain length is bounded, and occasional resize cost can be amortized over insertions. These are three different claims: worst-case per operation, expected lookup, and amortized growth.
Back to C06.05I. Retention and readiness#
Retrieve the pigeonhole principle and the difference between an abstract contract and a representation. Advance when you can choose a structure from an operation budget, draw a collision or invalidation failure, and explain why amortized constant time does not promise constant latency.