algorithm complexity
sum algorithm
computational complexity
algorithm analysis
complexity theory

What is the complexity of this sum algorithm?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Analyzing the complexity of algorithms is a core skill in computer science. Even for something as simple as computing the sum of an array, the choice of approach affects both time and space complexity. This article examines three common methods for summing elements, analyzes their complexities, and discusses practical trade-offs.

Three Approaches to Summing an Array

1. Iterative Approach

The simplest method walks through the array once, accumulating the sum in a variable.

python
1def iterative_sum(arr):
2    total = 0
3    for x in arr:
4        total += x
5    return total

Time complexity: O(n)O(n), where nn is the number of elements. Each element is visited exactly once.

Space complexity: O(1)O(1). Only a single accumulator variable is used, regardless of input size.

2. Recursive Approach

A recursive function reduces the problem by one element at each call until it reaches the base case.

python
1def recursive_sum(arr, i=0):
2    if i == len(arr):
3        return 0
4    return arr[i] + recursive_sum(arr, i + 1)

Time complexity: O(n)O(n). Each recursive call processes one element, and there are nn calls total.

Space complexity: O(n)O(n). Each recursive call adds a frame to the call stack. For an array of nn elements, the maximum stack depth is nn. This is a significant practical limitation: for large arrays (say, n>10,000n > 10{,}000), this approach will cause a stack overflow in most languages.

3. Divide and Conquer Approach

Split the array in half, sum each half recursively, and combine the results.

python
1def divide_and_conquer_sum(arr, low=0, high=None):
2    if high is None:
3        high = len(arr) - 1
4    if low == high:
5        return arr[low]
6    if low > high:
7        return 0
8    mid = (low + high) // 2
9    left_sum = divide_and_conquer_sum(arr, low, mid)
10    right_sum = divide_and_conquer_sum(arr, mid + 1, high)
11    return left_sum + right_sum

Time complexity: O(n)O(n). The recurrence is T(n)=2T(n/2)+O(1)T(n) = 2T(n/2) + O(1). By the Master Theorem (case 1, where a=2a = 2, b=2b = 2, and f(n)=O(1)f(n) = O(1)), this gives T(n)=O(n)T(n) = O(n). Intuitively, every element is still visited exactly once across all recursive calls.

Space complexity: O(logn)O(\log n). The recursion tree has depth log2n\log_2 n, and each level uses constant extra space. This is a significant improvement over the linear recursion approach.

Complexity Comparison Table

ApproachTimeSpaceStack Depth
IterativeO(n)O(n)O(1)O(1)O(1)O(1)
Recursive (linear)O(n)O(n)O(n)O(n)O(n)O(n)
Divide and ConquerO(n)O(n)O(logn)O(\log n)O(logn)O(\log n)

All three have the same time complexity because summing nn numbers inherently requires looking at each number at least once, giving a lower bound of Ω(n)\Omega(n). The key difference is in space usage.

Analyzing with the Master Theorem

The Master Theorem provides a shortcut for analyzing divide-and-conquer recurrences of the form:

T(n)=aT(nb)+f(n)T(n) = aT\left(\frac{n}{b}\right) + f(n)

For our divide-and-conquer sum:

  • a=2a = 2 (two subproblems)
  • b=2b = 2 (each subproblem is half the size)
  • f(n)=O(1)f(n) = O(1) (combining results is a single addition)

We compare f(n)f(n) with nlogba=nlog22=n1=nn^{\log_b a} = n^{\log_2 2} = n^1 = n. Since f(n)=O(1)f(n) = O(1) is polynomially smaller than nn, we are in Case 1, giving T(n)=Θ(n)T(n) = \Theta(n).

Practical Considerations

Tail Recursion Optimization

Some languages (like Scheme and certain C compilers with optimization flags) support tail call optimization (TCO). A tail-recursive version of the sum:

python
1def tail_recursive_sum(arr, i=0, acc=0):
2    if i == len(arr):
3        return acc
4    return tail_recursive_sum(arr, i + 1, acc + arr[i])

With TCO, this runs in O(1)O(1) space because the compiler reuses the same stack frame. However, Python and Java do not support TCO, so this optimization is language-dependent.

Parallel Processing

The divide-and-conquer approach naturally lends itself to parallelism. Each half of the array can be summed independently on a different processor core. With pp processors, the time complexity drops to O(n/p+logn)O(n/p + \log n), where n/pn/p is the work per processor and logn\log n accounts for the combination steps.

Numerical Stability

For floating-point numbers, the order of addition affects precision. The Kahan summation algorithm maintains a running compensation for lost low-order bits, achieving O(n)O(n) time and O(1)O(1) space while dramatically improving accuracy:

python
1def kahan_sum(arr):
2    total = 0.0
3    compensation = 0.0
4    for x in arr:
5        y = x - compensation
6        t = total + y
7        compensation = (t - total) - y
8        total = t
9    return total

Summary

For summing an array, the iterative approach is the clear winner in practice: O(n)O(n) time and O(1)O(1) space with no recursion overhead. The recursive approach is primarily pedagogical, demonstrating recursion at the cost of O(n)O(n) stack space. The divide-and-conquer approach reduces stack depth to O(logn)O(\log n) and enables parallelism, making it relevant for very large datasets processed across multiple cores. All three share the same O(n)O(n) time lower bound because every element must be examined.


Course illustration
Course illustration

All Rights Reserved.