2.4 Space complexity¶
Space complexity measures how an algorithm's memory use grows as the input gets larger. It is much like time complexity, with memory usage replacing runtime.
2.4.1 Space used by an algorithm¶
The memory an algorithm uses during execution falls into these main categories.
- Input space: Stores the input data.
- Temporary space: Stores variables, objects, function contexts, and other working data.
- Output space: Stores the output data.
Space complexity generally counts temporary space plus output space.
Temporary space can be divided further into three parts.
- Temporary data: Constants, variables, objects, and other data used during execution.
- Stack frames: Context data for function calls. Each call creates a frame at the top of the stack, which is released when the function returns.
- Instruction space: Compiled program instructions, usually ignored in the analysis.
We usually count temporary data, stack frames, and output data when analyzing space complexity, as shown in Figure 2-15.

Figure 2-15 Space used by an algorithm
The corresponding code is:
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, conventionally named in uppercase)
b = 0 # Temporary data (variable)
node = Node(0) # Temporary data (object)
c = function() # Stack frame (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 (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 (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 (function call)
return a + b + c; // Output data
}
/* Structure */
type node struct {
val int
next *node
}
/* Create a 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 (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 (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 (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 (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 (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 a 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 (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 (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 (function call)
a + b + c # Output data
end
2.4.2 Deriving space complexity¶
The approach is much like time complexity analysis, except that we count memory used rather than operations performed.
Unlike time complexity, we usually focus only on worst-case space complexity. Memory is a hard requirement: enough must be available for every valid input.
In the following code, "worst case" has two meanings.
- The worst input: When \(n < 10\), space complexity is \(O(1)\). When \(n > 10\), initializing
numsrequires \(O(n)\) space, so worst-case space complexity is \(O(n)\). - Peak memory during execution: Before the final line, the program uses \(O(1)\) space. Initializing
numsraises usage to \(O(n)\), so worst-case space complexity is \(O(n)\).
For recursive functions, remember to count stack frames. Consider the following code:
Both loop() and recur() take \(O(n)\) time, but their space complexities differ.
loop()callsfunction()\(n\) times. Each call returns and releases its stack frame before the next iteration, so space complexity remains \(O(1)\).- During
recur(), \(n\) calls can remain active at once, using \(O(n)\) space for stack frames.
2.4.3 Common complexity classes¶
For input size \(n\), Figure 2-16 shows common space complexities in increasing order of growth.

Figure 2-16 Common space complexity classes
1. Constant space \(O(1)\)¶
Constant space commonly occurs when the number of constants, variables, and objects is independent of input size \(n\).
Memory for variables initialized or functions called within a loop is released by the next iteration, so it does not accumulate. Space complexity remains \(O(1)\):
def function() -> int:
"""Function"""
# Perform some operations
return 0
def constant(n: int):
"""Constant complexity"""
# Constants, variables, and 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
# Function calls in the loop occupy O(1) space
for _ in range(n):
function()
/* Function */
int func() {
// Perform some operations
return 0;
}
/* Constant complexity */
void constant(int n) {
// Constants, variables, and 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;
}
// Function calls in the loop occupy O(1) space
for (int i = 0; i < n; i++) {
func();
}
}
/* Function */
int function() {
// Perform some operations
return 0;
}
/* Constant complexity */
void constant(int n) {
// Constants, variables, and 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;
}
// Function calls in the loop occupy O(1) space
for (int i = 0; i < n; i++) {
function();
}
}
/* Function */
int Function() {
// Perform some operations
return 0;
}
/* Constant complexity */
void Constant(int n) {
// Constants, variables, and 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;
}
// Function calls in the loop occupy O(1) space
for (int i = 0; i < n; i++) {
Function();
}
}
/* Function */
func function() int {
// Perform some operations...
return 0
}
/* Constant complexity */
func spaceConstant(n int) {
// Constants, variables, and 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
}
// Function calls in the loop occupy O(1) space
for i := 0; i < n; i++ {
function()
}
b += 0
c += 0
nums[0] = 0
node.val = 0
}
/* Function */
@discardableResult
func function() -> Int {
// Perform some operations
return 0
}
/* Constant complexity */
func constant(n: Int) {
// Constants, variables, and 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
}
// Function calls in the loop occupy O(1) space
for _ in 0 ..< n {
function()
}
}
/* Function */
function constFunc() {
// Perform some operations
return 0;
}
/* Constant complexity */
function constant(n) {
// Constants, variables, and 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;
}
// Function calls in the loop occupy O(1) space
for (let i = 0; i < n; i++) {
constFunc();
}
}
/* Function */
function constFunc(): number {
// Perform some operations
return 0;
}
/* Constant complexity */
function constant(n: number): void {
// Constants, variables, and 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;
}
// Function calls in the loop occupy O(1) space
for (let i = 0; i < n; i++) {
constFunc();
}
}
/* Function */
int function() {
// Perform some operations
return 0;
}
/* Constant complexity */
void constant(int n) {
// Constants, variables, and 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;
}
// Function calls in the loop occupy O(1) space
for (var i = 0; i < n; i++) {
function();
}
}
/* Function */
fn function() -> i32 {
// Perform some operations
return 0;
}
/* Constant complexity */
#[allow(unused)]
fn constant(n: i32) {
// Constants, variables, and 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;
}
// Function calls in the loop occupy O(1) space
for i in 0..n {
function();
}
}
/* Function */
int func() {
// Perform some operations
return 0;
}
/* Constant complexity */
void constant(int n) {
// Constants, variables, and 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;
}
// Function calls in the loop occupy O(1) space
for (int i = 0; i < n; i++) {
func();
}
}
/* Function */
fun function(): Int {
// Perform some operations
return 0
}
/* Constant complexity */
fun constant(n: Int) {
// Constants, variables, and 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
}
// Function calls in the loop occupy O(1) space
for (i in 0..<n) {
function()
}
}
### Function ###
def function
# Perform some operations
0
end
### Constant complexity ###
def constant(n)
# Constants, variables, and 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 }
# Function calls in the loop occupy O(1) space
(0...n).each { function }
end
Visualize execution
2. Linear space \(O(n)\)¶
Linear space commonly occurs in arrays, linked lists, stacks, and queues whose element counts are proportional to \(n\):
/* Linear complexity */
void linear(int n) {
// An 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 with n entries occupies O(n) space
unordered_map<int, string> map;
for (int i = 0; i < n; i++) {
map[i] = to_string(i);
}
}
/* Linear complexity */
void linear(int n) {
// An 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 with n entries occupies O(n) space
Map<Integer, String> map = new HashMap<>();
for (int i = 0; i < n; i++) {
map.put(i, String.valueOf(i));
}
}
/* Linear complexity */
void Linear(int n) {
// An 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 with n entries occupies O(n) space
Dictionary<int, string> map = [];
for (int i = 0; i < n; i++) {
map.Add(i, i.ToString());
}
}
/* Linear complexity */
func spaceLinear(n int) {
// An 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 with n entries occupies O(n) space
m := make(map[int]string, n)
for i := 0; i < n; i++ {
m[i] = strconv.Itoa(i)
}
}
/* Linear complexity */
func linear(n: Int) {
// An 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 with n entries occupies O(n) space
let map = Dictionary(uniqueKeysWithValues: (0 ..< n).map { ($0, "\($0)") })
}
/* Linear complexity */
function linear(n) {
// An 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 with n entries occupies O(n) space
const map = new Map();
for (let i = 0; i < n; i++) {
map.set(i, i.toString());
}
}
/* Linear complexity */
function linear(n: number): void {
// An 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 with n entries occupies O(n) space
const map = new Map();
for (let i = 0; i < n; i++) {
map.set(i, i.toString());
}
}
/* Linear complexity */
void linear(int n) {
// An 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 with n entries occupies O(n) space
Map<int, String> map = HashMap();
for (var i = 0; i < n; i++) {
map.putIfAbsent(i, () => i.toString());
}
}
/* Linear complexity */
#[allow(unused)]
fn linear(n: i32) {
// An 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 with n entries occupies O(n) space
let mut map = HashMap::new();
for i in 0..n {
map.insert(i, i.to_string());
}
}
/* Hash table */
typedef struct {
int key;
int val;
UT_hash_handle hh; // Implemented using uthash.h
} HashTable;
/* Linear complexity */
void linear(int n) {
// An 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);
}
// Free memory
for (int i = 0; i < n; i++) {
free(nodes[i]);
}
free(nodes);
// A hash table with n entries 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);
}
// Free memory
HashTable *curr, *tmp;
HASH_ITER(hh, h, curr, tmp) {
HASH_DEL(h, curr);
free(curr);
}
}
/* Linear complexity */
fun linear(n: Int) {
// An 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 with n entries occupies O(n) space
val map = mutableMapOf<Int, String>()
for (i in 0..<n) {
map[i] = i.toString()
}
}
Visualize execution
As shown in Figure 2-17, this function has recursion depth \(n\), with \(n\) active calls to linear_recur() at once, using \(O(n)\) space for stack frames:
Visualize execution

Figure 2-17 Linear space complexity from recursion
3. Quadratic space \(O(n^2)\)¶
Quadratic space commonly occurs in matrices and graphs whose element counts grow with the square of \(n\):
/* Quadratic complexity */
void quadratic(int n) {
// The matrix uses O(n^2) space
int[][] numMatrix = new int[n][n];
// The two-dimensional list occupies 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);
}
}
/* Quadratic complexity */
void Quadratic(int n) {
// The matrix uses O(n^2) space
int[,] numMatrix = new int[n, n];
// The two-dimensional list occupies 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);
}
}
/* Quadratic complexity */
function quadratic(n) {
// The matrix uses O(n^2) space
const numMatrix = Array(n)
.fill(null)
.map(() => Array(n).fill(null));
// The two-dimensional list occupies 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);
}
}
/* Quadratic complexity */
function quadratic(n: number): void {
// The matrix uses O(n^2) space
const numMatrix = Array(n)
.fill(null)
.map(() => Array(n).fill(null));
// The two-dimensional list occupies 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);
}
}
/* Quadratic complexity */
void quadratic(int n) {
// The matrix uses O(n^2) space
List<List<int>> numMatrix = List.generate(n, (_) => List.filled(n, 0));
// The two-dimensional list occupies 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);
}
}
/* Quadratic complexity */
#[allow(unused)]
fn quadratic(n: i32) {
// The matrix uses O(n^2) space
let num_matrix = vec![vec![0; n as usize]; n as usize];
// The two-dimensional list occupies 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);
}
}
/* Quadratic complexity */
void quadratic(int n) {
// The two-dimensional list occupies 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;
}
// Free memory
for (int i = 0; i < n; i++) {
free(numMatrix[i]);
}
free(numMatrix);
}
/* Quadratic complexity */
fun quadratic(n: Int) {
// A full n*n matrix uses O(n^2) space; only its outer array is allocated here
val numMatrix = arrayOfNulls<Array<Int>?>(n)
// The two-dimensional list occupies 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)
}
}
Visualize execution
As shown in Figure 2-18, the recursion depth is \(n\), and each call initializes an array. Their lengths are \(n\), \(n-1\), \(\dots\), \(2\), and \(1\), averaging \(n / 2\), so total space is \(O(n^2)\):
/* Quadratic complexity (recursive implementation) */
int quadraticRecur(int n) {
if (n <= 0)
return 0;
// The lengths of array nums are n, n-1, ..., 2, 1
int[] nums = new int[n];
System.out.println("Recursive call: n = " + n + " with nums length = " + nums.length);
return quadraticRecur(n - 1);
}
/* Quadratic complexity (recursive implementation) */
@discardableResult
func quadraticRecur(n: Int) -> Int {
if n <= 0 {
return 0
}
// The lengths of array nums are n, n-1, ..., 2, 1
let nums = Array(repeating: 0, count: n)
print("At recursion n = \(n), nums has length \(nums.count)")
return quadraticRecur(n: n - 1)
}
/* Quadratic complexity (recursive implementation) */
fn quadratic_recur(n: i32) -> i32 {
if n <= 0 {
return 0;
};
// The lengths of array nums are n, n-1, ..., 2, 1
let nums = vec![0; n as usize];
println!("In recursive call n = {}, nums has length {}", n, nums.len());
return quadratic_recur(n - 1);
}
/* Quadratic allocation total (recursive example; tailrec removes recursive stack growth) */
tailrec fun quadraticRecur(n: Int): Int {
if (n <= 0)
return 0
// The lengths of array nums are n, n-1, ..., 2, 1
val nums = Array(n) { 0 }
println("In the recursive call with n = $n, nums has length ${nums.size}")
return quadraticRecur(n - 1)
}
Visualize execution

Figure 2-18 Quadratic space complexity from recursion
4. Exponential space \(O(2^n)\)¶
Exponential space commonly occurs in binary trees. In Figure 2-19, a perfect binary tree with \(n\) levels contains \(2^n - 1\) nodes and uses \(O(2^n)\) space:
/* Exponential complexity (building a perfect binary tree) */
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);
}
Visualize execution

Figure 2-19 Exponential space used by a perfect binary tree
5. Logarithmic space \(O(\log n)\)¶
Logarithmic space often appears in divide-and-conquer algorithms. For example, merge sort splits an array of length \(n\) in half at each recursive step, producing a recursion tree of height \(\log n\) and using \(O(\log n)\) space for stack frames.
Another example is converting a number to a string. A positive integer \(n\) has \(\lfloor \log_{10} n \rfloor + 1\) digits, giving a string of length \(\lfloor \log_{10} n \rfloor + 1\). The space complexity is therefore \(O(\log_{10} n + 1) = O(\log n)\).
2.4.4 Balancing time and space¶
Ideally, we would minimize both time and space complexity. In practice, optimizing both simultaneously is usually difficult.
Reducing time complexity often increases space complexity, and vice versa. Using more memory to run faster is called trading space for time; accepting slower execution to use less memory is trading time for space.
The choice depends on which resource matters more. Time is usually more valuable than space, so trading space for time is common. For very large datasets, however, keeping space complexity under control is also crucial.