Algorithmics study and revision notes
2026-08-22
Ordering pays an up-front cost to make later questions structured. A sorted array supports logarithmic search; a sorted stream can be merged in one pass; a partition can isolate one rank without sorting everything. These algorithms also introduce the central divide-and-conquer questions: how are subproblems formed, why does recombination preserve correctness, and how is work distributed across recursive levels?
After studying this note, you should be able to:
lower_bound;You should understand arrays, half-open ranges, loops, recursion, probability as expectation, asymptotic notation, and arithmetic/geometric sums. The note restates the invariants and cost assumptions needed for each algorithm.
A sorting input is a sequence of records with keys. A comparator must behave consistently as a total order: comparisons cannot contradict one another. Sorting rearranges records into nondecreasing key order while preserving exactly the input multiset.
A sort is stable if equal-key records retain their original relative order. It is in-place when it uses only a small amount of auxiliary storage beyond the input; state whether recursion-stack space is included. An adaptive algorithm benefits from existing order.
The comparison model learns about arbitrary keys only through comparisons. Integer-key algorithms may inspect digits or bits and therefore use a stronger model. Claims of expected time must identify whether randomness comes from the input distribution or the algorithm.
For sorted A, define the lower bound of x
as the smallest index
with A[j] >= x, or
if no such index exists.
lower_bound(A, x):
lo = 0
hi = length(A)
while lo < hi:
mid = lo + floor((hi - lo) / 2)
if A[mid] < x:
lo = mid + 1
else:
hi = mid
return lo
The loop invariant is:
lo contains a value less than
x; andhi through n-1
contains a value at least x.The unknown boundary lies in the closed set of candidate positions
from lo through hi. Initially both value
claims are vacuous. If A[mid] < x, positions through
mid cannot be the boundary. Otherwise mid and
everything after it remain on the at-least side. Each update preserves
the invariant and strictly shortens hi-lo. At termination
lo==hi, so the unique remaining position is the lower
bound.
Membership is a separate question: x occurs exactly when
the returned position satisfies lo < n and
A[lo] == x. The interval length at least halves, so time is
and iterative auxiliary space is
.
Let A = [2,4,4,7,9] and x = 4.
lo |
hi |
mid |
Decision |
|---|---|---|---|
| 0 | 5 | 2 | A[2] >= 4, set hi=2 |
| 0 | 2 | 1 | A[1] >= 4, set hi=1 |
| 0 | 1 | 0 | A[0] < 4, set lo=1 |
The answer is position 1, the first of the equal keys. Notice that the maintained object is an answer position, possibly , not an interval containing every occurrence.
Assume all keys are distinct. A deterministic comparison sort induces a binary decision tree: each internal node compares two keys and each leaf identifies one input ordering. Correctness requires at least leaves.
A binary tree of height has at most leaves, so and
At least half the factors in are at least , hence
and therefore . This is a worst-case lower bound for comparison sorting, not for algorithms allowed to inspect restricted integer keys.
Insertion sort maintains a sorted prefix and inserts the next record into it.
insertion_sort(A):
for i = 1 to length(A) - 1:
x = A[i]
j = i
while j > 0 and x.key < A[j-1].key:
A[j] = A[j-1]
j = j - 1
A[j] = x
At the start of iteration i, A[0..i) is a
sorted permutation of the first
original records. Shifting larger records right and placing
x in the gap preserves sortedness and the multiset. At the
final iteration the prefix is the whole array.
Worst-case time is
,
because a reverse-sorted input shifts
records. An already sorted input takes
comparisons. Using < rather than <= in
the shift test makes this version stable. It is in-place and often
effective for small or nearly sorted ranges.
Merge sort recursively sorts two halves and combines them. The merge step repeatedly takes the smaller first unconsumed record:
merge(L, R):
i = 0; j = 0; out = empty sequence
while i < length(L) and j < length(R):
if L[i].key <= R[j].key:
append L[i] to out; i = i + 1
else:
append R[j] to out; j = j + 1
append the unconsumed suffix of L or R to out
return out
The merge invariant says out is sorted, contains exactly
the consumed records, and no unconsumed record is smaller than its last
record. Since L and R are sorted, choosing
their smaller front preserves this invariant. Taking from the left on
equality preserves stability.
Correctness of merge sort follows by induction on input length. Arrays of length at most one are sorted. The induction hypothesis sorts both smaller halves, and the merge argument combines them correctly. Its recurrence is
Standard array merging uses auxiliary storage. Linked sequences can be merged by relinking nodes, but recursive stack and representation costs still need counting.
Quicksort partitions a range around a pivot, then recursively sorts the unequal sides. A three-way partition handles duplicate keys explicitly:
partition3(A, lo, hi, pivot): // range [lo, hi)
lt = lo; i = lo; gt = hi
while i < gt:
if A[i].key < pivot:
swap A[lt], A[i]; lt = lt + 1; i = i + 1
else if A[i].key > pivot:
gt = gt - 1; swap A[i], A[gt]
else:
i = i + 1
return (lt, gt)
The invariant partitions the range into < pivot,
== pivot, unknown, and > pivot regions:
[lo,lt), [lt,i), [i,gt), and
[gt,hi). Each branch moves one unknown record to a known
region. At termination the unknown region is empty. Quicksort recurses
only on [lo,lt) and [gt,hi).
A pivot chosen uniformly from the current range gives expected time for every fixed input and expected when the keys are distinct. Three-way partitioning can be faster when many keys are equal; an all-equal range is finished in time. Repeated extreme pivots still give a worst case.
Time and recursion depth need separate safeguards. Ordinary recursive quicksort has expected stack depth and worst-case depth. Recursing on the smaller side and iterating over the larger side limits stack depth to even in the worst case. Typical in-place quicksort is not stable.
Start with a recurrence tree: label work per node, multiply by nodes per level, determine depth, and sum. The following common polynomial-gap form of the Master theorem then summarises recurrences
with constants , , comparable subproblem sizes, and a base case. Let .
Examples:
The theorem does not apply directly to , unequal irregular splits, or recurrences missing a shrinking factor. Expand or use substitution instead of forcing a case.
The following methods obtain stronger bounds by using structure beyond arbitrary key comparisons. For each one, identify the extra assumption before quoting its running time.
Counting sort assumes integer keys in
0..k-1. Count each key, convert counts to ending positions
with prefix sums, and scan the input from right to left while placing
records into an output array. The reverse scan plus prefix positions
makes the method stable. Time is
and auxiliary space is
.
It is useful only when the key range is manageable.
LSD radix sort applies a stable inner sort from the least significant digit to the most significant. After pass , records are ordered by their last digits; stability preserves the order established by earlier passes. With digits and radix , time is .
Bitwise MSD sorting partitions fixed-width unsigned keys by a mask, beginning with the highest bit, then recursively partitions the zero and one groups using the next bit.
Each record participates at most once per bit. For bits, time is ; this is linear only when is treated as fixed, and is when keys come from a universe of size . Signed integers and floating-point encodings require a deliberately chosen ordering transformation.
Bucket sort maps keys into ordered buckets, sorts each bucket, and concatenates. Expected linear time needs a distribution assumption that keeps total within-bucket work linear. A highly concentrated input can destroy that bound.
The element of rank is the record that would occupy index after sorting. Quickselect uses the same three-way partition but continues only in the region containing the desired rank:
quickselect(A, k):
require 0 <= k < length(A)
lo = 0; hi = length(A)
while true:
choose a uniformly random pivot from A[lo..hi)
(lt, gt) = partition3(A, lo, hi, pivot)
if k < lt: hi = lt
else if k >= gt: lo = gt
else: return A[k]
Partition correctness ensures that a rank outside the equal region lies in exactly one remaining side. A random pivot yields expected time because only one side is visited. Sorting first would cost . Deterministic median-of-medians pivoting gives worst-case time, but with more machinery and larger constants.
A skip list implements an ordered dictionary as levels of sorted linked lists. Level 0 contains every key. Independently promote each inserted key to the next level with probability , repeating until the first failure. Higher levels act as express lanes.
Search starts at the top-left sentinel. At each level, move right while the next key is smaller than the target; otherwise drop one level. Sortedness guarantees that moving down cannot skip the target. At level 0, the next node is either the target or its insertion position.
Insertion records the last node visited at every level, chooses a random height, and splices the new tower after those predecessors. Deletion unlinks the target at each level where it occurs. With promotion probability , the expected number of stored node occurrences is
The expected height and expected search, insertion, and deletion times are ; the worst case remains . These expectations are over the promotion choices. Skip lists trade deterministic rotation rules for a simple randomized representation.
| Method | Expected/best information | Worst-case time | Auxiliary space | Stable? |
|---|---|---|---|---|
| Insertion sort | when sorted | yes | ||
| Merge sort | for arrays | yes | ||
| Randomized three-way quicksort | expected ; when all keys are equal | ordinary recursion: expected stack, worst | usually no | |
| Counting sort | stable version | yes | ||
| Quickselect | expected | implementation-dependent | n/a |
No row is universally best. Stability, key model, existing order, memory, worst-case guarantees, record size, and implementation environment all affect the decision.
lower_bound as guaranteed
membership.lower_bound
invariant.lower_bound need to return
for some inputs?bisect
implements boundary search, while the sorting HOWTO
documents stable list.sort and sorted.
insort still shifts a list suffix in linear time, and
bisect functions are not safe for concurrent mutation of
the same sequence.qsort
and bsearch.
The comparator must define the required order; qsort is not
stable, and bsearch need not return the first equal
element.partition_point. A binary search is meaningful only
under the same total order used to sort, and an equal match is not
necessarily the first.ConcurrentSkipListMap is a concurrent ordered-map
implementation with expected average logarithmic basic operations.
Iterators are weakly consistent and bulk operations are not atomic
snapshots.lower_bound and assert all three clauses of
its loop invariant at every iteration. Exhaustively compare the result
with Python bisect_left on every nondecreasing array of
length at most 7 over keys 0,1,2, including absent and
duplicate targets.partition3.