What space complexity measures
Space complexity describes how much memory an algorithm needs as its input grows. Like time complexity, it is written in Big O notation and ignores constant factors. An algorithm that needs a fixed handful of variables is O(1). One that needs memory proportional to the input size n is O(n).
Two quantities are easy to mix up:
- Input space is the memory the input itself occupies, for example a slice of
nintegers. - Auxiliary space (extra space) is the additional memory the algorithm allocates while it runs, not counting the input.
Space complexity is the sum of the two. In practice, when people compare algorithms, they usually talk about auxiliary space, because the input is the same for every algorithm that solves the problem. Interview answers such as "O(1) extra space" mean the auxiliary part.
Constant space: O(1)
func sum(a, b int) int {
total := a + b
return total
}
The function uses the parameters a and b and one local variable, total. The number of variables doesn't depend on the values passed in, so the space is O(1). Every int takes the same 8 bytes here, however large its value is.
Linear space: O(n)
func generate(n int) []int {
numbers := []int{}
for i := 0; i < n; i++ {
numbers = append(numbers, i)
}
return numbers
}
The slice numbers ends up with n elements, so memory grows in step with n: O(n). Calling generate(5) returns [0 1 2 3 4].
A Go detail: append sometimes allocates a bigger backing array and copies the elements across, so for a moment the old and the new array both exist. The capacity grows by a constant factor each time, so the total is still proportional to n. The space complexity stays O(n).
Quadratic space: O(n²)
func generate(n int) [][]int {
numbers := make([][]int, n)
for i := 0; i < n; i++ {
numbers[i] = make([]int, n)
for j := 0; j < n; j++ {
numbers[i][j] = j
}
}
return numbers
}
This builds an n by n table: n rows, each a slice of n integers. That is n * n numbers, so the space is O(n²). For n = 3 it returns [[0 1 2] [0 1 2] [0 1 2]].
The first make creates the outer slice with n empty rows (each is nil). Each iteration then gives one row its own slice of n integers. A Go slice of slices doesn't build the rows for you.
Input space against auxiliary space
func sum(arr []int) int {
total := 0
for _, num := range arr {
total += num
}
return total
}
If arr has n elements, the input space is O(n). The function itself adds only two variables, total and num, so the auxiliary space is O(1). Because space complexity is input plus auxiliary space, the total is O(n) + O(1), which is O(n). When someone says this function "uses O(1) space", they mean the extra space.
Slices are passed as small headers
In Go, passing a slice to a function copies only the slice header: a pointer to the elements, a length and a capacity. The elements themselves are not copied. So passing a slice of a million integers costs a constant amount of extra memory, and the callee can change the caller's elements:
func reverseInPlace(arr []int) {
for i, j := 0, len(arr)-1; i < j; i, j = i+1, j-1 {
arr[i], arr[j] = arr[j], arr[i]
}
}
This reverses a slice with O(1) auxiliary space. After reverseInPlace on [1 2 3 4], the caller's slice holds [4 3 2 1]. A version that builds a second slice and returns it uses O(n) auxiliary space but leaves the original untouched. That trade between memory and safety comes up often.
The call stack counts too
Memory isn't only variables you declare. Every function call needs a stack frame, and a recursive function that goes n calls deep holds n frames at once:
func depth(n int) int {
if n == 0 {
return 0
}
return 1 + depth(n-1)
}
The function allocates no slices, yet depth(1000) has 1,000 calls waiting on each other at the deepest point. Its space is O(n). A loop doing the same job would be O(1). Go grows its stacks on demand, so deep recursion rarely fails quickly, but the memory is still used.
Common mistakes
- Forgetting hidden allocations. Building a new slice, string or map of size proportional to the input adds
O(n). Converting a string with[]rune(s)or[]byte(s)makes a copy, and that copy isO(n)too. - Counting the input as extra. A function that only reads its parameters and keeps a few counters is
O(1)auxiliary space, even though the input is large. - Treating a loop as memory. A loop that runs
ntimes but only reuses the same few variables is stillO(1)space. Time and space are separate questions. - Ignoring recursion depth. A recursive solution may be
O(n)in space even when it creates no data structure.
Key takeaways
- Space complexity is the memory an algorithm needs as a function of the input size, written in Big O.
- A fixed number of variables is
O(1). A slice that grows with the input isO(n). A grid ofnbynisO(n²). - Space complexity is input space plus auxiliary space; comparisons usually look at the auxiliary part.
- Slices are shared by header, so passing them is cheap and changing their elements affects the caller.
- Recursion depth uses stack memory and counts toward the total.