2.4 空間計算量¶
空間計算量(space complexity)は、アルゴリズムが占有するメモリ空間がデータ量の増加に伴ってどのように増えるかを測る指標です。この概念は時間計算量と非常によく似ており、「実行時間」を「占有メモリ空間」に置き換えるだけです。
2.4.1 アルゴリズムに関連する空間¶
アルゴリズムが実行中に使用するメモリ空間には、主に次の種類があります。
- 入力空間:アルゴリズムの入力データを格納するための空間。
- 一時空間:アルゴリズムの実行中に使用する変数、オブジェクト、関数コンテキストなどのデータを格納するための空間。
- 出力空間:アルゴリズムの出力データを格納するための空間。
一般に、空間計算量の集計範囲は「一時空間」と「出力空間」を合わせたものです。
一時空間はさらに三つに分けられます。
- 一時データ:アルゴリズム実行中の各種定数、変数、オブジェクトなどを保存するための空間。
- スタックフレーム空間:呼び出された関数のコンテキストデータを保存するための空間。システムは関数を呼び出すたびにスタックの先頭にスタックフレームを作成し、関数が戻るとその空間を解放します。
- 命令空間:コンパイル後のプログラム命令を保存するための空間で、実際の集計では通常無視されます。
プログラムの空間計算量を分析する際には、通常、一時データ、スタックフレーム空間、出力データの三つを数えます。以下の図に示すとおりです。

図 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\) 回呼び出しますが、各反復でのfunction()は戻るたびにスタックフレーム空間が解放されるため、空間計算量は依然として \(O(1)\) です。 - 再帰関数
recur()は実行中に未返却のrecur()が同時に \(n\) 個存在するため、\(O(n)\) のスタックフレーム空間を占有します。
2.4.3 よくある型¶
入力データサイズを \(n\) とすると、以下の図はよくある空間計算量の型を低い順から高い順に示しています。

図 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()
}
}
コードの可視化
以下の図に示すように、この関数の再帰の深さは \(n\) であり、同時に \(n\) 個の未返却 linear_recur() 関数が存在するため、\(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);
}
コードの可視化
以下の図に示すように、この関数の再帰の深さは \(n\) であり、各再帰関数の中で長さがそれぞれ \(n\)、\(n-1\)、\(\dots\)、\(2\)、\(1\) の配列を初期化しています。平均長は \(n / 2\) なので、全体では \(O(n^2)\) の空間を占有します。
コードの可視化

図 2-18 再帰関数が生み出す平方階の空間計算量
4. 指数階 \(O(2^n)\)¶
指数階は二分木によく現れます。以下の図を見ると、高さが \(n\) の「満二分木」のノード数は \(2^n - 1\) であり、\(O(2^n)\) の空間を占有します。
### 平方階 ###
def quadratic(n)
# 二次元リストは O(n^2) の空間を使用
Array.new(n) { Array.new(n, 0) }
end
# ## 平方階(再帰実装)###
def quadratic_recur(n)
return 0 unless n > 0
# 配列 nums の長さは n, n-1, ..., 2, 1
nums = Array.new(n, 0)
quadratic_recur(n - 1)
end
# ## 指数階(満二分木を構築)###
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
コードの可視化

図 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 時間と空間のトレードオフ¶
理想的には、アルゴリズムの時間計算量と空間計算量の両方を最適にしたいところです。しかし実際には、この二つを同時に最適化するのは通常きわめて困難です。
時間計算量を下げるには、通常、空間計算量を増やす代償が必要であり、その逆も同様です。メモリ空間を犠牲にして実行速度を上げる考え方を「空間を時間と引き換えにする」と呼び、その逆を「時間を空間と引き換えにする」と呼びます。
どちらの考え方を選ぶかは、何をより重視するかによって決まります。多くの場合、空間より時間のほうが貴重なので、「空間を時間と引き換えにする」戦略のほうが一般的です。もちろん、データ量が非常に大きい場合には、空間計算量を抑えることも同じくらい重要です。