Java Data Structures and Algorithms Masterclass
Asymptotic Complexity Analysis
Beyond the Stopwatch
How do you know if your code is fast? You could time it with a stopwatch, but that's a flawed approach. The same Java code might run faster on a new laptop than on an old desktop. Even on the same machine, the first run might be slower than the second because of how the Java Virtual Machine (JVM) warms up and optimises code.
To compare algorithms fairly, we need a method that ignores the hardware and specific runtime environment. This is where asymptotic analysis comes in. It provides a mathematical way to describe an algorithm's efficiency as its input size grows infinitely large. We're not measuring seconds; we're measuring how the number of operations scales with the input.
Complexity analysis helps us understand how fast (or slow) an algorithm will run as its input size grows.
This analysis focuses on two key resources: time and space.
- Time Complexity: How the runtime of an algorithm scales with the input size, .
- Space Complexity: How the amount of memory an algorithm needs scales with the input size, .
We use a special set of notations to describe these complexities, focusing on the worst-case, best-case, and average-case scenarios.
The Language of Growth
To talk about how complexity grows, we use three main notations from a family called asymptotic notations. They give us a language to define performance boundaries.
Big O
noun
Describes the upper bound of an algorithm's complexity. It tells us the worst-case scenario, guaranteeing that the algorithm's performance will not exceed a certain level.
Big O is the most common notation you'll encounter. When someone mentions the 'complexity' of an algorithm, they're usually talking about its Big O performance.
Next, we have Big Omega, which is the flip side of Big O.
Big Omega
noun
Describes the lower bound of an algorithm's complexity. It represents the best-case scenario, guaranteeing the algorithm will take at least a certain amount of time or space.
Finally, when an algorithm's upper and lower bounds are the same, we use Big Theta.
Big Theta
noun
Describes a tight bound on an algorithm's complexity. It's used when the best-case and worst-case performance grow at the same rate.
The graph shows how for an input size greater than , the actual function is sandwiched between the upper bound of Big O and the lower bound of Big Omega. When both bounds apply with the same growth function , we can say the algorithm is .
Analysing Java Code
Let's apply these concepts to Java. When analysing code, we count the number of basic operations (assignments, comparisons, arithmetic operations) relative to the input size .
The key is to ignore constant factors and lower-order terms. An algorithm that takes operations is simplified to , because as becomes very large, the term dominates everything else.
Consider a simple loop that finds the maximum value in an array.
int findMax(int[] array) {
int max = array[0]; // 1 operation
// Loop runs n times
for (int i = 1; i < array.length; i++) {
// 1 comparison per iteration
if (array[i] > max) {
max = array[i]; // 1 assignment (worst case)
}
}
return max; // 1 operation
}
Here, the loop is the dominant part of the algorithm. It runs times, where is the length of the array. Inside the loop, we have a comparison and a potential assignment. The total number of operations is roughly proportional to . Therefore, the time complexity is . We use Theta because you always have to check every element, making the best and worst cases the same.
What about nested loops? They often lead to polynomial complexity.
void printPairs(int[] array) {
// Outer loop runs n times
for (int i = 0; i < array.length; i++) {
// Inner loop runs n times
for (int j = 0; j < array.length; j++) {
System.out.println(array[i] + "," + array[j]);
}
}
}
The inner loop executes times for each execution of the outer loop. Since the outer loop also runs times, the total number of print operations is . The time complexity is . The space complexity for both examples is (or constant), because we only use a fixed number of variables regardless of the array size.
Recursion and Amortised Analysis
Analysing recursive functions involves setting up a recurrence relation. A classic example is the factorial function.
long factorial(int n) {
// Base case
if (n <= 1) {
return 1;
}
// Recursive step
return n * factorial(n - 1);
}
Let be the time to compute factorial(n). The function does a constant amount of work (a comparison and a multiplication) and then calls itself with . This gives us the recurrence relation:
Solving this relation shows that the total work is proportional to . Thus, the time complexity is . Each recursive call also adds a frame to the call stack, so the space complexity is also .
Sometimes, an operation can be very expensive, but it happens so rarely that its cost, when averaged over a sequence of operations, is small. This is the idea behind amortised analysis.
Amortised analysis gives the average performance of each operation in the worst case, over a sequence of operations.
A great example is Java's ArrayList. When you add an element, it usually takes constant time, . But if the internal array is full, ArrayList must create a new, larger array and copy all the old elements over. This single add operation is expensive—it takes time, where is the current size.
However, because the array size is typically doubled each time it resizes, these expensive operations happen infrequently. When you average the cost over many additions, the cost per operation comes out to be constant. We say the add operation has an amortised time complexity of .
Ready to test your understanding?
Why is timing code with a stopwatch an unreliable method for comparing algorithm efficiency across different machines?
What does Big O notation (e.g., ) primarily describe about an algorithm?
Understanding asymptotic analysis is fundamental to writing scalable and efficient software. It allows you to make informed decisions about which algorithms and data structures to use, which is a crucial skill in software engineering.