Welcome back! In our last lesson, we established the formal language of asymptotic analysis with Big-O, Big-Omega, and Big-Theta notations. We now have the vocabulary to discuss algorithm efficiency with precision.
Today, we'll put that theory into practice. Your goal for this lesson is to learn how to look at a piece of iterative Java code and determine its time and space complexity, especially when it involves loops and nested loops. This is a fundamental skill, not just for understanding algorithms, but for answering a core question you'll face in any SDE interview: "What is the complexity of your solution?" We will move from rigorous, line-by-line counting to the practical shortcuts that experienced engineers use.
From Exact Counts to Asymptotic Classes
When analyzing an algorithm, one could meticulously count every single operation: every variable assignment, every arithmetic calculation, every array access. This process, while thorough, is incredibly tedious and time-consuming.
Let's look at a resource that demonstrates this. The author of "Analyzing Complexity" from Cornell's CS 2110 course first performs a detailed, line-by-line analysis to find the exact operation count for two algorithms.
5. Analyzing Complexity | CS 2110
Please read the detailed analyses for the paritySplit() and hasDuplicates() methods to see how an exact operation count is derived.
Skim the line-by-line analysis for paritySplit() starting from the first analysis. You don't need to absorb every detail, just appreciate the process. Then, do the same for the hasDuplicates() method, which involves nested loops, starting from the second analysis. Notice how quickly the math becomes complex with summations.
As you can see, the final functions are and . In an interview, nobody expects you to derive these exact formulas. What they do expect is for you to arrive at the asymptotic complexity: and , respectively. Fortunately, we can get there much faster.
The Rules of the Game: Simplified Analysis
Asymptotic notation gives us the power to ignore constant factors and lower-order terms. This leads to two simple but powerful rules for analyzing code:
- Sequential Blocks: If you have two sequential blocks of code with complexities and , the total complexity is simply . You add the complexities, and the dominant term takes over.
- Nested Blocks: If a block of code with complexity is executed times (e.g., inside a loop), the total complexity is . You multiply the complexities.
Let's use these rules to analyze some common patterns.
Constant Time: O(1)
An operation or set of operations is constant time if its execution time does not depend on the input size. This is the holy grail of efficiency.
// O(1) time
int x = 5;
int y = 10;
int sum = x + y;
// Accessing an array element by index is also O(1)
int[] arr = {1, 2, 3, 4, 5};
int thirdElement = arr[2]; // This is O(1) regardless of arr.length
These operations take the same amount of time whether the input array has 5 elements or 5 million. The key is that the number of operations is fixed.
Linear Time: O(n)
This is one of the most common complexities. It occurs when you need to iterate through all elements of a collection once.
// O(n) time
void printAll(int[] arr) {
System.out.println("Printing elements:");
for (int i = 0; i < arr.length; i++) { // This loop runs n times
System.out.println(arr[i]); // This is an O(1) operation
}
}
Here, the loop runs arr.length times, which we'll call . Inside the loop, we perform a constant number of operations. Using our rules: the loop body is , and it runs times. Total complexity: .
Quadratic Time: O(n²)
Quadratic complexity often arises when you have nested loops iterating over the same data. A classic example is finding if a pair of elements in an array sums to a target value by checking every possible pair.
Let's examine the findPair code from GeeksforGeeks.
Time and Space Complexity - GeeksforGeeks
This article provides a simple, practical example of a nested loop algorithm. Focus on the Java implementation and its complexity analysis.
Find the section for "Example 1" and read about the problem to find a pair. Then, study the Java code implementation. Finally, read the complexity analysis at the end of the section, which explains why the nested loops lead to O(N^2) complexity.
The outer loop runs times, and for each of those iterations, the inner loop also runs times. This results in pairs being checked in the worst case. Total complexity: .
A common variation is a dependent nested loop, where the inner loop's bound depends on the outer loop's variable:
// O(n^2) time
void printPairs(int[] arr) {
for (int i = 0; i < arr.length; i++) {
for (int j = i; j < arr.length; j++) { // Inner loop starts from i
System.out.println(arr[i] + ", " + arr[j]);
}
}
}
You might be tempted to think this is faster. And it is! But how much faster? The first time, the inner loop runs times. The next time, , then , and so on. The total number of operations is the sum , which equals . In Big-O notation, we drop the constants and lower-order terms, so this is still .
Analyzing Space Complexity
Space complexity measures the amount of extra memory an algorithm needs to run, as a function of the input size. This is often called auxiliary space. We typically don't count the space taken by the input itself.
Constant Space: O(1)
An algorithm uses constant space if it requires a fixed amount of memory, regardless of the input size. In-place algorithms are a great example.
// O(1) space
void reverseArray(int[] arr) {
int left = 0;
int right = arr.length - 1;
while (left < right) {
// Swap elements
int temp = arr[left];
arr[left] = arr[right];
arr[right] = temp;
left++;
right--;
}
}
This algorithm only uses a few variables (left, right, temp). The number of variables doesn't change whether the array has 10 elements or 10 million. Thus, the auxiliary space is .
Linear Space: O(n)
An algorithm uses linear space if its memory usage grows proportionally to the input size. This often happens when you create a new collection that's a copy of the input or stores related data.
// O(n) space
int[] copyAndDouble(int[] arr) {
int[] newArr = new int[arr.length]; // Creates a new array of size n
for (int i = 0; i < arr.length; i++) {
newArr[i] = arr[i] * 2;
}
return newArr;
}
Here, newArr requires space proportional to the size of the input array arr. Therefore, the auxiliary space complexity is .
A very practical example for you as a Java developer involves using HashMap to count frequencies.
Time and Space Complexity - GeeksforGeeks
This section analyzes the space complexity of an algorithm that uses a hash map.
Read the section on Space Complexity, focusing on the example to find frequencies. Pay close attention to the Java code and the explanation of why the HashMap leads to O(N) space complexity. Note the useful distinction made between space complexity and auxiliary space.
Iterative vs. Recursive Space
The distinction between iterative and recursive solutions provides a clear illustration of space complexity. Look at this comparison for calculating a factorial.

We will cover recursion in depth later, but this visual starkly contrasts the space of an iterative approach with the space consumed by the call stack in a naive recursive one.
Visualizing the Difference
It's one thing to label an algorithm , and another to truly appreciate what that means for performance. This graph shows how different complexity classes scale as the input size n increases.

A Final "Gotcha": Java String Concatenation
Because you are a Java developer, it's worth noting a platform-specific detail that can affect complexity analysis. In Java, String objects are immutable. When you concatenate strings in a loop, you aren't just adding to the end of a string; you're creating a new String object in memory each time.
Let's analyze this subtle but important example.
5. Analyzing Complexity | CS 2110
The final section of this document contains an excellent, Java-specific example of how implementation details can hide complexity.
Read the part titled A Subtle Example. Analyze the starString method. Before reading the solution, try to determine its time and space complexity. Then, read the explanation about immutable strings and how StringBuilder offers a more performant solution.
This is a classic interview-style trap. Knowing that s = s + "*" inside a loop of iterations is an operation in Java demonstrates a deep understanding of both complexity and the language itself.
Conclusion
Today, we've bridged the gap from the theory of asymptotic notation to the practical skill of analyzing code. You now have the mental model to quickly assess the performance of iterative algorithms.
Here are the key takeaways:
- Simplify: Don't count every operation. Use the rules of adding sequential blocks and multiplying nested blocks. The dominant term is all that matters for Big-O.
- Time Complexity Patterns:
- A simple loop over elements is typically .
- A nested loop where both loops go to is .
- A loop whose counter is repeatedly divided (e.g.,
i /= 2) is . We'll explore this more.
- Space Complexity: This measures the extra memory (auxiliary space) an algorithm uses. is constant space (in-place), while is linear space (e.g., creating a copy of an input collection).
- Know Your Language: Language-specific features, like Java's immutable strings, can have surprising performance implications.
In our next lesson, we will take a more structured tour of the common complexity classes you saw on the graph—, , , , etc.—and associate each one with a concrete, classic algorithm.