Data Structures · Lecture Notes

Chapter 2: Algorithm Analysis

A program that solves a problem is often not enough. We need to choose between two programs that do the same job, predict how long ours will take on large inputs, and know when to look for something better. Algorithm analysis answers these questions. We start by timing programs, see where that breaks down, then count primitive operations and introduce Big O, Big Omega and Big Theta. The core of the chapter covers best, average and worst cases, reading complexity off loops, linear and binary search, and two solutions to the prefix averages problem. The last sections deal with memory, correctness and robustness.

Learning outcomes

Measuring and counting
Time an algorithm experimentally and analyze it by counting primitive operations.
Asymptotic notation
Express time and space complexity in O, Ω and Θ notation.
From code to complexity
Derive best-, average- and worst-case costs from loop patterns.
Comparison
Compare algorithms for the same problem by cost, correctness and robustness.

2.1Algorithms and measurement

Every topic in this course raises the same question: how long does this operation take? Searching an array, inserting into a list, deleting from a tree — each data structure makes some operations fast and others slow, and to say which structure suits which job we must give “fast” and “slow” a precise meaning. That is the job of algorithm analysis. We begin with the notion of an algorithm and two ways to measure its cost: timing it and counting its operations.

2.1.1What is an algorithm?

A recipe takes ingredients (the input), works through a sequence of steps, and produces a dish (the output). Its steps are explicit — “boil the water,” “add the rice,” “simmer for fifteen minutes” — so anyone who follows them gets the same dish, and it ends; nobody stirs forever. An algorithm has the same shape: a step-by-step method that takes an input, carries out its steps, and produces an output.

Definition 2.1

An algorithm is a method, made up of a finite number of precise and effective steps, that produces the correct output for every valid input of a problem. Finite means it eventually stops, precise that each step has exactly one meaning, and effective that each step can actually be carried out.

input5291algorithm (sorting)finite · precise · effectiveoutput1259
Figure 2.1: An algorithm applies its steps to the input to produce the output. Here a sorting algorithm turns 5, 2, 9, 1 into 1, 2, 5, 9.

What happens inside the box in Figure 2.1 — which numbers are compared and swapped, in what order — differs from algorithm to algorithm; many routes lead from the same input to the same output.

That is where analysis begins: of several algorithms for a problem, some need far less time and memory. On small inputs the difference may go unnoticed, but as the input grows one algorithm may finish in a second while another runs for days; Table 2.7 puts numbers on this gap.

Algorithms are grouped by the kind of problem they solve. Table 2.1 lists three groups that recur throughout the course.

Table 2.1: Some common groups of algorithms.
GroupWhat it doesExamples
SortingPuts data in a given order.bubble sort, merge sort
SearchingFinds a given item in a collection.linear search, binary search
GraphSolves problems from graph theory.shortest path, network flow

Two searching algorithms, linear and binary search, are studied in detail below (2.3.5–2.3.7). Both answer “Is the target in the array, and at which index?”, but one looks at every element and the other at only a few — one of the clearest demonstrations of why the choice of algorithm matters.

Common mistake

Treating an algorithm as the same thing as a program. An algorithm is a method that can be written in Java, in Python, or as pseudocode on paper, and its implementations in different languages have the same analysis. Analysis counts the steps of the method, not features of the language.

2.1.2What makes an algorithm good

A good algorithm solves its problem effectively, and “effectively” breaks down into four criteria — two about cost, two about reliability:

  • Time complexity: how the running time, or the number of operations, grows with the input.
  • Space complexity: how the memory used grows with the input size (2.4.1).
  • Correctness: the right result for every valid input. A wrong answer is useless, however fast (2.4.2).
  • Robustness: coping with unexpected inputs and situations without crashing or silently returning a wrong result (2.4.4).

The criteria are not equal. Correctness is non-negotiable: a fast algorithm with wrong answers is always worse than a slow one with right answers. Time and space decide only among correct algorithms. They sometimes conflict — some algorithms buy speed with memory — but not always: in the prefix averages example (2.3.12) the fast solution uses the same memory and simply skips needless work.

Most of this chapter is about time, which most often decides the choice of a data structure. Memory is analyzed with the same notation and methods.

2.1.3Empirical analysis: System.nanoTime()

The most natural way to learn how long a program takes is to run it and time it — empirical analysis. In Java, System.nanoTime() returns a number, and the difference between two calls is the elapsed time in nanoseconds (ns, billionths of a second).

The code below times hasDuplicates, which checks an array for repeated elements, at several sizes. It compares every pair of elements (code and analysis in 2.3.3); for now we only need to know that on an array without duplicates it examines every pair before returning false.

for (int n = 1000; n <= 64000; n *= 2) {
  int[] a = new int[n];
  for (int i = 0; i < n; i++) a[i] = i; // no duplicates
  long start = System.nanoTime();
  hasDuplicates(a);                    // the work being timed
  long elapsed = System.nanoTime() - start;  // ns
  System.out.println(n + " " + elapsed / 1e6 + " ms");
}
  • The outer loop doubles n from 1000 — 1000, 2000, 4000, …, 64000 — so seven sizes are tried.
  • new int[n] creates an array of n cells, and a[i] = i fills it with 0, 1, 2, …; all values differ, so there are no duplicates.
  • start = System.nanoTime() records the moment just before the call hasDuplicates(a).
  • System.nanoTime() - start is the elapsed time in nanoseconds; dividing by 106 gives milliseconds (ms), printed next to n.

The value of System.nanoTime() means nothing on its own; it is not the time of day, and only the difference between two readings matters. The time of day — a clock saying “it’s 2:05” — comes from System.currentTimeMillis(). For short intervals nanoTime() is better: its resolution is finer, and it does not jump when the system clock is adjusted.

Good measurements follow three principles:

  1. Measure. Read System.nanoTime() immediately before and after the work. Creating and filling the array stay outside; only the call to hasDuplicates is timed.
  2. Repeat. One measurement is unreliable. The Java Virtual Machine (JVM) interprets code at first and later compiles frequently run parts to machine code; this just-in-time (JIT) compilation slows the first measurements, and the garbage collector and other programs add noise. So we repeat each measurement and take the median — the middle value of the sorted measurements, which no single outlier can distort.
  3. Plot. Plot time against n. What matters is not the times themselves but how much the time grows when n doubles — the subject of the next subsection.

Table 2.2 shows one run of this code on a laptop. The numbers vary between machines and even between runs; we use them only to show how a measurement is read.

Table 2.2: A sample measurement of hasDuplicates (single run). The last column shows how many times longer each row took than the one before.
nTime (ms)Ratio
1,0008.01—
2,0000.860.11
4,0002.713.1
8,00010.643.9
16,00037.833.6
32,000146.113.9
64,000727.335.0

The first two rows look odd: 1000 elements took about nine times longer than 2000. The culprit is the JIT, not the algorithm — on the first call hasDuplicates is still interpreted, by the second it is compiled. The first row is a “warm-up” and is ignored. After that the ratio ranges from 3.1 to 5.0, mostly near 4. The 5.0 in the last row is noise: the large array no longer fits in the processor cache, or another program happened to run. Repeating and taking the median removes most such deviations.

Common mistake

Forgetting to fill the array when measuring the worst case. A Java array created with new int[n] is full of zeros, so hasDuplicates finds a[0] == a[1] on its first comparison and returns true. The measurement then shows microseconds even for 64,000 elements and makes the method look very fast. For the worst case, fill the array with distinct values, as a[i] = i does.

2.1.4The doubling experiment

Raw times say little: “727 ms for 64,000 elements” means nothing on another computer. What lasts is how the time grows with the input, and the practical way to find it is the doubling experiment: double the input size at each step and look at the ratio of consecutive times.

Suppose the running time is roughly T(n) ≈ c · nk, where the constant c depends on the machine and the exponent k on the algorithm. Doubling the input gives

T(2n) / T(n) ≈ c · (2n)k / (c · nk) = 2k

The constant c cancels, so the ratio is machine-independent: a faster computer shrinks c but not the ratio. A ratio of 2 means k = 1, linear growth; 4 means k = 2, quadratic; 8 means k = 3, cubic.

2×4×6×8×2.0×O(n)2.2×O(n log n)4.0×O(n²)8.0×O(n³)How much longer when n goes from 1000 to 2000?
Figure 2.2: Expected time ratios when n goes from 1000 to 2000: 2 for O(n), about 2.2 for O(n log n), 4 for O(n2), and 8 for O(n3).

Figure 2.2 shows the expected ratios for four growth rates. (The O(…) notation is defined in 2.2.1; read it for now as “grows roughly like.”) The interesting bar is n log n: the logarithm grows a little when n doubles, so the ratio is slightly above 2:

(2000 · log2 2000) / (1000 · log2 1000) = 2 · 10.966 / 9.966 ≈ 2.2

This is also why a doubling experiment struggles to tell n from n log n: noise easily blurs 2.0 and 2.2.

Example 2.1

Read Table 2.2 as a doubling experiment and predict the time for 128,000 elements.

  1. Warm-up. Drop the first row (1000 elements); it carries the JIT effect.
  2. Ratios. The rest are 3.1, 3.9, 3.6, 3.9 and 5.0 — far from both 2 and 8, clustered around 4.
  3. Exponent. 2k ≈ 4 gives k = 2: the time is roughly c · n2, quadratic.
  4. Prediction. 128,000 is twice 64,000, and quadratic growth quadruples the time: 4 · 727 ≈ 2909 ms, about 3 seconds. Using exactly 4 keeps the noise out; averaging the measured ratios would carry it into the prediction.

This agrees with the n(n − 1)/2 comparisons we will derive from the code of hasDuplicates (2.3.3), a count that grows like n2.

Common mistake

Looking at the difference instead of the ratio. From 8,000 to 16,000 the time rises from 10.64 ms to 37.83 ms; the 27 ms difference tells us nothing, while the ratio of about 3.6 does. A doubling experiment always divides, never subtracts.

2.1.5Limits of empirical analysis

Empirical analysis is concrete — it shows a real program on a real machine — but it has three serious limitations (Goodrich et al., Section 4.1):

  • Results depend on the environment: the processor, the memory, the JVM version, whatever else is running. Two algorithms must be timed on the same machine under the same conditions; comparing our timing with one from someone else’s machine is meaningless.
  • It covers only the inputs tried. An important kind of input that slows the algorithm down may never be tested. Someone who times hasDuplicates only on arrays of zeros never sees its worst case.
  • The algorithm must be fully implemented before anything can be measured, yet decisions are usually needed before the code exists. Implementing and timing ten methods just to pick one is expensive.

The way past these limits is to count an algorithm’s steps instead of timing them. Counting is hardware-independent, holds for every input, and needs no running code — pseudocode will do. Empirical analysis does not replace it but is a good check on it; in Example 2.1 measuring and counting agreed.

2.1.6Counting primitive operations: arrayMax()

First we must decide what to count. A line of Java may become several machine instructions whose timings vary between processors. To avoid such details we assume that certain basic operations each take constant time, and count only how often they run.

Definition 2.2

A primitive operation is a basic operation whose running time does not depend on the input size and is bounded by a constant. Assigning to a variable, comparing two values, an arithmetic operation (addition, multiplication, …), accessing an array cell by index, calling a method and returning from one are all primitive operations.

If each primitive operation takes at most some constant time, the running time is at most that constant times the operation count. The constant changes from machine to machine; the count does not — a machine-independent measure.

Our first example, arrayMax, finds the largest element of an array:

int arrayMax(int[] a) {
  int currentMax = a[0];
  for (int i = 1; i < a.length; i++) {
    if (a[i] > currentMax)
      currentMax = a[i];
  }
  return currentMax;
}

It takes the first element as a provisional maximum, scans the rest of the array, and updates currentMax whenever an element beats it. When the loop ends, currentMax is the largest element of the whole array.

Now count how often each statement runs, for an array of length n, numbering lines from the method header as line 1.

Table 2.3: How many times each primitive operation runs in arrayMax (n = a.length).
LineStatementTimes
2currentMax = a[0]1
3i = 11
3i < a.lengthn
4a[i] > currentMaxn − 1
5currentMax = a[i]0 … n − 1
3i++n − 1
7return1
  • The assignment before the loop (currentMax = a[0]) and the initialization i = 1 run once each.
  • The body runs for i = 1, 2, …, n − 1 — n − 1 iterations — so the comparison and i++ run n − 1 times each.
  • The condition i < a.length is tested once more than the body runs: n − 1 times true, then once false at i = n, ending the loop — n tests.
  • The update currentMax = a[i] runs only when the comparison is true, so its count depends on the data: anywhere from never to n − 1 times.

Writing u for the number of updates, the total is

1 + 1 + n + (n − 1) + u + (n − 1) + 1 = 3n + 1 + u,   0 ≤ u ≤ n − 1

In the best case, a[0] is the largest and nothing is updated: u = 0, total 3n + 1. In the worst case the array is increasing, every element beats all earlier ones, and every iteration updates: u = n − 1, total 3n + 1 + n − 1 = 4n.

The conclusion matters more than the numbers. Either way the count grows in proportion to n: double n and it roughly doubles. The factors 3 and 4 and the +1 are details that vary between machines anyway, so we drop them and say that arrayMax runs in Θ(n) time in both cases. Θ is defined precisely in 2.2.2.

Common mistake

Forgetting that a loop condition runs once more than the body. In for (int i = 1; i < n; i++) the body runs n − 1 times but the condition is tested n times, the last test ending the loop. The growth rate is unaffected, but exact-count questions come out wrong.

Which operations count separately is a convention. currentMax = a[i] really reads a cell and assigns a value; counting it as two operations turns 3n + 1 into another linear expression, but the growth stays linear. Changing the counting rules changes only constants — one more reason to drop them.

2.1.7Code trace: arrayMax(a)

Let us follow the count on a = {3, 8, 2, 9, 4}, so n = 5. Table 2.4 shows the variables at each step.

Table 2.4: Trace of arrayMax({3, 8, 2, 9, 4}).
StepiCondition i < 5a[i] > currentMaxcurrentMax
start———3
iteration 11true8 > 3 true8
iteration 22true2 > 8 false8
iteration 33true9 > 8 true9
iteration 44true4 > 9 false9
exit5false—returns 9

Initially currentMax = a[0] = 3. In iteration 1, a[1] = 8 beats 3 and becomes the maximum; in iteration 2, a[2] = 2 changes nothing; in iteration 3, a[3] = 9 > 8 takes over; in iteration 4, a[4] = 4 is smaller than 9. At i = 5 the condition fails and the method returns 9.

0318223944a[0..0] processedcurrentMax3 0318223944▲ ia[0..1] processedcurrentMax8
0318223944▲ ia[0..3] processedcurrentMax9 0318223944a[0..4] processedcurrentMax9
Figure 2.3: Four moments in the trace of arrayMax (left to right, top to bottom). (a) currentMax = a[0] = 3. (b) Iteration 1 finds 8. (c) Iteration 3 finds 9. (d) The loop has ended; a[0..4] is processed and the result is 9. The green line underneath marks the part processed so far.

Checking against the table: the condition was tested 5 times (4 true, 1 false), the comparison made 4 times, i++ run 4 times, and there were two updates (8 and 9), so u = 2. The total, 3 · 5 + 1 + 2 = 18, lies between the best-case 16 and the worst-case 20.

The green line in Figure 2.3 also hints at correctness: at the start of every iteration, currentMax is the largest element of the processed part, and when the loop ends that part is the whole array. In 2.4.3 this becomes a proof by loop invariant.

2.2Asymptotic notation

For arrayMax we found between 3n + 1 and 4n operations and kept only “proportional to n.” Every analysis prunes like this, and asymptotic notation does it systematically: it looks at what happens as the input grows very large, setting aside details that matter only for small inputs.

2.2.1Time complexity and Big O

Time complexity expresses an algorithm’s running time as a function of the input size n — the number of elements in an array, characters in a string, vertices and edges in a graph, depending on the problem. We rarely need the function’s exact form, only how it grows.

Big O gives an upper bound. “This algorithm is O(n)” means that for large n its running time stays below some constant multiple of n. Big O is usually applied to the worst case, since an upper bound there is a guarantee for every input — but that is a habit, not part of the definition (2.2.5).

Two classes come up constantly:

  • O(1): the operation count does not grow with the input. Reading the first element of an array takes the same steps for three elements or a million. O(1) does not mean identical times, only a bound independent of n.
  • O(n): the time grows linearly; doubling n at most roughly doubles it. One pass through an array is O(n).
input size n →workO(1)O(log n)O(n)O(n log n)O(n²)O(2ⁿ)
Figure 2.4: How 1, log n, n, n log n, n2 and 2n grow with the input size n (1 ≤ n ≤ 12, vertical axis cut off at 60). 2n and n2 shoot up while log n and 1 stay almost flat.

Even with n only up to 12, the differences in Figure 2.4 are plain: 2n leaves the chart near n = 6 and n2 at n ≈ 7.7, n log n runs clearly above n, and log n is only about 3.6 at 12. Table 2.7 shows how far the gaps open on large inputs.

Big O keeps only the fastest-growing term, dropping constant factors and slower terms:

3n2 + 5n + 2 → O(n2)  ·  4n + 100 → O(n)  ·  7 → O(1)

Why is this safe? In the first example, at n = 10 we have 3n2 = 300, 5n = 50 and 2, so the quadratic term is 85% of the total; at n = 1000, 3n2 = 3,000,000 against 5n = 5000, over 99.8%. The smaller terms’ share tends to zero. The factor 3 depends on the machine and leaves the growth rate alone: doubling n quadruples both 3n2 and n2.

Common mistake

Assuming O(1) is always fast. The constant can be large: an O(1) operation taking 1000 steps per call is slower than an O(n) operation taking 50 steps at n = 50. Asymptotic notation compares large n; on small inputs the constants can decide.

2.2.2Formal definitions of O, Ω and Θ

Now we make “below a constant multiple for large n” precise. Below, f(n) is the operation count of the algorithm and g(n) a simple comparison function such as n, n2 or log n.

Definition 2.3

f(n) = O(g(n)) if there exist a constant c > 0 and an integer n0 ≥ 1 such that f(n) ≤ c · g(n) for every n ≥ n0. Then g(n) is an upper bound on f(n): for large enough n, f grows no faster than g.

f(n) = Ω(g(n)) (Big Omega) under the same conditions with the inequality reversed: f(n) ≥ c · g(n) for every n ≥ n0. Then g(n) is a lower bound: f grows no slower than g.

f(n) = Θ(g(n)) (Big Theta) if both f(n) = O(g(n)) and f(n) = Ω(g(n)); that is, there exist c1, c2 > 0 and n0 with c1 · g(n) ≤ f(n) ≤ c2 · g(n) for every n ≥ n0. This is a tight bound: f and g grow at the same rate.

Each part has a job. The constant c captures why constant factors don’t matter: c = 3 closes the gap between 3n and n. The threshold n0 captures why small inputs don’t matter. And “for every n ≥ n0” demands that the bound hold everywhere past the threshold, not at scattered points.

We will apply all three definitions to f(n) = 2n + 10.

2040600481216n →n₀ = 5c·g(n) = 4nf(n) = 2n + 10
Figure 2.5: Big O. For n ≥ n0 = 5 the line f(n) = 2n + 10 stays below 4n, so f(n) = O(n) with c = 4. The shaded region is the range n ≥ n0 where the inequality is required.

Big O. In Figure 2.5, f starts at 10 and 4n at 0, but 4n is steeper and catches up where 2n + 10 = 4n, at n = 5. For n ≥ 5, 2n + 10 ≤ 4n, so c = 4 and n0 = 5 show f(n) = O(n). Below 5, f is larger — at n = 2, f = 14 while 4n = 8 — but the definition ignores these values.

2040600481216n →c·g(n) = 2nf(n) = 2n + 10
Figure 2.6: Big Omega. 2n + 10 ≥ 2n for every n ≥ 1, so f(n) = Ω(n). The line f lies above 2n everywhere.

Big Omega. For a lower bound, c = 2 works: since 10 is positive, 2n + 10 ≥ 2n for every n (Figure 2.6), so n0 = 1 and no threshold is needed. f(n) = Ω(n): f grows no slower than n.

2040600481216n →n₀ = 54n2nf(n)
Figure 2.7: Big Theta. For n ≥ 5, 2n ≤ f(n) ≤ 4n; f is squeezed between two lines, so f(n) = Θ(n).

Big Theta. Together, 2n ≤ 2n + 10 ≤ 4n for n ≥ 5 (Figure 2.7): f is trapped between lines of slope 2 and 4, so f(n) = Θ(n) — its growth rate is exactly n.

Think of an exam grade. “At most 90” is an upper bound (O), “at least 70” a lower bound (Ω), and together they narrow the grade down. Asymptotically, “at most” and “at least” hold up to constant factors, and when one function serves as both bounds, the growth rate is known exactly (Θ).

Which to write depends on what we know. O is the most common but can be loose: 2n + 10 = O(n2) is true (2n + 10 ≤ n2 for n ≥ 5) yet useless, since it hides the real growth. So we give the tightest bound we can, and when it is known, Θ states it most clearly.

Common mistake

Reading the “=” in f(n) = O(g(n)) as true equality. It means “f belongs to the class O(g).” n = O(n2) is true, but O(n2) = n does not follow. Equality is symmetric; this notation is not.

2.2.3Using the definition: finding c and n₀

Showing that an expression is in a Big O class means finding a c and n0. Any pair that works will do. Two approaches are common: bound every term by the largest, or solve the inequality directly.

Example 2.2

Show that f(n) = 3n2 + 5n + 2 is Θ(n2).

  1. Upper bound, first approach (enlarge the terms). For n ≥ 1, n ≤ n2 and 1 ≤ n2, so 5n ≤ 5n2 and 2 ≤ 2n2. Adding up, f(n) ≤ 3n2 + 5n2 + 2n2 = 10n2: c = 10, n0 = 1 give f(n) = O(n2).
  2. Upper bound, second approach (a smaller c). With c = 4, 3n2 + 5n + 2 ≤ 4n2 holds exactly when n2 − 5n − 2 ≥ 0. The positive root of this quadratic is (5 + √33)/2 ≈ 5.37, so it holds for n ≥ 6. Check: f(6) = 108 + 30 + 2 = 140 ≤ 4 · 36 = 144, but f(5) = 102 > 100. So c = 4, n0 = 6 is also valid.
  3. Lower bound. Since 5n + 2 is positive, f(n) ≥ 3n2 for every n ≥ 1: c = 3, n0 = 1 give f(n) = Ω(n2).
  4. Conclusion. 3n2 ≤ f(n) ≤ 10n2 for n ≥ 1, so f(n) = Θ(n2).

The approaches show a trade-off: the smaller c, the larger n0. The choice doesn’t matter; the definition only asks for some pair.

The first approach works for any polynomial. For n ≥ 1, every term of a degree-d polynomial with nonnegative coefficients is at most a multiple of nd, so the sum of the coefficients serves as c; the leading coefficient gives the lower bound.

Theorem 2.1 (Growth rate of polynomials)

Let f(n) = adnd + ad−1nd−1 + … + a1n + a0 with ad > 0 and all other coefficients nonnegative. Then f(n) = Θ(nd), because for n ≥ 1, adnd ≤ f(n) ≤ (ad + ad−1 + … + a0) · nd.

The theorem also holds with negative coefficients (as long as ad > 0), but n0 needs more care. For example, n(n − 1)/2 = n2/2 − n/2 is Θ(n2): for n ≥ 2, n/2 ≤ n2/4, so n2/4 ≤ n(n − 1)/2 ≤ n2/2. This expression returns in the analysis of hasDuplicates.

Common mistake

Letting c depend on n. “Choose c = n, so n2 ≤ c · n and n2 = O(n)” is wrong: c is a constant and cannot grow with n. Whatever constant we pick, n2 > c · n as soon as n > c, so n2 is not O(n).

2.2.4Rules of thumb

Hunting for c and n0 every time would be tiresome. A few rules that follow from the definition do most of the work:

  • Drop constant factors. For a constant k > 0, k · f(n) is in the same class as f(n); 5n and n/2 are both Θ(n).
  • In a sum, the largest term wins. If f1 = O(g1) and f2 = O(g2), then f1 + f2 = O(max(g1, g2)). Consecutive blocks of code combine this way: O(n) + O(n2) = O(n2).
  • In a product, the bounds multiply. If f1 = O(g1) and f2 = O(g2), then f1 · f2 = O(g1 · g2). This gives nested loops: n outer iterations around log n inner ones cost O(n log n).
  • The base of a logarithm doesn’t matter. Since loga n = logb n / logb a, logarithms to different bases differ by a constant factor — for example, log2 n = log10 n / log10 2 ≈ 3.32 · log10 n — so we just write O(log n).
  • Transitivity. If f = O(g) and g = O(h), then f = O(h); likewise for Ω and Θ.

For n = 1,000,000, log2 n ≈ 19.9 and log10 n = 6, but the ratio is about 3.32 for every n. When n grows a thousandfold, each grows by a fixed amount: about 10 for log2, exactly 3 for log10.

Exponentials are different: changing the base changes the growth rate. 3n / 2n = 1.5n grows without bound, so 3n is not O(2n). A constant added to the exponent, however, is just a constant factor: 2n+3 = 8 · 2n = Θ(2n).

Common mistake

Treating log2 n and log10 n as different classes. They differ by a factor of about 3.32 and both are O(log n). This is special to logarithms; for exponentials the base matters, and 2n and 3n are in different classes.

2.2.5Best, average and worst cases

arrayMax needed 3n + 1 operations on a decreasing array and 4n on an increasing one. Most algorithms vary far more, so “the running time” must say which input it means.

Definition 2.4

For a fixed input size n:

  • The best case is the cost of the input of size n that needs the fewest operations.
  • The worst case is the cost of the input of size n that needs the most operations.
  • The average case is the expected cost, assuming inputs are drawn from some probability distribution.

Table 2.5 puts the three bounds and the three cases side by side. The top half answers “what kind of bound?”, the bottom half “for which input?” — independent questions.

Table 2.5: Asymptotic bounds and case analyses, with linear search as the example in the right column.
ConceptWhat it says / asksLinear search
O(g(n)): upper boundFor large enough n, the work grows no faster than g(n).—
Ω(g(n)): lower boundFor large enough n, the work grows no slower than g(n).—
Θ(g(n)): tight boundUpper and lower bounds coincide; the growth is in the class of g(n).—
Best caseHow many operations does the most favorable valid input need?Θ(1) if the first element is the target
Average caseWhat is the expected cost under an assumed input distribution?Θ(n) if all target positions are equally likely
Worst caseWhat maximum cost is guaranteed for every valid input?Θ(n) if the target is last or absent

Linear search checks the elements one by one from the front and stops at the target (code in 2.3.5). Let us work out the three cases for [4, 8, 15, 16, 23, 42], n = 6.

0246481516234250worst = 6average ≈ 3.86best = 1target (50 is not in the array)comparisons
Figure 2.8: Comparisons made by linear search for each possible target in [4, 8, 15, 16, 23, 42]; 50 is not in the array. The best case is 1 comparison and the worst 6; with the seven targets equally likely, the average is about 3.86.
Example 2.3
  1. Cost of each target. 4 is found on the first comparison, 8 on the 2nd, 15 on the 3rd, 16 on the 4th, 23 on the 5th and 42 on the 6th. For 50, which is absent, all six elements are checked: 6 comparisons.
  2. Best case. 1 comparison, when the target is first. This does not depend on n: Θ(1).
  3. Worst case. 6, that is, n, when the target is last or absent: Θ(n).
  4. Average case. If the seven possible targets (six elements and one missing value) are equally likely, the average is (1 + 2 + 3 + 4 + 5 + 6 + 6) / 7 = 27 / 7 ≈ 3.86.

Another assumption gives another average. If the target is always present and the six positions are equally likely, it is (1 + 2 + … + 6)/6 = 21/6 = 3.5; in general, with n equally likely positions, (1 + 2 + … + n)/n = (n + 1)/2. That is still Θ(n): we look at about half the array, and half is proportional to n.

Which case to report depends on the purpose:

  • The worst case is a guarantee. “Never more than 4n steps on any input” is reliable information for a system designer, and usually easy to compute — hence the most often reported.
  • The average case describes what to expect in practice, but it assumes a probability distribution, misleads when the assumption is wrong, and is often harder to compute.
  • The best case alone is misleading. Almost any search is fast when the target happens to sit where it looks first, which says next to nothing about the algorithm in general.
Common mistake

Thinking that O means the worst case and Ω the best case. O, Ω and Θ bound the growth of a function; best, average and worst case specify whose cost we study. The ideas are independent, and any notation can describe any case. Linear search has a Θ(1) best case and a Θ(n) worst case, and its running time overall is O(n) and Ω(1).

2.2.6Amortized analysis

In some data structures most operations are cheap but an expensive one comes along now and then, and worst-case analysis paints too gloomy a picture. Amortized analysis divides the total cost of a long sequence of operations by the number of operations.

The standard example is an array that grows when full. Java’s ArrayList keeps its elements in an array; adding at the end usually means writing into an empty cell, but when the array is full a new one twice as large is allocated and every element copied over — an O(n) addition.

Example 2.4

Add 16 elements to an empty growable array of capacity 1 that doubles when full.

  1. Resizes. The array fills at capacities 1, 2, 4 and 8, so the 2nd, 3rd, 5th and 9th additions resize, copying 1, 2, 4 and 8 elements. The 16th addition fills the final capacity of 16 exactly without a resize.
  2. Total work. 16 writes plus 1 + 2 + 4 + 8 = 15 copies: 31 steps.
  3. Per addition. 31 / 16 ≈ 1.94 steps. One addition (the 9th) copied 8 elements, yet the average stays below 2.

In general, n additions copy 1 + 2 + 4 + … elements; the last term is below n, so the sum is below 2n, and with the writes the total is below 3n. The cost per addition is constant: adding at the end is amortized O(1).

This is not average-case analysis, which computes an expected cost under a probability distribution. No probability is involved: for every sequence of operations, the worst included, the total is below 3n — a guarantee for the sequence rather than for each operation.

Common mistake

Reading amortized O(1) as “every operation is O(1).” An addition that triggers a resize still takes O(n), and in a real-time system — a controller whose every operation must meet a deadline, say — that occasional delay can matter. An amortized bound speaks only about totals over long sequences.

2.2.7Big O classes

Most analyses land in one of a few standard classes, listed in Table 2.6 from slowest- to fastest-growing.

Table 2.6: Common Big O classes.
NotationNameExampleWhy?
O(1)constantaccessing the first element of an arraysame access time whatever the array size
O(log n)logarithmicbinary search in a sorted arraythe search space halves at each step
O(n)linearscanning an array from start to endeach element is processed once
O(n log n)linearithmicmerge sortdivide and conquer: log n levels, n work per level
O(n2)quadraticcomparing all pairs of elementstwo nested loops
O(2n)exponentialgenerating all subsetsa set of n elements has 2n subsets

O(log n) algorithms shrink the problem by a constant fraction per step, finishing in about 20 steps even for a million elements. O(n) algorithms look at each element a constant number of times; since reading the input alone takes n steps, many problems allow nothing better. O(n log n) is the class of good sorting algorithms, O(n2) the natural result of comparing every element with every other, and O(2n) the price of trying every possibility — each increase of n by one doubles the time.

Merge sort’s “log n levels” deserve a word. It splits the array in two, sorts each half the same way, and merges. An array of n elements can be halved log2 n times before the pieces are single elements, and the merges on each level touch n elements in total: log n levels times n work is n log n.

2.2.8What happens as n grows?

Numbers show the gaps best. Table 2.7 gives each function at three values of n.

Table 2.7: Values of growth functions (rounded).
Complexityn = 10n = 100n = 1,000
log2 n3.36.610
n101001,000
n log2 n336649,966
n210010,0001,000,000
2n1,024≈ 1.3 · 1030≈ 1.1 · 10301

Two things stand out. log2 n barely moves: a hundredfold increase in n takes it only from 3.3 to 10. And 2n, still reasonable at n = 10 (1,024), is a 31-digit number at n = 100.

Example 2.5

How long does a computer doing 109 operations per second need for a 2n-step algorithm with n = 100?

  1. Steps. 2100 ≈ 1.27 · 1030.
  2. Seconds. 1.27 · 1030 / 109 = 1.27 · 1021 seconds.
  3. Years. A year is about 365.25 · 24 · 3600 ≈ 3.16 · 107 seconds, so 1.27 · 1021 / 3.16 · 107 ≈ 4 · 1013 years.

The universe is about 1.4 · 1010 years old, so this is some three thousand times its age. The same computer does n2 = 10,000 steps in 10 microseconds.

Faster hardware cannot rescue an exponential algorithm: a computer a thousand times faster would still need 4 · 1010 years. A thousandfold speedup lets a 2n algorithm handle an n only 10 larger in the same time (210 ≈ 1000), but a quadratic one an n about 32 times larger (√1000 ≈ 31.6). On large inputs the choice of algorithm matters far more than the hardware.

2.3From code to complexity

Now for the real task: reading a piece of code and stating its complexity. We seldom need to count every operation; what matters is how often the loops run. Loop-free code is O(1), and loops fall into a handful of patterns. We introduce the patterns, then apply them to sum, linear and binary search, duplicate checking and prefix averages.

2.3.1Loop patterns

A loop costs the number of iterations times the cost of one pass through the body; with a constant-cost body, that is proportional to the iteration count. Table 2.8 lists the common patterns, their counts for n = 16, and their classes.

Table 2.8: Loop patterns and how many times the body runs.
PatternBody runsn = 16Class
for (i = 0; i < n; i++)n16O(n)
for (i = 0; i < n; i += 2)n / 28O(n)
for (i = 1; i < n; i *= 2)⌈log2 n⌉4O(log n)
nested: i < n, j < nn2256O(n2)
nested: i < n, j < in(n − 1)/2120O(n2)
two consecutive i < n loops2n32O(n)

Simple loop. i goes from 0 up by 1 and stops at n: values 0, 1, …, n − 1, so n iterations.

Step of 2. i takes 0, 2, 4, …: n/2 iterations for even n, ⌈n/2⌉ for odd. With a step of 2 or 100, the count is a fixed fraction of n and the loop is O(n); a bigger step speeds it up without changing its class.

Doubling loop. i doubles — 1, 2, 4, 8, … — while below n. After k iterations i = 2k, so the loop stops the first time 2k ≥ n, at k = ⌈log2 n⌉. For n = 16, i is 1, 2, 4, 8 and stops at 16: 4 iterations (traced in 2.3.2). A loop that starts i at n and halves it behaves the same way.

Nested loops. The inner loop runs in full on every outer iteration. If it runs n times regardless of the outer loop, the total is n · n = n2. If its bound depends on the outer variable (j < i), we add instead of multiplying: 0 iterations at i = 0, 1 at i = 1, …, n − 1 at i = n − 1, for a total of

0 + 1 + 2 + … + (n − 1) = n(n − 1)/2

This formula is easy to remember: write the sum forward and backward and add the rows term by term — 0 + (n − 1), 1 + (n − 2), … — giving n pairs of n − 1, which is twice the sum. For n = 16, 16 · 15 / 2 = 120. The inner loop averages half the outer count, but the factor 1/2 is dropped and the result is still O(n2).

Consecutive loops. One loop after another adds costs: n + n = 2n, which is O(n) — far cheaper than nesting.

Two general rules follow:

  • Consecutive blocks add, and the largest dominates: O(n) + O(n2) = O(n2). A linear scan followed by a nested loop costs what the nested loop costs.
  • Nested loops multiply: outer iterations × inner iterations — or, if the inner count depends on the outer variable, the sum over all iterations.
Example 2.6

What is s after this code runs, and what is its complexity? (n = 16)

for (i = 0; i < n; i++) s++;
for (i = 0; i < n; i++) for (j = 1; j < n; j *= 2) s++;

  1. First block. A simple loop: 16 iterations, s becomes 16. O(n).
  2. Second block, inner loop. j is 1, 2, 4, 8: ⌈log2 16⌉ = 4 iterations, independent of the outer variable.
  3. Second block, total. 16 outer iterations of 4 inner ones: 16 · 4 = 64. O(n log n).
  4. Whole fragment. s = 16 + 64 = 80, and O(n) + O(n log n) = O(n log n).
Common mistake

Memorizing “two nested loops means n2.” That holds only when both loops run in proportion to n. With an inner j *= 2 the total is n log n; with an inner loop of fixed length (say 10) it is O(n). Find each loop’s count separately, then multiply.

2.3.2Code trace: countSteps(16)

Here is a doubling loop inside a method. countSteps counts how many doublings take 1 up to n:

int countSteps(int n) {
  int k = 0;
  for (int i = 1; i < n; i *= 2)
    k++;
  return k;
}

The counter k starts at 0, i starts at 1 and doubles via i *= 2, and each iteration increments k, so at the end k is the number of iterations.

Table 2.9: Trace of countSteps(16).
Iterationi at starti < 16?k (after)
11true1
22true2
34true3
48true4
—16false, loop endsreturns 4
12345678910111213141516▲ in
12345678910111213141516▲ i = n
Figure 2.9: countSteps(16) on a number line. Top: the third iteration, after i has jumped from 1 to 2 and from 2 to 4. Bottom: the final state; i jumps from 8 to 16 and the loop ends because i = n. Each jump is twice as long as the one before.

Figure 2.9 shows why the loop runs so few times: each jump doubles the previous one — 1, 2, 4, 8 — so four jumps reach 16, skipping most of the numbers on the way.

The result, 4, is log2 16; in general countSteps(n) returns ⌈log2 n⌉. It grows very slowly: countSteps(17) = 5, countSteps(1000) = 10, countSteps(16000) = 14. A thousandfold larger n adds only about 10 iterations, because 210 = 1024 ≈ 1000. The method is O(log n).

Common mistake

Starting a doubling loop at 0. In for (int i = 0; i < n; i *= 2), i stays 0 because 0 · 2 = 0, the condition stays true, and the loop never ends. Doubling loops start at 1; halving loops must stop before reaching 0.

2.3.3Three methods, three classes

Three short methods, each taking an int array and differing only in loop structure:

O(1): the first element.

int first(int[] a) {
    return a[0];
}

No loop: one array access and a return. Indexing computes the address directly — start address plus index times element size — regardless of the array’s length. The method is O(1).

O(n): the sum.

int sum(int[] a) {
    int s = 0;
    for (int x : a)  // n times
        s += x;
    return s;
}

for (int x : a) is the enhanced for (for-each) loop: x takes each element in turn. The body — one addition, one assignment — runs n times; the code outside the loop is constant. The method is O(n).

O(n2): checking for duplicates.

boolean hasDuplicates(int[] a) {
    for (int i = 0; i < a.length; i++)
        for (int j = i + 1; j < a.length; j++)
            if (a[i] == a[j])
                return true;
    return false;
}
// outer loop n, inner loop ≈ n/2
// → n(n−1)/2 comparisons

For each index i, the inner loop compares a[i] with every element to its right, starting at j = i + 1. That start ensures no element is compared with itself and each pair only once (after a[0] vs a[3], there is no a[3] vs a[0]). The method returns true at the first equal pair, false if there is none.

In the worst case, with no duplicates, the inner loop runs n − 1 times for i = 0, n − 2 for i = 1, …, once for i = n − 2, and not at all for i = n − 1:

(n − 1) + (n − 2) + … + 1 + 0 = n(n − 1)/2 = n2/2 − n/2

Dropping constants and the smaller term gives O(n2): about n/2 inner iterations times n outer ones is still quadratic. This is also the number of distinct pairs among n elements, so any algorithm that examines every pair makes at least this many comparisons.

The best case finds a match at once: if a[0] == a[1], one comparison suffices, Θ(1) — exactly the array of zeros in 2.1.3.

2.3.4Code trace: sum()

Tracing sum together with its caller also shows how a method call proceeds.

int sum(int[] a) {
    int s = 0;
    for (int x : a) {
        s += x;
    }
    return s;
}

// in main:
System.out.println(sum(a));

Let a = {7, 2, 9} in main. Calling sum(a) pushes a new stack frame holding the parameters and locals (s, x). The parameter a refers to the same array as in main: a Java array variable is a reference holding the array’s location, and only the location is passed, so the call costs the same for any array size.

Table 2.10: Trace of sum({7, 2, 9}).
StepxsNotes
call——new frame pushed; a refers to the same array
s = 0—0the sum is initialized
iteration 1770 + 7
iteration 2297 + 2
iteration 39189 + 9
loop ends—18no elements left
return——frame popped; 18 goes back to the caller and println prints 18

The body ran n = 3 times. Pushing the frame, initializing s, returning and printing do not depend on the array size; with 3 million elements only the iteration count would change. The time has the form c1 · n + c2: O(n).

Common mistake

Assuming that passing an array copies it and takes O(n). Java passes the reference, so the call is O(1). As a consequence, a[0] = 100 inside the method also changes the array in main — they are the same array.

2.3.5Linear search

Linear search, the simplest way to find a value, examines the elements in order from the front.

int linearSearch(int[] a, int target) {
    for (int i = 0; i < a.length; i++)
        if (a[i] == target) return i;
    return -1;
}

Each iteration compares a[i] with the target; on a match the method returns i at once. If the loop finishes, the target is absent and the method returns -1, which, not being a valid index, means “not found.”

We trace it on a sorted array, a = {2, 5, 8, 12, 16, 23, 38, 56, 72, 91} (n = 10), with target 23.

Table 2.11: Trace of linearSearch(a, 23).
ia[i]a[i] == 23?Result
02noi advances
15noi advances
28noi advances
312noi advances
416noi advances
523yesreturn 5
205182123164235386567728919▲ i
Figure 2.10: The last step of linear search. The faded cells (0–4) were ruled out one at a time; 23 is found at index 5. Each step rules out just one element.

The target took 6 comparisons. Linear search ignores the fact that the array is sorted: learning that 2 is less than 23 says nothing about the other elements, so each step rules out one element (Figure 2.10). A target that is last or absent (say 40) would cost all 10 comparisons.

The analysis matches Example 2.3: best case (target first) 1 comparison, Θ(1); worst case (last or absent) n, Θ(n); average with equally likely positions (n + 1)/2, also Θ(n). With only a few variables, the extra space is O(1).

Linear search’s strength is that it needs no sorting, and on an unsorted array nothing is faster: we cannot rule out an element without looking at it, so the worst case must examine them all.

2.3.6Binary search

A sorted array allows much better. Nobody searches a phone book from page one: we open it in the middle, set aside the half that cannot contain the name, and repeat on the rest. Binary search is this method as an algorithm.

int binarySearch(int[] a, int target) {
    int lo = 0, hi = a.length - 1;
    while (lo <= hi) {
        int mid = (lo + hi) / 2;
        if (a[mid] == target) return mid;
        if (a[mid] < target) lo = mid + 1;
        else hi = mid - 1;
    }
    return -1;
}
  • lo and hi bound the range that could hold the target, initially the whole array: lo = 0, hi = a.length - 1.
  • while (lo <= hi): the search continues while the range is nonempty; lo > hi means it is empty.
  • mid = (lo + hi) / 2 is the middle index. Integer division in Java discards the fraction: (0 + 9)/2 = 4.
  • If a[mid] == target, mid is returned.
  • If a[mid] < target, the sorted order puts the target right of mid: lo = mid + 1 discards the left half, middle included.
  • Otherwise a[mid] > target and the target can only be to the left: hi = mid - 1.
  • If the loop ends, -1 is returned.

The power lies in one comparison discarding half the range. In linear search, a[i] ≠ target rules out one element; in binary search, a[mid] < target tells us, thanks to the sorted order, that everything left of mid is smaller too.

Same array, same target: a = {2, 5, 8, 12, 16, 23, 38, 56, 72, 91}, target 23.

Table 2.12: Trace of binarySearch(a, 23).
Iterationlohimida[mid]Decision
10941616 < 23 → lo = 5
25975656 > 23 → hi = 6
356523equal → return 5
021528312416523638756872991▲ lo▲ hi▲ mid
021528312416523638756872991▲ lo▲ hi▲ mid
021528312416523638756872991▲ lomid▲ hi
Figure 2.11: The three iterations of binarySearch(a, 23) (top to bottom). Iteration 1: range 0–9, mid = 4. Iteration 2: the left half is gone, range 5–9, mid = 7. Iteration 3: the right side is gone too, range 5–6, mid = 5, and 23 is found. Faded cells have been ruled out.

In iteration 1, mid = (0 + 9)/2 = 4 and a[4] = 16 is less than 23, so lo = 5: five elements gone in one comparison. In iteration 2, mid = (5 + 9)/2 = 7 and a[7] = 56 is too large, so hi = 6. In iteration 3, mid = (5 + 6)/2 = 5 and a[5] = 23: found.

Three iterations against linear search’s six comparisons, the range halving each time — 10, 5, 2. A twofold gain on this small array becomes dramatic on large ones.

Common mistake

Using binary search on an unsorted array. Concluding from a[mid] < target that everything to the left is smaller is valid only for sorted data. On unsorted data the method raises no error but may return -1 for a value that is present. In {5, 1, 3}, 1 is found at once at mid = 1, but a search for 5 sees a[1] = 1 < 5, discards the left half, and never finds it.

2.3.7The worst case of binary search

The worst case is an absent target, which shrinks the range all the way down. Let us search the same array for 40, which would lie between 38 and 56.

Table 2.13: Trace of binarySearch(a, 40); the target is not in the array.
Iterationlohimida[mid]Decision
10941616 < 40 → lo = 5
25975656 > 40 → hi = 6
35652323 < 40 → lo = 6
46663838 < 40 → lo = 7
—76——7 ≤ 6 false → return -1
021528312416523638756872991▲ lohimid
021528312416523638756872991▲ hi▲ lo
Figure 2.12: The end of binarySearch(a, 40). Top: iteration 4, the range down to one element (index 6, value 38). Bottom: after the loop, lo = 7 and hi = 6; lo has passed hi and the range is empty.

The first two iterations match the search for 23. In iteration 3, a[5] = 23 is too small, so lo = 6, leaving only 38 at index 6. In iteration 4, 38 is too small as well, so lo = 7. Now lo = 7 > hi = 6 (Figure 2.12), the loop ends, and the method returns -1.

As a bonus, the final lo is where the target would be inserted: 40 belongs between 38 (index 6) and 56 (index 7), at index 7. Some algorithms for inserting into sorted arrays exploit this.

Even the worst case took just 4 iterations, against 10 comparisons for linear search. The following theorem explains why.

Theorem 2.2 (Iterations of binary search)

On a sorted array of n ≥ 1 elements, binarySearch makes at most ⌊log2 n⌋ + 1 iterations. Binary search therefore runs in O(log n) time.

Let the range hold m elements. An unsuccessful iteration keeps only the part left or right of mid, and discards the middle too, so at most ⌊m/2⌋ elements remain. Starting from n and at least halving each time, at most ⌊n/2k⌋ remain after k iterations — zero once 2k > n. So after k = ⌊log2 n⌋ + 1 iterations the range is certainly empty.

For n = 10, ⌊log2 10⌋ + 1 = 3 + 1 = 4, and the counts went 10 → 5 → 2 → 1 → 0. Table 2.14 compares the worst cases of the two searches for several sizes.

Table 2.14: Worst-case comparisons for linear search and iterations for binary search.
nLinear searchBinary search (⌊log2 n⌋ + 1)
10104
1,0001,00010
1,0241,02411
1,000,0001,000,00020

For a million sorted elements, binary search needs at most 20 iterations where linear search may need a million comparisons — a factor of fifty thousand that widens as n grows. The price is keeping the array sorted, easily paid when it is sorted once and searched many times.

Common mistake

Computing the middle as (lo + hi) / 2, which overflows on very large arrays. A Java int holds at most 231 − 1 ≈ 2.1 · 109, so with lo = 1,500,000,000 and hi = 2,000,000,000, lo + hi = 3.5 · 109 is too large. Java raises no error: the sum silently wraps to a negative number, (lo + hi) / 2 becomes -397483648, and a[mid] throws ArrayIndexOutOfBoundsException. The safe form is lo + (hi - lo) / 2; hi - lo never overflows, and the result is the correct 1750000000. This bug went unnoticed for years in the Java library’s own binary search (Bloch, 2006).

2.3.8Code trace: hasDuplicates()

Now we trace hasDuplicates from 2.3.3 on a = {4, 7, 1, 7}, where 7 appears twice.

boolean hasDuplicates(int[] a) {
    for (int i = 0; i < a.length; i++)
        for (int j = i + 1; j < a.length; j++)
            if (a[i] == a[j])
                return true;
    return false;
}
Table 2.15: Trace of hasDuplicates({4, 7, 1, 7}).
Comparisonija[i] vs a[j]Result
1014 vs 7different
2024 vs 1different
3034 vs 7different
4127 vs 1different
5137 vs 7equal → return true
04172137▲ i▲ j 04172137▲ i▲ j
Figure 2.13: The first and last comparisons in the trace of hasDuplicates. Left, i = 0, j = 1: 4 is compared with 7. Right, i = 1, j = 3: the two 7s are equal and the method returns true.

With i = 0, a[0] = 4 is compared with the three elements to its right, none equal. With i = 1, the inner loop starts at j = 2: a[1] = 7 is compared with 1, then with a[3] = 7 — a match, so the method returns true on the fifth comparison, and the i = 2 iteration never runs.

Without duplicates — for {4, 7, 1, 9}, say — all pairs would be compared: n(n − 1)/2 = 4 · 3 / 2 = 6 comparisons. This O(n2) worst case is why the times in Table 2.2 roughly quadrupled with each doubling.

We can do better: after sorting, equal elements are neighbors, so O(n log n) for the sort plus an O(n) scan suffices. The next example turns a quadratic algorithm into a linear one another way: by cutting redundant work.

2.3.9The prefix averages problem

How different can two algorithms for the same problem be in cost? A concrete problem answers that (Goodrich et al., Section 4.3).

Definition 2.5

The prefix averages problem: given an array x[0..n−1] of n numbers, compute the array a whose entry for each j is the average of the first j + 1 elements:

a[j] = (x[0] + x[1] + … + x[j]) / (j + 1),   j = 0, 1, …, n − 1

A “prefix” is a part that starts at the beginning, x[0..j], and we want every prefix’s average. Examples abound: a stock’s average daily price since January, a student’s grade average since the start of term. Moving averages and financial trend analysis rest on similar calculations.

Example 2.7

Compute the prefix averages of x = [4, 8, 6, 2].

  1. a[0] = 4 / 1 = 4.0
  2. a[1] = (4 + 8) / 2 = 12 / 2 = 6.0
  3. a[2] = (4 + 8 + 6) / 3 = 18 / 3 = 6.0
  4. a[3] = (4 + 8 + 6 + 2) / 4 = 20 / 4 = 5.0

Result: a = [4.0, 6.0, 6.0, 5.0].

x04182632a4.06.06.05.0a[3] = (4 + 8 + 6 + 2) / 4 = 5.0
Figure 2.14: The last step of prefix averages: a[3] is the average of x[0..3], (4 + 8 + 6 + 2)/4 = 5.0. The green cells were computed in earlier steps.

There is a repetition here: 18, the numerator of a[2], is just 12, the numerator of a[1], plus x[2] = 6. The two solutions differ on exactly this point. The first recomputes each sum from scratch and takes O(n2); the second carries the sum forward and takes O(n).

2.3.10prefixAverage1: summing from scratch

double[] prefixAverage1(double[] x) {
  int n = x.length;
  double[] a = new double[n];
  for (int j = 0; j < n; j++) {
    double total = 0;
    for (int i = 0; i <= j; i++)
      total += x[i];
    a[j] = total / (j + 1);
  }
  return a;
}
  • a = new double[n] allocates the result array, which Java fills with 0.0.
  • The outer loop computes one prefix average per j.
  • double total = 0 sits inside the outer loop, so the sum restarts from zero for every j.
  • The inner loop adds x[0] through x[j] to total. The condition is i <= j; with <, x[j] would be left out.
  • a[j] = total / (j + 1) stores the average; total is a double, so the fraction is kept.

Table 2.16 shows every inner iteration for x = [4, 8, 6, 2].

Table 2.16: Trace of prefixAverage1([4, 8, 6, 2]).
jElements addedtotal (intermediate values)Additionsa[j]
0x[0]0 → 4.014.0 / 1 = 4.0
1x[0], x[1]0 → 4.0 → 12.0212.0 / 2 = 6.0
2x[0] … x[2]0 → 4.0 → 12.0 → 18.0318.0 / 3 = 6.0
3x[0] … x[3]0 → 4.0 → 12.0 → 18.0 → 20.0420.0 / 4 = 5.0
x04182632a4.06.06.0total18.0
x04182632a4.06.06.05.0total20.0
Figure 2.15: Two moments from the trace of prefixAverage1. Top: j = 2 has finished, total = 18.0, a[2] = 6.0. Bottom: j = 3 has finished; total restarted from zero, re-added all four elements and reached 20.0, so a[3] = 5.0.

The third column exposes the weakness: x[0] = 4 is added four times, x[1] = 8 three times, x[2] = 6 twice. Each iteration forgets the previous sum and redoes the same additions.

Iteration j runs the inner loop j + 1 times, so the total number of additions is

1 + 2 + … + n = n(n + 1)/2

For n = 4 that is 4 · 5 / 2 = 10, the 1 + 2 + 3 + 4 of the table. The expression n2/2 + n/2 is Θ(n2) by Theorem 2.1. The extra space is O(n), for the result array a.

2.3.11prefixAverage2: a running sum

The cure is to carry the sum forward: the sum of prefix j is the sum of prefix j − 1 plus x[j].

double[] prefixAverage2(double[] x) {
  int n = x.length;
  double[] a = new double[n];
  double total = 0;
  for (int j = 0; j < n; j++) {
    total += x[j];
    a[j] = total / (j + 1);
  }
  return a;
}

Two small changes decide everything: double total = 0 has moved outside the loop, so the sum persists, and the inner loop has become the single line total += x[j], which turns the sum of x[0..j−1] into the sum of x[0..j].

Table 2.17: Trace of prefixAverage2([4, 8, 6, 2]).
jtotal += x[j]totala[j]
start—0.0—
0+ 44.04.0 / 1 = 4.0
1+ 812.012.0 / 2 = 6.0
2+ 618.018.0 / 3 = 6.0
3+ 220.020.0 / 4 = 5.0
x04182632a4.06.0total12.0
x04182632a4.06.06.05.0total20.0
Figure 2.16: Two moments from the trace of prefixAverage2. Top: j = 1; only x[1] = 8 is added to the previous 4.0, giving total = 12.0 and a[1] = 6.0. Bottom: the last iteration, total = 20.0, a[3] = 5.0.

The result, [4.0, 6.0, 6.0, 5.0], matches prefixAverage1, with 4 additions instead of 10. Each of the n iterations does one addition, one division and one assignment, so prefixAverage2 runs in Θ(n) time, again with O(n) extra space for the result.

Common mistake

Putting double total = 0 inside the loop of prefixAverage2. Then total resets every iteration and the code computes a[j] = x[j] / (j + 1): for x = [4, 8, 6, 2], [4.0, 4.0, 2.0, 0.5]. The whole point of a running sum is that it survives between iterations.

2.3.12Comparing the two solutions

Here are the addition counts side by side:

Table 2.18: Additions performed for prefix averages.
nprefixAverage1: n(n + 1)/2prefixAverage2: n
105510
1005,050100
1,000500,5001,000
1,000,000500,000,500,0001,000,000
20406078123456789101112nprefixAverage1: n(n+1)/2prefixAverage2: n
Figure 2.17: Additions for n = 1 … 12. The bars of prefixAverage1 climb quickly as n(n + 1)/2; those of prefixAverage2 grow linearly as n.

At n = 10 the factor of 5.5 may seem minor. At n = 106, though, prefixAverage1 performs about 5 · 1011 additions against 106; at 109 additions per second, that is about 500 seconds (over 8 minutes) versus 1 millisecond. The ratio, (n + 1)/2, keeps growing with n.

The gap comes from removing repeated work: prefixAverage2 remembers the previous sum, prefixAverage1 forgets it and starts over. Saving a result and reusing it underlies many efficient algorithms, and the first step in speeding one up is often to ask: “Am I doing the same work more than once?”

Both use the same space — an n-cell result array and a few variables — so the speedup cost no memory.

2.4Space, correctness and robustness

So far we have measured only time. Three criteria of a good algorithm remain (2.1.2): memory, correctness and robustness. Memory is measured with the same tools as time; correctness and robustness are demonstrated and tested.

2.4.1Space complexity

Space complexity describes how an algorithm’s memory use grows with the input size, in Big O, with constants and smaller terms dropped.

The input itself is usually not counted. The array passed to arrayMax was in memory before the call; the method’s own memory is just currentMax and i. Memory used beyond the input is extra space, and this is what analyses usually measure — though they should say whether the input is counted.

s n cells n × n
Figure 2.18: Three space classes (left to right). O(1): a single variable. O(n): an array of n cells. O(n2): an n × n matrix.

Figure 2.18 shows the three basic classes:

  • O(1), constant space: a few variables, independent of the input size. arrayMax, sum, linearSearch, binarySearch and hasDuplicates use only such variables (the maximum, the sum, indices, lo, hi, mid).
  • O(n), linear space: a new array the size of the input, like the result array a of prefixAverage1 and prefixAverage2.
  • O(n2), quadratic space: an n × n matrix, such as a table of distances between n cities. For n = 10,000 that is a hundred million cells — about 400 MB of 4-byte ints.

Memory efficiency matters most on constrained systems — embedded devices, phones, servers handling thousands of requests at once — but no memory is unlimited, and a quadratic-space algorithm exhausts even the largest machine once n is big enough.

An easily overlooked item is the call stack. Each call pushes a frame (2.3.4) that stays until the method returns. A loop uses one frame, but a method that calls itself — a recursive method — has as many frames open as its call depth. A recursion n levels deep uses O(n) extra space without allocating a single array.

Example 2.8

Two ways to reverse an array:

  1. With a new array. Allocate b = new int[n] and write b[i] = a[n − 1 − i]. Time O(n), extra space O(n).
  2. In place. Start i at the front and j at the back; swap a[i] and a[j], increment i, decrement j, and continue while i < j. The swap needs one temporary variable. Time O(n), extra space O(1).

Same time, different memory. The in-place version, however, modifies its input; if the original is needed later, a new array is unavoidable.

Common mistake

Forgetting the returned array, or counting the input, when computing extra space. prefixAverage2 seems to need only a few variables, but the double[n] result array is new, so its extra space is O(n). Conversely, the array arrayMax receives is not part of its extra space, which is O(1).

2.4.2Correctness

A fast algorithm with wrong results is worthless, so correctness comes before cost.

Definition 2.6

An algorithm is correct if, for every valid input, it produces the expected output in finite time. It must halt, and when it halts its result must be right.

“Every valid input” is the crux: inputs are infinite and cannot all be tried. Four tools help:

  • Test cases. Run the algorithm on inputs with known answers — arrayMax({3, 8, 2, 9, 4}) must return 9. Tests are excellent at finding bugs but prove nothing: an untested input may still fail.
  • Edge cases. Bugs hide at the boundaries. Always try an empty input, a single element, extreme values, identical elements, and sorted or reverse-sorted arrays. For arrayMax: a one-element array, the maximum at the front, back and middle, and all-negative numbers.
  • Loop invariant. A property that holds on every iteration, from which correctness is proved for all inputs, unlike testing (2.4.3).
  • Counterexample. A single input proves an algorithm wrong. Correctness is a claim about all inputs; incorrectness needs just one.
Example 2.9

Is this version of arrayMax correct?

int currentMax = 0;
for (int i = 0; i < a.length; i++) if (a[i] > currentMax) currentMax = a[i];
return currentMax;

  1. Testing. It returns 9 for {3, 8, 2, 9, 4} and 3 for {1, 2, 3}; every test with positive numbers passes.
  2. Suspicion. The initial 0 is not an element of the array. If all elements are negative, none can beat currentMax.
  3. Counterexample. {-3, -1, -7} returns 0. The answer is −1, and 0 is not even in the array.

One counterexample settles it. The fix is to start from an element of the array, currentMax = a[0] — exactly how arrayMax is written in 2.1.6.

0318223944▲ icurrentMax = max(a[0..2]) = 8Invariant:currentMax is the largest of a[0..i−1]at the start of every iteration.At exit i = n → correct result.
Figure 2.19: The loop invariant for arrayMax. At the start of the iteration with i = 3, the processed part is a[0..2] and currentMax = 8 is its largest element. When the loop ends, i = n and the processed part is the whole array.

2.4.3Proof by loop invariant

A test says “it works on these inputs”; a proof says “it works on every input.” For loops, the handiest proof tool is the loop invariant.

Definition 2.7

A loop invariant is a statement that is true at the start of every iteration of a loop. A proof by invariant has three steps:

  • Initialization: the statement is true before the first iteration.
  • Maintenance: if it is true at the start of an iteration, it is still true when the iteration ends (at the start of the next).
  • Termination: when the loop ends, the statement shows that the algorithm is correct.

This is induction in disguise: initialization is the base case, maintenance the inductive step, and termination links the statement to the result.

Theorem 2.3 (Correctness of arrayMax)

For every array a with at least one element, arrayMax(a) returns the largest element of the array.

Proof. The invariant: at the start of each iteration, currentMax is the largest of a[0..i−1].

  • Initialization. Before the first iteration, i = 1 and currentMax = a[0], the largest (and only) element of a[0..0].
  • Maintenance. Suppose currentMax is the largest of a[0..i−1]. If a[i] is larger, currentMax = a[i], which beats the old maximum and so all earlier elements: it is the largest of a[0..i]. Otherwise currentMax is unchanged and, being at least a[i], remains the largest of a[0..i]. After i++, this reads “the largest of a[0..i−1].”
  • Termination. i grows by 1 per iteration and the loop stops at i = n, so it ends in finite time, and the invariant then says currentMax is the largest of a[0..n−1] — the whole array. The method returns it. ∎

Figure 2.19 shows the invariant for {3, 8, 2, 9, 4} at i = 3: currentMax = 8 is the maximum of the green part, a[0..2] = {3, 8, 2}; the next element, 9, is larger, so the iteration ends with currentMax = 9, the maximum of a[0..3].

The theorem requires “at least one element”: an empty array has no a[0], and initialization fails. This is not a gap in the proof but a precondition of the method; what should happen when it is violated is a question of robustness.

Common mistake

Thinking an invariant must hold mid-iteration. It must be true at the start of each iteration and may be briefly broken inside one. In prefixAverage2, “total is the sum of x[0..j−1]” breaks after total += x[j] and is restored by j++ — which is just what the maintenance step shows.

2.4.4Robustness

Correctness concerns valid inputs, but real programs also meet invalid ones: an empty file, a missing line, a malformed number. Robustness is the ability to cope with them. A robust algorithm neither crashes nor silently produces a wrong result outside the normal range; it either handles the input sensibly or reports the problem clearly.

Robustness is tested from four angles:

Table 2.19: Situations in which robustness is tested.
SituationWhat is testedExample
Invalid inputsHandling inappropriate or unexpected inputsnull, negative size
Faulty dataHandling corrupt or incomplete datamissing line, wrong format
Large data setsProcessing very large inputs108 records
OverloadBehavior under heavy loadthousands of simultaneous requests

Each is a real source of failures. Given an empty array, arrayMax throws ArrayIndexOutOfBoundsException at a[0]; given null, NullPointerException. Either stops the program, but neither message states the cause. The binary search overflow (2.3.7) is a large-data robustness problem: flawless on small arrays, broken beyond a billion elements.

A robust version checks its precondition and reports violations clearly:

int arrayMaxChecked(int[] a) {
  if (a == null || a.length == 0)
    throw new IllegalArgumentException("Array must not be empty");
  int currentMax = a[0];
  for (int i = 1; i < a.length; i++)
    if (a[i] > currentMax)
      currentMax = a[i];
  return currentMax;
}

The first line catches both invalid cases. a == null comes first, and || skips its right side when the left is true, so the length of a null array is never read. Invalid input raises IllegalArgumentException with a message saying what is wrong; the rest is identical to arrayMax.

The check costs at most two comparisons, O(1), and the method stays Θ(n). Robustness is often this cheap, so checking preconditions is a good habit, especially in methods others will call.

What to do with an empty array is a design decision: throw an exception, or return a special value — Java’s OptionalInt exists for this. The one wrong choice is a silent, meaningless value, like the 0 that the faulty version in Example 2.9 returns, leading the caller to believe the array’s largest element is 0.

Common mistake

Equating robustness with “not crashing.” A program that swallows an error and carries on with a wrong value doesn’t crash, but it isn’t robust; a robust one notices the problem and reports it. A silent wrong result is more dangerous than a loud crash, because it slips unnoticed into other computations.

2.5Common mistakes

Table 2.20 collects the chapter’s most important mistakes.

Table 2.20: Common misconceptions and mistakes in algorithm analysis.
Misconception or mistakeThe truth
“O = worst case”O is an upper bound; best and worst case are separate ideas, and O, Ω, Θ apply to any case.
“O(1) is always fast”The constant can be large: an O(1) operation of 1000 steps per call is slower than an n-step O(n) one while n < 1000.
“Nested loops = n2”With an inner j *= 2 it is n log n; with a fixed-length inner loop, O(n).
“The base of the logarithm matters”log2 n and log10 n differ by a constant factor; both are O(log n).
(lo + hi) / 2 overflowThe int overflows on very large arrays; lo + (hi - lo) / 2 is safe.
Comparing arrays with a == b== compares references; use Arrays.equals(a, b) for contents.
Ignoring warm-up and noise when timingThe first run is slow because of the JIT; repeat and take the median.
Measuring the worst case with the wrong inputAn array of zeros is the best case of hasDuplicates; the worst case needs distinct values.

The array comparison mistake is common when testing methods like hasDuplicates. After int[] a = {1, 2}; int[] b = {1, 2};, a == b is false: the variables refer to two separate arrays, and == asks “same array?”, not “same contents?”. Arrays.equals(a, b) compares element by element and returns true, in O(n) time; == compares two references in O(1).

The lesson of “O(1) is always fast” applies to every class. A linear algorithm of 1000n steps beats a quadratic one of n2 steps only when n > 1000; below that the quadratic one is faster. Asymptotic analysis says which wins as inputs grow; for small inputs, the constants matter too.

2.6Summary

  • An algorithm is a method of finite, precise and effective steps that is correct for every valid input, judged by time, space, correctness and robustness.
  • Empirical analysis times code with System.nanoTime(), taking the median of repeated runs after a warm-up. In a doubling experiment, T(2n)/T(n) ≈ 2k means T(n) ≈ c · nk. It depends on hardware, covers only the inputs tried, and needs complete code.
  • Counting primitive operations is hardware-independent and valid for all inputs. arrayMax performs 3n + 1 operations at best and 4n at worst, both Θ(n).
  • f(n) = O(g(n)): for some c > 0 and n0, f(n) ≤ c · g(n) for every n ≥ n0 (upper bound); Ω is a lower bound, Θ both (tight bound). Constant factors and smaller terms are dropped, a degree-d polynomial is Θ(nd), and the base of a logarithm doesn’t matter.
  • Best, average and worst case specify which input’s cost we study, independently of the bound notations. The worst case is a guarantee, the average case assumes a distribution, the best case alone misleads. Amortized analysis divides the total cost of a long sequence by its length; adding to a growable array is amortized O(1).
  • Common classes, slowest-growing first: O(1), O(log n), O(n), O(n log n), O(n2), O(2n). At 109 operations per second, 2n steps for n = 100 take about 4 · 1013 years.
  • Loop patterns: single loop O(n); doubling or halving loop O(log n); two full nested loops O(n2); n outer around log n inner O(n log n). Consecutive blocks add; nested loops multiply.
  • Linear search is O(n) and needs no sorting. Binary search halves the range of a sorted array each iteration, making at most ⌊log2 n⌋ + 1 iterations: O(log n). Compute the middle as lo + (hi - lo) / 2.
  • hasDuplicates compares all pairs: n(n − 1)/2 comparisons in the worst case, O(n2). For prefix averages, prefixAverage1 re-sums each prefix with n(n + 1)/2 additions, O(n2); prefixAverage2 carries the sum with n additions, O(n).
  • Space complexity usually measures extra memory: a few variables O(1), a new array O(n), an n × n matrix O(n2). Recursion also counts the call stack.
  • Correctness is shown with tests, edge cases and loop invariants (initialization, maintenance, termination); one counterexample proves an algorithm wrong. A robust algorithm detects invalid input and reports it clearly.
Table 2.21: Chapter summary.
TopicKey points
Empirical analysistime depends on hardware and input; doubling experiment: T(2n)/T(n) ≈ 2k → T(n) ≈ c · nk
Counting primitive operationsassignment, comparison, arithmetic, array access; hardware-independent, valid for all inputs
O, Ω, Θupper, lower and tight bounds; constant factors and smaller terms are dropped
Casesbest, average, worst; worst case for guarantees, amortized for long sequences of operations
Loop patternssingle loop O(n) · nested O(n2) · halving O(log n) · outer n, inner log n → O(n log n)
Choosing an algorithmprefix averages O(n2) → O(n); binary search on a sorted array O(log n)
Space, correctnessextra memory is measured in Big O too; a fast algorithm that is not correct and robust is worthless

These tools will be used to compare the operations of every data structure that follows. Table 2.22 previews the costs of common operations on structures covered later in the course, to show how differently the same operation can cost.

Table 2.22: Operation costs on the fundamental data structures. † If the position (the previous node) is known. * Average case; O(n) in the worst case.
Data structureAccess ith elementSearchInsertDelete
ArrayO(1)O(n)O(n)O(n)
Linked listO(n)O(n)O(1)†O(1)†
Stacktop onlyO(n)O(1) pushO(1) pop
Queuefront onlyO(n)O(1) enqueueO(1) dequeue
Map / set (hash)—O(1)*O(1)*O(1)*

The first row ties back to this chapter. Accessing the ith element is an address computation, as in first: O(1). Searching an unsorted array is linear search, O(n); a sorted array allows binary search in O(log n). Inserting or deleting in the middle shifts the elements that follow: O(n).

2.7Key terms

Each term links to the course glossary, which gives its Turkish equivalent and its use in other chapters.

2.8Test Yourself

  1. How many times is currentMax updated in arrayMax({5, 1, 7, 7, 2})? With the counting rules of Table 2.3, how many primitive operations are performed?

    Answer

    1 > 5 false, 7 > 5 true (update), 7 > 7 false, 2 > 7 false: a single update, u = 1, since the second 7 is only equal. Total 3n + 1 + u = 3 · 5 + 1 + 1 = 17.

  2. For f(n) = 4n + 7, give c and n0 showing f(n) = O(n). Is f(n) = Θ(n)?

    Answer

    With c = 5, 4n + 7 ≤ 5n for n ≥ 7, so n0 = 7 (c = 11, n0 = 1 also works). Since 4n + 7 ≥ 4n for every n ≥ 1, f(n) = Ω(n), and together f(n) = Θ(n).

  3. Which are true? (a) n2 + 100n = O(n2) (b) 2n+1 = O(2n) (c) n log n = O(n) (d) 4n = O(2n) (e) log10 n = Θ(log2 n)

    Answer

    (a), (b) and (e). (a): n2 + 100n ≤ 101n2 for n ≥ 1. (b): 2n+1 = 2 · 2n, a constant factor. (e): the logarithms differ by a constant factor. (c) is false: n log n / n = log n grows without bound. (d) is false: 4n / 2n = 2n grows without bound.

  4. How many iterations does for (int i = 1; i <= n; i *= 3) s++; make for n = 81? What is its complexity?

    Answer

    i is 1, 3, 9, 27, 81 (the condition is <=, so 81 counts), then 243 ends the loop: 5 iterations, log3 81 + 1. O(log n); base 3 does not change the class.

  5. What is s after this code with n = 10? What is the complexity?

    for (i = 0; i < n; i++) for (j = 0; j < 5; j++) s++;
    for (k = 0; k < n; k++) for (m = 0; m < k; m++) s++;

    Answer

    First block: 10 outer times a fixed 5 inner, 50; O(n). Second: 0 + 1 + … + 9 = 10 · 9 / 2 = 45; O(n2). So s = 95, and the larger block dominates: O(n2).

  6. For a = {2, 5, 8, 12, 16, 23, 38, 56, 72, 91}, how many iterations do binarySearch(a, 91) and binarySearch(a, 2) make? Give lo, hi and mid for each.

    Answer

    For 91: (0, 9, mid 4, 16 < 91) → lo = 5; (5, 9, 7, 56) → lo = 8; (8, 9, 8, 72) → lo = 9; (9, 9, 9, 91) found, returns 9. 4 iterations. For 2: (0, 9, 4, 16 > 2) → hi = 3; (0, 3, 1, 5 > 2) → hi = 0; (0, 0, 0, 2) found, returns 0. 3 iterations. Both are within ⌊log2 10⌋ + 1 = 4.

  7. Linear search runs on 9 elements. If the target is always present and equally likely at each position, how many comparisons are made on average? How many if it is absent?

    Answer

    A target at position k (from 1) costs k comparisons, so the average is (1 + 2 + … + 9)/9 = 45/9 = 5, matching (n + 1)/2. An absent target costs all 9.

  8. A doubling experiment gives 40, 81, 160 and 322 ms for n = 1000, 2000, 4000 and 8000. What is the growth class? About how long will n = 16,000 take?

    Answer

    The ratios 81/40 ≈ 2.03, 160/81 ≈ 1.98, 322/160 ≈ 2.01 give 2k ≈ 2, so k = 1: linear. n = 16,000 should take about 2 · 322 = 644 ms.

  9. A student starts arrayMax with currentMax = 0 and loops from i = 0. Give a counterexample. Which invariant fails at initialization?

    Answer

    {-4, -2, -9} returns 0 instead of −2. The invariant “at the start of each iteration, currentMax is the largest of a[0..i−1]” fails: initially i = 0, a[0..−1] is empty and has no largest element, and 0 is not in the array. With initialization broken, the proof collapses and arrays of negative numbers give wrong results.

  10. What is the extra space of each? (Input not counted.) (a) sum (b) prefixAverage1 (c) a method that builds, from an n-element array, the n × n matrix of a[i] + a[j] for every pair (i, j) (d) countSteps

    Answer

    (a) O(1): just s and x. (b) O(n): the result array a. (c) O(n2): n2 cells. (d) O(1): just k and i.

2.9Further reading

  • M. T. Goodrich, R. Tamassia, and M. H. Goldwasser, Data Structures and Algorithms in Java (6th ed., 2014), Chapter 4: empirical analysis, counting primitive operations, Big O, Omega and Theta, and the prefix averages example, in an order close to these notes and with Java code.
  • R. Sedgewick and K. Wayne, Algorithms (4th ed., 2011), Section 1.4: the doubling experiment and predicting running time from observations, with detailed examples of empirical and mathematical analysis working together.
  • T. H. Cormen, C. E. Leiserson, R. L. Rivest, and C. Stein, Introduction to Algorithms (4th ed., 2022), Chapters 2 and 3: correctness proofs by loop invariant and the formal definitions of asymptotic notation, in more mathematical language.
  • J. Bloch, “Extra, Extra — Read All About It: Nearly All Binary Searches and Mergesorts are Broken” (Google Research Blog, 2006): a short account of how the middle-index overflow surfaced in real software.

2.10References

  1. J. Bloch, “Extra, Extra — Read All About It: Nearly All Binary Searches and Mergesorts are Broken”, Google Research Blog, June 2, 2006.
  2. T. H. Cormen, C. E. Leiserson, R. L. Rivest, C. Stein, Introduction to Algorithms, 4th ed., MIT Press, 2022.
  3. M. T. Goodrich, R. Tamassia, M. H. Goldwasser, Data Structures and Algorithms in Java, 6th ed., Wiley, 2014.
  4. R. Sedgewick, K. Wayne, Algorithms, 4th ed., Addison-Wesley, 2011.

Related pages: Chapter 2 slides · Study questions · Glossary · © Sercan Külcü. All rights reserved.