2.4 空間計算量¶
空間計算量(space complexity)は、データ量が増えるにつれて、アルゴリズムのメモリ使用量がどのように増えるかを測ります。時間計算量とよく似ており、「実行時間」を「メモリ使用量」に置き換えた概念です。
2.4.1 アルゴリズムに関わる空間¶
実行中に使うメモリ空間は、主に次の種類に分かれます。
- 入力空間:入力データを保存します。
- 一時空間:実行中の変数、オブジェクト、関数の実行状態などを保存します。
- 出力空間:出力データを保存します。
通常、空間計算量には「一時空間」と「出力空間」を含めます。
一時空間は、さらに三つに分けられます。
- 一時データ:実行中の定数、変数、オブジェクトなどを保存します。
- スタックフレーム:呼び出した関数の実行状態を保存します。関数を呼ぶたびにスタックの先頭にフレームが作られ、戻ると解放されます。
- 命令空間:コンパイルされた命令を保存します。実際の集計では通常、無視します。
プログラムの空間計算量では、図 2-15 のように、通常は一時データ、スタックフレーム、出力データの三つを数えます。

図 2-15 アルゴリズムが使う空間
関連するコードを次に示します。
class Node:
"""クラス"""
def __init__(self, x: int):
self.val: int = x # ノードの値
self.next: Node | None = None # 次のノードへの参照
def function() -> int:
"""関数"""
# 何らかの処理を実行...
return 0
def algorithm(n) -> int: # 入力データ
A = 0 # 一時データ(定数、通常は大文字で表す)
b = 0 # 一時データ(変数)
node = Node(0) # 一時データ(オブジェクト)
c = function() # スタックフレーム(関数呼び出し)
return A + b + c # 出力データ
/* 構造体 */
struct Node {
int val;
Node *next;
Node(int x) : val(x), next(nullptr) {}
};
/* 関数 */
int func() {
// 何らかの処理を実行...
return 0;
}
int algorithm(int n) { // 入力データ
const int a = 0; // 一時データ(定数)
int b = 0; // 一時データ(変数)
Node* node = new Node(0); // 一時データ(オブジェクト)
int c = func(); // スタックフレーム(関数呼び出し)
return a + b + c; // 出力データ
}
/* クラス */
class Node {
int val;
Node next;
Node(int x) { val = x; }
}
/* 関数 */
int function() {
// 何らかの処理を実行...
return 0;
}
int algorithm(int n) { // 入力データ
final int a = 0; // 一時データ(定数)
int b = 0; // 一時データ(変数)
Node node = new Node(0); // 一時データ(オブジェクト)
int c = function(); // スタックフレーム(関数呼び出し)
return a + b + c; // 出力データ
}
/* クラス */
class Node(int x) {
int val = x;
Node next;
}
/* 関数 */
int Function() {
// 何らかの処理を実行...
return 0;
}
int Algorithm(int n) { // 入力データ
const int a = 0; // 一時データ(定数)
int b = 0; // 一時データ(変数)
Node node = new(0); // 一時データ(オブジェクト)
int c = Function(); // スタックフレーム(関数呼び出し)
return a + b + c; // 出力データ
}
/* 構造体 */
type node struct {
val int
next *node
}
/* node 構造体を作成 */
func newNode(val int) *node {
return &node{val: val}
}
/* 関数 */
func function() int {
// 何らかの処理を実行...
return 0
}
func algorithm(n int) int { // 入力データ
const a = 0 // 一時データ(定数)
b := 0 // 一時データ(変数)
newNode(0) // 一時データ(オブジェクト)
c := function() // スタックフレーム(関数呼び出し)
return a + b + c // 出力データ
}
/* クラス */
class Node {
var val: Int
var next: Node?
init(x: Int) {
val = x
}
}
/* 関数 */
func function() -> Int {
// 何らかの処理を実行...
return 0
}
func algorithm(n: Int) -> Int { // 入力データ
let a = 0 // 一時データ(定数)
var b = 0 // 一時データ(変数)
let node = Node(x: 0) // 一時データ(オブジェクト)
let c = function() // スタックフレーム(関数呼び出し)
return a + b + c // 出力データ
}
/* クラス */
class Node {
val;
next;
constructor(val) {
this.val = val === undefined ? 0 : val; // ノードの値
this.next = null; // 次のノードへの参照
}
}
/* 関数 */
function constFunc() {
// 何らかの処理を実行
return 0;
}
function algorithm(n) { // 入力データ
const a = 0; // 一時データ(定数)
let b = 0; // 一時データ(変数)
const node = new Node(0); // 一時データ(オブジェクト)
const c = constFunc(); // スタックフレーム(関数呼び出し)
return a + b + c; // 出力データ
}
/* クラス */
class Node {
val: number;
next: Node | null;
constructor(val?: number) {
this.val = val === undefined ? 0 : val; // ノードの値
this.next = null; // 次のノードへの参照
}
}
/* 関数 */
function constFunc(): number {
// 何らかの処理を実行
return 0;
}
function algorithm(n: number): number { // 入力データ
const a = 0; // 一時データ(定数)
let b = 0; // 一時データ(変数)
const node = new Node(0); // 一時データ(オブジェクト)
const c = constFunc(); // スタックフレーム(関数呼び出し)
return a + b + c; // 出力データ
}
/* クラス */
class Node {
int val;
Node next;
Node(this.val, [this.next]);
}
/* 関数 */
int function() {
// 何らかの処理を実行...
return 0;
}
int algorithm(int n) { // 入力データ
const int a = 0; // 一時データ(定数)
int b = 0; // 一時データ(変数)
Node node = Node(0); // 一時データ(オブジェクト)
int c = function(); // スタックフレーム(関数呼び出し)
return a + b + c; // 出力データ
}
use std::rc::Rc;
use std::cell::RefCell;
/* 構造体 */
struct Node {
val: i32,
next: Option<Rc<RefCell<Node>>>,
}
/* Node 構造体を作成 */
impl Node {
fn new(val: i32) -> Self {
Self { val: val, next: None }
}
}
/* 関数 */
fn function() -> i32 {
// 何らかの処理を実行...
return 0;
}
fn algorithm(n: i32) -> i32 { // 入力データ
const a: i32 = 0; // 一時データ(定数)
let mut b = 0; // 一時データ(変数)
let node = Node::new(0); // 一時データ(オブジェクト)
let c = function(); // スタックフレーム(関数呼び出し)
return a + b + c; // 出力データ
}
/* クラス */
class Node(var _val: Int) {
var next: Node? = null
}
/* 関数 */
fun function(): Int {
// 何らかの処理を実行...
return 0
}
fun algorithm(n: Int): Int { // 入力データ
val a = 0 // 一時データ(定数)
var b = 0 // 一時データ(変数)
val node = Node(0) // 一時データ(オブジェクト)
val c = function() // スタックフレーム(関数呼び出し)
return a + b + c // 出力データ
}
### クラス ###
class Node
attr_accessor :val # ノードの値
attr_accessor :next # 次のノードへの参照
def initialize(x)
@val = x
end
end
### 関数 ###
def function
# 何らかの処理を実行...
0
end
### アルゴリズム ###
def algorithm(n) # 入力データ
a = 0 # 一時データ(定数)
b = 0 # 一時データ(変数)
node = Node.new(0) # 一時データ(オブジェクト)
c = function # スタックフレーム(関数呼び出し)
a + b + c # 出力データ
end
2.4.2 見積もり方¶
空間計算量の見積もり方は時間計算量とほぼ同じで、数える対象を「操作回数」から「使う空間の大きさ」に変えます。
ただし時間計算量と異なり、通常は最悪空間計算量だけに注目します。メモリ容量は必ず満たすべき要件であり、どの入力でも十分なメモリを確保できるようにする必要があるためです。
次のコードでは、「最悪」には二つの意味があります。
- 最悪の入力を基準にする:\(n < 10\) なら \(O(1)\) ですが、\(n > 10\) なら配列
numsが \(O(n)\) の空間を使うため、最悪空間計算量は \(O(n)\) です。 - 実行中の最大メモリ使用量を基準にする:たとえば、最後の行の実行前は \(O(1)\) ですが、配列
numsを初期化すると \(O(n)\) の空間を使うため、最悪空間計算量は \(O(n)\) です。
再帰関数では、スタックフレームを数え忘れないよう注意が必要です。次のコードを見てください。
関数 loop() と recur() の時間計算量はどちらも \(O(n)\) ですが、空間計算量は異なります。
loop()はfunction()を \(n\) 回呼びますが、毎回戻ってスタックフレームを解放するため、空間計算量は \(O(1)\) のままです。- 再帰関数
recur()では、戻っていないrecur()が同時に \(n\) 個存在するため、スタックフレームに \(O(n)\) の空間を使います。
2.4.3 代表的な種類¶
入力の大きさを \(n\) とし、代表的な空間計算量を小さい順に図 2-16 に示します。

図 2-16 代表的な空間計算量
1. 定数のオーダー \(O(1)\)¶
定数のオーダーは、入力の大きさ \(n\) によって個数が変わらない定数、変数、オブジェクトによく見られます。
ループ内の変数の初期化や関数呼び出しで使ったメモリは、次の反復に入ると解放され、累積しないため、空間計算量は \(O(1)\) のままです。
/* 関数 */
int func() {
// 何らかの処理を実行する
return 0;
}
/* 定数オーダー */
void constant(int n) {
// 定数、変数、オブジェクトは O(1) の領域を使用する
const int a = 0;
int b = 0;
vector<int> nums(10000);
ListNode node(0);
// ループ内の変数は O(1) の領域を使用する
for (int i = 0; i < n; i++) {
int c = 0;
}
// ループ内の関数は O(1) の領域を使用する
for (int i = 0; i < n; i++) {
func();
}
}
/* 関数 */
int function() {
// 何らかの処理を実行する
return 0;
}
/* 定数オーダー */
void constant(int n) {
// 定数、変数、オブジェクトは O(1) の領域を使用する
final int a = 0;
int b = 0;
int[] nums = new int[10000];
ListNode node = new ListNode(0);
// ループ内の変数は O(1) の領域を使用する
for (int i = 0; i < n; i++) {
int c = 0;
}
// ループ内の関数は O(1) の領域を使用する
for (int i = 0; i < n; i++) {
function();
}
}
/* 関数 */
int Function() {
// 何らかの処理を実行する
return 0;
}
/* 定数オーダー */
void Constant(int n) {
// 定数、変数、オブジェクトは O(1) の領域を使用する
int a = 0;
int b = 0;
int[] nums = new int[10000];
ListNode node = new(0);
// ループ内の変数は O(1) の領域を使用する
for (int i = 0; i < n; i++) {
int c = 0;
}
// ループ内の関数は O(1) の領域を使用する
for (int i = 0; i < n; i++) {
Function();
}
}
/* 関数 */
func function() int {
// 何らかの処理を実行する...
return 0
}
/* 定数オーダー */
func spaceConstant(n int) {
// 定数、変数、オブジェクトは O(1) の領域を使用する
const a = 0
b := 0
nums := make([]int, 10000)
node := newNode(0)
// ループ内の変数は O(1) の領域を使用する
var c int
for i := 0; i < n; i++ {
c = 0
}
// ループ内の関数は O(1) の領域を使用する
for i := 0; i < n; i++ {
function()
}
b += 0
c += 0
nums[0] = 0
node.val = 0
}
/* 関数 */
@discardableResult
func function() -> Int {
// 何らかの処理を実行する
return 0
}
/* 定数オーダー */
func constant(n: Int) {
// 定数、変数、オブジェクトは O(1) の領域を使用する
let a = 0
var b = 0
let nums = Array(repeating: 0, count: 10000)
let node = ListNode(x: 0)
// ループ内の変数は O(1) の領域を使用する
for _ in 0 ..< n {
let c = 0
}
// ループ内の関数は O(1) の領域を使用する
for _ in 0 ..< n {
function()
}
}
/* 関数 */
function constFunc() {
// 何らかの処理を実行する
return 0;
}
/* 定数オーダー */
function constant(n) {
// 定数、変数、オブジェクトは O(1) の領域を使用する
const a = 0;
const b = 0;
const nums = new Array(10000);
const node = new ListNode(0);
// ループ内の変数は O(1) の領域を使用する
for (let i = 0; i < n; i++) {
const c = 0;
}
// ループ内の関数は O(1) の領域を使用する
for (let i = 0; i < n; i++) {
constFunc();
}
}
/* 関数 */
function constFunc(): number {
// 何らかの処理を実行する
return 0;
}
/* 定数オーダー */
function constant(n: number): void {
// 定数、変数、オブジェクトは O(1) の領域を使用する
const a = 0;
const b = 0;
const nums = new Array(10000);
const node = new ListNode(0);
// ループ内の変数は O(1) の領域を使用する
for (let i = 0; i < n; i++) {
const c = 0;
}
// ループ内の関数は O(1) の領域を使用する
for (let i = 0; i < n; i++) {
constFunc();
}
}
/* 関数 */
int function() {
// 何らかの処理を実行する
return 0;
}
/* 定数オーダー */
void constant(int n) {
// 定数、変数、オブジェクトは O(1) の領域を使用する
final int a = 0;
int b = 0;
List<int> nums = List.filled(10000, 0);
ListNode node = ListNode(0);
// ループ内の変数は O(1) の領域を使用する
for (var i = 0; i < n; i++) {
int c = 0;
}
// ループ内の関数は O(1) の領域を使用する
for (var i = 0; i < n; i++) {
function();
}
}
/* 関数 */
fn function() -> i32 {
// 何らかの処理を実行する
return 0;
}
/* 定数オーダー */
#[allow(unused)]
fn constant(n: i32) {
// 定数、変数、オブジェクトは O(1) の領域を使用する
const A: i32 = 0;
let b = 0;
let nums = vec![0; 10000];
let node = ListNode::new(0);
// ループ内の変数は O(1) の領域を使用する
for i in 0..n {
let c = 0;
}
// ループ内の関数は O(1) の領域を使用する
for i in 0..n {
function();
}
}
/* 関数 */
int func() {
// 何らかの処理を実行する
return 0;
}
/* 定数オーダー */
void constant(int n) {
// 定数、変数、オブジェクトは O(1) の領域を使用する
const int a = 0;
int b = 0;
int nums[1000];
ListNode *node = newListNode(0);
free(node);
// ループ内の変数は O(1) の領域を使用する
for (int i = 0; i < n; i++) {
int c = 0;
}
// ループ内の関数は O(1) の領域を使用する
for (int i = 0; i < n; i++) {
func();
}
}
/* 関数 */
fun function(): Int {
// 何らかの処理を実行する
return 0
}
/* 定数オーダー */
fun constant(n: Int) {
// 定数、変数、オブジェクトは O(1) の領域を使用する
val a = 0
var b = 0
val nums = Array(10000) { 0 }
val node = ListNode(0)
// ループ内の変数は O(1) の領域を使用する
for (i in 0..<n) {
val c = 0
}
// ループ内の関数は O(1) の領域を使用する
for (i in 0..<n) {
function()
}
}
実行の可視化
2. 線形のオーダー \(O(n)\)¶
線形のオーダーは、要素数が \(n\) に比例する配列、連結リスト、スタック、キューなどによく見られます。
/* 線形オーダー */
void linear(int n) {
// 長さ n の配列は O(n) の領域を使用する
vector<int> nums(n);
// 長さ n のリストは O(n) の領域を使用する
vector<ListNode> nodes;
for (int i = 0; i < n; i++) {
nodes.push_back(ListNode(i));
}
// 長さ n のハッシュテーブルは O(n) の領域を使用する
unordered_map<int, string> map;
for (int i = 0; i < n; i++) {
map[i] = to_string(i);
}
}
/* 線形オーダー */
void linear(int n) {
// 長さ n の配列は O(n) の領域を使用する
int[] nums = new int[n];
// 長さ n のリストは O(n) の領域を使用する
List<ListNode> nodes = new ArrayList<>();
for (int i = 0; i < n; i++) {
nodes.add(new ListNode(i));
}
// 長さ n のハッシュテーブルは O(n) の領域を使用する
Map<Integer, String> map = new HashMap<>();
for (int i = 0; i < n; i++) {
map.put(i, String.valueOf(i));
}
}
/* 線形オーダー */
void Linear(int n) {
// 長さ n の配列は O(n) の領域を使用する
int[] nums = new int[n];
// 長さ n のリストは O(n) の領域を使用する
List<ListNode> nodes = [];
for (int i = 0; i < n; i++) {
nodes.Add(new ListNode(i));
}
// 長さ n のハッシュテーブルは O(n) の領域を使用する
Dictionary<int, string> map = [];
for (int i = 0; i < n; i++) {
map.Add(i, i.ToString());
}
}
/* 線形オーダー */
func spaceLinear(n int) {
// 長さ n の配列は O(n) の領域を使用する
_ = make([]int, n)
// 長さ n のリストは O(n) の領域を使用する
var nodes []*node
for i := 0; i < n; i++ {
nodes = append(nodes, newNode(i))
}
// 長さ n のハッシュテーブルは O(n) の領域を使用する
m := make(map[int]string, n)
for i := 0; i < n; i++ {
m[i] = strconv.Itoa(i)
}
}
/* 線形オーダー */
func linear(n: Int) {
// 長さ n の配列は O(n) の領域を使用する
let nums = Array(repeating: 0, count: n)
// 長さ n のリストは O(n) の領域を使用する
let nodes = (0 ..< n).map { ListNode(x: $0) }
// 長さ n のハッシュテーブルは O(n) の領域を使用する
let map = Dictionary(uniqueKeysWithValues: (0 ..< n).map { ($0, "\($0)") })
}
/* 線形オーダー */
function linear(n) {
// 長さ n の配列は O(n) の領域を使用する
const nums = new Array(n);
// 長さ n のリストは O(n) の領域を使用する
const nodes = [];
for (let i = 0; i < n; i++) {
nodes.push(new ListNode(i));
}
// 長さ n のハッシュテーブルは O(n) の領域を使用する
const map = new Map();
for (let i = 0; i < n; i++) {
map.set(i, i.toString());
}
}
/* 線形オーダー */
function linear(n: number): void {
// 長さ n の配列は O(n) の領域を使用する
const nums = new Array(n);
// 長さ n のリストは O(n) の領域を使用する
const nodes: ListNode[] = [];
for (let i = 0; i < n; i++) {
nodes.push(new ListNode(i));
}
// 長さ n のハッシュテーブルは O(n) の領域を使用する
const map = new Map();
for (let i = 0; i < n; i++) {
map.set(i, i.toString());
}
}
/* 線形オーダー */
void linear(int n) {
// 長さ n の配列は O(n) の領域を使用する
List<int> nums = List.filled(n, 0);
// 長さ n のリストは O(n) の領域を使用する
List<ListNode> nodes = [];
for (var i = 0; i < n; i++) {
nodes.add(ListNode(i));
}
// 長さ n のハッシュテーブルは O(n) の領域を使用する
Map<int, String> map = HashMap();
for (var i = 0; i < n; i++) {
map.putIfAbsent(i, () => i.toString());
}
}
/* 線形オーダー */
#[allow(unused)]
fn linear(n: i32) {
// 長さ n の配列は O(n) の領域を使用する
let mut nums = vec![0; n as usize];
// 長さ n のリストは O(n) の領域を使用する
let mut nodes = Vec::new();
for i in 0..n {
nodes.push(ListNode::new(i))
}
// 長さ n のハッシュテーブルは O(n) の領域を使用する
let mut map = HashMap::new();
for i in 0..n {
map.insert(i, i.to_string());
}
}
/* ハッシュテーブル */
typedef struct {
int key;
int val;
UT_hash_handle hh; // uthash.h を用いて実装する
} HashTable;
/* 線形オーダー */
void linear(int n) {
// 長さ n の配列は O(n) の領域を使用する
int *nums = malloc(sizeof(int) * n);
free(nums);
// 長さ n のリストは O(n) の領域を使用する
ListNode **nodes = malloc(sizeof(ListNode *) * n);
for (int i = 0; i < n; i++) {
nodes[i] = newListNode(i);
}
// メモリの解放
for (int i = 0; i < n; i++) {
free(nodes[i]);
}
free(nodes);
// 長さ n のハッシュテーブルは O(n) の領域を使用する
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);
}
// メモリの解放
HashTable *curr, *tmp;
HASH_ITER(hh, h, curr, tmp) {
HASH_DEL(h, curr);
free(curr);
}
}
/* 線形オーダー */
fun linear(n: Int) {
// 長さ n の配列は O(n) の領域を使用する
val nums = Array(n) { 0 }
// 長さ n のリストは O(n) の領域を使用する
val nodes = mutableListOf<ListNode>()
for (i in 0..<n) {
nodes.add(ListNode(i))
}
// 長さ n のハッシュテーブルは O(n) の領域を使用する
val map = mutableMapOf<Int, String>()
for (i in 0..<n) {
map[i] = i.toString()
}
}
実行の可視化
図 2-17 の関数は再帰深度が \(n\) で、まだ戻っていない linear_recur() が同時に \(n\) 個存在するため、スタックフレームに \(O(n)\) の空間を使います。
実行の可視化

図 2-17 再帰関数による線形の空間計算量
3. 二乗のオーダー \(O(n^2)\)¶
二乗のオーダーは、要素数が \(n\) の二乗に比例する行列やグラフによく見られます。
/* 二次オーダー */
void quadratic(int n) {
// 行列は O(n^2) の領域を使用する
int[][] numMatrix = new int[n][n];
// 二次元リストは O(n^2) の領域を使用する
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);
}
}
/* 二次オーダー */
function quadratic(n) {
// 行列は O(n^2) の領域を使用する
const numMatrix = Array(n)
.fill(null)
.map(() => Array(n).fill(null));
// 二次元リストは O(n^2) の領域を使用する
const numList = [];
for (let i = 0; i < n; i++) {
const tmp = [];
for (let j = 0; j < n; j++) {
tmp.push(0);
}
numList.push(tmp);
}
}
/* 二次オーダー */
function quadratic(n: number): void {
// 行列は O(n^2) の領域を使用する
const numMatrix = Array(n)
.fill(null)
.map(() => Array(n).fill(null));
// 二次元リストは O(n^2) の領域を使用する
const numList = [];
for (let i = 0; i < n; i++) {
const tmp = [];
for (let j = 0; j < n; j++) {
tmp.push(0);
}
numList.push(tmp);
}
}
/* 二次オーダー */
void quadratic(int n) {
// 行列は O(n^2) の領域を使用する
List<List<int>> numMatrix = List.generate(n, (_) => List.filled(n, 0));
// 二次元リストは O(n^2) の領域を使用する
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);
}
}
/* 二次オーダー */
#[allow(unused)]
fn quadratic(n: i32) {
// 行列は O(n^2) の領域を使用する
let num_matrix = vec![vec![0; n as usize]; n as usize];
// 二次元リストは O(n^2) の領域を使用する
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);
}
}
/* 二次オーダー */
void quadratic(int n) {
// 二次元リストは O(n^2) の領域を使用する
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;
}
// メモリの解放
for (int i = 0; i < n; i++) {
free(numMatrix[i]);
}
free(numMatrix);
}
/* 二次オーダー */
fun quadratic(n: Int) {
// 行列は O(n^2) の領域を使用する
val numMatrix = arrayOfNulls<Array<Int>?>(n)
// 二次元リストは O(n^2) の領域を使用する
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)
}
}
実行の可視化
図 2-18 の関数は再帰深度が \(n\) で、各呼び出しで長さ \(n\)、\(n-1\)、\(\dots\)、\(2\)、\(1\) の配列を初期化します。平均の長さは \(n / 2\) なので、全体では \(O(n^2)\) の空間を使います。
実行の可視化

図 2-18 再帰関数による二乗の空間計算量
4. 指数のオーダー \(O(2^n)\)¶
指数のオーダーは二分木によく見られます。図 2-19 の、層数が \(n\) の「パーフェクト二分木」はノード数が \(2^n - 1\) で、\(O(2^n)\) の空間を使います。
実行の可視化

図 2-19 パーフェクト二分木による指数の空間計算量
5. 対数のオーダー \(O(\log n)\)¶
対数のオーダーは分割統治によく見られます。たとえばマージソートは、長さ \(n\) の配列を各再帰で中央から半分に分け、高さ \(\log n\) の再帰木を作るため、スタックフレームに \(O(\log n)\) の空間を使います。
数値から文字列への変換も例になります。正の整数 \(n\) の桁数は \(\lfloor \log_{10} n \rfloor + 1\) なので、文字列の長さも \(\lfloor \log_{10} n \rfloor + 1\) です。したがって、空間計算量は \(O(\log_{10} n + 1) = O(\log n)\) です。
2.4.4 時間と空間のトレードオフ¶
理想的には、時間計算量も空間計算量も最適にしたいところです。しかし実際には、両方を同時に最適化するのは非常に難しいことが多いです。
時間計算量を減らすには、空間計算量を増やす必要があることが多く、その逆も同様です。メモリを使って実行を速くする考え方を「空間を使って時間を節約する」、その逆を「時間を使って空間を節約する」と呼びます。
どちらを選ぶかは、何を重視するかによります。多くの場合、空間より時間の方が貴重なため、空間を使って時間を節約する戦略が一般的です。ただし、データ量が非常に多い場合は、空間計算量を抑えることも重要です。