* Retranslate the complete Japanese online edition from Chinese * Fit Japanese homepage labels and localize heading permalinks * Point Japanese contribution instructions to the Japanese preview * Replace locale conditionals with multilingual UI tables
24 KiB
空間計算量
空間計算量(space complexity)は、データ量が増えるにつれて、アルゴリズムのメモリ使用量がどのように増えるかを測ります。時間計算量とよく似ており、「実行時間」を「メモリ使用量」に置き換えた概念です。
アルゴリズムに関わる空間
実行中に使うメモリ空間は、主に次の種類に分かれます。
- 入力空間:入力データを保存します。
- 一時空間:実行中の変数、オブジェクト、関数の実行状態などを保存します。
- 出力空間:出力データを保存します。
通常、空間計算量には「一時空間」と「出力空間」を含めます。
一時空間は、さらに三つに分けられます。
- 一時データ:実行中の定数、変数、オブジェクトなどを保存します。
- スタックフレーム:呼び出した関数の実行状態を保存します。関数を呼ぶたびにスタックの先頭にフレームが作られ、戻ると解放されます。
- 命令空間:コンパイルされた命令を保存します。実際の集計では通常、無視します。
プログラムの空間計算量では、下図のように、通常は一時データ、スタックフレーム、出力データの三つを数えます。
関連するコードを次に示します。
=== "Python"
```python title=""
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 # 出力データ
```
=== "C++"
```cpp title=""
/* 構造体 */
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; // 出力データ
}
```
=== "Java"
```java title=""
/* クラス */
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; // 出力データ
}
```
=== "C#"
```csharp title=""
/* クラス */
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; // 出力データ
}
```
=== "Go"
```go title=""
/* 構造体 */
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 // 出力データ
}
```
=== "Swift"
```swift title=""
/* クラス */
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 // 出力データ
}
```
=== "JS"
```javascript title=""
/* クラス */
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; // 出力データ
}
```
=== "TS"
```typescript title=""
/* クラス */
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; // 出力データ
}
```
=== "Dart"
```dart title=""
/* クラス */
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; // 出力データ
}
```
=== "Rust"
```rust title=""
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; // 出力データ
}
```
=== "C"
```c title=""
/* 関数 */
int func() {
// 何らかの処理を実行...
return 0;
}
int algorithm(int n) { // 入力データ
const int a = 0; // 一時データ(定数)
int b = 0; // 一時データ(変数)
int c = func(); // スタックフレーム(関数呼び出し)
return a + b + c; // 出力データ
}
```
=== "Kotlin"
```kotlin title=""
/* クラス */
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 // 出力データ
}
```
=== "Ruby"
```ruby title=""
### クラス ###
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
```
見積もり方
空間計算量の見積もり方は時間計算量とほぼ同じで、数える対象を「操作回数」から「使う空間の大きさ」に変えます。
ただし時間計算量と異なり、通常は最悪空間計算量だけに注目します。メモリ容量は必ず満たすべき要件であり、どの入力でも十分なメモリを確保できるようにする必要があるためです。
次のコードでは、「最悪」には二つの意味があります。
- 最悪の入力を基準にする:
n < 10ならO(1)ですが、n > 10なら配列numsがO(n)の空間を使うため、最悪空間計算量はO(n)です。 - 実行中の最大メモリ使用量を基準にする:たとえば、最後の行の実行前は
O(1)ですが、配列numsを初期化するとO(n)の空間を使うため、最悪空間計算量はO(n)です。
=== "Python"
```python title=""
def algorithm(n: int):
a = 0 # O(1)
b = [0] * 10000 # O(1)
if n > 10:
nums = [0] * n # O(n)
```
=== "C++"
```cpp title=""
void algorithm(int n) {
int a = 0; // O(1)
vector<int> b(10000); // O(1)
if (n > 10)
vector<int> nums(n); // O(n)
}
```
=== "Java"
```java title=""
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)
}
```
=== "C#"
```csharp title=""
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)
}
}
```
=== "Go"
```go title=""
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)
}
```
=== "Swift"
```swift title=""
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)
}
}
```
=== "JS"
```javascript title=""
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)
}
}
```
=== "TS"
```typescript title=""
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)
}
}
```
=== "Dart"
```dart title=""
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)
}
}
```
=== "Rust"
```rust title=""
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)
}
}
```
=== "C"
```c title=""
void algorithm(int n) {
int a = 0; // O(1)
int b[10000]; // O(1)
if (n > 10)
int nums[n] = {0}; // O(n)
}
```
=== "Kotlin"
```kotlin title=""
fun algorithm(n: Int) {
val a = 0 // O(1)
val b = IntArray(10000) // O(1)
if (n > 10) {
val nums = IntArray(n) // O(n)
}
}
```
=== "Ruby"
```ruby title=""
def algorithm(n)
a = 0 # O(1)
b = Array.new(10000) # O(1)
nums = Array.new(n) if n > 10 # O(n)
end
```
再帰関数では、スタックフレームを数え忘れないよう注意が必要です。次のコードを見てください。
=== "Python"
```python title=""
def function() -> int:
# 何らかの処理を実行
return 0
def loop(n: int):
"""ループの空間計算量は O(1)"""
for _ in range(n):
function()
def recur(n: int):
"""再帰の空間計算量は O(n)"""
if n == 1:
return
return recur(n - 1)
```
=== "C++"
```cpp title=""
int func() {
// 何らかの処理を実行
return 0;
}
/* ループの空間計算量は O(1) */
void loop(int n) {
for (int i = 0; i < n; i++) {
func();
}
}
/* 再帰の空間計算量は O(n) */
void recur(int n) {
if (n == 1) return;
recur(n - 1);
}
```
=== "Java"
```java title=""
int function() {
// 何らかの処理を実行
return 0;
}
/* ループの空間計算量は O(1) */
void loop(int n) {
for (int i = 0; i < n; i++) {
function();
}
}
/* 再帰の空間計算量は O(n) */
void recur(int n) {
if (n == 1) return;
recur(n - 1);
}
```
=== "C#"
```csharp title=""
int Function() {
// 何らかの処理を実行
return 0;
}
/* ループの空間計算量は O(1) */
void Loop(int n) {
for (int i = 0; i < n; i++) {
Function();
}
}
/* 再帰の空間計算量は O(n) */
int Recur(int n) {
if (n == 1) return 1;
return Recur(n - 1);
}
```
=== "Go"
```go title=""
func function() int {
// 何らかの処理を実行
return 0
}
/* ループの空間計算量は O(1) */
func loop(n int) {
for i := 0; i < n; i++ {
function()
}
}
/* 再帰の空間計算量は O(n) */
func recur(n int) {
if n == 1 {
return
}
recur(n - 1)
}
```
=== "Swift"
```swift title=""
@discardableResult
func function() -> Int {
// 何らかの処理を実行
return 0
}
/* ループの空間計算量は O(1) */
func loop(n: Int) {
for _ in 0 ..< n {
function()
}
}
/* 再帰の空間計算量は O(n) */
func recur(n: Int) {
if n == 1 {
return
}
recur(n: n - 1)
}
```
=== "JS"
```javascript title=""
function constFunc() {
// 何らかの処理を実行
return 0;
}
/* ループの空間計算量は O(1) */
function loop(n) {
for (let i = 0; i < n; i++) {
constFunc();
}
}
/* 再帰の空間計算量は O(n) */
function recur(n) {
if (n === 1) return;
return recur(n - 1);
}
```
=== "TS"
```typescript title=""
function constFunc(): number {
// 何らかの処理を実行
return 0;
}
/* ループの空間計算量は O(1) */
function loop(n: number): void {
for (let i = 0; i < n; i++) {
constFunc();
}
}
/* 再帰の空間計算量は O(n) */
function recur(n: number): void {
if (n === 1) return;
return recur(n - 1);
}
```
=== "Dart"
```dart title=""
int function() {
// 何らかの処理を実行
return 0;
}
/* ループの空間計算量は O(1) */
void loop(int n) {
for (int i = 0; i < n; i++) {
function();
}
}
/* 再帰の空間計算量は O(n) */
void recur(int n) {
if (n == 1) return;
recur(n - 1);
}
```
=== "Rust"
```rust title=""
fn function() -> i32 {
// 何らかの処理を実行
return 0;
}
/* ループの空間計算量は O(1) */
fn loop(n: i32) {
for i in 0..n {
function();
}
}
/* 再帰の空間計算量は O(n) */
fn recur(n: i32) {
if n == 1 {
return;
}
recur(n - 1);
}
```
=== "C"
```c title=""
int func() {
// 何らかの処理を実行
return 0;
}
/* ループの空間計算量は O(1) */
void loop(int n) {
for (int i = 0; i < n; i++) {
func();
}
}
/* 再帰の空間計算量は O(n) */
void recur(int n) {
if (n == 1) return;
recur(n - 1);
}
```
=== "Kotlin"
```kotlin title=""
fun function(): Int {
// 何らかの処理を実行
return 0
}
/* ループの空間計算量は O(1) */
fun loop(n: Int) {
for (i in 0..<n) {
function()
}
}
/* 再帰の空間計算量は O(n) */
fun recur(n: Int) {
if (n == 1) return
return recur(n - 1)
}
```
=== "Ruby"
```ruby title=""
def function
# 何らかの処理を実行
0
end
### ループの空間計算量は O(1) ###
def loop(n)
(0...n).each { function }
end
### 再帰の空間計算量は O(n) ###
def recur(n)
return if n == 1
recur(n - 1)
end
```
関数 loop() と recur() の時間計算量はどちらも O(n) ですが、空間計算量は異なります。
loop()はfunction()をn回呼びますが、毎回戻ってスタックフレームを解放するため、空間計算量はO(1)のままです。- 再帰関数
recur()では、戻っていないrecur()が同時にn個存在するため、スタックフレームにO(n)の空間を使います。
代表的な種類
入力の大きさを n とし、代表的な空間計算量を小さい順に下図に示します。
\begin{aligned}
& O(1) < O(\log n) < O(n) < O(n^2) < O(2^n) \newline
& \text{定数} < \text{対数} < \text{線形} < \text{二乗} < \text{指数}
\end{aligned}
定数のオーダー O(1)
定数のオーダーは、入力の大きさ n によって個数が変わらない定数、変数、オブジェクトによく見られます。
ループ内の変数の初期化や関数呼び出しで使ったメモリは、次の反復に入ると解放され、累積しないため、空間計算量は O(1) のままです。
[file]{space_complexity}-[class]{}-[func]{constant}
線形のオーダー O(n)
線形のオーダーは、要素数が n に比例する配列、連結リスト、スタック、キューなどによく見られます。
[file]{space_complexity}-[class]{}-[func]{linear}
下図の関数は再帰深度が n で、まだ戻っていない linear_recur() が同時に n 個存在するため、スタックフレームに O(n) の空間を使います。
[file]{space_complexity}-[class]{}-[func]{linear_recur}
二乗のオーダー O(n^2)
二乗のオーダーは、要素数が n の二乗に比例する行列やグラフによく見られます。
[file]{space_complexity}-[class]{}-[func]{quadratic}
下図の関数は再帰深度が n で、各呼び出しで長さ $n$、$n-1$、$\dots$、$2$、1 の配列を初期化します。平均の長さは n / 2 なので、全体では O(n^2) の空間を使います。
[file]{space_complexity}-[class]{}-[func]{quadratic_recur}
指数のオーダー O(2^n)
指数のオーダーは二分木によく見られます。下図の、層数が n の「パーフェクト二分木」はノード数が 2^n - 1 で、O(2^n) の空間を使います。
[file]{space_complexity}-[class]{}-[func]{build_tree}
対数のオーダー 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) です。
時間と空間のトレードオフ
理想的には、時間計算量も空間計算量も最適にしたいところです。しかし実際には、両方を同時に最適化するのは非常に難しいことが多いです。
時間計算量を減らすには、空間計算量を増やす必要があることが多く、その逆も同様です。メモリを使って実行を速くする考え方を「空間を使って時間を節約する」、その逆を「時間を使って空間を節約する」と呼びます。
どちらを選ぶかは、何を重視するかによります。多くの場合、空間より時間の方が貴重なため、空間を使って時間を節約する戦略が一般的です。ただし、データ量が非常に多い場合は、空間計算量を抑えることも重要です。




