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 (nn \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 nn0n \ge n_0.

The Five Rules of Growth

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

  • The Rule: f(n)cg(n)f(n) \le c \cdot g(n) for some c>0c > 0 and all nn0n \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)cg(n)f(n) \ge c \cdot g(n) for some c>0c > 0 and all nn0n \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: c1g(n)f(n)c2g(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 nn0n \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)<cg(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)>cg(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 nn \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)
limnf(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 limn3n2+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)
limnf(n)g(n)=C(0C<)\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)
limnf(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)
limnf(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)
limnf(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)
7logn7\log nΘ(logn)\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 nn \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 rr_\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 A100A \ge 100 to prevent collisions with working registers r1r4).

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