Analyzing loops and nested loops
On This Lesson 11 topics
Lesson Overview
You can now read Big O. This lesson is about finding it in real code. Almost every beginner's analysis comes down to the same activity: look at the loops, count how often the innermost work runs, simplify. The method is short, but it has traps, and the most common ones are counting only the outer loop and missing the loop hidden inside a library call.
Why this matters: code reviews, interviews, and performance bugs all depend on being able to look at ten lines and say "that is O(n²)" with confidence, and to be right.
This lesson builds on "Big O notation" (BEG-039-L02), on for loops and nested loops (BEG-013), and on List, Set, and contains from the Collections Framework (BEG-030).
Learning Objectives
By the end of this lesson you will be able to:
- Follow a three-step procedure to find a method's Big O from its loops.
- Add the costs of loops that run one after the other, and multiply the costs of loops that are nested.
- Recognize halving or doubling loops as O(log n).
- Analyze a triangular nested loop (inner loop starting at
i + 1). - Find hidden loops in
list.contains,list.indexOf, andlist.remove. - Handle code whose loops run over two different inputs.
Written Explanation
The procedure
- Choose the basic operation: the work in the innermost loop, such as one comparison.
- Count how often it runs as a function of
n. For nested loops, multiply the counts; for loops one after the other, add them. - Simplify with the two rules from L02 (drop constants, keep the fastest-growing term).
I ran each pattern below with n = 1,000 and counted the basic operation. The counts are real.
Pattern 1: loops one after the other add
for (int i = 0; i < n; i++) steps++;
for (int i = 0; i < n; i++) steps++;Result: 2,000. That is n + n = 2n, which is O(n). Adding another loop over n makes 3n, still O(n). Do not multiply loops that are not nested.
Pattern 2: nested loops multiply
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
steps++;Result: 1,000,000, which is n × n, O(n²). If the outer loop runs n times and each time the inner loop runs n times, the body runs n × n times.
What if a quick loop also sits next to the nested pair? Running n steps followed by the nested loops gave 1,001,000, that is n² + n. The n is dropped: O(n²). The nested part dominates.
Pattern 3: the triangular loop
for (int i = 0; i < n; i++)
for (int j = i + 1; j < n; j++)
steps++;Result: 499,500. The inner loop runs n - 1 times for the first i, then n - 2, and so on down to 0. The total is n × (n - 1) / 2: for n = 1,000, 1,000 × 999 / 2 = 499,500. This is the same count you met in the bubble sort lesson. It is about half of n × n, but half is a constant, so it is O(n²). A nested loop that "starts later" is not cheaper in Big O.
Pattern 4: the inner loop is a constant
for (int i = 0; i < n; i++)
for (int j = 0; j < 5; j++)
steps++;Result: 5,000, which is 5n, O(n). The inner loop runs a fixed 5 times regardless of n. Nested loops give O(n²) only when both loops grow with n.
Pattern 5: halving and doubling
for (int i = 1; i < n; i *= 2) steps++;Result: 10. The counter doubles each time: 1, 2, 4, 8, ... , 512, and the loop stops as soon as i reaches n. The number of doublings needed to reach n is about log₂ n, which is 10 for 1,000. Counting down with i /= 2 is the same by symmetry. A loop whose counter is multiplied or divided by a constant is O(log n).
Put that loop inside an n-iteration loop:
for (int i = 0; i < n; i++)
for (int j = 1; j < n; j *= 2) steps++;Result: 10,000, which is n × log n, O(n log n). Multiply, as for any nested loop.
Hidden loops
The most expensive mistake is a loop you cannot see. Some library methods contain their own loop:
list.contains(x)andlist.indexOf(x)comparexagainst elements one by one until a match. For a missing value that is allnelements.list.remove(x)has to findxfirst, andlist.remove(index)shifts every later element one place to the left. TheArrayListdocumentation saysget,set, andsizerun in constant time,addin amortized constant time, and "all of the other operations run in linear time (roughly speaking)". It also saysremove(int)shifts the subsequent elements.
A hidden O(n) call inside an O(n) loop gives O(n²), exactly like the nested loops above.
To make this visible I wrote a small class whose equals increments a counter, put 1,000 of them in an ArrayList, and called contains with 1,000 values that are not in the list:
list contains in loop, equals calls: 1000000That is 1,000 × 1,000. The code has one visible loop and a million comparisons. Putting the same elements in a HashSet and doing the same 1,000 lookups made zero calls to equals in this run, because each lookup went straight to a bucket that did not hold an equal element. The HashSet documentation promises constant-time add, remove, contains, and size, assuming a good hash function; a lookup that finds a bucket with equal-looking entries may still call equals, so this is a trend, not a guarantee of zero.
Two different inputs
If one loop runs over orders (size n) and another over customers (size m), the cost is expressed in both: a nested pair is O(n × m), not O(n²). Do not rename m to n unless the two really are the same size.
Examples
Example 1: classify three small loops (simple)
// A
for (int i = 0; i < n; i++) sum += a[i];
for (int i = 0; i < n; i++) sum += a[i] * 2;
// B
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++) sum += a[i] * a[j];
// C
for (int i = n; i > 0; i /= 2) sum += i;A is n + n = 2n, so O(n). B is n × n, so O(n²). C halves i each time, so O(log n).
Example 2: two passes are not nested (practical)
Find the highest price and the total of an array of prices in two separate loops:
static int maxAndTotal(int[] prices) {
int max = prices[0], total = 0;
for (int p : prices) { if (p > max) max = p; }
for (int p : prices) { total += p; }
return max + total;
}Calling it with {4, 9, 2} printed 24 (9 + 15). The work is n + n = 2n: O(n). Learners sometimes see "two loops" and say O(n²); that is only true if one loop is inside the other. A single pass could do both jobs, but the Big O would be the same, so the two-loop version is not a performance problem.
Example 3: the hidden loop in an order check (realistic)
A system has orders and a List<Integer> customerIds. We want to count orders whose customer is not known:
static int countUnknown(List<Order> orders, List<Integer> customerIds) {
int unknown = 0;
for (Order o : orders) { // n orders
if (!customerIds.contains(o.customerId())) { // contains scans m ids
unknown++;
}
}
return unknown;
}The loop is visible; the second loop is inside contains. Missing customers force a scan of all m ids, so the worst case is O(n × m). With 5,000 orders and 5,000 customers that is up to 25,000,000 comparisons. Copying the ids into a HashSet first changes the test to a hash lookup:
Set<Integer> known = new HashSet<>(customerIds); // one pass: O(m)
for (Order o : orders) {
if (!known.contains(o.customerId())) unknown++; // O(1) average each
}The cost becomes O(m + n). Both versions printed 2 for a four-order test (two orders had unknown customers). L06 builds exactly this kind of speed-up and times it.
Backend Insight
Hidden loops are the commonest source of slow backend code. An endpoint that fetches a list of orders and, for each one, calls a method that searches another list is a nested loop whether or not the source code shows two for keywords. The same shape appears when a loop makes a database call per item: one visible loop, n queries. You will meet that pattern again with databases.
Two practical review habits:
- For each loop, ask what is called inside it, and what each of those calls costs in terms of the data size.
- Notice when two different sizes are in play. The orders table and the customers table grow at different rates, and the cost is a product of both.
Common Mistakes
Mistake 1: Counting only the outer loop
- Incorrect: "One
forloop, so O(n)." - Correct: "One
forloop whose body callslist.containson ann-element list, so O(n²)."
Mistake 2: Multiplying loops that are not nested
Two loops one after the other add: n + n = 2n, O(n). Only a loop inside a loop multiplies.
Mistake 3: Assuming every nested loop is O(n²)
If the inner loop has a fixed bound, like 5 or 12, it is a constant and the nest is O(n). If the inner loop halves or doubles, the nest is O(n log n).
Mistake 4: Forgetting the start value of the inner loop
An inner loop starting at i + 1 runs about half as many times. It is still O(n²), because half is a constant. Do not reduce it to O(n).
Exercises
Try each exercise yourself before opening the solutions.
Level A: Foundation
A1. Give the Big O: (a) three loops one after the other, each over n; (b) a loop over n containing a loop over n; (c) for (int i = n; i > 1; i /= 2).
Show solution A1
A1. (a) 3n, so O(n). (b) O(n²). (c) O(log n).
A2. Name two List methods that contain a hidden loop.
Show solution A2
A2. contains and indexOf (also remove(Object)). remove(int index) shifts elements, which is also a hidden loop in effect.
A3. A loop over n has an inner loop for (int j = 0; j < 10; j++). Big O?
Show solution A3
A3. 10n, so O(n).
Level B: Application
B1. Analyze:
for (int i = 0; i < n; i++) {
for (int j = i; j < n; j++) {
count++;
}
}
for (int k = 0; k < n; k++) {
count++;
}How many times does count++ run for n = 4? What is the Big O?
Show solution B1
B1. For n = 4 the first nest runs 4 + 3 + 2 + 1 = 10 times and the second loop 4 times: 14. In general n(n + 1) / 2 + n, which simplifies to O(n²).
B2. A method loops over a List<String> emails of size n and calls seen.contains(email) on an ArrayList seen that grows by one each time (seen.add(email) at the end of each iteration). Big O in the worst case, and why?
Show solution B2
B2. O(n²). Each contains scans up to i items, so the total is up to 0 + 1 + 2 + ... + (n - 1), about n²/2, which is O(n²). The loop looks single; the hidden scan makes it quadratic. Using a HashSet for seen brings it to O(n) average.
B3. Analyze: for (int i = 1; i < n; i *= 3) for (int j = 0; j < n; j++) count++;.
Show solution B3
B3. The outer loop runs about log₃ n times (i = 1, 3, 9, ...), so the total is n × log n, O(n log n). The base of the log does not matter.
Level C: Challenge
C1. Write static int countPairsWithSum(int[] a, int target) that counts pairs i < j with a[i] + a[j] == target, using nested loops, and add a steps counter to the inner loop body. For an array of length 10, how many steps does it make? What is its Big O?
Show solution C1
C1.
static long steps;
static int countPairsWithSum(int[] a, int target) {
int count = 0;
for (int i = 0; i < a.length; i++) {
for (int j = i + 1; j < a.length; j++) {
steps++;
if (a[i] + a[j] == target) count++;
}
}
return count;
}For length 10 it makes 9 + 8 + ... + 1 = 45 steps (10 × 9 / 2). The Big O is O(n²).
C2. A method takes List<String> a (size n) and List<String> b (size m) and, for each element of a, checks b.contains. Give the Big O. Describe a change that lowers it, and give the new Big O.
Show solution C2
C2. O(n × m): the loop over a runs n times and each b.contains scans up to m items. Copy b into a HashSet<String> once (O(m)) and test set.contains for each element of a (O(1) average), for O(n + m) in total on average.
Mini Challenge
Write four small methods and predict their Big O before running them:
- A single loop that counts how many names in a
List<String>start with"A". - A nested loop that prints every pair of names from the list.
- A loop that calls
names.contains(...)for each name in a second list. - A loop that halves a number until it reaches 1.
Add a steps counter to each and run them for n = 100 and n = 1,000.
Check your work: your predicted classes match the step counts, and for the hidden-loop method you counted the work inside contains (for example by counting equals calls or by reasoning about its scan), not just the outer loop.
Summary & Next Lesson
Key takeaways
- Procedure: pick the basic operation, count how often it runs, simplify.
- Loops one after the other add; loops nested multiply.
- A loop that halves or doubles its counter is O(log n); nested in an
nloop it gives O(n log n). - A triangular nested loop is still O(n²). A nested loop with a constant inner bound is only O(n).
list.contains,list.indexOf, andlist.removehide an O(n) loop; aHashSetorHashMaplookup is O(1) on average.- Loops over two different inputs give a cost in
nandm.
Next lesson
You have been counting steps, that is, time. The next lesson, "Space complexity", counts the other resource: the extra memory an algorithm needs as n grows.
Self-check
- A loop over
ncallslist.indexOf(x)on ann-elementArrayList. Big O? - Why is
for (int j = i + 1; j < n; j++)insidefor (int i ...)still O(n²)? - Loop A runs
nsteps and then loop B runsnsteps. Total? - What is the Big O of
for (int i = 1; i < n; i *= 2)? - Why can't you always rename two different input sizes both to
n?
Show answers
- O(n²), because the hidden scan inside
indexOfis O(n). - It runs about
n²/2times; the half is a constant that Big O drops. 2n, so O(n).- O(log n).
- If the inputs have different sizes, the cost depends on both, for example O(n × m), and one might be small while the other is large.
References
- Oracle, ArrayList (Java SE 21 API). The class description says
size,get, andsetrun in constant time,addin amortized constant time, and the other operations in linear time (roughly speaking); theremove(int)description says later elements are shifted left. - Oracle, HashSet (Java SE 21 API). It states constant-time performance for
add,remove,contains, andsize, assuming the hash function disperses the elements properly. - Princeton University, Analysis of Algorithms (Algorithms, 4th edition). It supports counting a basic operation and using the leading term of the count.
- All step counts and the
equalscall counts above were produced by running the code on JDK 21. I did not fetch documentation forString.indexOf, so it is deliberately left out of the list of hidden loops.
Practice
Apply this lesson with the module's lab, exercises and quiz.
Marking a lesson complete is optional and you can undo it any time. Your progress stays in this browser.