--- comments: true --- # 2.3   時間計算量 実行時間はアルゴリズムの効率を直感的かつ正確に反映します。あるコードの実行時間を正確に見積もりたい場合、どのようにすればよいでしょうか? 1. **実行プラットフォームを特定する**。ハードウェア構成、プログラミング言語、システム環境などが含まれ、これらの要因はいずれもコードの実行効率に影響します。 2. **各種計算操作に必要な実行時間を評価する**。例えば加算 `+` には 1 ns 、乗算 `*` には 10 ns 、出力 `print()` には 5 ns などが必要です。 3. **コード中のすべての計算操作を数える**。そして各操作の実行時間を合計することで、実行時間を得ます。 例えば次のコードでは、入力データサイズを $n$ とします: === "Python" ```python title="" # ある実行プラットフォーム上で def algorithm(n: int): a = 2 # 1 ns a = a + 1 # 1 ns a = a * 2 # 10 ns # n 回ループ for _ in range(n): # 1 ns print(0) # 5 ns ``` === "C++" ```cpp title="" // ある実行プラットフォーム上で void algorithm(int n) { int a = 2; // 1 ns a = a + 1; // 1 ns a = a * 2; // 10 ns // n 回ループ for (int i = 0; i < n; i++) { // 1 ns cout << 0 << endl; // 5 ns } } ``` === "Java" ```java title="" // ある実行プラットフォーム上で void algorithm(int n) { int a = 2; // 1 ns a = a + 1; // 1 ns a = a * 2; // 10 ns // n 回ループ for (int i = 0; i < n; i++) { // 1 ns System.out.println(0); // 5 ns } } ``` === "C#" ```csharp title="" // ある実行プラットフォーム上で void Algorithm(int n) { int a = 2; // 1 ns a = a + 1; // 1 ns a = a * 2; // 10 ns // n 回ループ for (int i = 0; i < n; i++) { // 1 ns Console.WriteLine(0); // 5 ns } } ``` === "Go" ```go title="" // ある実行プラットフォーム上で func algorithm(n int) { a := 2 // 1 ns a = a + 1 // 1 ns a = a * 2 // 10 ns // n 回ループ for i := 0; i < n; i++ { // 1 ns fmt.Println(a) // 5 ns } } ``` === "Swift" ```swift title="" // ある実行プラットフォーム上で func algorithm(n: Int) { var a = 2 // 1 ns a = a + 1 // 1 ns a = a * 2 // 10 ns // n 回ループ for _ in 0 ..< n { // 1 ns print(0) // 5 ns } } ``` === "JS" ```javascript title="" // ある実行プラットフォーム上で function algorithm(n) { var a = 2; // 1 ns a = a + 1; // 1 ns a = a * 2; // 10 ns // n 回ループ for(let i = 0; i < n; i++) { // 1 ns console.log(0); // 5 ns } } ``` === "TS" ```typescript title="" // ある実行プラットフォーム上で function algorithm(n: number): void { var a: number = 2; // 1 ns a = a + 1; // 1 ns a = a * 2; // 10 ns // n 回ループ for(let i = 0; i < n; i++) { // 1 ns console.log(0); // 5 ns } } ``` === "Dart" ```dart title="" // ある実行プラットフォーム上で void algorithm(int n) { int a = 2; // 1 ns a = a + 1; // 1 ns a = a * 2; // 10 ns // n 回ループ for (int i = 0; i < n; i++) { // 1 ns print(0); // 5 ns } } ``` === "Rust" ```rust title="" // ある実行プラットフォーム上で fn algorithm(n: i32) { let mut a = 2; // 1 ns a = a + 1; // 1 ns a = a * 2; // 10 ns // n 回ループ for _ in 0..n { // 1 ns println!("{}", 0); // 5 ns } } ``` === "C" ```c title="" // ある実行プラットフォーム上で void algorithm(int n) { int a = 2; // 1 ns a = a + 1; // 1 ns a = a * 2; // 10 ns // n 回ループ for (int i = 0; i < n; i++) { // 1 ns printf("%d", 0); // 5 ns } } ``` === "Kotlin" ```kotlin title="" // ある実行プラットフォーム上で fun algorithm(n: Int) { var a = 2 // 1 ns a = a + 1 // 1 ns a = a * 2 // 10 ns // n 回ループ for (i in 0.. 図 2-7   アルゴリズム A、B、C の時間増加傾向

アルゴリズムの実行時間を直接数える方法と比べて、時間計算量分析にはどのような特徴があるでしょうか? - **時間計算量はアルゴリズム効率を有効に評価できます**。例えばアルゴリズム `B` の実行時間は線形に増加するため、$n > 1$ ではアルゴリズム `A` より遅く、$n > 1000000$ ではアルゴリズム `C` より遅くなります。実際、入力データサイズ $n$ が十分に大きければ、「定数階」のアルゴリズムは必ず「線形階」のアルゴリズムより優れます。これが実行時間の増加傾向の意味です。 - **時間計算量の見積もり方法はより簡潔です**。実行プラットフォームや計算操作の種類は、アルゴリズム実行時間の増加傾向とは無関係です。そのため時間計算量分析では、すべての計算操作の実行時間を同じ「単位時間」とみなしてよく、「計算操作の実行時間を数える」作業を「計算操作の個数を数える」作業へ簡略化できます。これにより見積もりの難易度は大きく下がります。 - **時間計算量には一定の限界もあります**。例えばアルゴリズム `A` と `C` の時間計算量は同じでも、実際の実行時間には大きな差があります。同様に、アルゴリズム `B` の時間計算量は `C` より高いものの、入力データサイズ $n$ が小さい場合にはアルゴリズム `B` のほうが明らかに優れます。このような場合、時間計算量だけでアルゴリズム効率の高低を判断するのは難しいことがあります。もっとも、こうした問題があっても、複雑度分析は依然としてアルゴリズム効率を評価する最も有効で一般的な方法です。 ## 2.3.2   関数の漸近上界 入力サイズが $n$ の次の関数を考えます: === "Python" ```python title="" def algorithm(n: int): a = 1 # +1 a = a + 1 # +1 a = a * 2 # +1 # n 回ループ for i in range(n): # +1 print(0) # +1 ``` === "C++" ```cpp title="" void algorithm(int n) { int a = 1; // +1 a = a + 1; // +1 a = a * 2; // +1 // n 回ループ for (int i = 0; i < n; i++) { // +1(各反復で i ++ を実行) cout << 0 << endl; // +1 } } ``` === "Java" ```java title="" void algorithm(int n) { int a = 1; // +1 a = a + 1; // +1 a = a * 2; // +1 // n 回ループ for (int i = 0; i < n; i++) { // +1(各反復で i ++ を実行) System.out.println(0); // +1 } } ``` === "C#" ```csharp title="" void Algorithm(int n) { int a = 1; // +1 a = a + 1; // +1 a = a * 2; // +1 // n 回ループ for (int i = 0; i < n; i++) { // +1(各反復で i ++ を実行) Console.WriteLine(0); // +1 } } ``` === "Go" ```go title="" func algorithm(n int) { a := 1 // +1 a = a + 1 // +1 a = a * 2 // +1 // n 回ループ for i := 0; i < n; i++ { // +1 fmt.Println(a) // +1 } } ``` === "Swift" ```swift title="" func algorithm(n: Int) { var a = 1 // +1 a = a + 1 // +1 a = a * 2 // +1 // n 回ループ for _ in 0 ..< n { // +1 print(0) // +1 } } ``` === "JS" ```javascript title="" function algorithm(n) { var a = 1; // +1 a += 1; // +1 a *= 2; // +1 // n 回ループ for(let i = 0; i < n; i++){ // +1(各反復で i ++ を実行) console.log(0); // +1 } } ``` === "TS" ```typescript title="" function algorithm(n: number): void{ var a: number = 1; // +1 a += 1; // +1 a *= 2; // +1 // n 回ループ for(let i = 0; i < n; i++){ // +1(各反復で i ++ を実行) console.log(0); // +1 } } ``` === "Dart" ```dart title="" void algorithm(int n) { int a = 1; // +1 a = a + 1; // +1 a = a * 2; // +1 // n 回ループ for (int i = 0; i < n; i++) { // +1(各反復で i ++ を実行) print(0); // +1 } } ``` === "Rust" ```rust title="" fn algorithm(n: i32) { let mut a = 1; // +1 a = a + 1; // +1 a = a * 2; // +1 // n 回ループ for _ in 0..n { // +1(各反復で i ++ を実行) println!("{}", 0); // +1 } } ``` === "C" ```c title="" void algorithm(int n) { int a = 1; // +1 a = a + 1; // +1 a = a * 2; // +1 // n 回ループ for (int i = 0; i < n; i++) { // +1(各反復で i ++ を実行) printf("%d", 0); // +1 } } ``` === "Kotlin" ```kotlin title="" fun algorithm(n: Int) { var a = 1 // +1 a = a + 1 // +1 a = a * 2 // +1 // n 回ループ for (i in 0..ビッグ $O$ 記法(big-$O$ notation)と呼ばれ、関数 $T(n)$ の漸近上界(asymptotic upper bound)を表します。 時間計算量の分析は本質的に「操作回数 $T(n)$」の漸近上界を求めることであり、明確な数学的定義があります。 !!! note "関数の漸近上界" 正の実数 $c$ と実数 $n_0$ が存在し、すべての $n > n_0$ について $T(n) \leq c \cdot f(n)$ が成り立つならば、$f(n)$ は $T(n)$ の漸近上界の 1 つであるとみなせます。これを $T(n) = O(f(n))$ と記します。 下図のように、漸近上界を求めるとは関数 $f(n)$ を探すことであり、$n$ が無限大へ近づくときに $T(n)$ と $f(n)$ が同じ増加オーダーにあり、定数係数 $c$ だけが異なる状態を表します。 ![関数の漸近上界](time_complexity.assets/asymptotic_upper_bound.png){ class="animation-figure" }

図 2-8   関数の漸近上界

## 2.3.3   求め方 漸近上界はやや数学色が強い概念ですが、完全に理解できていなくても心配はいりません。まずは求め方を押さえ、実践を重ねる中で徐々にその数学的意味をつかめば十分です。 定義より、$f(n)$ が定まれば時間計算量 $O(f(n))$ が得られます。では、漸近上界 $f(n)$ をどのように決めればよいのでしょうか。大きく 2 段階あります。まず操作回数を数え、その後で漸近上界を判断します。 ### 1.   第 1 ステップ:操作回数を数える コードについては、上から下へ 1 行ずつ数えれば十分です。しかし、前述の $c \cdot f(n)$ における定数係数 $c$ は任意に大きく取れるため、**操作回数 $T(n)$ に含まれるさまざまな係数や定数項は無視できます**。この原則から、次のような簡略化のコツが得られます。 1. **$T(n)$ 中の定数を無視する**。それらはすべて $n$ と無関係なので、時間計算量には影響しません。 2. **すべての係数を省略する**。例えば $2n$ 回や $5n + 1$ 回のループは、いずれも $n$ 回と簡略化できます。$n$ の前の係数は時間計算量に影響しないからです。 3. **ループが入れ子のときは乗算を使う**。総操作回数は外側のループと内側のループの操作回数の積に等しく、各ループ層には引き続き `1.` と `2.` のコツをそれぞれ適用できます。 次の関数では、上記のコツを使って操作回数を数えられます: === "Python" ```python title="" def algorithm(n: int): a = 1 # +0(コツ 1) a = a + n # +0(コツ 1) # +n(コツ 2) for i in range(5 * n + 1): print(0) # +n*n(コツ 3) for i in range(2 * n): for j in range(n + 1): print(0) ``` === "C++" ```cpp title="" void algorithm(int n) { int a = 1; // +0(コツ 1) a = a + n; // +0(コツ 1) // +n(コツ 2) for (int i = 0; i < 5 * n + 1; i++) { cout << 0 << endl; } // +n*n(コツ 3) for (int i = 0; i < 2 * n; i++) { for (int j = 0; j < n + 1; j++) { cout << 0 << endl; } } } ``` === "Java" ```java title="" void algorithm(int n) { int a = 1; // +0(コツ 1) a = a + n; // +0(コツ 1) // +n(コツ 2) for (int i = 0; i < 5 * n + 1; i++) { System.out.println(0); } // +n*n(コツ 3) for (int i = 0; i < 2 * n; i++) { for (int j = 0; j < n + 1; j++) { System.out.println(0); } } } ``` === "C#" ```csharp title="" void Algorithm(int n) { int a = 1; // +0(コツ 1) a = a + n; // +0(コツ 1) // +n(コツ 2) for (int i = 0; i < 5 * n + 1; i++) { Console.WriteLine(0); } // +n*n(コツ 3) for (int i = 0; i < 2 * n; i++) { for (int j = 0; j < n + 1; j++) { Console.WriteLine(0); } } } ``` === "Go" ```go title="" func algorithm(n int) { a := 1 // +0(コツ 1) a = a + n // +0(コツ 1) // +n(コツ 2) for i := 0; i < 5 * n + 1; i++ { fmt.Println(0) } // +n*n(コツ 3) for i := 0; i < 2 * n; i++ { for j := 0; j < n + 1; j++ { fmt.Println(0) } } } ``` === "Swift" ```swift title="" func algorithm(n: Int) { var a = 1 // +0(コツ 1) a = a + n // +0(コツ 1) // +n(コツ 2) for _ in 0 ..< (5 * n + 1) { print(0) } // +n*n(コツ 3) for _ in 0 ..< (2 * n) { for _ in 0 ..< (n + 1) { print(0) } } } ``` === "JS" ```javascript title="" function algorithm(n) { let a = 1; // +0(コツ 1) a = a + n; // +0(コツ 1) // +n(コツ 2) for (let i = 0; i < 5 * n + 1; i++) { console.log(0); } // +n*n(コツ 3) for (let i = 0; i < 2 * n; i++) { for (let j = 0; j < n + 1; j++) { console.log(0); } } } ``` === "TS" ```typescript title="" function algorithm(n: number): void { let a = 1; // +0(コツ 1) a = a + n; // +0(コツ 1) // +n(コツ 2) for (let i = 0; i < 5 * n + 1; i++) { console.log(0); } // +n*n(コツ 3) for (let i = 0; i < 2 * n; i++) { for (let j = 0; j < n + 1; j++) { console.log(0); } } } ``` === "Dart" ```dart title="" void algorithm(int n) { int a = 1; // +0(コツ 1) a = a + n; // +0(コツ 1) // +n(コツ 2) for (int i = 0; i < 5 * n + 1; i++) { print(0); } // +n*n(コツ 3) for (int i = 0; i < 2 * n; i++) { for (int j = 0; j < n + 1; j++) { print(0); } } } ``` === "Rust" ```rust title="" fn algorithm(n: i32) { let mut a = 1; // +0(コツ 1) a = a + n; // +0(コツ 1) // +n(コツ 2) for i in 0..(5 * n + 1) { println!("{}", 0); } // +n*n(コツ 3) for i in 0..(2 * n) { for j in 0..(n + 1) { println!("{}", 0); } } } ``` === "C" ```c title="" void algorithm(int n) { int a = 1; // +0(コツ 1) a = a + n; // +0(コツ 1) // +n(コツ 2) for (int i = 0; i < 5 * n + 1; i++) { printf("%d", 0); } // +n*n(コツ 3) for (int i = 0; i < 2 * n; i++) { for (int j = 0; j < n + 1; j++) { printf("%d", 0); } } } ``` === "Kotlin" ```kotlin title="" fun algorithm(n: Int) { var a = 1 // +0(コツ 1) a = a + n // +0(コツ 1) // +n(コツ 2) for (i in 0..<5 * n + 1) { println(0) } // +n*n(コツ 3) for (i in 0..<2 * n) { for (j in 0.. 表 2-2   異なる操作回数に対応する時間計算量

| 操作回数 $T(n)$ | 時間計算量 $O(f(n))$ | | ---------------------- | -------------------- | | $100000$ | $O(1)$ | | $3n + 2$ | $O(n)$ | | $2n^2 + 3n + 2$ | $O(n^2)$ | | $n^3 + 10000n^2$ | $O(n^3)$ | | $2^n + 10000n^{10000}$ | $O(2^n)$ |
## 2.3.4   よくある種類 入力データサイズを $n$ とすると、よくある時間計算量の種類は次図のとおりです(小さい順に並べています)。 $$ \begin{aligned} & O(1) < O(\log n) < O(n) < O(n \log n) < O(n^2) < O(2^n) < O(n!) \newline & \text{定数階} < \text{対数階} < \text{線形階} < \text{線形対数階} < \text{平方階} < \text{指数階} < \text{階乗階} \end{aligned} $$ ![よくある時間計算量の種類](time_complexity.assets/time_complexity_common_types.png){ class="animation-figure" }

図 2-9   よくある時間計算量の種類

### 1.   定数階 $O(1)$ {data-toc-label="1.   定数階"} 定数階の操作回数は入力データサイズ $n$ と無関係であり、$n$ が変化しても増減しません。 次の関数では、操作回数 `size` が大きい可能性はありますが、入力データサイズ $n$ とは無関係なので、時間計算量は依然として $O(1)$ です: === "Python" ```python title="time_complexity.py" def constant(n: int) -> int: """定数階""" count = 0 size = 100000 for _ in range(size): count += 1 return count ``` === "C++" ```cpp title="time_complexity.cpp" /* 定数階 */ int constant(int n) { int count = 0; int size = 100000; for (int i = 0; i < size; i++) count++; return count; } ``` === "Java" ```java title="time_complexity.java" /* 定数階 */ int constant(int n) { int count = 0; int size = 100000; for (int i = 0; i < size; i++) count++; return count; } ``` === "C#" ```csharp title="time_complexity.cs" /* 定数階 */ int Constant(int n) { int count = 0; int size = 100000; for (int i = 0; i < size; i++) count++; return count; } ``` === "Go" ```go title="time_complexity.go" /* 定数階 */ func constant(n int) int { count := 0 size := 100000 for i := 0; i < size; i++ { count++ } return count } ``` === "Swift" ```swift title="time_complexity.swift" /* 定数階 */ func constant(n: Int) -> Int { var count = 0 let size = 100_000 for _ in 0 ..< size { count += 1 } return count } ``` === "JS" ```javascript title="time_complexity.js" /* 定数階 */ function constant(n) { let count = 0; const size = 100000; for (let i = 0; i < size; i++) count++; return count; } ``` === "TS" ```typescript title="time_complexity.ts" /* 定数階 */ function constant(n: number): number { let count = 0; const size = 100000; for (let i = 0; i < size; i++) count++; return count; } ``` === "Dart" ```dart title="time_complexity.dart" /* 定数階 */ int constant(int n) { int count = 0; int size = 100000; for (var i = 0; i < size; i++) { count++; } return count; } ``` === "Rust" ```rust title="time_complexity.rs" /* 定数階 */ fn constant(n: i32) -> i32 { _ = n; let mut count = 0; let size = 100_000; for _ in 0..size { count += 1; } count } ``` === "C" ```c title="time_complexity.c" /* 定数階 */ int constant(int n) { int count = 0; int size = 100000; int i = 0; for (int i = 0; i < size; i++) { count++; } return count; } ``` === "Kotlin" ```kotlin title="time_complexity.kt" /* 定数階 */ fun constant(n: Int): Int { var count = 0 val size = 100000 for (i in 0..
全画面で見る >
### 2.   線形階 $O(n)$ {data-toc-label="2.   線形階"} 線形階の操作回数は入力データサイズ $n$ に対して線形に増加します。線形階は通常、単一ループに現れます: === "Python" ```python title="time_complexity.py" def linear(n: int) -> int: """線形階""" count = 0 for _ in range(n): count += 1 return count ``` === "C++" ```cpp title="time_complexity.cpp" /* 線形階 */ int linear(int n) { int count = 0; for (int i = 0; i < n; i++) count++; return count; } ``` === "Java" ```java title="time_complexity.java" /* 線形階 */ int linear(int n) { int count = 0; for (int i = 0; i < n; i++) count++; return count; } ``` === "C#" ```csharp title="time_complexity.cs" /* 線形階 */ int Linear(int n) { int count = 0; for (int i = 0; i < n; i++) count++; return count; } ``` === "Go" ```go title="time_complexity.go" /* 線形階 */ func linear(n int) int { count := 0 for i := 0; i < n; i++ { count++ } return count } ``` === "Swift" ```swift title="time_complexity.swift" /* 線形階 */ func linear(n: Int) -> Int { var count = 0 for _ in 0 ..< n { count += 1 } return count } ``` === "JS" ```javascript title="time_complexity.js" /* 線形階 */ function linear(n) { let count = 0; for (let i = 0; i < n; i++) count++; return count; } ``` === "TS" ```typescript title="time_complexity.ts" /* 線形階 */ function linear(n: number): number { let count = 0; for (let i = 0; i < n; i++) count++; return count; } ``` === "Dart" ```dart title="time_complexity.dart" /* 線形階 */ int linear(int n) { int count = 0; for (var i = 0; i < n; i++) { count++; } return count; } ``` === "Rust" ```rust title="time_complexity.rs" /* 線形階 */ fn linear(n: i32) -> i32 { let mut count = 0; for _ in 0..n { count += 1; } count } ``` === "C" ```c title="time_complexity.c" /* 線形階 */ int linear(int n) { int count = 0; for (int i = 0; i < n; i++) { count++; } return count; } ``` === "Kotlin" ```kotlin title="time_complexity.kt" /* 線形階 */ fun linear(n: Int): Int { var count = 0 for (i in 0.. 配列走査や連結リスト走査などの操作の時間計算量はいずれも $O(n)$ であり、ここでの $n$ は配列または連結リストの長さです: === "Python" ```python title="time_complexity.py" def array_traversal(nums: list[int]) -> int: """線形時間(配列を走査)""" count = 0 # ループ回数は配列長に比例する for num in nums: count += 1 return count ``` === "C++" ```cpp title="time_complexity.cpp" /* 線形時間(配列を走査) */ int arrayTraversal(vector &nums) { int count = 0; // ループ回数は配列長に比例する for (int num : nums) { count++; } return count; } ``` === "Java" ```java title="time_complexity.java" /* 線形時間(配列を走査) */ int arrayTraversal(int[] nums) { int count = 0; // ループ回数は配列長に比例する for (int num : nums) { count++; } return count; } ``` === "C#" ```csharp title="time_complexity.cs" /* 線形時間(配列を走査) */ int ArrayTraversal(int[] nums) { int count = 0; // ループ回数は配列長に比例する foreach (int num in nums) { count++; } return count; } ``` === "Go" ```go title="time_complexity.go" /* 線形時間(配列を走査) */ func arrayTraversal(nums []int) int { count := 0 // ループ回数は配列長に比例する for range nums { count++ } return count } ``` === "Swift" ```swift title="time_complexity.swift" /* 線形時間(配列を走査) */ func arrayTraversal(nums: [Int]) -> Int { var count = 0 // ループ回数は配列長に比例する for _ in nums { count += 1 } return count } ``` === "JS" ```javascript title="time_complexity.js" /* 線形時間(配列を走査) */ function arrayTraversal(nums) { let count = 0; // ループ回数は配列長に比例する for (let i = 0; i < nums.length; i++) { count++; } return count; } ``` === "TS" ```typescript title="time_complexity.ts" /* 線形時間(配列を走査) */ function arrayTraversal(nums: number[]): number { let count = 0; // ループ回数は配列長に比例する for (let i = 0; i < nums.length; i++) { count++; } return count; } ``` === "Dart" ```dart title="time_complexity.dart" /* 線形時間(配列を走査) */ int arrayTraversal(List nums) { int count = 0; // ループ回数は配列長に比例する for (var _num in nums) { count++; } return count; } ``` === "Rust" ```rust title="time_complexity.rs" /* 線形時間(配列を走査) */ fn array_traversal(nums: &[i32]) -> i32 { let mut count = 0; // ループ回数は配列長に比例する for _ in nums { count += 1; } count } ``` === "C" ```c title="time_complexity.c" /* 線形時間(配列を走査) */ int arrayTraversal(int *nums, int n) { int count = 0; // ループ回数は配列長に比例する for (int i = 0; i < n; i++) { count++; } return count; } ``` === "Kotlin" ```kotlin title="time_complexity.kt" /* 線形時間(配列を走査) */ fun arrayTraversal(nums: IntArray): Int { var count = 0 // ループ回数は配列長に比例する for (num in nums) { count++ } return count } ``` === "Ruby" ```ruby title="time_complexity.rb" ### 線形階 ### def linear(n) count = 0 (0...n).each { count += 1 } count end # ## 線形階(配列を走査)### def array_traversal(nums) count = 0 # ループ回数は配列長に比例する for num in nums count += 1 end count end ``` ??? pythontutor "コードの可視化"
注意すべきなのは、**入力データサイズ $n$ は入力データの型に応じて具体的に定める必要がある**ということです。例えば 1 つ目の例では変数 $n$ が入力データサイズであり、2 つ目の例では配列長 $n$ がデータサイズです。 ### 3.   平方階 $O(n^2)$ {data-toc-label="3.   平方階"} 平方階の操作回数は入力データサイズ $n$ に対して二乗のオーダーで増加します。平方階は通常、入れ子ループに現れ、外側のループと内側のループの時間計算量がともに $O(n)$ であるため、全体の時間計算量は $O(n^2)$ になります: === "Python" ```python title="time_complexity.py" def quadratic(n: int) -> int: """二乗階""" count = 0 # ループ回数はデータサイズ n の二乗に比例する for i in range(n): for j in range(n): count += 1 return count ``` === "C++" ```cpp title="time_complexity.cpp" /* 二乗階 */ int quadratic(int n) { int count = 0; // ループ回数はデータサイズ n の二乗に比例する for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { count++; } } return count; } ``` === "Java" ```java title="time_complexity.java" /* 二乗階 */ int quadratic(int n) { int count = 0; // ループ回数はデータサイズ n の二乗に比例する for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { count++; } } return count; } ``` === "C#" ```csharp title="time_complexity.cs" /* 二乗階 */ int Quadratic(int n) { int count = 0; // ループ回数はデータサイズ n の二乗に比例する for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { count++; } } return count; } ``` === "Go" ```go title="time_complexity.go" /* 二乗階 */ func quadratic(n int) int { count := 0 // ループ回数はデータサイズ n の二乗に比例する for i := 0; i < n; i++ { for j := 0; j < n; j++ { count++ } } return count } ``` === "Swift" ```swift title="time_complexity.swift" /* 二乗階 */ func quadratic(n: Int) -> Int { var count = 0 // ループ回数はデータサイズ n の二乗に比例する for _ in 0 ..< n { for _ in 0 ..< n { count += 1 } } return count } ``` === "JS" ```javascript title="time_complexity.js" /* 二乗階 */ function quadratic(n) { let count = 0; // ループ回数はデータサイズ n の二乗に比例する for (let i = 0; i < n; i++) { for (let j = 0; j < n; j++) { count++; } } return count; } ``` === "TS" ```typescript title="time_complexity.ts" /* 二乗階 */ function quadratic(n: number): number { let count = 0; // ループ回数はデータサイズ n の二乗に比例する for (let i = 0; i < n; i++) { for (let j = 0; j < n; j++) { count++; } } return count; } ``` === "Dart" ```dart title="time_complexity.dart" /* 二乗階 */ int quadratic(int n) { int count = 0; // ループ回数はデータサイズ n の二乗に比例する for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { count++; } } return count; } ``` === "Rust" ```rust title="time_complexity.rs" /* 二乗階 */ fn quadratic(n: i32) -> i32 { let mut count = 0; // ループ回数はデータサイズ n の二乗に比例する for _ in 0..n { for _ in 0..n { count += 1; } } count } ``` === "C" ```c title="time_complexity.c" /* 二乗階 */ int quadratic(int n) { int count = 0; // ループ回数はデータサイズ n の二乗に比例する for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { count++; } } return count; } ``` === "Kotlin" ```kotlin title="time_complexity.kt" /* 二乗階 */ fun quadratic(n: Int): Int { var count = 0 // ループ回数はデータサイズ n の二乗に比例する for (i in 0.. 以下の図は、定数階・線形階・平方階の 3 種類の時間計算量を比較したものです。 ![定数階、線形階、平方階の時間計算量](time_complexity.assets/time_complexity_constant_linear_quadratic.png){ class="animation-figure" }

図 2-10   定数階、線形階、平方階の時間計算量

バブルソートを例にとると、外側のループは $n - 1$ 回実行され、内側のループは $n-1$、$n-2$、$\dots$、$2$、$1$ 回実行され、平均すると $n / 2$ 回です。したがって時間計算量は $O((n - 1) n / 2) = O(n^2)$ となります: === "Python" ```python title="time_complexity.py" def bubble_sort(nums: list[int]) -> int: """二次時間(バブルソート)""" count = 0 # カウンタ # 外側のループ:未ソート区間は [0, i] for i in range(len(nums) - 1, 0, -1): # 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換 for j in range(i): if nums[j] > nums[j + 1]: # nums[j] と nums[j + 1] を交換 tmp: int = nums[j] nums[j] = nums[j + 1] nums[j + 1] = tmp count += 3 # 要素交換には 3 回の単位操作が含まれる return count ``` === "C++" ```cpp title="time_complexity.cpp" /* 二次時間(バブルソート) */ int bubbleSort(vector &nums) { int count = 0; // カウンタ // 外側のループ:未ソート区間は [0, i] for (int i = nums.size() - 1; i > 0; i--) { // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換 for (int j = 0; j < i; j++) { if (nums[j] > nums[j + 1]) { // nums[j] と nums[j + 1] を交換 int tmp = nums[j]; nums[j] = nums[j + 1]; nums[j + 1] = tmp; count += 3; // 要素交換には 3 回の単位操作が含まれる } } } return count; } ``` === "Java" ```java title="time_complexity.java" /* 二次時間(バブルソート) */ int bubbleSort(int[] nums) { int count = 0; // カウンタ // 外側のループ:未ソート区間は [0, i] for (int i = nums.length - 1; i > 0; i--) { // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換 for (int j = 0; j < i; j++) { if (nums[j] > nums[j + 1]) { // nums[j] と nums[j + 1] を交換 int tmp = nums[j]; nums[j] = nums[j + 1]; nums[j + 1] = tmp; count += 3; // 要素交換には 3 回の単位操作が含まれる } } } return count; } ``` === "C#" ```csharp title="time_complexity.cs" /* 二次時間(バブルソート) */ int BubbleSort(int[] nums) { int count = 0; // カウンタ // 外側のループ:未ソート区間は [0, i] for (int i = nums.Length - 1; i > 0; i--) { // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換 for (int j = 0; j < i; j++) { if (nums[j] > nums[j + 1]) { // nums[j] と nums[j + 1] を交換 (nums[j + 1], nums[j]) = (nums[j], nums[j + 1]); count += 3; // 要素交換には 3 回の単位操作が含まれる } } } return count; } ``` === "Go" ```go title="time_complexity.go" /* 二次時間(バブルソート) */ func bubbleSort(nums []int) int { count := 0 // カウンタ // 外側のループ:未ソート区間は [0, i] for i := len(nums) - 1; i > 0; i-- { // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換 for j := 0; j < i; j++ { if nums[j] > nums[j+1] { // nums[j] と nums[j + 1] を交換 tmp := nums[j] nums[j] = nums[j+1] nums[j+1] = tmp count += 3 // 要素交換には 3 回の単位操作が含まれる } } } return count } ``` === "Swift" ```swift title="time_complexity.swift" /* 二次時間(バブルソート) */ func bubbleSort(nums: inout [Int]) -> Int { var count = 0 // カウンタ // 外側のループ:未ソート区間は [0, i] for i in nums.indices.dropFirst().reversed() { // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換 for j in 0 ..< i { if nums[j] > nums[j + 1] { // nums[j] と nums[j + 1] を交換 let tmp = nums[j] nums[j] = nums[j + 1] nums[j + 1] = tmp count += 3 // 要素交換には 3 回の単位操作が含まれる } } } return count } ``` === "JS" ```javascript title="time_complexity.js" /* 二次時間(バブルソート) */ function bubbleSort(nums) { let count = 0; // カウンタ // 外側のループ:未ソート区間は [0, i] for (let i = nums.length - 1; i > 0; i--) { // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換 for (let j = 0; j < i; j++) { if (nums[j] > nums[j + 1]) { // nums[j] と nums[j + 1] を交換 let tmp = nums[j]; nums[j] = nums[j + 1]; nums[j + 1] = tmp; count += 3; // 要素交換には 3 回の単位操作が含まれる } } } return count; } ``` === "TS" ```typescript title="time_complexity.ts" /* 二次時間(バブルソート) */ function bubbleSort(nums: number[]): number { let count = 0; // カウンタ // 外側のループ:未ソート区間は [0, i] for (let i = nums.length - 1; i > 0; i--) { // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換 for (let j = 0; j < i; j++) { if (nums[j] > nums[j + 1]) { // nums[j] と nums[j + 1] を交換 let tmp = nums[j]; nums[j] = nums[j + 1]; nums[j + 1] = tmp; count += 3; // 要素交換には 3 回の単位操作が含まれる } } } return count; } ``` === "Dart" ```dart title="time_complexity.dart" /* 二次時間(バブルソート) */ int bubbleSort(List nums) { int count = 0; // カウンタ // 外側のループ:未ソート区間は [0, i] for (var i = nums.length - 1; i > 0; i--) { // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換 for (var j = 0; j < i; j++) { if (nums[j] > nums[j + 1]) { // nums[j] と nums[j + 1] を交換 int tmp = nums[j]; nums[j] = nums[j + 1]; nums[j + 1] = tmp; count += 3; // 要素交換には 3 回の単位操作が含まれる } } } return count; } ``` === "Rust" ```rust title="time_complexity.rs" /* 二次時間(バブルソート) */ fn bubble_sort(nums: &mut [i32]) -> i32 { let mut count = 0; // カウンタ // 外側のループ:未ソート区間は [0, i] for i in (1..nums.len()).rev() { // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換 for j in 0..i { if nums[j] > nums[j + 1] { // nums[j] と nums[j + 1] を交換 let tmp = nums[j]; nums[j] = nums[j + 1]; nums[j + 1] = tmp; count += 3; // 要素交換には 3 回の単位操作が含まれる } } } count } ``` === "C" ```c title="time_complexity.c" /* 二次時間(バブルソート) */ int bubbleSort(int *nums, int n) { int count = 0; // カウンタ // 外側のループ:未ソート区間は [0, i] for (int i = n - 1; i > 0; i--) { // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換 for (int j = 0; j < i; j++) { if (nums[j] > nums[j + 1]) { // nums[j] と nums[j + 1] を交換 int tmp = nums[j]; nums[j] = nums[j + 1]; nums[j + 1] = tmp; count += 3; // 要素交換には 3 回の単位操作が含まれる } } } return count; } ``` === "Kotlin" ```kotlin title="time_complexity.kt" /* 二次時間(バブルソート) */ fun bubbleSort(nums: IntArray): Int { var count = 0 // カウンタ // 外側のループ:未ソート区間は [0, i] for (i in nums.size - 1 downTo 1) { // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換 for (j in 0.. nums[j + 1]) { // nums[j] と nums[j + 1] を交換 val temp = nums[j] nums[j] = nums[j + 1] nums[j + 1] = temp count += 3 // 要素交換には 3 回の単位操作が含まれる } } } return count } ``` === "Ruby" ```ruby title="time_complexity.rb" ### 平方階 ### def quadratic(n) count = 0 # ループ回数はデータサイズ n の二乗に比例する for i in 0...n for j in 0...n count += 1 end end count end # ## 平方階(バブルソート)### def bubble_sort(nums) count = 0 # カウンタ # 外側のループ:未ソート区間は [0, i] for i in (nums.length - 1).downto(0) # 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換 for j in 0...i if nums[j] > nums[j + 1] # nums[j] と nums[j + 1] を交換 tmp = nums[j] nums[j] = nums[j + 1] nums[j + 1] = tmp count += 3 # 要素交換には 3 回の単位操作が含まれる end end end count end ``` ??? pythontutor "コードの可視化"
### 4.   指数階 $O(2^n)$ {data-toc-label="4.   指数階"} 生物学における「細胞分裂」は指数階増加の典型例です。初期状態では細胞が $1$ 個あり、1 回分裂すると $2$ 個、2 回分裂すると $4$ 個となり、以下同様に、$n$ 回分裂すると $2^n$ 個の細胞になります。 以下の図とコードは細胞分裂の過程を模擬したもので、時間計算量は $O(2^n)$ です。なお、入力の $n$ は分裂回数を表し、戻り値 `count` は総分裂回数を表します。 === "Python" ```python title="time_complexity.py" def exponential(n: int) -> int: """指数時間(ループ実装)""" count = 0 base = 1 # 細胞は各ラウンドで 2 つに分裂し、数列 1, 2, 4, 8, ..., 2^(n-1) を形成する for _ in range(n): for _ in range(base): count += 1 base *= 2 # count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1 return count ``` === "C++" ```cpp title="time_complexity.cpp" /* 指数時間(ループ実装) */ int exponential(int n) { int count = 0, base = 1; // 細胞は各ラウンドで 2 つに分裂し、数列 1, 2, 4, 8, ..., 2^(n-1) を形成する for (int i = 0; i < n; i++) { for (int j = 0; j < base; j++) { count++; } base *= 2; } // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1 return count; } ``` === "Java" ```java title="time_complexity.java" /* 指数時間(ループ実装) */ int exponential(int n) { int count = 0, base = 1; // 細胞は各ラウンドで 2 つに分裂し、数列 1, 2, 4, 8, ..., 2^(n-1) を形成する for (int i = 0; i < n; i++) { for (int j = 0; j < base; j++) { count++; } base *= 2; } // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1 return count; } ``` === "C#" ```csharp title="time_complexity.cs" /* 指数時間(ループ実装) */ int Exponential(int n) { int count = 0, bas = 1; // 細胞は各ラウンドで 2 つに分裂し、数列 1, 2, 4, 8, ..., 2^(n-1) を形成する for (int i = 0; i < n; i++) { for (int j = 0; j < bas; j++) { count++; } bas *= 2; } // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1 return count; } ``` === "Go" ```go title="time_complexity.go" /* 指数時間(ループ実装) */ func exponential(n int) int { count, base := 0, 1 // 細胞は各ラウンドで 2 つに分裂し、数列 1, 2, 4, 8, ..., 2^(n-1) を形成する for i := 0; i < n; i++ { for j := 0; j < base; j++ { count++ } base *= 2 } // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1 return count } ``` === "Swift" ```swift title="time_complexity.swift" /* 指数時間(ループ実装) */ func exponential(n: Int) -> Int { var count = 0 var base = 1 // 細胞は各ラウンドで 2 つに分裂し、数列 1, 2, 4, 8, ..., 2^(n-1) を形成する for _ in 0 ..< n { for _ in 0 ..< base { count += 1 } base *= 2 } // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1 return count } ``` === "JS" ```javascript title="time_complexity.js" /* 指数時間(ループ実装) */ function exponential(n) { let count = 0, base = 1; // 細胞は各ラウンドで 2 つに分裂し、数列 1, 2, 4, 8, ..., 2^(n-1) を形成する for (let i = 0; i < n; i++) { for (let j = 0; j < base; j++) { count++; } base *= 2; } // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1 return count; } ``` === "TS" ```typescript title="time_complexity.ts" /* 指数時間(ループ実装) */ function exponential(n: number): number { let count = 0, base = 1; // 細胞は各ラウンドで 2 つに分裂し、数列 1, 2, 4, 8, ..., 2^(n-1) を形成する for (let i = 0; i < n; i++) { for (let j = 0; j < base; j++) { count++; } base *= 2; } // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1 return count; } ``` === "Dart" ```dart title="time_complexity.dart" /* 指数時間(ループ実装) */ int exponential(int n) { int count = 0, base = 1; // 細胞は各ラウンドで 2 つに分裂し、数列 1, 2, 4, 8, ..., 2^(n-1) を形成する for (var i = 0; i < n; i++) { for (var j = 0; j < base; j++) { count++; } base *= 2; } // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1 return count; } ``` === "Rust" ```rust title="time_complexity.rs" /* 指数時間(ループ実装) */ fn exponential(n: i32) -> i32 { let mut count = 0; let mut base = 1; // 細胞は各ラウンドで 2 つに分裂し、数列 1, 2, 4, 8, ..., 2^(n-1) を形成する for _ in 0..n { for _ in 0..base { count += 1 } base *= 2; } // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1 count } ``` === "C" ```c title="time_complexity.c" /* 指数時間(ループ実装) */ int exponential(int n) { int count = 0; int bas = 1; // 細胞は各ラウンドで 2 つに分裂し、数列 1, 2, 4, 8, ..., 2^(n-1) を形成する for (int i = 0; i < n; i++) { for (int j = 0; j < bas; j++) { count++; } bas *= 2; } // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1 return count; } ``` === "Kotlin" ```kotlin title="time_complexity.kt" /* 指数時間(ループ実装) */ fun exponential(n: Int): Int { var count = 0 var base = 1 // 細胞は各ラウンドで 2 つに分裂し、数列 1, 2, 4, 8, ..., 2^(n-1) を形成する for (i in 0.. nums[j + 1] # nums[j] と nums[j + 1] を交換 tmp = nums[j] nums[j] = nums[j + 1] nums[j + 1] = tmp count += 3 # 要素交換には 3 回の単位操作が含まれる end end end count end # ## 指数階(ループ実装)### def exponential(n) count, base = 0, 1 # 細胞は各ラウンドで 2 つに分裂し、数列 1, 2, 4, 8, ..., 2^(n-1) を形成する (0...n).each do (0...base).each { count += 1 } base *= 2 end # count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1 count end ``` ??? pythontutor "コードの可視化"
![指数階の時間計算量](time_complexity.assets/time_complexity_exponential.png){ class="animation-figure" }

図 2-11   指数階の時間計算量

実際のアルゴリズムでも、指数階は再帰関数によく現れます。例えば次のコードでは、再帰的に 2 つへ分岐し、$n$ 回分裂した後に停止します: === "Python" ```python title="time_complexity.py" def exp_recur(n: int) -> int: """指数時間(再帰実装)""" if n == 1: return 1 return exp_recur(n - 1) + exp_recur(n - 1) + 1 ``` === "C++" ```cpp title="time_complexity.cpp" /* 指数時間(再帰実装) */ int expRecur(int n) { if (n == 1) return 1; return expRecur(n - 1) + expRecur(n - 1) + 1; } ``` === "Java" ```java title="time_complexity.java" /* 指数時間(再帰実装) */ int expRecur(int n) { if (n == 1) return 1; return expRecur(n - 1) + expRecur(n - 1) + 1; } ``` === "C#" ```csharp title="time_complexity.cs" /* 指数時間(再帰実装) */ int ExpRecur(int n) { if (n == 1) return 1; return ExpRecur(n - 1) + ExpRecur(n - 1) + 1; } ``` === "Go" ```go title="time_complexity.go" /* 指数時間(再帰実装) */ func expRecur(n int) int { if n == 1 { return 1 } return expRecur(n-1) + expRecur(n-1) + 1 } ``` === "Swift" ```swift title="time_complexity.swift" /* 指数時間(再帰実装) */ func expRecur(n: Int) -> Int { if n == 1 { return 1 } return expRecur(n: n - 1) + expRecur(n: n - 1) + 1 } ``` === "JS" ```javascript title="time_complexity.js" /* 指数時間(再帰実装) */ function expRecur(n) { if (n === 1) return 1; return expRecur(n - 1) + expRecur(n - 1) + 1; } ``` === "TS" ```typescript title="time_complexity.ts" /* 指数時間(再帰実装) */ function expRecur(n: number): number { if (n === 1) return 1; return expRecur(n - 1) + expRecur(n - 1) + 1; } ``` === "Dart" ```dart title="time_complexity.dart" /* 指数時間(再帰実装) */ int expRecur(int n) { if (n == 1) return 1; return expRecur(n - 1) + expRecur(n - 1) + 1; } ``` === "Rust" ```rust title="time_complexity.rs" /* 指数時間(再帰実装) */ fn exp_recur(n: i32) -> i32 { if n == 1 { return 1; } exp_recur(n - 1) + exp_recur(n - 1) + 1 } ``` === "C" ```c title="time_complexity.c" /* 指数時間(再帰実装) */ int expRecur(int n) { if (n == 1) return 1; return expRecur(n - 1) + expRecur(n - 1) + 1; } ``` === "Kotlin" ```kotlin title="time_complexity.kt" /* 指数時間(再帰実装) */ fun expRecur(n: Int): Int { if (n == 1) { return 1 } return expRecur(n - 1) + expRecur(n - 1) + 1 } ``` === "Ruby" ```ruby title="time_complexity.rb" ### 平方階 ### def quadratic(n) count = 0 # ループ回数はデータサイズ n の二乗に比例する for i in 0...n for j in 0...n count += 1 end end count end # ## 平方階(バブルソート)### def bubble_sort(nums) count = 0 # カウンタ # 外側のループ:未ソート区間は [0, i] for i in (nums.length - 1).downto(0) # 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換 for j in 0...i if nums[j] > nums[j + 1] # nums[j] と nums[j + 1] を交換 tmp = nums[j] nums[j] = nums[j + 1] nums[j + 1] = tmp count += 3 # 要素交換には 3 回の単位操作が含まれる end end end count end # ## 指数階(ループ実装)### def exponential(n) count, base = 0, 1 # 細胞は各ラウンドで 2 つに分裂し、数列 1, 2, 4, 8, ..., 2^(n-1) を形成する (0...n).each do (0...base).each { count += 1 } base *= 2 end # count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1 count end # ## 指数階(再帰実装)### def exp_recur(n) return 1 if n == 1 exp_recur(n - 1) + exp_recur(n - 1) + 1 end ``` ??? pythontutor "コードの可視化"
指数階の増加は非常に速く、全探索法(ブルートフォース、バックトラッキングなど)によく見られます。データ規模が大きい問題では、指数階は受け入れられず、通常は動的計画法や貪欲法などを使って解く必要があります。 ### 5.   対数階 $O(\log n)$ {data-toc-label="5.   対数階"} 指数階とは逆に、対数階は「各ラウンドで半分になる」状況を表します。入力データサイズを $n$ とすると、各ラウンドで半減するため、ループ回数は $\log_2 n$、すなわち $2^n$ の逆関数になります。 以下の図とコードは、「各ラウンドで半分になる」過程を模擬したもので、時間計算量は $O(\log_2 n)$、簡潔には $O(\log n)$ と書きます: === "Python" ```python title="time_complexity.py" def logarithmic(n: int) -> int: """対数時間(ループ実装)""" count = 0 while n > 1: n = n / 2 count += 1 return count ``` === "C++" ```cpp title="time_complexity.cpp" /* 対数時間(ループ実装) */ int logarithmic(int n) { int count = 0; while (n > 1) { n = n / 2; count++; } return count; } ``` === "Java" ```java title="time_complexity.java" /* 対数時間(ループ実装) */ int logarithmic(int n) { int count = 0; while (n > 1) { n = n / 2; count++; } return count; } ``` === "C#" ```csharp title="time_complexity.cs" /* 対数時間(ループ実装) */ int Logarithmic(int n) { int count = 0; while (n > 1) { n /= 2; count++; } return count; } ``` === "Go" ```go title="time_complexity.go" /* 対数時間(ループ実装) */ func logarithmic(n int) int { count := 0 for n > 1 { n = n / 2 count++ } return count } ``` === "Swift" ```swift title="time_complexity.swift" /* 対数時間(ループ実装) */ func logarithmic(n: Int) -> Int { var count = 0 var n = n while n > 1 { n = n / 2 count += 1 } return count } ``` === "JS" ```javascript title="time_complexity.js" /* 対数時間(ループ実装) */ function logarithmic(n) { let count = 0; while (n > 1) { n = n / 2; count++; } return count; } ``` === "TS" ```typescript title="time_complexity.ts" /* 対数時間(ループ実装) */ function logarithmic(n: number): number { let count = 0; while (n > 1) { n = n / 2; count++; } return count; } ``` === "Dart" ```dart title="time_complexity.dart" /* 対数時間(ループ実装) */ int logarithmic(int n) { int count = 0; while (n > 1) { n = n ~/ 2; count++; } return count; } ``` === "Rust" ```rust title="time_complexity.rs" /* 対数時間(ループ実装) */ fn logarithmic(mut n: i32) -> i32 { let mut count = 0; while n > 1 { n = n / 2; count += 1; } count } ``` === "C" ```c title="time_complexity.c" /* 対数時間(ループ実装) */ int logarithmic(int n) { int count = 0; while (n > 1) { n = n / 2; count++; } return count; } ``` === "Kotlin" ```kotlin title="time_complexity.kt" /* 対数時間(ループ実装) */ fun logarithmic(n: Int): Int { var n1 = n var count = 0 while (n1 > 1) { n1 /= 2 count++ } return count } ``` === "Ruby" ```ruby title="time_complexity.rb" ### 平方階 ### def quadratic(n) count = 0 # ループ回数はデータサイズ n の二乗に比例する for i in 0...n for j in 0...n count += 1 end end count end # ## 平方階(バブルソート)### def bubble_sort(nums) count = 0 # カウンタ # 外側のループ:未ソート区間は [0, i] for i in (nums.length - 1).downto(0) # 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換 for j in 0...i if nums[j] > nums[j + 1] # nums[j] と nums[j + 1] を交換 tmp = nums[j] nums[j] = nums[j + 1] nums[j + 1] = tmp count += 3 # 要素交換には 3 回の単位操作が含まれる end end end count end # ## 指数階(ループ実装)### def exponential(n) count, base = 0, 1 # 細胞は各ラウンドで 2 つに分裂し、数列 1, 2, 4, 8, ..., 2^(n-1) を形成する (0...n).each do (0...base).each { count += 1 } base *= 2 end # count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1 count end # ## 指数階(再帰実装)### def exp_recur(n) return 1 if n == 1 exp_recur(n - 1) + exp_recur(n - 1) + 1 end # ## 対数階(ループ実装)### def logarithmic(n) count = 0 while n > 1 n /= 2 count += 1 end count end ``` ??? pythontutor "コードの可視化"
![対数階の時間計算量](time_complexity.assets/time_complexity_logarithmic.png){ class="animation-figure" }

図 2-12   対数階の時間計算量

指数階と同様に、対数階も再帰関数によく現れます。次のコードは高さ $\log_2 n$ の再帰木を形成します: === "Python" ```python title="time_complexity.py" def log_recur(n: int) -> int: """対数時間(再帰実装)""" if n <= 1: return 0 return log_recur(n / 2) + 1 ``` === "C++" ```cpp title="time_complexity.cpp" /* 対数時間(再帰実装) */ int logRecur(int n) { if (n <= 1) return 0; return logRecur(n / 2) + 1; } ``` === "Java" ```java title="time_complexity.java" /* 対数時間(再帰実装) */ int logRecur(int n) { if (n <= 1) return 0; return logRecur(n / 2) + 1; } ``` === "C#" ```csharp title="time_complexity.cs" /* 対数時間(再帰実装) */ int LogRecur(int n) { if (n <= 1) return 0; return LogRecur(n / 2) + 1; } ``` === "Go" ```go title="time_complexity.go" /* 対数時間(再帰実装) */ func logRecur(n int) int { if n <= 1 { return 0 } return logRecur(n/2) + 1 } ``` === "Swift" ```swift title="time_complexity.swift" /* 対数時間(再帰実装) */ func logRecur(n: Int) -> Int { if n <= 1 { return 0 } return logRecur(n: n / 2) + 1 } ``` === "JS" ```javascript title="time_complexity.js" /* 対数時間(再帰実装) */ function logRecur(n) { if (n <= 1) return 0; return logRecur(n / 2) + 1; } ``` === "TS" ```typescript title="time_complexity.ts" /* 対数時間(再帰実装) */ function logRecur(n: number): number { if (n <= 1) return 0; return logRecur(n / 2) + 1; } ``` === "Dart" ```dart title="time_complexity.dart" /* 対数時間(再帰実装) */ int logRecur(int n) { if (n <= 1) return 0; return logRecur(n ~/ 2) + 1; } ``` === "Rust" ```rust title="time_complexity.rs" /* 対数時間(再帰実装) */ fn log_recur(n: i32) -> i32 { if n <= 1 { return 0; } log_recur(n / 2) + 1 } ``` === "C" ```c title="time_complexity.c" /* 対数時間(再帰実装) */ int logRecur(int n) { if (n <= 1) return 0; return logRecur(n / 2) + 1; } ``` === "Kotlin" ```kotlin title="time_complexity.kt" /* 対数時間(再帰実装) */ fun logRecur(n: Int): Int { if (n <= 1) return 0 return logRecur(n / 2) + 1 } ``` === "Ruby" ```ruby title="time_complexity.rb" ### 平方階 ### def quadratic(n) count = 0 # ループ回数はデータサイズ n の二乗に比例する for i in 0...n for j in 0...n count += 1 end end count end # ## 平方階(バブルソート)### def bubble_sort(nums) count = 0 # カウンタ # 外側のループ:未ソート区間は [0, i] for i in (nums.length - 1).downto(0) # 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換 for j in 0...i if nums[j] > nums[j + 1] # nums[j] と nums[j + 1] を交換 tmp = nums[j] nums[j] = nums[j + 1] nums[j + 1] = tmp count += 3 # 要素交換には 3 回の単位操作が含まれる end end end count end # ## 指数階(ループ実装)### def exponential(n) count, base = 0, 1 # 細胞は各ラウンドで 2 つに分裂し、数列 1, 2, 4, 8, ..., 2^(n-1) を形成する (0...n).each do (0...base).each { count += 1 } base *= 2 end # count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1 count end # ## 指数階(再帰実装)### def exp_recur(n) return 1 if n == 1 exp_recur(n - 1) + exp_recur(n - 1) + 1 end # ## 対数階(ループ実装)### def logarithmic(n) count = 0 while n > 1 n /= 2 count += 1 end count end # ## 対数階(再帰実装)### def log_recur(n) return 0 unless n > 1 log_recur(n / 2) + 1 end ``` ??? pythontutor "コードの可視化"
対数階は分割統治に基づくアルゴリズムによく現れ、「1 つを複数に分ける」「複雑なものを単純化する」という考え方を体現しています。増加は緩やかで、定数階に次いで理想的な時間計算量です。 !!! tip "$O(\log n)$ の底は何か?" 正確には、「$m$ 個に分ける」場合に対応する時間計算量は $O(\log_m n)$ です。そして対数の底の変換公式により、底が異なっても同値な時間計算量が得られます: $$ O(\log_m n) = O(\log_k n / \log_k m) = O(\log_k n) $$ つまり、底 $m$ は複雑度に影響を与えずに変換できます。そのため通常は底 $m$ を省略し、対数階を単に $O(\log n)$ と記します。 ### 6.   線形対数階 $O(n \log n)$ {data-toc-label="6.   線形対数階"} 線形対数階は入れ子ループによく現れ、2 層のループの時間計算量はそれぞれ $O(\log n)$ と $O(n)$ です。関連するコードは次のとおりです: === "Python" ```python title="time_complexity.py" def linear_log_recur(n: int) -> int: """線形対数時間""" if n <= 1: return 1 # 二つに分割すると、部分問題の規模は半分になる count = linear_log_recur(n // 2) + linear_log_recur(n // 2) # 現在の部分問題には n 個の操作が含まれる for _ in range(n): count += 1 return count ``` === "C++" ```cpp title="time_complexity.cpp" /* 線形対数時間 */ int linearLogRecur(int n) { if (n <= 1) return 1; int count = linearLogRecur(n / 2) + linearLogRecur(n / 2); for (int i = 0; i < n; i++) { count++; } return count; } ``` === "Java" ```java title="time_complexity.java" /* 線形対数時間 */ int linearLogRecur(int n) { if (n <= 1) return 1; int count = linearLogRecur(n / 2) + linearLogRecur(n / 2); for (int i = 0; i < n; i++) { count++; } return count; } ``` === "C#" ```csharp title="time_complexity.cs" /* 線形対数時間 */ int LinearLogRecur(int n) { if (n <= 1) return 1; int count = LinearLogRecur(n / 2) + LinearLogRecur(n / 2); for (int i = 0; i < n; i++) { count++; } return count; } ``` === "Go" ```go title="time_complexity.go" /* 線形対数時間 */ func linearLogRecur(n int) int { if n <= 1 { return 1 } count := linearLogRecur(n/2) + linearLogRecur(n/2) for i := 0; i < n; i++ { count++ } return count } ``` === "Swift" ```swift title="time_complexity.swift" /* 線形対数時間 */ func linearLogRecur(n: Int) -> Int { if n <= 1 { return 1 } var count = linearLogRecur(n: n / 2) + linearLogRecur(n: n / 2) for _ in stride(from: 0, to: n, by: 1) { count += 1 } return count } ``` === "JS" ```javascript title="time_complexity.js" /* 線形対数時間 */ function linearLogRecur(n) { if (n <= 1) return 1; let count = linearLogRecur(n / 2) + linearLogRecur(n / 2); for (let i = 0; i < n; i++) { count++; } return count; } ``` === "TS" ```typescript title="time_complexity.ts" /* 線形対数時間 */ function linearLogRecur(n: number): number { if (n <= 1) return 1; let count = linearLogRecur(n / 2) + linearLogRecur(n / 2); for (let i = 0; i < n; i++) { count++; } return count; } ``` === "Dart" ```dart title="time_complexity.dart" /* 線形対数時間 */ int linearLogRecur(int n) { if (n <= 1) return 1; int count = linearLogRecur(n ~/ 2) + linearLogRecur(n ~/ 2); for (var i = 0; i < n; i++) { count++; } return count; } ``` === "Rust" ```rust title="time_complexity.rs" /* 線形対数時間 */ fn linear_log_recur(n: i32) -> i32 { if n <= 1 { return 1; } let mut count = linear_log_recur(n / 2) + linear_log_recur(n / 2); for _ in 0..n { count += 1; } return count; } ``` === "C" ```c title="time_complexity.c" /* 線形対数時間 */ int linearLogRecur(int n) { if (n <= 1) return 1; int count = linearLogRecur(n / 2) + linearLogRecur(n / 2); for (int i = 0; i < n; i++) { count++; } return count; } ``` === "Kotlin" ```kotlin title="time_complexity.kt" /* 線形対数時間 */ fun linearLogRecur(n: Int): Int { if (n <= 1) return 1 var count = linearLogRecur(n / 2) + linearLogRecur(n / 2) for (i in 0.. 1 count = linear_log_recur(n / 2) + linear_log_recur(n / 2) (0...n).each { count += 1 } count end ``` ??? pythontutor "コードの可視化"
下図は線形対数階がどのように生じるかを示しています。二分木の各層の操作総数はすべて $n$ であり、木全体は $\log_2 n + 1$ 層あるため、時間計算量は $O(n \log n)$ です。 ![線形対数階の時間計算量](time_complexity.assets/time_complexity_logarithmic_linear.png){ class="animation-figure" }

図 2-13   線形対数階の時間計算量

主なソートアルゴリズムの時間計算量は通常 $O(n \log n)$ であり、例えばクイックソート、マージソート、ヒープソートなどがあります。 ### 7.   階乗階 $O(n!)$ {data-toc-label="7.   階乗階"} 階乗階は、数学における「全順列」の問題に対応します。互いに重複しない $n$ 個の要素が与えられたとき、そのすべての並べ方を求めると、通り数は次のようになります: $$ n! = n \times (n - 1) \times (n - 2) \times \dots \times 2 \times 1 $$ 階乗は通常、再帰で実装されます。以下の図とコードのように、第 1 層では $n$ 個に分岐し、第 2 層では $n - 1$ 個に分岐し、以下同様に、第 $n$ 層で分岐が停止します: === "Python" ```python title="time_complexity.py" def factorial_recur(n: int) -> int: """階乗時間(再帰実装)""" if n == 0: return 1 count = 0 # 1個から n 個に分裂 for _ in range(n): count += factorial_recur(n - 1) return count ``` === "C++" ```cpp title="time_complexity.cpp" /* 階乗時間(再帰実装) */ int factorialRecur(int n) { if (n == 0) return 1; int count = 0; // 1個から n 個に分裂 for (int i = 0; i < n; i++) { count += factorialRecur(n - 1); } return count; } ``` === "Java" ```java title="time_complexity.java" /* 階乗時間(再帰実装) */ int factorialRecur(int n) { if (n == 0) return 1; int count = 0; // 1個から n 個に分裂 for (int i = 0; i < n; i++) { count += factorialRecur(n - 1); } return count; } ``` === "C#" ```csharp title="time_complexity.cs" /* 階乗時間(再帰実装) */ int FactorialRecur(int n) { if (n == 0) return 1; int count = 0; // 1個から n 個に分裂 for (int i = 0; i < n; i++) { count += FactorialRecur(n - 1); } return count; } ``` === "Go" ```go title="time_complexity.go" /* 階乗時間(再帰実装) */ func factorialRecur(n int) int { if n == 0 { return 1 } count := 0 // 1個から n 個に分裂 for i := 0; i < n; i++ { count += factorialRecur(n - 1) } return count } ``` === "Swift" ```swift title="time_complexity.swift" /* 階乗時間(再帰実装) */ func factorialRecur(n: Int) -> Int { if n == 0 { return 1 } var count = 0 // 1個から n 個に分裂 for _ in 0 ..< n { count += factorialRecur(n: n - 1) } return count } ``` === "JS" ```javascript title="time_complexity.js" /* 階乗時間(再帰実装) */ function factorialRecur(n) { if (n === 0) return 1; let count = 0; // 1個から n 個に分裂 for (let i = 0; i < n; i++) { count += factorialRecur(n - 1); } return count; } ``` === "TS" ```typescript title="time_complexity.ts" /* 階乗時間(再帰実装) */ function factorialRecur(n: number): number { if (n === 0) return 1; let count = 0; // 1個から n 個に分裂 for (let i = 0; i < n; i++) { count += factorialRecur(n - 1); } return count; } ``` === "Dart" ```dart title="time_complexity.dart" /* 階乗時間(再帰実装) */ int factorialRecur(int n) { if (n == 0) return 1; int count = 0; // 1個から n 個に分裂 for (var i = 0; i < n; i++) { count += factorialRecur(n - 1); } return count; } ``` === "Rust" ```rust title="time_complexity.rs" /* 階乗時間(再帰実装) */ fn factorial_recur(n: i32) -> i32 { if n == 0 { return 1; } let mut count = 0; // 1個から n 個に分裂 for _ in 0..n { count += factorial_recur(n - 1); } count } ``` === "C" ```c title="time_complexity.c" /* 階乗時間(再帰実装) */ int factorialRecur(int n) { if (n == 0) return 1; int count = 0; for (int i = 0; i < n; i++) { count += factorialRecur(n - 1); } return count; } ``` === "Kotlin" ```kotlin title="time_complexity.kt" /* 階乗時間(再帰実装) */ fun factorialRecur(n: Int): Int { if (n == 0) return 1 var count = 0 // 1個から n 個に分裂 for (i in 0.. 1 count = linear_log_recur(n / 2) + linear_log_recur(n / 2) (0...n).each { count += 1 } count end # ## 階乗階(再帰実装)### def factorial_recur(n) return 1 if n == 0 count = 0 # 1個から n 個に分裂 (0...n).each { count += factorial_recur(n - 1) } count end ``` ??? pythontutor "コードの可視化"
![階乗階の時間計算量](time_complexity.assets/time_complexity_factorial.png){ class="animation-figure" }

図 2-14   階乗階の時間計算量

注意すべき点として、$n \geq 4$ なら常に $n! > 2^n$ なので、階乗階は指数階よりもさらに速く増加し、$n$ が大きい場合にはやはり受け入れられません。 ## 2.3.5   最悪・最良・平均時間計算量 **アルゴリズムの時間効率は固定ではなく、入力データの分布に左右されることが多いです**。長さ $n$ の配列 `nums` を考えます。`nums` は $1$ から $n$ までの数字で構成され、各数字は 1 回だけ現れます。ただし要素の順序はランダムにシャッフルされており、目標は要素 $1$ のインデックスを返すことです。ここから次の結論が得られます。 - `nums = [?, ?, ..., 1]`、つまり末尾の要素が $1$ の場合は、配列全体を最後まで走査する必要があり、**最悪時間計算量 $O(n)$** になります。 - `nums = [1, ?, ?, ...]`、つまり先頭要素が $1$ の場合は、配列がどれだけ長くてもそれ以上走査する必要がなく、**最良時間計算量 $\Omega(1)$** になります。 「最悪時間計算量」は関数の漸近上界に対応し、ビッグ $O$ 記法で表します。同様に、「最良時間計算量」は関数の漸近下界に対応し、$\Omega$ 記法で表します: === "Python" ```python title="worst_best_time_complexity.py" def random_numbers(n: int) -> list[int]: """要素が 1, 2, ..., n で順序がシャッフルされた配列を生成する""" # 配列 nums =: 1, 2, 3, ..., n を生成する nums = [i for i in range(1, n + 1)] # 配列要素をランダムにシャッフル random.shuffle(nums) return nums def find_one(nums: list[int]) -> int: """配列 nums 内で数値 1 のインデックスを探す""" for i in range(len(nums)): # 要素 1 が配列の先頭にあるとき、最良時間計算量 O(1) となる # 要素 1 が配列の末尾にあるとき、最悪時間計算量 O(n) となる if nums[i] == 1: return i return -1 ``` === "C++" ```cpp title="worst_best_time_complexity.cpp" /* 要素が { 1, 2, ..., n } で、順序がシャッフルされた配列を生成 */ vector randomNumbers(int n) { vector nums(n); // 配列 nums = { 1, 2, 3, ..., n } を生成 for (int i = 0; i < n; i++) { nums[i] = i + 1; } // システム時刻を使って乱数シードを生成する unsigned seed = chrono::system_clock::now().time_since_epoch().count(); // 配列要素をランダムにシャッフル shuffle(nums.begin(), nums.end(), default_random_engine(seed)); return nums; } /* 配列 nums 内で数値 1 のインデックスを探す */ int findOne(vector &nums) { for (int i = 0; i < nums.size(); i++) { // 要素 1 が配列の先頭にあるとき、最良時間計算量 O(1) となる // 要素 1 が配列の末尾にあるとき、最悪時間計算量 O(n) となる if (nums[i] == 1) return i; } return -1; } ``` === "Java" ```java title="worst_best_time_complexity.java" /* 要素が { 1, 2, ..., n } で、順序がシャッフルされた配列を生成 */ int[] randomNumbers(int n) { Integer[] nums = new Integer[n]; // 配列 nums = { 1, 2, 3, ..., n } を生成 for (int i = 0; i < n; i++) { nums[i] = i + 1; } // 配列要素をランダムにシャッフル Collections.shuffle(Arrays.asList(nums)); // Integer[] -> int[] int[] res = new int[n]; for (int i = 0; i < n; i++) { res[i] = nums[i]; } return res; } /* 配列 nums 内で数値 1 のインデックスを探す */ int findOne(int[] nums) { for (int i = 0; i < nums.length; i++) { // 要素 1 が配列の先頭にあるとき、最良時間計算量 O(1) となる // 要素 1 が配列の末尾にあるとき、最悪時間計算量 O(n) となる if (nums[i] == 1) return i; } return -1; } ``` === "C#" ```csharp title="worst_best_time_complexity.cs" /* 要素が { 1, 2, ..., n } で、順序がシャッフルされた配列を生成 */ int[] RandomNumbers(int n) { int[] nums = new int[n]; // 配列 nums = { 1, 2, 3, ..., n } を生成 for (int i = 0; i < n; i++) { nums[i] = i + 1; } // 配列要素をランダムにシャッフル for (int i = 0; i < nums.Length; i++) { int index = new Random().Next(i, nums.Length); (nums[i], nums[index]) = (nums[index], nums[i]); } return nums; } /* 配列 nums 内で数値 1 のインデックスを探す */ int FindOne(int[] nums) { for (int i = 0; i < nums.Length; i++) { // 要素 1 が配列の先頭にあるとき、最良時間計算量 O(1) となる // 要素 1 が配列の末尾にあるとき、最悪時間計算量 O(n) となる if (nums[i] == 1) return i; } return -1; } ``` === "Go" ```go title="worst_best_time_complexity.go" /* 要素が { 1, 2, ..., n } で、順序がシャッフルされた配列を生成 */ func randomNumbers(n int) []int { nums := make([]int, n) // 配列 nums = { 1, 2, 3, ..., n } を生成 for i := 0; i < n; i++ { nums[i] = i + 1 } // 配列要素をランダムにシャッフル rand.Shuffle(len(nums), func(i, j int) { nums[i], nums[j] = nums[j], nums[i] }) return nums } /* 配列 nums 内で数値 1 のインデックスを探す */ func findOne(nums []int) int { for i := 0; i < len(nums); i++ { // 要素 1 が配列の先頭にあるとき、最良時間計算量 O(1) となる // 要素 1 が配列の末尾にあるとき、最悪時間計算量 O(n) となる if nums[i] == 1 { return i } } return -1 } ``` === "Swift" ```swift title="worst_best_time_complexity.swift" /* 要素が { 1, 2, ..., n } で、順序がシャッフルされた配列を生成 */ func randomNumbers(n: Int) -> [Int] { // 配列 nums = { 1, 2, 3, ..., n } を生成 var nums = Array(1 ... n) // 配列要素をランダムにシャッフル nums.shuffle() return nums } /* 配列 nums 内で数値 1 のインデックスを探す */ func findOne(nums: [Int]) -> Int { for i in nums.indices { // 要素 1 が配列の先頭にあるとき、最良時間計算量 O(1) となる // 要素 1 が配列の末尾にあるとき、最悪時間計算量 O(n) となる if nums[i] == 1 { return i } } return -1 } ``` === "JS" ```javascript title="worst_best_time_complexity.js" /* 要素が { 1, 2, ..., n } で、順序がシャッフルされた配列を生成 */ function randomNumbers(n) { const nums = Array(n); // 配列 nums = { 1, 2, 3, ..., n } を生成 for (let i = 0; i < n; i++) { nums[i] = i + 1; } // 配列要素をランダムにシャッフル for (let i = 0; i < n; i++) { const r = Math.floor(Math.random() * (i + 1)); const temp = nums[i]; nums[i] = nums[r]; nums[r] = temp; } return nums; } /* 配列 nums 内で数値 1 のインデックスを探す */ function findOne(nums) { for (let i = 0; i < nums.length; i++) { // 要素 1 が配列の先頭にあるとき、最良時間計算量 O(1) となる // 要素 1 が配列の末尾にあるとき、最悪時間計算量 O(n) となる if (nums[i] === 1) { return i; } } return -1; } ``` === "TS" ```typescript title="worst_best_time_complexity.ts" /* 要素が { 1, 2, ..., n } で、順序がシャッフルされた配列を生成 */ function randomNumbers(n: number): number[] { const nums = Array(n); // 配列 nums = { 1, 2, 3, ..., n } を生成 for (let i = 0; i < n; i++) { nums[i] = i + 1; } // 配列要素をランダムにシャッフル for (let i = 0; i < n; i++) { const r = Math.floor(Math.random() * (i + 1)); const temp = nums[i]; nums[i] = nums[r]; nums[r] = temp; } return nums; } /* 配列 nums 内で数値 1 のインデックスを探す */ function findOne(nums: number[]): number { for (let i = 0; i < nums.length; i++) { // 要素 1 が配列の先頭にあるとき、最良時間計算量 O(1) となる // 要素 1 が配列の末尾にあるとき、最悪時間計算量 O(n) となる if (nums[i] === 1) { return i; } } return -1; } ``` === "Dart" ```dart title="worst_best_time_complexity.dart" /* 要素が { 1, 2, ..., n } で、順序がシャッフルされた配列を生成 */ List randomNumbers(int n) { final nums = List.filled(n, 0); // 配列 nums = { 1, 2, 3, ..., n } を生成 for (var i = 0; i < n; i++) { nums[i] = i + 1; } // 配列要素をランダムにシャッフル nums.shuffle(); return nums; } /* 配列 nums 内で数値 1 のインデックスを探す */ int findOne(List nums) { for (var i = 0; i < nums.length; i++) { // 要素 1 が配列の先頭にあるとき、最良時間計算量 O(1) となる // 要素 1 が配列の末尾にあるとき、最悪時間計算量 O(n) となる if (nums[i] == 1) return i; } return -1; } ``` === "Rust" ```rust title="worst_best_time_complexity.rs" /* 要素が { 1, 2, ..., n } で、順序がシャッフルされた配列を生成 */ fn random_numbers(n: i32) -> Vec { // 配列 nums = { 1, 2, 3, ..., n } を生成 let mut nums = (1..=n).collect::>(); // 配列要素をランダムにシャッフル nums.shuffle(&mut thread_rng()); nums } /* 配列 nums 内で数値 1 のインデックスを探す */ fn find_one(nums: &[i32]) -> Option { for i in 0..nums.len() { // 要素 1 が配列の先頭にあるとき、最良時間計算量 O(1) となる // 要素 1 が配列の末尾にあるとき、最悪時間計算量 O(n) となる if nums[i] == 1 { return Some(i); } } None } ``` === "C" ```c title="worst_best_time_complexity.c" /* 要素が { 1, 2, ..., n } で、順序がシャッフルされた配列を生成 */ int *randomNumbers(int n) { // ヒープ領域にメモリを確保する(要素数 n、要素型 int の一次元可変長配列を作成) int *nums = (int *)malloc(n * sizeof(int)); // 配列 nums = { 1, 2, 3, ..., n } を生成 for (int i = 0; i < n; i++) { nums[i] = i + 1; } // 配列要素をランダムにシャッフル for (int i = n - 1; i > 0; i--) { int j = rand() % (i + 1); int temp = nums[i]; nums[i] = nums[j]; nums[j] = temp; } return nums; } /* 配列 nums 内で数値 1 のインデックスを探す */ int findOne(int *nums, int n) { for (int i = 0; i < n; i++) { // 要素 1 が配列の先頭にあるとき、最良時間計算量 O(1) となる // 要素 1 が配列の末尾にあるとき、最悪時間計算量 O(n) となる if (nums[i] == 1) return i; } return -1; } ``` === "Kotlin" ```kotlin title="worst_best_time_complexity.kt" /* 要素が { 1, 2, ..., n } で、順序がシャッフルされた配列を生成 */ fun randomNumbers(n: Int): Array { val nums = IntArray(n) // 配列 nums = { 1, 2, 3, ..., n } を生成 for (i in 0..(n) for (i in 0..): Int { for (i in nums.indices) { // 要素 1 が配列の先頭にあるとき、最良時間計算量 O(1) となる // 要素 1 が配列の末尾にあるとき、最悪時間計算量 O(n) となる if (nums[i] == 1) return i } return -1 } ``` === "Ruby" ```ruby title="worst_best_time_complexity.rb" ### 1, 2, ..., n を要素とする配列を生成し、順序をシャッフルする ### def random_numbers(n) # 配列 nums =: 1, 2, 3, ..., n を生成する nums = Array.new(n) { |i| i + 1 } # 配列要素をランダムにシャッフル nums.shuffle! end ### 配列 nums 内の数値 1 のインデックスを探す ### def find_one(nums) for i in 0...nums.length # 要素 1 が配列の先頭にあるとき、最良時間計算量 O(1) となる # 要素 1 が配列の末尾にあるとき、最悪時間計算量 O(n) となる return i if nums[i] == 1 end -1 end ``` ??? pythontutor "コードの可視化"
実際には、最良時間計算量を使うことはあまりありません。通常それが実現する確率はごく低く、誤解を招く可能性があるからです。**一方で最悪時間計算量はより実用的で、効率の安全側の目安を与えてくれる**ため、安心してアルゴリズムを使えます。 上の例から分かるように、最悪時間計算量と最良時間計算量は「特殊なデータ分布」でのみ現れ、その発生確率は低いことが多く、アルゴリズムの実行効率をそのまま正確に反映するわけではありません。それに対して、**平均時間計算量はランダム入力におけるアルゴリズムの実行効率を表せる**ため、$\Theta$ 記法で表します。 一部のアルゴリズムでは、ランダムなデータ分布における平均的な状況を比較的簡単に求められます。例えば上の例では、入力配列はシャッフルされているため、要素 $1$ が任意のインデックスに現れる確率は等しいです。したがってアルゴリズムの平均ループ回数は配列長の半分 $n / 2$ となり、平均時間計算量は $\Theta(n / 2) = \Theta(n)$ です。 しかし、より複雑なアルゴリズムでは、平均時間計算量を計算するのはしばしば困難です。データ分布に対する全体の数学的期待値を分析するのが難しいからです。そのような場合、通常は最悪時間計算量をアルゴリズム効率の評価基準として用います。 !!! question "なぜ $\Theta$ 記号をあまり見かけないのか?" おそらく $O$ 記号のほうが口にしやすいため、平均時間計算量を表すのにもよく使われます。ただし厳密には、この用法は正確ではありません。本書や他の資料で「平均時間計算量 $O(n)$」のような表現を見かけた場合は、そのまま $\Theta(n)$ と理解してください。