To measure code speed, we compare our actual code's operation count to a simplified reference "ruler".

FunctionWhat it isExample
f(n)f(n)Your actual function (exact operations in your code)3n2+2n+53n^2 + 2n + 5
g(n)g(n)The reference function (simplified "ruler")n2n^2

The 5 Bound Types

This notation tells you how f(n)f(n) and g(n)g(n) compare asymptotically as the input size (nn) approaches infinity.

Bound TypeNotationPlain EnglishAnalogyLimit (n→∞n \to \infty)
Strict Uppero(g(n))o(g(n))ff grows strictly slower than gg<<=0= 0
UpperO(g(n))O(g(n))ff grows no faster than gg≤\leConstant or 00
TightΘ(g(n))\Theta(g(n))ff and gg have the same growth rate≈\approxPositive constant
LowerΩ(g(n))\Omega(g(n))ff grows at least as fast as gg≥\gePositive constant or ∞\infty
Strict Lowerω(g(n))\omega(g(n))ff grows strictly faster than gg>>=∞= \infty

Mathematical Definition

In computer science, we use two anchor points to prove these bounds:

  • cc (The Multiplier): A fixed positive constant used to scale the reference function.
  • n0n_0 (The Threshold): The point on a graph beyond which the inequality holds for all n≥n0n \ge n_0.

The Five Rules of Growth

1. Big O (OO): The Ceiling (Upper Bound)

  • The Rule: f(n)≤c⋅g(n)f(n) \le c \cdot g(n) for some c>0c > 0 and all n≥n0n \ge n_0.
  • Meaning: Growth stays at or below the reference curve (Worst-case ceiling).

2. Big Omega (Ω\Omega): The Floor (Lower Bound)

  • The Rule: f(n)≥c⋅g(n)f(n) \ge c \cdot g(n) for some c>0c > 0 and all n≥n0n \ge n_0.
  • Meaning: Growth stays at or above the reference curve (Best-case floor).

3. Big Theta (Θ\Theta): The Sandwich (Tight Bound)

  • The Rule: c1⋅g(n)≤f(n)≤c2⋅g(n)c_1 \cdot g(n) \le f(n) \le c_2 \cdot g(n) for constants c1,c2>0c_1, c_2 > 0 and all n≥n0n \ge n_0.
  • Meaning: f(n)f(n) is asymptotically bounded above and below by the same curve.

4. Little o (oo): Strictly Under

  • The Rule: f(n)<c⋅g(n)f(n) < c \cdot g(n) for all c>0c > 0 and sufficiently large nn.
  • Meaning: f(n)f(n) grows strictly slower than g(n)g(n) (i.e., g(n)g(n) dominates completely).

5. Little omega (ω\omega): Strictly Over

  • The Rule: f(n)>c⋅g(n)f(n) > c \cdot g(n) for all c>0c > 0 and sufficiently large nn.
  • Meaning: f(n)f(n) grows strictly faster than g(n)g(n) (i.e., f(n)f(n) dominates completely).

Proof via Limits

To evaluate these asymptotic bounds using calculus, compare the ratio of the functions as n→∞n \to \infty

  • i.e. f(n)=3n2+5n+10f(n) = 3n^2 + 5n + 10 becomes n2n^2

Tight Bound: Θ(g(n))\Theta(g(n))

This is the "exact" growth rate.

latex (rendered)
lim⁡n→∞f(n)g(n)=C(0<C<∞)\lim_{n \to \infty} \frac{f(n)}{g(n)} = C \quad (0 < C < \infty)
  • Result: 3n2+5n=Θ(n2)3n^2 + 5n = \Theta(n^2) because lim⁡n→∞3n2+5nn2=3\lim_{n \to \infty} \frac{3n^2 + 5n}{n^2} = 3, which is a positive constant.

Upper Bound: O(g(n))O(g(n))

This is the "ceiling." f(n)f(n) grows no faster than g(n)g(n).

latex (rendered)
lim⁡n→∞f(n)g(n)=C(0≤C<∞)\lim_{n \to \infty} \frac{f(n)}{g(n)} = C \quad (0 \le C < \infty)
  • Result: 3n2=O(n2)3n^2 = O(n^2) and 3n2=O(n3)3n^2 = O(n^3) because the limits evaluate to 33 and 00, respectively.

Lower Bound: Ω(g(n))\Omega(g(n))

This is the "floor." f(n)f(n) grows at least as fast as g(n)g(n).

latex (rendered)
lim⁡n→∞f(n)g(n)>0(positive constant or ∞)\lim_{n \to \infty} \frac{f(n)}{g(n)} > 0 \quad (\text{positive constant or } \infty)
  • Result: For f(n)=3n2f(n) = 3n^2, both Ω(n2)\Omega(n^2) and Ω(n)\Omega(n) are valid because the limits evaluate to 33 and ∞\infty, respectively.

Strict Upper Bound: o(g(n))o(g(n))

This is a "loose ceiling." g(n)g(n) must eventually grow strictly faster than f(n)f(n).

latex (rendered)
lim⁡n→∞f(n)g(n)=0\lim_{n \to \infty} \frac{f(n)}{g(n)} = 0
  • Result: 3n2=o(n3)3n^2 = o(n^3) because the limit evaluates to 00.

Strict Lower Bound: ω(g(n))\omega(g(n))

This is a "loose floor." g(n)g(n) must be strictly slower than f(n)f(n).

latex (rendered)
lim⁡n→∞f(n)g(n)=∞\lim_{n \to \infty} \frac{f(n)}{g(n)} = \infty
  • Result: 3n2=ω(n)3n^2 = \omega(n) because the limit evaluates to ∞\infty.

What about "O(1)O(1)" (Constant Time)

When g(n)=1g(n) = 1, the operation count does not depend on the input size (nn).

  • Θ(1)\Theta(1) (Exact Constant): The algorithm always takes a constant number of operations (e.g., accessing an array element by index: arr[0]).
  • O(1)O(1) (Constant Ceiling): The algorithm takes at most a constant amount of time.
  • Ω(1)\Omega(1) (Constant Floor): The algorithm takes at least some constant amount of time. (Since any executed instruction requires non-zero time, Ω(1)\Omega(1) is a trivial lower bound for almost every program).

Examples

Function f(n)f(n)Tight Bound Θ\ThetaValid Upper OOValid Lower Ω\OmegaStrict Upper ooStrict Lower ω\omega
5n+35n + 3Θ(n)\Theta(n)O(n2)O(n^2)Ω(1)\Omega(1)o(n2)o(n^2)ω(1)\omega(1)
3n2+2n3n^2 + 2nΘ(n2)\Theta(n^2)O(n3)O(n^3)Ω(n)\Omega(n)o(n3)o(n^3)ω(n)\omega(n)
7log⁡n7\log nΘ(log⁡n)\Theta(\log n)O(n)O(n)Ω(1)\Omega(1)o(n)o(n)ω(1)\omega(1)
2n+n32^n + n^3Θ(2n)\Theta(2^n)O(3n)O(3^n)Ω(n3)\Omega(n^3)o(3n)o(3^n)ω(n3)\omega(n^3)

Asymptotically Incomparable Functions

Sometimes neither function bounds the other due to oscillation.

Example:

  • Let f(n)=nf(n) = n
  • Let g(n)=n1+sin⁡(n)g(n) = n^{1 + \sin(n)}

Because sin⁡(n)\sin(n) oscillates continuously between −1-1 and 11:

  1. When sin⁡(n)=−1\sin(n) = -1, g(n)=n0=1g(n) = n^0 = 1, making f(n)f(n) larger.
  2. When sin⁡(n)=1\sin(n) = 1, g(n)=n2g(n) = n^2, making g(n)g(n) larger.

Since their relative order oscillates indefinitely as n→∞n \to \infty, f(n)f(n) and g(n)g(n) are asymptotically incomparable.

Data-Dependent Bounds (No Single Tight Bound)

cpp
for (int i = 0; i < n; i++) {
    if (arr[i] == target) {  // Data-dependent
        for (int j = 0; j < n; j++) {
            sum++;
        }
    }
}
  • Best Case (Ω(n)\Omega(n)): Target is never found. The inner loop never executes; only the outer loop runs (Tbest(n)=Θ(n)T_{\text{best}}(n) = \Theta(n)).
  • Worst Case (O(n2)O(n^2)): Every element matches the target. The inner loop executes on every iteration (Tworst(n)=Θ(n2)T_{\text{worst}}(n) = \Theta(n^2)).
  • Conclusion: Without assumptions about the input data distribution, there is no single Θ\Theta bound describing the algorithm across all inputs.

The RA Machine Architecture

The Random Access (RA) model is a theoretical abstraction bridging the gap between Turing machines and real silicon. Instead of a single sequential tape, the RA model provides an infinite array of discrete registers and an accumulator.

INPUT TAPECPU / ALU[ Program Counter (PC) ][ Accumulator (r0) ]MEMORY / REGISTERSr1 | r2 | r3 | r4 | ... | r_infOUTPUT[ READ ][ STORE ][ LOAD ][ WRITE ]

  • Accumulator (r0): All arithmetic and logic operations occur here.
  • Registers (r1 to r∞r_\infty): Memory cells holding arbitrarily large integers.
  • Input/Output Tapes: Sequential read-only input and write-only output.

The Instruction Set

(Note: In the x86 mappings below, eax corresponds to Accumulator r0, and ebx/esi correspond to general registers).

RA InstructionOperationC++ Equivalentx86 Assembly Equivalent
LOAD rir0 ← rir0 = reg[i];mov eax, ebx
LOADI cr0 ← cr0 = c;mov eax, c
LOAD *rir0 ← reg[ri]r0 = reg[reg[i]];mov eax, dword ptr [esi]
STORE riri ← r0reg[i] = r0;mov ebx, eax
STORE *rireg[ri] ← r0reg[reg[i]] = r0;mov dword ptr [esi], eax
ADD rir0 ← r0 + rir0 += reg[i];add eax, ebx
SUB rir0 ← r0 - rir0 -= reg[i];sub eax, ebx
MULT rir0 ← r0 * rir0 *= reg[i];imul ebx
DIV rir0 ← r0 / rir0 /= reg[i];idiv ebx
INC riri ← ri + 1reg[i]++;inc ebx
DEC riri ← ri - 1reg[i]--;dec ebx
JUMP labelpc ← labelgoto label;jmp label
JUMPZ labelif r0 == 0 jumpif (r0 == 0) goto label;cmp eax, 0; je label
JUMPP labelif r0 > 0 jumpif (r0 > 0) goto label;cmp eax, 0; jg label
READ riri ← inputstd::cin >> reg[i];(Syscall)
WRITEoutput ← r0std::cout << r0;(Syscall)
HALTStop executionexit(0);hlt

Example 1: Iterative Summation (The N-Numbers Problem)

The Problem:

  1. Read an integer NN from the input tape.
  2. Sum NN sequential values starting at register r100 (i.e., r[100] through r[100 + N - 1]).
  3. Output the final sum to the tape.

The Code Solution:

asm
READ r1      // Tape -> r1 (r1 holds counter N)
LOADI 100    // r0 = 100
STORE r2     // r2 = 100 (pointer to array elements)
LOADI 0      // r0 = 0
STORE r3     // r3 = 0 (running sum)

LOOP:
       LOAD r1      // Check loop counter
       JUMPZ FINISH // If N == 0, terminate loop

       LOAD *r2     // r0 = reg[r2] (dereference pointer)
       ADD r3       // r0 = reg[r2] + running_sum
       STORE r3     // running_sum = r0

       INC r2       // Advance memory pointer
       DEC r1       // Decrement counter N
       JUMP LOOP    // Next iteration

FINISH:
       LOAD r3      // Load total sum into Accumulator
       WRITE        // Output Accumulator to tape
       HALT

Example 2: Pointer Indirection (Nested Lookup)

The Problem:

  1. Read the base address AA of array a from the input tape.
  2. Read index i from the input tape.
  3. Array a is stored contiguously starting at register AA (reg[A] = a[0], reg[A+1] = a[1], etc.).
  4. Compute a[a[i]] and write the result to the output tape.

(Assume A≥100A \ge 100 to prevent collisions with working registers r1–r4).

text
INPUT TAPE:
[ Row 1: A ] <-- Tape Head (Base address)
[ Row 2: i ] <-- Index

MAIN MEMORY (Registers):
r1     : [ base address A ]
r2     : [ index i ]
r3     : [ pointer to a[i] ]
r4     : [ pointer to a[a[i]] ]
...
r[A]   : [ a[0] ]
r[A+1] : [ a[1] ]

And this is how we should it:

  1. Read inputs: We need to pull A and i off the tape and store them in working registers (r1 and r2).

  2. Find a[i]: The memory address of a[i] is simply the base address plus the index (A + i). We calculate this, store the pointer in r3, and then use indirect loading (LOAD *r3) to get the actual value of a[i].

  3. Find a[a[i]]: Now we repeat the process. The memory address of a[a[i]] is the base address plus our newly found value (A + a[i]). We calculate this, store the pointer in r4, and indirectly load it to get our final answer.

The Code Solution:

asm
READ r1      // r1 = A
READ r2      // r2 = i

LOAD r1      // r0 = A
ADD r2       // r0 = A + i
STORE r3     // r3 = A + i (pointer to a[i])

LOAD *r3     // r0 = a[i] (dereference first pointer)
ADD r1       // r0 = A + a[i] (address of a[a[i]])
STORE r4     // r4 = A + a[i]

LOAD *r4     // r0 = a[a[i]] (dereference second pointer)
WRITE        // Output result
HALT