Welcome back. In our previous lesson, we established the formal language of asymptotic analysis, learning to use Big O, Big Omega, and Big Theta notations to describe the growth rates of algorithms. You now have the theoretical vocabulary to discuss efficiency.
Today, we will put that theory into practice. This lesson focuses on applying these notations to determine the time complexity of concrete Java code, specifically simple iterative structures like single and nested loops. Your goal is to move from understanding what means to being able to look at a piece of code and confidently determine that it is . This skill is the bedrock of practical algorithm analysis, essential for both writing performant code and succeeding in technical interviews.
From Operation Counting to Asymptotic Analysis
In our last session, we saw that precisely counting every single operation (e.g., ) is tedious and ultimately unnecessary for understanding an algorithm's scaling behavior. The power of asymptotic notation is that it allows us to ignore constant factors and lower-order terms, focusing only on the dominant term that dictates growth as the input size, , becomes large.
This leads to a much more practical, simplified approach to analysis. We will develop a set of rules for analyzing common code structures. Let's begin with the most fundamental building block: the single loop.
Rule 1: Analyzing Single Loops
The most common pattern you'll encounter is a loop that iterates a number of times proportional to the input size.
A great way to transition from detailed counting to simplified analysis is presented in the lecture notes from Cornell University's CS 2110 course. Please read the following sections, which first perform a detailed operation count on two example methods and then introduce the simplified rules we'll be using.
5. Analyzing Complexity | CS 2110 - Cornell: Computer Science
This resource provides a clear, step-by-step analysis of Java code, making the connection between raw operation counts and Big O notation explicit.
First, read the section "Measuring Runtime" to see the paritySplit and hasDuplicates methods that will serve as our running examples. Next, read the section "Counting Basic Operations". This demonstrates the tedious method of counting every single operation for the paritySplit method. The key takeaway is seeing how a function like T(N) = 27N+7 is derived. Don't get bogged down in the exact counts; focus on the process. Finally, read the simplified analysis of paritySplit. This is the crucial part. It shows how we can ignore the constant-time initializations and return statement, and focus on the loop. The loop runs O(N) times and its body does O(1) work, so the total complexity is O(N) \times O(1) = O(N). This is the mental model you should aim to develop.
As you saw, the general rules for single loops are:
- Linear Complexity : A loop is linear if its counter variable is incremented or decremented by a constant amount in each iteration (e.g.,
i++,i--,i += 5). The loop will run approximately times, which is . TheparitySplitmethod is a perfect example. - Constant Complexity : A loop has constant time complexity if it runs a fixed number of times, regardless of the input size
N. For example,for (int i = 0; i < 100; i++).
Rule 2: Analyzing Nested Loops
When one loop is placed inside another, their complexities are multiplied.
The simplest case is when both the outer and inner loops run times. The inner loop's body will execute times, resulting in complexity.
A slightly more complex, but very common, pattern is when the inner loop's number of iterations depends on the outer loop's counter. Let's analyze the hasDuplicates method from the Cornell notes, which exemplifies this.
5. Analyzing Complexity | CS 2110 - Cornell: Computer Science
We will now apply the same analysis method to a nested loop structure.
Return to the section "Counting Basic Operations" and read the detailed analysis for hasDuplicates. Notice how the number of inner loop executions forms an arithmetic series: \sum_{i=0}^{N-1} i = \frac{N(N-1)}{2}. This sum simplifies to \frac{1}{2}N^2 - \frac{1}{2}N, where the dominant term is N^2. Now, read the simplified analysis that follows. It approximates the inner loop as running O(N) times in the worst case. Since the outer loop also runs O(N) times, the total complexity is O(N) \times O(N) = O(N^2). This shortcut is extremely useful.
The video below, by Abdul Bari, provides a helpful visual walkthrough of this exact analysis, tracing the values of i and j and showing how the sum of the series builds up.
This video visually demonstrates the analysis of a dependent nested loop.
Watch the segment from this section, where the presenter traces a loop where j < i. This visualization reinforces why the total number of operations approximates an N^2 function.
Rule 3: Analyzing Logarithmic Loops
Not all loops are linear. Consider a loop where the counter variable is multiplied or divided by a constant in each step.
// Example 1: Multiplication
for (int i = 1; i < n; i *= 2) {
// constant time work
}
// Example 2: Division
for (int i = n; i > 0; i /= 2) {
// constant time work
}
How many iterations do these loops perform?
- In Example 1,
itakes values 1, 2, 4, 8, 16, ..., until it reachesn. This is asking, "How many times do we multiply 1 by 2 to getn?" The answer is . - In Example 2,
itakes valuesn,n/2,n/4, ..., until it reaches 1. This is asking, "How many times do we dividenby 2 to get 1?" The answer is also .
These loops have logarithmic time complexity, . This is a significant improvement over linear time. For an input of size 1 million, an algorithm takes ~1 million steps, while an algorithm takes only ~20 steps.
The following video provides an excellent and clear explanation of this concept.
1.5.2 Time Complexity Example #2
This video breaks down the analysis of multiplicative and divisive loops.
Watch the explanation for a multiplicative loop (i *= 2). Next, see the analysis for a divisive loop (i /= 2). Pay attention to how the number of steps, k, relates to n through an exponential function (e.g., 2^k = n), which is then solved for k using a logarithm.
Rules for Combinations
Now we can combine these basic rules:
- Sequential Loops: If you have loops that run one after another (sequentially), you add their complexities. The largest term dominates.
- Nested Loops: If loops are nested, you multiply their complexities.
- Outer loop , inner loop Total:
- Outer loop , inner loop Total:

Let's Practice
Your background in designing software flows will be useful here, as analyzing complexity is about understanding the flow of execution. Let's apply these rules to a few examples from the Cornell exercises.
5. Analyzing Complexity | CS 2110 - Cornell: Computer Science
These exercises provide a great opportunity to test your understanding on varied loop structures.
Find the "Exercises" section at the end of the article. For each of the following Java methods, try to determine the time complexity in Big O notation before reading my explanation below. static double variance(int[] a) static void bar(int[] a) static int[] numGreaterThan(int[] a, int[] b) static void method3(int[] nums)
Analysis of Practice Problems:
-
variance(int[] a):- This method contains a single
forloop that runs fromi = 0toa.length - 1. - This is a classic linear loop. The work inside the loop (additions, multiplications, array access) is all .
- The operations after the loop are also .
- Complexity: , where is
a.length.
- This method contains a single
-
bar(int[] a):- This method has three nested loops.
- The outer loop runs times (
ifrom 0 to ). - The second loop runs approximately times in the worst case (
jfromito ). - The innermost loop runs a constant 100 times. This is an loop.
- Multiplying the complexities: .
- Complexity: .
-
numGreaterThan(int[] a, int[] b):- This is a fascinating case. It looks like a standard nested loop, which might lead you to guess , where is
a.lengthand isb.length. - However, look closely at the inner
whileloop's counter,j. It is not reset with each iteration of the outerforloop. - The
forloop pointerimoves from0toM-1(total ofMsteps). - The
whileloop pointerjmoves from0toN-1over the entire lifetime of the method call (total ofNsteps). - Since each pointer traverses its respective array only once, the total number of fundamental steps is proportional to the sum of the array lengths.
- Complexity: . This is a common and efficient pattern, often called a "two-pointer" approach.
- This is a fascinating case. It looks like a standard nested loop, which might lead you to guess , where is
-
method3(int[] nums):- This method has a
forloop that iterates times. - Inside the loop, it calls
binarySearch. In the previous section of the Cornell notes, we saw thatbinarySearchhas a complexity of because it repeatedly halves the search space. - We have an operation being performed times.
- Multiplying the complexities: .
- Complexity: .
- This method has a
Conclusion
In this lesson, we transitioned from the theory of asymptotic notation to the practical skill of analyzing iterative code. By establishing a few core rules, you can now quickly and effectively determine the time complexity of many common algorithms.
Here are the key takeaways:
- Single Linear Loops (): The loop counter changes by a constant amount.
- Nested Loops (): Complexity is found by multiplying the complexities of the outer and inner loops. Be wary of dependent loops and loops with non-resetting counters.
- Logarithmic Loops (): The loop counter changes by a multiplicative or divisive factor.
- Sequential Loops: Add complexities; the highest order term dominates.
You now have the tools to analyze how an algorithm scales. In our next lesson, we will focus on comparing these common complexity classes—O(1), O(log n), O(n), O(n log n), O(n²), and O(2^n). This will build your intuition for why choosing an O(N log N) algorithm over an O(N^2) one can be the difference between a program that is lightning-fast and one that is unusably slow.