انتقل إلى المحتوى
أمين أبوزيد
منشورBEG-039-L03

تحليل الحلقات والحلقات المتداخلة

  • الدرس 3 من 6
  • 12 دقيقة قراءة
  • مبتدئ
في هذا الدرس 11 موضوعات

هذا الدرس متاح حاليًا باللغة الإنجليزية فقط، ولم تُنشر نسخته العربية بعد.

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, and list.remove.
  • Handle code whose loops run over two different inputs.

Written Explanation

The procedure

  1. Choose the basic operation: the work in the innermost loop, such as one comparison.
  2. Count how often it runs as a function of n. For nested loops, multiply the counts; for loops one after the other, add them.
  3. 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

java
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

java
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

java
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

java
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

java
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:

java
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) and list.indexOf(x) compare x against elements one by one until a match. For a missing value that is all n elements.
  • list.remove(x) has to find x first, and list.remove(index) shifts every later element one place to the left. The ArrayList documentation says get, set, and size run in constant time, add in amortized constant time, and "all of the other operations run in linear time (roughly speaking)". It also says remove(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:

text
list contains in loop, equals calls: 1000000

That 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)

java
// 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:

java
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:

java
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:

java
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 for loop, so O(n)."
  • Correct: "One for loop whose body calls list.contains on an n-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).

أظهر الحل A1

A1. (a) 3n, so O(n). (b) O(n²). (c) O(log n).

A2. Name two List methods that contain a hidden loop.

أظهر الحل 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?

أظهر الحل A3

A3. 10n, so O(n).

Level B: Application

B1. Analyze:

java
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?

أظهر الحل 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?

أظهر الحل 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++;.

أظهر الحل 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?

أظهر الحل C1

C1.

java
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.

أظهر الحل 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:

  1. A single loop that counts how many names in a List<String> start with "A".
  2. A nested loop that prints every pair of names from the list.
  3. A loop that calls names.contains(...) for each name in a second list.
  4. 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 n loop 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, and list.remove hide an O(n) loop; a HashSet or HashMap lookup is O(1) on average.
  • Loops over two different inputs give a cost in n and m.

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

  1. A loop over n calls list.indexOf(x) on an n-element ArrayList. Big O?
  2. Why is for (int j = i + 1; j < n; j++) inside for (int i ...) still O(n²)?
  3. Loop A runs n steps and then loop B runs n steps. Total?
  4. What is the Big O of for (int i = 1; i < n; i *= 2)?
  5. Why can't you always rename two different input sizes both to n?
أظهر الإجابات
  1. O(n²), because the hidden scan inside indexOf is O(n).
  2. It runs about n²/2 times; the half is a constant that Big O drops.
  3. 2n, so O(n).
  4. O(log n).
  5. 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, and set run in constant time, add in amortized constant time, and the other operations in linear time (roughly speaking); the remove(int) description says later elements are shifted left.
  • Oracle, HashSet (Java SE 21 API). It states constant-time performance for add, remove, contains, and size, 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 equals call counts above were produced by running the code on JDK 21. I did not fetch documentation for String.indexOf, so it is deliberately left out of the list of hidden loops.

التطبيق

طبّق هذا الدرس بمختبر الوحدة وتمارينها واختبارها.

المختبر والتمارين الاختبار

تعليم الدرس كمكتمل اختياري ويمكنك التراجع عنه في أي وقت. يبقى تقدمك محفوظًا في هذا المتصفح.

اكتب للبحث في المستويات والمراحل والوحدات والدروس.