Skip to content

2.4   Space Complexity

Space complexity measures the growth trend of memory space occupied by an algorithm as the data size increases. This concept is very similar to time complexity, except that "running time" is replaced with "occupied memory space".

The memory space used by an algorithm during execution mainly includes the following types.

  • Input space: Used to store the input data of the algorithm.
  • Temporary space: Used to store variables, objects, function contexts, and other data during the algorithm's execution.
  • Output space: Used to store the output data of the algorithm.

In general, the scope of space complexity statistics is "temporary space" plus "output space".

Temporary space can be further divided into three parts.

  • Temporary data: Used to save various constants, variables, objects, etc., during the algorithm's execution.
  • Stack frame space: Used to save the context data of called functions. The system creates a stack frame at the top of the stack each time a function is called, and the stack frame space is released after the function returns.
  • Instruction space: Used to save compiled program instructions, which are usually ignored in actual statistics.

When analyzing the space complexity of a program, we usually count three parts: temporary data, stack frame space, and output data, as shown in the following figure.

Algorithm-related space

Figure 2-15   Algorithm-related space

The related code is as follows:

class Node:
    """Class"""
    def __init__(self, x: int):
        self.val: int = x              # Node value
        self.next: Node | None = None  # Reference to the next node

def function() -> int:
    """Function"""
    # Perform some operations...
    return 0

def algorithm(n) -> int:  # Input data
    A = 0                 # Temporary data (constant, usually represented by uppercase letters)
    b = 0                 # Temporary data (variable)
    node = Node(0)        # Temporary data (object)
    c = function()        # Stack frame space (function call)
    return A + b + c      # Output data
/* Structure */
struct Node {
    int val;
    Node *next;
    Node(int x) : val(x), next(nullptr) {}
};

/* Function */
int func() {
    // Perform some operations...
    return 0;
}

int algorithm(int n) {        // Input data
    const int a = 0;          // Temporary data (constant)
    int b = 0;                // Temporary data (variable)
    Node* node = new Node(0); // Temporary data (object)
    int c = func();           // Stack frame space (function call)
    return a + b + c;         // Output data
}
/* Class */
class Node {
    int val;
    Node next;
    Node(int x) { val = x; }
}

/* Function */
int function() {
    // Perform some operations...
    return 0;
}

int algorithm(int n) {        // Input data
    final int a = 0;          // Temporary data (constant)
    int b = 0;                // Temporary data (variable)
    Node node = new Node(0);  // Temporary data (object)
    int c = function();       // Stack frame space (function call)
    return a + b + c;         // Output data
}
/* Class */
class Node(int x) {
    int val = x;
    Node next;
}

/* Function */
int Function() {
    // Perform some operations...
    return 0;
}

int Algorithm(int n) {        // Input data
    const int a = 0;          // Temporary data (constant)
    int b = 0;                // Temporary data (variable)
    Node node = new(0);       // Temporary data (object)
    int c = Function();       // Stack frame space (function call)
    return a + b + c;         // Output data
}
/* Structure */
type node struct {
    val  int
    next *node
}

/* Create node structure */
func newNode(val int) *node {
    return &node{val: val}
}

/* Function */
func function() int {
    // Perform some operations...
    return 0
}

func algorithm(n int) int { // Input data
    const a = 0             // Temporary data (constant)
    b := 0                  // Temporary data (variable)
    newNode(0)              // Temporary data (object)
    c := function()         // Stack frame space (function call)
    return a + b + c        // Output data
}
/* Class */
class Node {
    var val: Int
    var next: Node?

    init(x: Int) {
        val = x
    }
}

/* Function */
func function() -> Int {
    // Perform some operations...
    return 0
}

func algorithm(n: Int) -> Int { // Input data
    let a = 0             // Temporary data (constant)
    var b = 0             // Temporary data (variable)
    let node = Node(x: 0) // Temporary data (object)
    let c = function()    // Stack frame space (function call)
    return a + b + c      // Output data
}
/* Class */
class Node {
    val;
    next;
    constructor(val) {
        this.val = val === undefined ? 0 : val; // Node value
        this.next = null;                       // Reference to the next node
    }
}

/* Function */
function constFunc() {
    // Perform some operations
    return 0;
}

function algorithm(n) {       // Input data
    const a = 0;              // Temporary data (constant)
    let b = 0;                // Temporary data (variable)
    const node = new Node(0); // Temporary data (object)
    const c = constFunc();    // Stack frame space (function call)
    return a + b + c;         // Output data
}
/* Class */
class Node {
    val: number;
    next: Node | null;
    constructor(val?: number) {
        this.val = val === undefined ? 0 : val; // Node value
        this.next = null;                       // Reference to the next node
    }
}

/* Function */
function constFunc(): number {
    // Perform some operations
    return 0;
}

function algorithm(n: number): number { // Input data
    const a = 0;                        // Temporary data (constant)
    let b = 0;                          // Temporary data (variable)
    const node = new Node(0);           // Temporary data (object)
    const c = constFunc();              // Stack frame space (function call)
    return a + b + c;                   // Output data
}
/* Class */
class Node {
  int val;
  Node next;
  Node(this.val, [this.next]);
}

/* Function */
int function() {
  // Perform some operations...
  return 0;
}

int algorithm(int n) {  // Input data
  const int a = 0;      // Temporary data (constant)
  int b = 0;            // Temporary data (variable)
  Node node = Node(0);  // Temporary data (object)
  int c = function();   // Stack frame space (function call)
  return a + b + c;     // Output data
}
use std::rc::Rc;
use std::cell::RefCell;

/* Structure */
struct Node {
    val: i32,
    next: Option<Rc<RefCell<Node>>>,
}

/* Create Node structure */
impl Node {
    fn new(val: i32) -> Self {
        Self { val: val, next: None }
    }
}

/* Function */
fn function() -> i32 {
    // Perform some operations...
    return 0;
}

fn algorithm(n: i32) -> i32 {       // Input data
    const a: i32 = 0;               // Temporary data (constant)
    let mut b = 0;                  // Temporary data (variable)
    let node = Node::new(0);        // Temporary data (object)
    let c = function();             // Stack frame space (function call)
    return a + b + c;               // Output data
}
/* Function */
int func() {
    // Perform some operations...
    return 0;
}

int algorithm(int n) { // Input data
    const int a = 0;   // Temporary data (constant)
    int b = 0;         // Temporary data (variable)
    int c = func();    // Stack frame space (function call)
    return a + b + c;  // Output data
}
/* Class */
class Node(var _val: Int) {
    var next: Node? = null
}

/* Function */
fun function(): Int {
    // Perform some operations...
    return 0
}

fun algorithm(n: Int): Int { // Input data
    val a = 0                // Temporary data (constant)
    var b = 0                // Temporary data (variable)
    val node = Node(0)       // Temporary data (object)
    val c = function()       // Stack frame space (function call)
    return a + b + c         // Output data
}
### Class ###
class Node
    attr_accessor :val      # Node value
    attr_accessor :next     # Reference to the next node

    def initialize(x)
        @val = x
    end
end

### Function ###
def function
    # Perform some operations...
    0
end

### Algorithm ###
def algorithm(n)        # Input data
    a = 0               # Temporary data (constant)
    b = 0               # Temporary data (variable)
    node = Node.new(0)  # Temporary data (object)
    c = function        # Stack frame space (function call)
    a + b + c           # Output data
end

2.4.2   Calculation Method

The calculation method for space complexity is roughly the same as for time complexity, except that the statistical object is changed from "number of operations" to "size of space used".

Unlike time complexity, we usually only focus on the worst-case space complexity. This is because memory space is a hard requirement, and we must ensure that sufficient memory space is reserved for all input data.

Observe the following code. The "worst case" in worst-case space complexity has two meanings.

  1. Based on the worst input data: When \(n < 10\), the space complexity is \(O(1)\); but when \(n > 10\), the initialized array nums occupies \(O(n)\) space, so the worst-case space complexity is \(O(n)\).
  2. Based on the peak memory during algorithm execution: For example, before executing the last line, the program occupies \(O(1)\) space; when initializing the array nums, the program occupies \(O(n)\) space, so the worst-case space complexity is \(O(n)\).
def algorithm(n: int):
    a = 0               # O(1)
    b = [0] * 10000     # O(1)
    if n > 10:
        nums = [0] * n  # O(n)
void algorithm(int n) {
    int a = 0;               // O(1)
    vector<int> b(10000);    // O(1)
    if (n > 10)
        vector<int> nums(n); // O(n)
}
void algorithm(int n) {
    int a = 0;                   // O(1)
    int[] b = new int[10000];    // O(1)
    if (n > 10)
        int[] nums = new int[n]; // O(n)
}
void Algorithm(int n) {
    int a = 0;                   // O(1)
    int[] b = new int[10000];    // O(1)
    if (n > 10) {
        int[] nums = new int[n]; // O(n)
    }
}
func algorithm(n int) {
    a := 0                      // O(1)
    b := make([]int, 10000)     // O(1)
    var nums []int
    if n > 10 {
        nums := make([]int, n)  // O(n)
    }
    fmt.Println(a, b, nums)
}
func algorithm(n: Int) {
    let a = 0 // O(1)
    let b = Array(repeating: 0, count: 10000) // O(1)
    if n > 10 {
        let nums = Array(repeating: 0, count: n) // O(n)
    }
}
function algorithm(n) {
    const a = 0;                   // O(1)
    const b = new Array(10000);    // O(1)
    if (n > 10) {
        const nums = new Array(n); // O(n)
    }
}
function algorithm(n: number): void {
    const a = 0;                   // O(1)
    const b = new Array(10000);    // O(1)
    if (n > 10) {
        const nums = new Array(n); // O(n)
    }
}
void algorithm(int n) {
  int a = 0;                            // O(1)
  List<int> b = List.filled(10000, 0);  // O(1)
  if (n > 10) {
    List<int> nums = List.filled(n, 0); // O(n)
  }
}
fn algorithm(n: i32) {
    let a = 0;                              // O(1)
    let b = [0; 10000];                     // O(1)
    if n > 10 {
        let nums = vec![0; n as usize];     // O(n)
    }
}
void algorithm(int n) {
    int a = 0;               // O(1)
    int b[10000];            // O(1)
    if (n > 10)
        int nums[n] = {0};   // O(n)
}
fun algorithm(n: Int) {
    val a = 0                    // O(1)
    val b = IntArray(10000)      // O(1)
    if (n > 10) {
        val nums = IntArray(n)   // O(n)
    }
}
def algorithm(n)
    a = 0                           # O(1)
    b = Array.new(10000)            # O(1)
    nums = Array.new(n) if n > 10   # O(n)
end

In recursive functions, it is necessary to count the stack frame space. Observe the following code:

def function() -> int:
    # Perform some operations
    return 0

def loop(n: int):
    """Loop has space complexity of O(1)"""
    for _ in range(n):
        function()

def recur(n: int):
    """Recursion has space complexity of O(n)"""
    if n == 1:
        return
    return recur(n - 1)
int func() {
    // Perform some operations
    return 0;
}
/* Loop has space complexity of O(1) */
void loop(int n) {
    for (int i = 0; i < n; i++) {
        func();
    }
}
/* Recursion has space complexity of O(n) */
void recur(int n) {
    if (n == 1) return;
    recur(n - 1);
}
int function() {
    // Perform some operations
    return 0;
}
/* Loop has space complexity of O(1) */
void loop(int n) {
    for (int i = 0; i < n; i++) {
        function();
    }
}
/* Recursion has space complexity of O(n) */
void recur(int n) {
    if (n == 1) return;
    recur(n - 1);
}
int Function() {
    // Perform some operations
    return 0;
}
/* Loop has space complexity of O(1) */
void Loop(int n) {
    for (int i = 0; i < n; i++) {
        Function();
    }
}
/* Recursion has space complexity of O(n) */
int Recur(int n) {
    if (n == 1) return 1;
    return Recur(n - 1);
}
func function() int {
    // Perform some operations
    return 0
}

/* Loop has space complexity of O(1) */
func loop(n int) {
    for i := 0; i < n; i++ {
        function()
    }
}

/* Recursion has space complexity of O(n) */
func recur(n int) {
    if n == 1 {
        return
    }
    recur(n - 1)
}
@discardableResult
func function() -> Int {
    // Perform some operations
    return 0
}

/* Loop has space complexity of O(1) */
func loop(n: Int) {
    for _ in 0 ..< n {
        function()
    }
}

/* Recursion has space complexity of O(n) */
func recur(n: Int) {
    if n == 1 {
        return
    }
    recur(n: n - 1)
}
function constFunc() {
    // Perform some operations
    return 0;
}
/* Loop has space complexity of O(1) */
function loop(n) {
    for (let i = 0; i < n; i++) {
        constFunc();
    }
}
/* Recursion has space complexity of O(n) */
function recur(n) {
    if (n === 1) return;
    return recur(n - 1);
}
function constFunc(): number {
    // Perform some operations
    return 0;
}
/* Loop has space complexity of O(1) */
function loop(n: number): void {
    for (let i = 0; i < n; i++) {
        constFunc();
    }
}
/* Recursion has space complexity of O(n) */
function recur(n: number): void {
    if (n === 1) return;
    return recur(n - 1);
}
int function() {
  // Perform some operations
  return 0;
}
/* Loop has space complexity of O(1) */
void loop(int n) {
  for (int i = 0; i < n; i++) {
    function();
  }
}
/* Recursion has space complexity of O(n) */
void recur(int n) {
  if (n == 1) return;
  recur(n - 1);
}
fn function() -> i32 {
    // Perform some operations
    return 0;
}
/* Loop has space complexity of O(1) */
fn loop(n: i32) {
    for i in 0..n {
        function();
    }
}
/* Recursion has space complexity of O(n) */
fn recur(n: i32) {
    if n == 1 {
        return;
    }
    recur(n - 1);
}
int func() {
    // Perform some operations
    return 0;
}
/* Loop has space complexity of O(1) */
void loop(int n) {
    for (int i = 0; i < n; i++) {
        func();
    }
}
/* Recursion has space complexity of O(n) */
void recur(int n) {
    if (n == 1) return;
    recur(n - 1);
}
fun function(): Int {
    // Perform some operations
    return 0
}
/* Loop has space complexity of O(1) */
fun loop(n: Int) {
    for (i in 0..<n) {
        function()
    }
}
/* Recursion has space complexity of O(n) */
fun recur(n: Int) {
    if (n == 1) return
    return recur(n - 1)
}
def function
    # Perform some operations
    0
end

### Loop has space complexity of O(1) ###
def loop(n)
    (0...n).each { function }
end

### Recursion has space complexity of O(n) ###
def recur(n)
    return if n == 1
    recur(n - 1)
end

The time complexity of both functions loop() and recur() is \(O(n)\), but their space complexities are different.

  • The function loop() calls function() \(n\) times in a loop. In each iteration, function() returns and releases its stack frame space, so the space complexity remains \(O(1)\).
  • The recursive function recur() has \(n\) unreturned recur() instances existing simultaneously during execution, thus occupying \(O(n)\) stack frame space.

2.4.3   Common Types

Let the input data size be \(n\). The following figure shows common types of space complexity (arranged from low to high).

\[ \begin{aligned} O(1) < O(\log n) < O(n) < O(n^2) < O(2^n) \newline \text{Constant} < \text{Logarithmic} < \text{Linear} < \text{Quadratic} < \text{Exponential} \end{aligned} \]

Common types of space complexity

Figure 2-16   Common types of space complexity

1.   Constant Order \(O(1)\)

Constant order is common in constants, variables, and objects whose quantity is independent of the input data size \(n\).

It should be noted that memory occupied by initializing variables or calling functions in a loop is released when entering the next iteration, so it does not accumulate space, and the space complexity remains \(O(1)\):

space_complexity.py
def function() -> int:
    """Function"""
    # Perform some operations
    return 0

def constant(n: int):
    """Constant order"""
    # Constants, variables, objects occupy O(1) space
    a = 0
    nums = [0] * 10000
    node = ListNode(0)
    # Variables in the loop occupy O(1) space
    for _ in range(n):
        c = 0
    # Functions in the loop occupy O(1) space
    for _ in range(n):
        function()
space_complexity.cpp
/* Function */
int func() {
    // Perform some operations
    return 0;
}

/* Constant order */
void constant(int n) {
    // Constants, variables, objects occupy O(1) space
    const int a = 0;
    int b = 0;
    vector<int> nums(10000);
    ListNode node(0);
    // Variables in the loop occupy O(1) space
    for (int i = 0; i < n; i++) {
        int c = 0;
    }
    // Functions in the loop occupy O(1) space
    for (int i = 0; i < n; i++) {
        func();
    }
}
space_complexity.java
/* Function */
int function() {
    // Perform some operations
    return 0;
}

/* Constant order */
void constant(int n) {
    // Constants, variables, objects occupy O(1) space
    final int a = 0;
    int b = 0;
    int[] nums = new int[10000];
    ListNode node = new ListNode(0);
    // Variables in the loop occupy O(1) space
    for (int i = 0; i < n; i++) {
        int c = 0;
    }
    // Functions in the loop occupy O(1) space
    for (int i = 0; i < n; i++) {
        function();
    }
}
space_complexity.cs
/* Function */
int Function() {
    // Perform some operations
    return 0;
}

/* Constant order */
void Constant(int n) {
    // Constants, variables, objects occupy O(1) space
    int a = 0;
    int b = 0;
    int[] nums = new int[10000];
    ListNode node = new(0);
    // Variables in the loop occupy O(1) space
    for (int i = 0; i < n; i++) {
        int c = 0;
    }
    // Functions in the loop occupy O(1) space
    for (int i = 0; i < n; i++) {
        Function();
    }
}
space_complexity.go
/* Function */
func function() int {
    // Perform some operations...
    return 0
}

/* Constant order */
func spaceConstant(n int) {
    // Constants, variables, objects occupy O(1) space
    const a = 0
    b := 0
    nums := make([]int, 10000)
    node := newNode(0)
    // Variables in the loop occupy O(1) space
    var c int
    for i := 0; i < n; i++ {
        c = 0
    }
    // Functions in the loop occupy O(1) space
    for i := 0; i < n; i++ {
        function()
    }
    b += 0
    c += 0
    nums[0] = 0
    node.val = 0
}
space_complexity.swift
/* Function */
@discardableResult
func function() -> Int {
    // Perform some operations
    return 0
}

/* Constant order */
func constant(n: Int) {
    // Constants, variables, objects occupy O(1) space
    let a = 0
    var b = 0
    let nums = Array(repeating: 0, count: 10000)
    let node = ListNode(x: 0)
    // Variables in the loop occupy O(1) space
    for _ in 0 ..< n {
        let c = 0
    }
    // Functions in the loop occupy O(1) space
    for _ in 0 ..< n {
        function()
    }
}
space_complexity.js
/* Function */
function constFunc() {
    // Perform some operations
    return 0;
}

/* Constant order */
function constant(n) {
    // Constants, variables, objects occupy O(1) space
    const a = 0;
    const b = 0;
    const nums = new Array(10000);
    const node = new ListNode(0);
    // Variables in the loop occupy O(1) space
    for (let i = 0; i < n; i++) {
        const c = 0;
    }
    // Functions in the loop occupy O(1) space
    for (let i = 0; i < n; i++) {
        constFunc();
    }
}
space_complexity.ts
/* Function */
function constFunc(): number {
    // Perform some operations
    return 0;
}

/* Constant order */
function constant(n: number): void {
    // Constants, variables, objects occupy O(1) space
    const a = 0;
    const b = 0;
    const nums = new Array(10000);
    const node = new ListNode(0);
    // Variables in the loop occupy O(1) space
    for (let i = 0; i < n; i++) {
        const c = 0;
    }
    // Functions in the loop occupy O(1) space
    for (let i = 0; i < n; i++) {
        constFunc();
    }
}
space_complexity.dart
/* Function */
int function() {
  // Perform some operations
  return 0;
}

/* Constant order */
void constant(int n) {
  // Constants, variables, objects occupy O(1) space
  final int a = 0;
  int b = 0;
  List<int> nums = List.filled(10000, 0);
  ListNode node = ListNode(0);
  // Variables in the loop occupy O(1) space
  for (var i = 0; i < n; i++) {
    int c = 0;
  }
  // Functions in the loop occupy O(1) space
  for (var i = 0; i < n; i++) {
    function();
  }
}
space_complexity.rs
/* Function */
fn function() -> i32 {
    // Perform some operations
    return 0;
}

/* Constant order */
#[allow(unused)]
fn constant(n: i32) {
    // Constants, variables, objects occupy O(1) space
    const A: i32 = 0;
    let b = 0;
    let nums = vec![0; 10000];
    let node = ListNode::new(0);
    // Variables in the loop occupy O(1) space
    for i in 0..n {
        let c = 0;
    }
    // Functions in the loop occupy O(1) space
    for i in 0..n {
        function();
    }
}
space_complexity.c
/* Function */
int func() {
    // Perform some operations
    return 0;
}

/* Constant order */
void constant(int n) {
    // Constants, variables, objects occupy O(1) space
    const int a = 0;
    int b = 0;
    int nums[1000];
    ListNode *node = newListNode(0);
    free(node);
    // Variables in the loop occupy O(1) space
    for (int i = 0; i < n; i++) {
        int c = 0;
    }
    // Functions in the loop occupy O(1) space
    for (int i = 0; i < n; i++) {
        func();
    }
}
space_complexity.kt
/* Function */
fun function(): Int {
    // Perform some operations
    return 0
}

/* Constant order */
fun constant(n: Int) {
    // Constants, variables, objects occupy O(1) space
    val a = 0
    var b = 0
    val nums = Array(10000) { 0 }
    val node = ListNode(0)
    // Variables in the loop occupy O(1) space
    for (i in 0..<n) {
        val c = 0
    }
    // Functions in the loop occupy O(1) space
    for (i in 0..<n) {
        function()
    }
}
space_complexity.rb
### Function ###
def function
  # Perform some operations
  0
end

### Constant time ###
def constant(n)
  # Constants, variables, objects occupy O(1) space
  a = 0
  nums = [0] * 10000
  node = ListNode.new

  # Variables in the loop occupy O(1) space
  (0...n).each { c = 0 }
  # Functions in the loop occupy O(1) space
  (0...n).each { function }
end

2.   Linear Order \(O(n)\)

Linear order is common in arrays, linked lists, stacks, queues, etc., where the number of elements is proportional to \(n\):

space_complexity.py
def linear(n: int):
    """Linear order"""
    # A list of length n occupies O(n) space
    nums = [0] * n
    # A hash table of length n occupies O(n) space
    hmap = dict[int, str]()
    for i in range(n):
        hmap[i] = str(i)
space_complexity.cpp
/* Linear order */
void linear(int n) {
    // Array of length n uses O(n) space
    vector<int> nums(n);
    // A list of length n occupies O(n) space
    vector<ListNode> nodes;
    for (int i = 0; i < n; i++) {
        nodes.push_back(ListNode(i));
    }
    // A hash table of length n occupies O(n) space
    unordered_map<int, string> map;
    for (int i = 0; i < n; i++) {
        map[i] = to_string(i);
    }
}
space_complexity.java
/* Linear order */
void linear(int n) {
    // Array of length n uses O(n) space
    int[] nums = new int[n];
    // A list of length n occupies O(n) space
    List<ListNode> nodes = new ArrayList<>();
    for (int i = 0; i < n; i++) {
        nodes.add(new ListNode(i));
    }
    // A hash table of length n occupies O(n) space
    Map<Integer, String> map = new HashMap<>();
    for (int i = 0; i < n; i++) {
        map.put(i, String.valueOf(i));
    }
}
space_complexity.cs
/* Linear order */
void Linear(int n) {
    // Array of length n uses O(n) space
    int[] nums = new int[n];
    // A list of length n occupies O(n) space
    List<ListNode> nodes = [];
    for (int i = 0; i < n; i++) {
        nodes.Add(new ListNode(i));
    }
    // A hash table of length n occupies O(n) space
    Dictionary<int, string> map = [];
    for (int i = 0; i < n; i++) {
        map.Add(i, i.ToString());
    }
}
space_complexity.go
/* Linear order */
func spaceLinear(n int) {
    // Array of length n uses O(n) space
    _ = make([]int, n)
    // A list of length n occupies O(n) space
    var nodes []*node
    for i := 0; i < n; i++ {
        nodes = append(nodes, newNode(i))
    }
    // A hash table of length n occupies O(n) space
    m := make(map[int]string, n)
    for i := 0; i < n; i++ {
        m[i] = strconv.Itoa(i)
    }
}
space_complexity.swift
/* Linear order */
func linear(n: Int) {
    // Array of length n uses O(n) space
    let nums = Array(repeating: 0, count: n)
    // A list of length n occupies O(n) space
    let nodes = (0 ..< n).map { ListNode(x: $0) }
    // A hash table of length n occupies O(n) space
    let map = Dictionary(uniqueKeysWithValues: (0 ..< n).map { ($0, "\($0)") })
}
space_complexity.js
/* Linear order */
function linear(n) {
    // Array of length n uses O(n) space
    const nums = new Array(n);
    // A list of length n occupies O(n) space
    const nodes = [];
    for (let i = 0; i < n; i++) {
        nodes.push(new ListNode(i));
    }
    // A hash table of length n occupies O(n) space
    const map = new Map();
    for (let i = 0; i < n; i++) {
        map.set(i, i.toString());
    }
}
space_complexity.ts
/* Linear order */
function linear(n: number): void {
    // Array of length n uses O(n) space
    const nums = new Array(n);
    // A list of length n occupies O(n) space
    const nodes: ListNode[] = [];
    for (let i = 0; i < n; i++) {
        nodes.push(new ListNode(i));
    }
    // A hash table of length n occupies O(n) space
    const map = new Map();
    for (let i = 0; i < n; i++) {
        map.set(i, i.toString());
    }
}
space_complexity.dart
/* Linear order */
void linear(int n) {
  // Array of length n uses O(n) space
  List<int> nums = List.filled(n, 0);
  // A list of length n occupies O(n) space
  List<ListNode> nodes = [];
  for (var i = 0; i < n; i++) {
    nodes.add(ListNode(i));
  }
  // A hash table of length n occupies O(n) space
  Map<int, String> map = HashMap();
  for (var i = 0; i < n; i++) {
    map.putIfAbsent(i, () => i.toString());
  }
}
space_complexity.rs
/* Linear order */
#[allow(unused)]
fn linear(n: i32) {
    // Array of length n uses O(n) space
    let mut nums = vec![0; n as usize];
    // A list of length n occupies O(n) space
    let mut nodes = Vec::new();
    for i in 0..n {
        nodes.push(ListNode::new(i))
    }
    // A hash table of length n occupies O(n) space
    let mut map = HashMap::new();
    for i in 0..n {
        map.insert(i, i.to_string());
    }
}
space_complexity.c
/* Hash table */
typedef struct {
    int key;
    int val;
    UT_hash_handle hh; // Implemented using uthash.h
} HashTable;

/* Linear order */
void linear(int n) {
    // Array of length n uses O(n) space
    int *nums = malloc(sizeof(int) * n);
    free(nums);

    // A list of length n occupies O(n) space
    ListNode **nodes = malloc(sizeof(ListNode *) * n);
    for (int i = 0; i < n; i++) {
        nodes[i] = newListNode(i);
    }
    // Memory release
    for (int i = 0; i < n; i++) {
        free(nodes[i]);
    }
    free(nodes);

    // A hash table of length n occupies O(n) space
    HashTable *h = NULL;
    for (int i = 0; i < n; i++) {
        HashTable *tmp = malloc(sizeof(HashTable));
        tmp->key = i;
        tmp->val = i;
        HASH_ADD_INT(h, key, tmp);
    }

    // Memory release
    HashTable *curr, *tmp;
    HASH_ITER(hh, h, curr, tmp) {
        HASH_DEL(h, curr);
        free(curr);
    }
}
space_complexity.kt
/* Linear order */
fun linear(n: Int) {
    // Array of length n uses O(n) space
    val nums = Array(n) { 0 }
    // A list of length n occupies O(n) space
    val nodes = mutableListOf<ListNode>()
    for (i in 0..<n) {
        nodes.add(ListNode(i))
    }
    // A hash table of length n occupies O(n) space
    val map = mutableMapOf<Int, String>()
    for (i in 0..<n) {
        map[i] = i.toString()
    }
}
space_complexity.rb
### Linear time ###
def linear(n)
  # A list of length n occupies O(n) space
  nums = Array.new(n, 0)

  # A hash table of length n occupies O(n) space
  hmap = {}
  for i in 0...n
    hmap[i] = i.to_s
  end
end

As shown in the following figure, the recursion depth of this function is \(n\), meaning that there are \(n\) unreturned linear_recur() functions existing simultaneously, using \(O(n)\) stack frame space:

space_complexity.py
def linear_recur(n: int):
    """Linear order (recursive implementation)"""
    print("Recursion n =", n)
    if n == 1:
        return
    linear_recur(n - 1)
space_complexity.cpp
/* Linear order (recursive implementation) */
void linearRecur(int n) {
    cout << "Recursion n = " << n << endl;
    if (n == 1)
        return;
    linearRecur(n - 1);
}
space_complexity.java
/* Linear order (recursive implementation) */
void linearRecur(int n) {
    System.out.println("Recursion n = " + n);
    if (n == 1)
        return;
    linearRecur(n - 1);
}
space_complexity.cs
/* Linear order (recursive implementation) */
void LinearRecur(int n) {
    Console.WriteLine("Recursion n = " + n);
    if (n == 1) return;
    LinearRecur(n - 1);
}
space_complexity.go
/* Linear order (recursive implementation) */
func spaceLinearRecur(n int) {
    fmt.Println("Recursion n =", n)
    if n == 1 {
        return
    }
    spaceLinearRecur(n - 1)
}
space_complexity.swift
/* Linear order (recursive implementation) */
func linearRecur(n: Int) {
    print("Recursion n = \(n)")
    if n == 1 {
        return
    }
    linearRecur(n: n - 1)
}
space_complexity.js
/* Linear order (recursive implementation) */
function linearRecur(n) {
    console.log(`Recursion n = ${n}`);
    if (n === 1) return;
    linearRecur(n - 1);
}
space_complexity.ts
/* Linear order (recursive implementation) */
function linearRecur(n: number): void {
    console.log(`Recursion n = ${n}`);
    if (n === 1) return;
    linearRecur(n - 1);
}
space_complexity.dart
/* Linear order (recursive implementation) */
void linearRecur(int n) {
  print('Recursion n = $n');
  if (n == 1) return;
  linearRecur(n - 1);
}
space_complexity.rs
/* Linear order (recursive implementation) */
fn linear_recur(n: i32) {
    println!("Recursion n = {}", n);
    if n == 1 {
        return;
    };
    linear_recur(n - 1);
}
space_complexity.c
/* Linear order (recursive implementation) */
void linearRecur(int n) {
    printf("Recursion n = %d\r\n", n);
    if (n == 1)
        return;
    linearRecur(n - 1);
}
space_complexity.kt
/* Linear order (recursive implementation) */
fun linearRecur(n: Int) {
    println("Recursion n = $n")
    if (n == 1)
        return
    linearRecur(n - 1)
}
space_complexity.rb
### Linear space (recursive) ###
def linear_recur(n)
  puts "Recursion n = #{n}"
  return if n == 1
  linear_recur(n - 1)
end

Linear order space complexity generated by recursive function

Figure 2-17   Linear order space complexity generated by recursive function

3.   Quadratic Order \(O(n^2)\)

Quadratic order is common in matrices and graphs, where the number of elements is quadratically related to \(n\):

space_complexity.py
def quadratic(n: int):
    """Quadratic order"""
    # A 2D list occupies O(n^2) space
    num_matrix = [[0] * n for _ in range(n)]
space_complexity.cpp
/* Exponential order */
void quadratic(int n) {
    // 2D list uses O(n^2) space
    vector<vector<int>> numMatrix;
    for (int i = 0; i < n; i++) {
        vector<int> tmp;
        for (int j = 0; j < n; j++) {
            tmp.push_back(0);
        }
        numMatrix.push_back(tmp);
    }
}
space_complexity.java
/* Exponential order */
void quadratic(int n) {
    // Matrix uses O(n^2) space
    int[][] numMatrix = new int[n][n];
    // 2D list uses O(n^2) space
    List<List<Integer>> numList = new ArrayList<>();
    for (int i = 0; i < n; i++) {
        List<Integer> tmp = new ArrayList<>();
        for (int j = 0; j < n; j++) {
            tmp.add(0);
        }
        numList.add(tmp);
    }
}
space_complexity.cs
/* Exponential order */
void Quadratic(int n) {
    // Matrix uses O(n^2) space
    int[,] numMatrix = new int[n, n];
    // 2D list uses O(n^2) space
    List<List<int>> numList = [];
    for (int i = 0; i < n; i++) {
        List<int> tmp = [];
        for (int j = 0; j < n; j++) {
            tmp.Add(0);
        }
        numList.Add(tmp);
    }
}
space_complexity.go
/* Exponential order */
func spaceQuadratic(n int) {
    // Matrix uses O(n^2) space
    numMatrix := make([][]int, n)
    for i := 0; i < n; i++ {
        numMatrix[i] = make([]int, n)
    }
}
space_complexity.swift
/* Exponential order */
func quadratic(n: Int) {
    // 2D list uses O(n^2) space
    let numList = Array(repeating: Array(repeating: 0, count: n), count: n)
}
space_complexity.js
/* Exponential order */
function quadratic(n) {
    // Matrix uses O(n^2) space
    const numMatrix = Array(n)
        .fill(null)
        .map(() => Array(n).fill(null));
    // 2D list uses O(n^2) space
    const numList = [];
    for (let i = 0; i < n; i++) {
        const tmp = [];
        for (let j = 0; j < n; j++) {
            tmp.push(0);
        }
        numList.push(tmp);
    }
}
space_complexity.ts
/* Exponential order */
function quadratic(n: number): void {
    // Matrix uses O(n^2) space
    const numMatrix = Array(n)
        .fill(null)
        .map(() => Array(n).fill(null));
    // 2D list uses O(n^2) space
    const numList = [];
    for (let i = 0; i < n; i++) {
        const tmp = [];
        for (let j = 0; j < n; j++) {
            tmp.push(0);
        }
        numList.push(tmp);
    }
}
space_complexity.dart
/* Exponential order */
void quadratic(int n) {
  // Matrix uses O(n^2) space
  List<List<int>> numMatrix = List.generate(n, (_) => List.filled(n, 0));
  // 2D list uses O(n^2) space
  List<List<int>> numList = [];
  for (var i = 0; i < n; i++) {
    List<int> tmp = [];
    for (int j = 0; j < n; j++) {
      tmp.add(0);
    }
    numList.add(tmp);
  }
}
space_complexity.rs
/* Exponential order */
#[allow(unused)]
fn quadratic(n: i32) {
    // Matrix uses O(n^2) space
    let num_matrix = vec![vec![0; n as usize]; n as usize];
    // 2D list uses O(n^2) space
    let mut num_list = Vec::new();
    for i in 0..n {
        let mut tmp = Vec::new();
        for j in 0..n {
            tmp.push(0);
        }
        num_list.push(tmp);
    }
}
space_complexity.c
/* Exponential order */
void quadratic(int n) {
    // 2D list uses O(n^2) space
    int **numMatrix = malloc(sizeof(int *) * n);
    for (int i = 0; i < n; i++) {
        int *tmp = malloc(sizeof(int) * n);
        for (int j = 0; j < n; j++) {
            tmp[j] = 0;
        }
        numMatrix[i] = tmp;
    }

    // Memory release
    for (int i = 0; i < n; i++) {
        free(numMatrix[i]);
    }
    free(numMatrix);
}
space_complexity.kt
/* Exponential order */
fun quadratic(n: Int) {
    // Matrix uses O(n^2) space
    val numMatrix = arrayOfNulls<Array<Int>?>(n)
    // 2D list uses O(n^2) space
    val numList = mutableListOf<MutableList<Int>>()
    for (i in 0..<n) {
        val tmp = mutableListOf<Int>()
        for (j in 0..<n) {
            tmp.add(0)
        }
        numList.add(tmp)
    }
}
space_complexity.rb
### Quadratic time ###
def quadratic(n)
  # 2D list uses O(n^2) space
  Array.new(n) { Array.new(n, 0) }
end

As shown in the following figure, the recursion depth of this function is \(n\), and an array is initialized in each recursive function with lengths of \(n\), \(n-1\), \(\dots\), \(2\), \(1\), with an average length of \(n / 2\), thus occupying \(O(n^2)\) space overall:

space_complexity.py
def quadratic_recur(n: int) -> int:
    """Quadratic order (recursive implementation)"""
    if n <= 0:
        return 0
    # Array nums length is n, n-1, ..., 2, 1
    nums = [0] * n
    return quadratic_recur(n - 1)
space_complexity.cpp
/* Quadratic order (recursive implementation) */
int quadraticRecur(int n) {
    if (n <= 0)
        return 0;
    vector<int> nums(n);
    cout << "In recursion n = " << n << ", nums length = " << nums.size() << endl;
    return quadraticRecur(n - 1);
}
space_complexity.java
/* Quadratic order (recursive implementation) */
int quadraticRecur(int n) {
    if (n <= 0)
        return 0;
    // Array nums has length n, n-1, ..., 2, 1
    int[] nums = new int[n];
    System.out.println("In recursion n = " + n + ", nums length = " + nums.length);
    return quadraticRecur(n - 1);
}
space_complexity.cs
/* Quadratic order (recursive implementation) */
int QuadraticRecur(int n) {
    if (n <= 0) return 0;
    int[] nums = new int[n];
    Console.WriteLine("Recursion n = " + n + ", nums length = " + nums.Length);
    return QuadraticRecur(n - 1);
}
space_complexity.go
/* Quadratic order (recursive implementation) */
func spaceQuadraticRecur(n int) int {
    if n <= 0 {
        return 0
    }
    nums := make([]int, n)
    fmt.Printf("In recursion n = %d, nums length = %d \n", n, len(nums))
    return spaceQuadraticRecur(n - 1)
}
space_complexity.swift
/* Quadratic order (recursive implementation) */
@discardableResult
func quadraticRecur(n: Int) -> Int {
    if n <= 0 {
        return 0
    }
    // Array nums has length n, n-1, ..., 2, 1
    let nums = Array(repeating: 0, count: n)
    print("In recursion n = \(n), nums length = \(nums.count)")
    return quadraticRecur(n: n - 1)
}
space_complexity.js
/* Quadratic order (recursive implementation) */
function quadraticRecur(n) {
    if (n <= 0) return 0;
    const nums = new Array(n);
    console.log(`In recursion n = ${n}, nums length = ${nums.length}`);
    return quadraticRecur(n - 1);
}
space_complexity.ts
/* Quadratic order (recursive implementation) */
function quadraticRecur(n: number): number {
    if (n <= 0) return 0;
    const nums = new Array(n);
    console.log(`In recursion n = ${n}, nums length = ${nums.length}`);
    return quadraticRecur(n - 1);
}
space_complexity.dart
/* Quadratic order (recursive implementation) */
int quadraticRecur(int n) {
  if (n <= 0) return 0;
  List<int> nums = List.filled(n, 0);
  print('In recursion n = $n, nums length = ${nums.length}');
  return quadraticRecur(n - 1);
}
space_complexity.rs
/* Quadratic order (recursive implementation) */
fn quadratic_recur(n: i32) -> i32 {
    if n <= 0 {
        return 0;
    };
    // Array nums has length n, n-1, ..., 2, 1
    let nums = vec![0; n as usize];
    println!("In recursion n = {}, nums length = {}", n, nums.len());
    return quadratic_recur(n - 1);
}
space_complexity.c
/* Quadratic order (recursive implementation) */
int quadraticRecur(int n) {
    if (n <= 0)
        return 0;
    int *nums = malloc(sizeof(int) * n);
    printf("In recursion n = %d, nums length = %d\r\n", n, n);
    int res = quadraticRecur(n - 1);
    free(nums);
    return res;
}
space_complexity.kt
/* Quadratic order (recursive implementation) */
tailrec fun quadraticRecur(n: Int): Int {
    if (n <= 0)
        return 0
    // Array nums has length n, n-1, ..., 2, 1
    val nums = Array(n) { 0 }
    println("In recursion n = $n, nums length = ${nums.size}")
    return quadraticRecur(n - 1)
}
space_complexity.rb
### Quadratic space (recursive) ###
def quadratic_recur(n)
  return 0 unless n > 0

  # Array nums has length n, n-1, ..., 2, 1
  nums = Array.new(n, 0)
  quadratic_recur(n - 1)
end

Quadratic order space complexity generated by recursive function

Figure 2-18   Quadratic order space complexity generated by recursive function

4.   Exponential Order \(O(2^n)\)

Exponential order is common in binary trees. Observe the following figure: a "full binary tree" with \(n\) levels has \(2^n - 1\) nodes, occupying \(O(2^n)\) space:

space_complexity.py
def build_tree(n: int) -> TreeNode | None:
    """Exponential order (build full binary tree)"""
    if n == 0:
        return None
    root = TreeNode(0)
    root.left = build_tree(n - 1)
    root.right = build_tree(n - 1)
    return root
space_complexity.cpp
/* Driver Code */
TreeNode *buildTree(int n) {
    if (n == 0)
        return nullptr;
    TreeNode *root = new TreeNode(0);
    root->left = buildTree(n - 1);
    root->right = buildTree(n - 1);
    return root;
}
space_complexity.java
/* Driver Code */
TreeNode buildTree(int n) {
    if (n == 0)
        return null;
    TreeNode root = new TreeNode(0);
    root.left = buildTree(n - 1);
    root.right = buildTree(n - 1);
    return root;
}
space_complexity.cs
/* Driver Code */
TreeNode? BuildTree(int n) {
    if (n == 0) return null;
    TreeNode root = new(0) {
        left = BuildTree(n - 1),
        right = BuildTree(n - 1)
    };
    return root;
}
space_complexity.go
/* Driver Code */
func buildTree(n int) *TreeNode {
    if n == 0 {
        return nil
    }
    root := NewTreeNode(0)
    root.Left = buildTree(n - 1)
    root.Right = buildTree(n - 1)
    return root
}
space_complexity.swift
/* Driver Code */
func buildTree(n: Int) -> TreeNode? {
    if n == 0 {
        return nil
    }
    let root = TreeNode(x: 0)
    root.left = buildTree(n: n - 1)
    root.right = buildTree(n: n - 1)
    return root
}
space_complexity.js
/* Driver Code */
function buildTree(n) {
    if (n === 0) return null;
    const root = new TreeNode(0);
    root.left = buildTree(n - 1);
    root.right = buildTree(n - 1);
    return root;
}
space_complexity.ts
/* Driver Code */
function buildTree(n: number): TreeNode | null {
    if (n === 0) return null;
    const root = new TreeNode(0);
    root.left = buildTree(n - 1);
    root.right = buildTree(n - 1);
    return root;
}
space_complexity.dart
/* Driver Code */
TreeNode? buildTree(int n) {
  if (n == 0) return null;
  TreeNode root = TreeNode(0);
  root.left = buildTree(n - 1);
  root.right = buildTree(n - 1);
  return root;
}
space_complexity.rs
/* Driver Code */
fn build_tree(n: i32) -> Option<Rc<RefCell<TreeNode>>> {
    if n == 0 {
        return None;
    };
    let root = TreeNode::new(0);
    root.borrow_mut().left = build_tree(n - 1);
    root.borrow_mut().right = build_tree(n - 1);
    return Some(root);
}
space_complexity.c
/* Driver Code */
TreeNode *buildTree(int n) {
    if (n == 0)
        return NULL;
    TreeNode *root = newTreeNode(0);
    root->left = buildTree(n - 1);
    root->right = buildTree(n - 1);
    return root;
}
space_complexity.kt
/* Driver Code */
fun buildTree(n: Int): TreeNode? {
    if (n == 0)
        return null
    val root = TreeNode(0)
    root.left = buildTree(n - 1)
    root.right = buildTree(n - 1)
    return root
}
space_complexity.rb
### Exponential space (build full binary tree) ###
def build_tree(n)
  return if n == 0

  TreeNode.new.tap do |root|
    root.left = build_tree(n - 1)
    root.right = build_tree(n - 1)
  end
end

Exponential order space complexity generated by full binary tree

Figure 2-19   Exponential order space complexity generated by full binary tree

5.   Logarithmic Order \(O(\log n)\)

Logarithmic order is common in divide-and-conquer algorithms. For example, merge sort: given an input array of length \(n\), each recursion divides the array in half from the midpoint, forming a recursion tree of height \(\log n\), using \(O(\log n)\) stack frame space.

Another example is converting a number to a string. Given a positive integer \(n\), it has \(\lfloor \log_{10} n \rfloor + 1\) digits, i.e., the corresponding string length is \(\lfloor \log_{10} n \rfloor + 1\), so the space complexity is \(O(\log_{10} n + 1) = O(\log n)\).

2.4.4   Trading Time for Space

Ideally, we hope that both the time complexity and space complexity of an algorithm can reach optimal. However, in practice, optimizing both time complexity and space complexity simultaneously is usually very difficult.

Reducing time complexity usually comes at the cost of increasing space complexity, and vice versa. The approach of sacrificing memory space to improve algorithm execution speed is called "trading space for time"; conversely, it is called "trading time for space".

The choice of which approach depends on which aspect we value more. In most cases, time is more precious than space, so "trading space for time" is usually the more common strategy. Of course, when the data volume is very large, controlling space complexity is also very important.

Feel free to drop your insights, questions or suggestions