Algorithmics study and revision notes
2026-08-22
A sequence has a first item, a last item, and a position for every item between them. Two representations dominate: contiguous arrays make positions cheap to compute, while linked nodes make local restructuring cheap. Neither is universally superior; the workload determines which costs matter.
Linear structures also separate an interface from its implementation. Arrays or links can realise a stack, queue, or deque if they preserve the same observable behaviour.
After studying this note, you should be able to:
You should understand arrays, records, references or pointers, loops, modular arithmetic, and / notation. The preceding note introduced geometric sums and amortised analysis.
A sequence ADT stores an ordered finite collection. A possible interface includes:
size();get(i) and set(i,x) for
;insert(i,x) for
;
andremove(i) for
.These operations specify observable behaviour, not storage. An array list and a linked list can implement the same interface with different costs.
Every representation needs an invariant. An array list with logical size and capacity maintains
with live elements exactly in positions 0 through
n-1. A linked list maintains a chain reachable from
head, ending at tail; its recorded size must
equal the number of reachable nodes. Operations must preserve these
facts even for empty and one-element structures.
An array stores equal-sized elements contiguously. If its base address is , each element occupies bytes, and indexing starts at zero, then
One multiplication and addition locate any valid position, giving indexed access in the word-RAM model. Bounds checking, if present, is also constant time.
Contiguity gives strong spatial locality: a sequential scan tends to
reuse cache lines and needs little metadata. Structural changes are less
convenient. Inserting at position
shifts the suffix A[i..n) one position right, so it moves
elements. Deletion similarly closes a gap.
Appending to a fixed-capacity array is only while an unused slot exists. Deleting the last element is , but an array that is already full cannot grow in place merely because its last position is known.
A singly linked node contains a value and a next
reference. A doubly linked node also contains prev. Nodes
may be scattered through memory, so the
th
node is found by following
links from the head:
time.
Given a reference p to a singly linked node, insertion
after it is local:
insert_after(p, x):
q = new_node(x)
q.next = p.next
p.next = q
if tail == p:
tail = q
size = size + 1
The assignment order matters. Saving p.next in
q.next before changing p.next preserves the
old suffix. Reversing these steps can lose every node after
p.
Deletion in a singly linked list normally needs the predecessor:
delete_after(p):
require p.next != null
q = p.next
p.next = q.next
if tail == q:
tail = p
release(q)
size = size - 1
Consequently, deleting the tail of a singly linked list with only
head and tail references is
:
its predecessor must be found. It is
only when that predecessor is already supplied.
A doubly linked list can unlink a known node q in
by joining q.prev to q.next and updating both
reverse links. It pays for this convenience with another reference per
node and more updates. Sentinel nodes can replace many null boundary
cases with uniform links, but the sentinel itself must never be returned
as data.
Memory management is part of the representation. Manual-memory implementations must not dereference a released node or leak an unlinked one. Garbage collection prevents explicit deallocation errors but does not restore nodes made unreachable by incorrect pointer updates.
An XOR list combines previous and next addresses in one word. It illustrates that representation can trade metadata for complicated navigation. It interacts poorly with garbage collection, memory safety, debugging, and modern portability, so it is not a default replacement for a doubly linked list.
A stack is last-in, first-out. push(x)
adds an item, top() observes the newest item, and
pop() removes and returns it. An array uses its logical end
as the top; a singly linked implementation uses its head. Both give
constant-time operations apart from occasional dynamic-array
resizing.
A queue is first-in, first-out.
enqueue(x) adds at the back, front() observes
the oldest item, and dequeue() removes it. A linked queue
stores both head and tail. An array queue should be circular; shifting
every remaining element after each dequeue would make removal
linear.
A deque supports insertion and removal at both ends.
It can implement either a stack or queue and is useful in sliding-window
and bidirectional search algorithms. “Deque” names the structure;
dequeue commonly names the removal operation of a
queue.
All interfaces need explicit underflow behaviour. They may reject
pop on an empty stack, return an optional value, or raise
an exception, but the contract must choose one.
Let a circular queue have an array of positive capacity
,
a head index, and a logical size. Logical
element
is stored at
Tracking size makes the empty state size == 0 and the
full state size == c; no slot needs to remain unused.
enqueue(x):
require size < c
A[(head + size) mod c] = x
size = size + 1
dequeue():
require size > 0
x = A[head]
head = (head + 1) mod c
size = size - 1
return x
The invariant is that the size logical elements occupy
the cyclic positions beginning at head, in queue order, and
.
Enqueue writes exactly the next free cyclic position; dequeue advances
over exactly the oldest position. Both preserve the invariant in
time.
Start with capacity five, head = 0, and
size = 0. Enqueue 3,6,7, then dequeue once,
then enqueue 5,2,9.
| Operation | Physical array | head |
size |
Logical queue |
|---|---|---|---|---|
| enqueue 3 | [3,_,_,_,_] |
0 | 1 | [3] |
| enqueue 6 | [3,6,_,_,_] |
0 | 2 | [3,6] |
| enqueue 7 | [3,6,7,_,_] |
0 | 3 | [3,6,7] |
| dequeue | [3,6,7,_,_] |
1 | 2 | [6,7] |
| enqueue 5 | [3,6,7,5,_] |
1 | 3 | [6,7,5] |
| enqueue 2 | [3,6,7,5,2] |
1 | 4 | [6,7,5,2] |
| enqueue 9 | [9,6,7,5,2] |
1 | 5 | [6,7,5,2,9] |
The physical order is not the logical order; head,
size, and modular indexing provide the interpretation.
The trace deliberately leaves the removed value 3 in its
old physical slot until wraparound overwrites it. In a managed-memory
implementation, clearing a removed slot may be necessary so that the
queue does not retain an otherwise unreachable object; this changes
neither the logical queue nor the asymptotic bound.
A dynamic array keeps the array invariant while changing capacity. On append, double a full capacity, copy the live elements, then write the new item:
append(x):
if size == capacity:
resize(max(1, 2 * capacity))
A[size] = x
size = size + 1
One resize can copy elements. Across appends from an empty array, however, copies occur at capacities . Their total is less than , and the ordinary writes add another . Thus the total cost is and append costs amortised, while remaining in the worst case for one operation.
An accounting view reaches the same result: charge each append a constant number of credits; one pays for its own write and the rest accumulate to pay for the next copy.
Shrinking immediately when the size falls below one-half can cause thrashing: alternating one insertion and one deletion near the threshold repeatedly reallocates. A common policy halves capacity only when size falls to at most one-quarter of capacity. Immediately after shrinking, the array is at most half full, so many updates are required before the next resize. Keep a small minimum capacity to avoid a zero-sized representation.
The table separates locating a position from updating an already known location.
| Structure and available references | Index | Append | Delete tail | Insert/delete at known local position |
|---|---|---|---|---|
| Fixed array with spare slot | shifts | |||
| Dynamic array | amortised ; worst | amortised ; worst if it shrinks | shifts | |
| Singly linked, head and tail | after known predecessor | |||
| Doubly linked, head and tail | for known node |
Searching unsorted values is in all four structures. Linked insertion “at index ” is not constant time unless the relevant node is already known; locating it costs . Arrays usually have better locality and smaller metadata. Links support stable node references and local splicing but incur allocation, pointer, and cache costs.
size to exceed capacity or
failing to define empty-operation behaviour.q.next = p.next precede
p.next = q during singly linked insertion?head and tail
require an additional convention to distinguish full and empty?listobject.c shows the over-allocation and shifting
machinery behind Python lists. These capacity details belong to CPython
and are not a promise of the Python language.collections.deque supplies thread-safe appends and pops
at either end and optional bounded capacity. Indexing slows toward the
middle, so it is not a drop-in replacement for array indexing.Vec, the growable ring
buffer VecDeque, and LinkedList, including
operation costs. The documentation notes that contiguous structures
usually beat linked lists unless their specific operations are
needed.ArrayDeque implements a resizable deque suitable for
stacks and queues. It is not thread-safe and rejects null,
so these semantics must be included in a client contract.deque. Generate legal random
enqueue/dequeue sequences while asserting contents, size, and the
physical-position invariant after every operation; test the chosen empty
and full error contracts separately.