Create your own
Lesson illustration

Analyzing Time and Space Complexity of Java Loops

Hello again. In the previous lesson, you practiced a coding-interview workflow: clarify the contract, model the problem with examples and a baseline, choose and explain an approach, then trace it against edge cases. Complexity analysis gives the “why” behind many of those choices. It lets you say not merely that a solution works, but whether it will still work when an array has millions of elements.

This lesson develops a repeatable way to derive time complexity and space complexity for Java code with sequential and nested loops. By the end, you should be able to look at a loop structure, count its meaningful repetitions, simplify the resulting expression, and state an interview-ready complexity conclusion.


What complexity measures

Time complexity is the rate at which an algorithm’s work grows as input size grows. It is not a stopwatch measurement: the same Java code may run at different speeds on different machines, JVM configurations, or workloads. Complexity abstracts away those machine-specific differences and focuses on growth.

Let denote the input size. For an array problem, is usually nums.length.

If a loop executes once per array element and does constant work each time, its work is proportional to :

Here represents a fixed amount of work per iteration, such as a comparison, assignment, or array access. In asymptotic analysis, we ignore fixed constant multipliers:

In interviews, it is conventional to report this as time. Strictly speaking:

  • is an asymptotic upper bound.
  • is an asymptotic lower bound.
  • means the growth rate is tightly bounded above and below by .

For a loop that always scans every element once, saying “ time” is accepted interview language, while “ time” is more precise.

Calculating Time Complexity | Data Structures and Algorithms| GeeksforGeeks

Watch “Calculating Time Complexity” by GeeksforGeeks for a compact visual walkthrough of the three patterns you will use most often: one loop, nested loops, and sequential statements.

Watch one loop to connect a constant-time body repeated n times with linear growth. Then watch nested loops and sequential code. Focus on the distinction between multiplying work for genuinely nested loops and adding work for blocks that run one after another.

Two conventions will keep your analysis useful and consistent:

  1. State the case being analyzed. Unless the interviewer asks otherwise, give worst-case time complexity. An early return may make a best case faster, but it does not erase the possible worst case.
  2. Ignore constants and lower-order terms. For large , grows like , so it is .

This is not permission to ignore all detail. First derive a reasonable expression from the code; only then simplify it.


A three-step method for loop analysis

When code becomes longer, do not guess from how many for keywords you see. Use this process:

  1. Define . Identify what input quantity can grow: array length, string length, number of rows, and so on.
  2. Count the dominant operation. Usually this is the work in the innermost loop body: a comparison, update, or method call.
  3. Compose the counts. Add sequential blocks. For nested loops, determine how many times the inner body runs across all outer iterations. Then remove constants and lower-order terms.

The Princeton material gives a more detailed version of this idea, including exact operation counts before asymptotic simplification.

[PDF] Analysis of Algorithms - cs.Princeton

Read the selected slides from Princeton’s “Analysis of Algorithms” lecture. They connect Java loop code to operation frequencies, show why triangular nested loops are still quadratic, and distinguish memory used by arrays from fixed-size local variables.

On slides 19–23, read cost and frequency and then follow the 1-sum and 2-sum examples. Notice that exact instruction counting is possible but often more detailed than an interview requires. Next, on slides 24–26, read the simplification principle. The important habit is to derive the repeated inner work first, then discard terms that do not control growth. On slides 29–31, review the common growth-rate table, especially the single-loop, double-loop, and triple-loop frameworks. Finally, on slides 42–48, examine the array-memory examples and the allocation example near the end, from the repeated allocation discussion. Focus on what remains live in memory, rather than merely how many allocations occur over the full execution.


Sequential loops: add their costs

Consider a method that makes two independent passes through an input array.

public static int countPositivesAndSum(int[] nums) {
    int positiveCount = 0;
    int sum = 0;

    for (int i = 0; i < nums.length; i++) {
        if (nums[i] > 0) {
            positiveCount++;
        }
    }

    for (int i = 0; i < nums.length; i++) {
        sum += nums[i];
    }

    return positiveCount + sum;
}

Let .

The first loop may inspect all elements. Its time is . The second loop also inspects all elements, so its time is . The loops are sequential, meaning one completes before the other begins. Their costs are added:

After dropping the constant multiplier and constant term:

A frequent error is to call this because it contains two loops. That would be correct only if one loop ran fully inside every iteration of the other.

A useful spoken explanation is:

“There are two separate full passes through the array. Their work adds to , which simplifies to linear time, .”

The same logic applies to more sequential blocks:

Code patternUnsimplified workTime complexity
One full scan
Three full scans
A full scan plus all pairs
A constant setup plus a full scan

The largest-growing term dominates. It is not enough to count loops; you must determine the work each loop performs.


Nested loops: count the inner-body executions

Now consider code that checks every ordered combination of two array positions.

public static int countEqualPairs(int[] nums) {
    int count = 0;

    for (int i = 0; i < nums.length; i++) {
        for (int j = 0; j < nums.length; j++) {
            if (nums[i] == nums[j]) {
                count++;
            }
        }
    }

    return count;
}

The outer loop runs times. For each outer iteration, the inner loop runs times. The comparison in the body therefore runs:

times. The time complexity is:

or, in typical interview phrasing, .

The critical wording is “for each outer iteration.” The inner index j resets to 0 every time i advances. This is why the work multiplies.

The supplied “Order of Growth” reference summarizes this standard pattern.

The table links common Java code frameworks to their growth rates: one full loop is linear, two full nested loops are quadratic, and three full nested loops are cubic. It also contrasts these with logarithmic and exponential patterns.

Dependent bounds: sum, do not blindly multiply

Not every nested loop executes exactly times. Consider the pair-checking baseline from the previous lesson:

public static int countZeroSumPairs(int[] nums) {
    int count = 0;

    for (int i = 0; i < nums.length; i++) {
        for (int j = i + 1; j < nums.length; j++) {
            if (nums[i] + nums[j] == 0) {
                count++;
            }
        }
    }

    return count;
}

Here, j does not reset to 0. It starts at i + 1, so the number of inner iterations changes:

Outer index iInner-loop iterations
0
1
2

The total number of pair checks is:

This is a triangular sum:

After ignoring the coefficient and the lower-order term , the time complexity remains:

This implementation does roughly half as many comparisons as the version that includes every ordered pair, but both are quadratic. The precise count matters for practical performance; the asymptotic class matters when comparing how solutions scale.

For a triple nested loop where each loop can run across the full input, the same reasoning gives:

So its time complexity is . This becomes impractical rapidly as input size grows.


Nested loops are not automatically quadratic

The loop structure matters more than indentation. A nested loop is quadratic only when its combined number of iterations grows quadratically.

public static int countHalvingsForEachElement(int[] nums) {
    int count = 0;

    for (int i = 0; i < nums.length; i++) {
        for (int size = nums.length; size > 1; size /= 2) {
            count++;
        }
    }

    return count;
}

The outer loop executes times. The inner loop repeatedly halves size. After inner iterations:

This happens when is proportional to . Therefore, the total time is:

You do not need to master every logarithmic pattern today. The key interview habit is to inspect the loop update:

  • i++ across an input-sized range usually gives .
  • size /= 2 usually gives .
  • Nested costs combine according to the actual iteration counts.

This distinction will become especially useful when you study binary search and divide-and-conquer algorithms later in the course.


Space complexity: count memory that remains needed

Space complexity measures memory usage as the input grows. In coding interviews, report auxiliary space unless the interviewer says otherwise: memory used in addition to the input itself.

For this method:

public static int findMaximum(int[] nums) {
    int max = Integer.MIN_VALUE;

    for (int i = 0; i < nums.length; i++) {
        if (nums[i] > max) {
            max = nums[i];
        }
    }

    return max;
}

The input array occupies memory, but it was provided to the method. The method itself uses only a fixed number of variables: max and i. Their number does not grow with .

  • Time:
  • Auxiliary space:

A loop variable does not become space merely because its value changes times. Space concerns how much memory is simultaneously required, not how many times a variable is updated.

Allocating an array changes the space cost

public static int[] copyNegativesAsZero(int[] nums) {
    int[] result = new int[nums.length];

    for (int i = 0; i < nums.length; i++) {
        result[i] = Math.max(nums[i], 0);
    }

    return result;
}

The new result array has one slot per input element:

The loop is still linear in time. Time and space are separate dimensions:

If an interviewer treats returned data separately, describe it clearly:

“The algorithm uses space for the returned array. Apart from the output, it uses auxiliary space.”

Nesting does not itself consume quadratic space

This code has quadratic time but constant auxiliary space:

public static int countPairsAboveTarget(int[] nums, int target) {
    int count = 0;

    for (int i = 0; i < nums.length; i++) {
        for (int j = i + 1; j < nums.length; j++) {
            if (nums[i] + nums[j] > target) {
                count++;
            }
        }
    }

    return count;
}

The nested loops do many operations, but the method retains only count, i, and j. Therefore:

By contrast, a two-dimensional allocation requires quadratic memory:

int[][] table = new int[n][n];

The table contains a number of entries proportional to , so its space complexity is .

One Java-specific nuance is worth retaining: repeated allocation does not automatically mean all allocated objects coexist. If a loop creates a temporary int[n] array, processes it, and does not store a reference to it, the algorithm’s peak live auxiliary space can still be , even though total allocation work over the whole method may be larger. If the program stores every temporary array in a list, then those arrays remain live and the space can grow to .


A compact interview narration

When asked for complexity, give the reasoning before the result. For the pair-checking code, a strong explanation is:

“Let be the array length. The outer loop runs times. For each index, the inner loop checks later elements, so the total number of checks is , which is . That is time. The code uses only counters and loop indices beyond the input array, so auxiliary space is .”

That explanation demonstrates that you understand the code rather than reciting a pattern.

Before finalizing an answer, make four quick checks:

  • What exactly is ?
  • Are the loops sequential, independently nested, or nested with dependent bounds?
  • Does any operation inside the loop have nonconstant cost or allocate growing storage?
  • Am I reporting input space, auxiliary space, and output space clearly enough for the prompt?

Key takeaways

Complexity analysis is a disciplined counting exercise:

  • Sequential blocks add. Two linear scans take , which simplifies to .
  • Independent nested loops multiply. Two full -iteration loops produce time.
  • Dependent inner bounds require a sum. A triangular count such as is still .
  • Inspect updates, not just nesting. A halving loop is , so nesting it inside a linear loop gives .
  • Loop count and space are different. Nested loops can use extra space; arrays, maps, lists, and matrices create space that grows with input size.

Next, you will use these tools to compare alternative solutions: when a hash-map solution is worth its extra memory, when a quadratic baseline is acceptable, and how practical implementation trade-offs affect the choice.

Can't find a good explanation? Sign up and we'll make it for you

Sign up