This commit is contained in:
krahets
2025-10-17 05:33:23 +08:00
parent 9278f3c659
commit 68bb9afb16
113 changed files with 35936 additions and 0 deletions

View File

@@ -0,0 +1,311 @@
---
comments: true
---
# 11.3   バブルソート
<u>バブルソート</u>は、隣接する要素を継続的に比較し交換することで動作します。このプロセスは泡が底から上に上昇するようなものなので、「バブルソート」と名付けられました。
下図に示すように、バブリングプロセスは要素交換を使用してシミュレートできます:配列の左端から開始して右に移動し、隣接する要素の各ペアを比較します。左の要素が右の要素より大きい場合は、それらを交換します。横断後、最大要素は配列の右端にバブルアップします。
=== "<1>"
![Simulating bubble process using element swap](bubble_sort.assets/bubble_operation_step1.png){ class="animation-figure" }
=== "<2>"
![bubble_operation_step2](bubble_sort.assets/bubble_operation_step2.png){ class="animation-figure" }
=== "<3>"
![bubble_operation_step3](bubble_sort.assets/bubble_operation_step3.png){ class="animation-figure" }
=== "<4>"
![bubble_operation_step4](bubble_sort.assets/bubble_operation_step4.png){ class="animation-figure" }
=== "<5>"
![bubble_operation_step5](bubble_sort.assets/bubble_operation_step5.png){ class="animation-figure" }
=== "<6>"
![bubble_operation_step6](bubble_sort.assets/bubble_operation_step6.png){ class="animation-figure" }
=== "<7>"
![bubble_operation_step7](bubble_sort.assets/bubble_operation_step7.png){ class="animation-figure" }
<p align="center"> 図 11-4 &nbsp; Simulating bubble process using element swap </p>
## 11.3.1 &nbsp; アルゴリズムプロセス
配列の長さを$n$とします。バブルソートのステップは下図に示されます:
1. まず、$n$個の要素に対して1回の「バブル」パスを実行し、**最大要素を正しい位置に交換します**。
2. 次に、残りの$n - 1$個の要素に対して「バブル」パスを実行し、**2番目に大きい要素を正しい位置に交換します**。
3. この方法で続行します;$n - 1$回のパスの後、**最大$n - 1$個の要素が正しい位置に移動されます**。
4. 残りの唯一の要素は**必ず**最小であるため、**さらなる**ソートは必要ありません。この時点で、配列はソートされます。
![Bubble sort process](bubble_sort.assets/bubble_sort_overview.png){ class="animation-figure" }
<p align="center"> 図 11-5 &nbsp; Bubble sort process </p>
コード例は以下の通りです:
=== "Python"
```python title="bubble_sort.py"
def bubble_sort(nums: list[int]):
"""バブルソート"""
n = len(nums)
# 外側のループ:未ソート範囲は [0, i]
for i in range(n - 1, 0, -1):
# 内側のループ:未ソート範囲 [0, i] の最大要素を範囲の右端に移動
for j in range(i):
if nums[j] > nums[j + 1]:
# nums[j] と nums[j + 1] を交換
nums[j], nums[j + 1] = nums[j + 1], nums[j]
```
=== "C++"
```cpp title="bubble_sort.cpp"
/* バブルソート */
void bubbleSort(vector<int> &nums) {
// 外側ループ:未ソート範囲は[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]を交換
// ここではstdのswapを使用
swap(nums[j], nums[j + 1]);
}
}
}
}
```
=== "Java"
```java title="bubble_sort.java"
/* バブルソート */
void bubbleSort(int[] nums) {
// 外側ループ: 未ソート範囲は [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;
}
}
}
}
```
=== "C#"
```csharp title="bubble_sort.cs"
[class]{bubble_sort}-[func]{BubbleSort}
```
=== "Go"
```go title="bubble_sort.go"
[class]{}-[func]{bubbleSort}
```
=== "Swift"
```swift title="bubble_sort.swift"
[class]{}-[func]{bubbleSort}
```
=== "JS"
```javascript title="bubble_sort.js"
[class]{}-[func]{bubbleSort}
```
=== "TS"
```typescript title="bubble_sort.ts"
[class]{}-[func]{bubbleSort}
```
=== "Dart"
```dart title="bubble_sort.dart"
[class]{}-[func]{bubbleSort}
```
=== "Rust"
```rust title="bubble_sort.rs"
[class]{}-[func]{bubble_sort}
```
=== "C"
```c title="bubble_sort.c"
[class]{}-[func]{bubbleSort}
```
=== "Kotlin"
```kotlin title="bubble_sort.kt"
[class]{}-[func]{bubbleSort}
```
=== "Ruby"
```ruby title="bubble_sort.rb"
[class]{}-[func]{bubble_sort}
```
=== "Zig"
```zig title="bubble_sort.zig"
[class]{}-[func]{bubbleSort}
```
## 11.3.2 &nbsp; 効率の最適化
「バブリング」のラウンド中に交換が発生しない場合、配列はすでにソートされているため、すぐに戻ることができます。これを検出するために、`flag`変数を追加できます;パスで交換が行われない場合は、フラグを設定して早期に戻ります。
この最適化があっても、バブルソートの最悪時間計算量と平均時間計算量は$O(n^2)$のままです。ただし、入力配列がすでにソートされている場合、最良ケース時間計算量は$O(n)$まで低くなる可能性があります。
=== "Python"
```python title="bubble_sort.py"
def bubble_sort_with_flag(nums: list[int]):
"""バブルソート(フラグによる最適化)"""
n = len(nums)
# 外側のループ:未ソート範囲は [0, i]
for i in range(n - 1, 0, -1):
flag = False # フラグを初期化
# 内側のループ:未ソート範囲 [0, i] の最大要素を範囲の右端に移動
for j in range(i):
if nums[j] > nums[j + 1]:
# nums[j] と nums[j + 1] を交換
nums[j], nums[j + 1] = nums[j + 1], nums[j]
flag = True # 要素を交換したことを記録
if not flag:
break # この回の「バブリング」で要素が交換されなかった場合、終了
```
=== "C++"
```cpp title="bubble_sort.cpp"
/* バブルソート(フラグ最適化版)*/
void bubbleSortWithFlag(vector<int> &nums) {
// 外側ループ:未ソート範囲は[0, i]
for (int i = nums.size() - 1; i > 0; i--) {
bool flag = false; // フラグを初期化
// 内側ループ:未ソート範囲[0, i]内の最大要素を範囲の右端に交換
for (int j = 0; j < i; j++) {
if (nums[j] > nums[j + 1]) {
// nums[j]とnums[j + 1]を交換
// ここではstdのswapを使用
swap(nums[j], nums[j + 1]);
flag = true; // 交換された要素を記録
}
}
if (!flag)
break; // この回の「バブリング」で要素が交換されなかった場合、終了
}
}
```
=== "Java"
```java title="bubble_sort.java"
/* バブルソート(フラグによる最適化) */
void bubbleSortWithFlag(int[] nums) {
// 外側ループ: 未ソート範囲は [0, i]
for (int i = nums.length - 1; i > 0; i--) {
boolean flag = false; // フラグを初期化
// 内側ループ: 未ソート範囲 [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;
flag = true; // 交換された要素を記録
}
}
if (!flag)
break; // この「バブリング」ラウンドで要素が交換されなかった場合、終了
}
}
```
=== "C#"
```csharp title="bubble_sort.cs"
[class]{bubble_sort}-[func]{BubbleSortWithFlag}
```
=== "Go"
```go title="bubble_sort.go"
[class]{}-[func]{bubbleSortWithFlag}
```
=== "Swift"
```swift title="bubble_sort.swift"
[class]{}-[func]{bubbleSortWithFlag}
```
=== "JS"
```javascript title="bubble_sort.js"
[class]{}-[func]{bubbleSortWithFlag}
```
=== "TS"
```typescript title="bubble_sort.ts"
[class]{}-[func]{bubbleSortWithFlag}
```
=== "Dart"
```dart title="bubble_sort.dart"
[class]{}-[func]{bubbleSortWithFlag}
```
=== "Rust"
```rust title="bubble_sort.rs"
[class]{}-[func]{bubble_sort_with_flag}
```
=== "C"
```c title="bubble_sort.c"
[class]{}-[func]{bubbleSortWithFlag}
```
=== "Kotlin"
```kotlin title="bubble_sort.kt"
[class]{}-[func]{bubbleSortWithFlag}
```
=== "Ruby"
```ruby title="bubble_sort.rb"
[class]{}-[func]{bubble_sort_with_flag}
```
=== "Zig"
```zig title="bubble_sort.zig"
[class]{}-[func]{bubbleSortWithFlag}
```
## 11.3.3 &nbsp; アルゴリズムの特性
- **$O(n^2)$の時間計算量、適応ソート。** 各「バブリング」ラウンドは長さ$n - 1$、$n - 2$、$\dots$、$2$、$1$の配列セグメントを横断し、合計は$(n - 1) n / 2$となります。`flag`最適化により、配列がすでにソートされている場合、最良ケース時間計算量は$O(n)$に達する可能性があります。
- **$O(1)$の空間計算量、インプレースソート。** ポインタ$i$と$j$によって定数量の追加空間のみが使用されます。
- **安定ソート。** 等しい要素は「バブリング」中に交換されないため、元の順序が保持され、これは安定ソートになります。

View File

@@ -0,0 +1,206 @@
---
comments: true
---
# 11.8 &nbsp; バケットソート
前述のソートアルゴリズムはすべて「比較ベースのソートアルゴリズム」で、値を比較することで要素をソートします。このようなソートアルゴリズムは $O(n \log n)$ より良い時間計算量を持つことはできません。次に、線形時間計算量を達成できるいくつかの「非比較ソートアルゴリズム」について議論します。
<u>バケットソート</u>は分割統治戦略の典型的な応用です。一連の順序付けられたバケットを設定し、各バケットがデータの範囲を含み、入力データをこれらのバケットに均等に分散させることで動作します。そして、各バケット内のデータを個別にソートします。最後に、すべてのバケットからのソート済みデータを順次マージして最終結果を生成します。
## 11.8.1 &nbsp; アルゴリズムの過程
長さ $n$ の配列で、$[0, 1)$ の範囲の浮動小数点数を考えてみます。バケットソートの過程は以下の図に示されています。
1. $k$ 個のバケットを初期化し、$n$ 個の要素をこれらの $k$ 個のバケットに分散させます。
2. 各バケットを個別にソートします(プログラミング言語の組み込みソート関数を使用)。
3. 最小から最大のバケットの順序で結果をマージします。
![バケットソートアルゴリズムの過程](bucket_sort.assets/bucket_sort_overview.png){ class="animation-figure" }
<p align="center"> 図 11-13 &nbsp; バケットソートアルゴリズムの過程 </p>
コードは以下の通りです:
=== "Python"
```python title="bucket_sort.py"
def bucket_sort(nums: list[float]):
"""バケットソート"""
# k = n/2 個のバケットを初期化、各バケットに平均2個の要素を配置することを期待
k = len(nums) // 2
buckets = [[] for _ in range(k)]
# 1. 配列要素を各バケットに分散
for num in nums:
# 入力データ範囲は [0, 1)、num * k を使用してインデックス範囲 [0, k-1] にマッピング
i = int(num * k)
# num をバケット i に追加
buckets[i].append(num)
# 2. 各バケットをソート
for bucket in buckets:
# 組み込みソート関数を使用、他のソートアルゴリズムに置き換えることも可能
bucket.sort()
# 3. バケットを走査して結果をマージ
i = 0
for bucket in buckets:
for num in bucket:
nums[i] = num
i += 1
```
=== "C++"
```cpp title="bucket_sort.cpp"
/* バケットソート */
void bucketSort(vector<float> &nums) {
// k = n/2個のバケットを初期化、各バケットに2つの要素を割り当てることを期待
int k = nums.size() / 2;
vector<vector<float>> buckets(k);
// 1. 配列要素を各バケットに分配
for (float num : nums) {
// 入力データ範囲は[0, 1)、num * kを使用してインデックス範囲[0, k-1]にマップ
int i = num * k;
// bucket_idxバケットに数値を追加
buckets[i].push_back(num);
}
// 2. 各バケットをソート
for (vector<float> &bucket : buckets) {
// 組み込みソート関数を使用、他のソートアルゴリズムに置き換えることも可能
sort(bucket.begin(), bucket.end());
}
// 3. バケットを走査して結果をマージ
int i = 0;
for (vector<float> &bucket : buckets) {
for (float num : bucket) {
nums[i++] = num;
}
}
}
```
=== "Java"
```java title="bucket_sort.java"
/* バケットソート */
void bucketSort(float[] nums) {
// k = n/2 個のバケットを初期化、各バケットに期待される要素数は 2 個
int k = nums.length / 2;
List<List<Float>> buckets = new ArrayList<>();
for (int i = 0; i < k; i++) {
buckets.add(new ArrayList<>());
}
// 1. 配列要素を各バケットに分散
for (float num : nums) {
// 入力データ範囲は [0, 1)、num * k を使ってインデックス範囲 [0, k-1] にマッピング
int i = (int) (num * k);
// num をバケット i に追加
buckets.get(i).add(num);
}
// 2. 各バケットをソート
for (List<Float> bucket : buckets) {
// 組み込みソート関数を使用、他のソートアルゴリズムに置き換えることも可能
Collections.sort(bucket);
}
// 3. バケットを走査して結果をマージ
int i = 0;
for (List<Float> bucket : buckets) {
for (float num : bucket) {
nums[i++] = num;
}
}
}
```
=== "C#"
```csharp title="bucket_sort.cs"
[class]{bucket_sort}-[func]{BucketSort}
```
=== "Go"
```go title="bucket_sort.go"
[class]{}-[func]{bucketSort}
```
=== "Swift"
```swift title="bucket_sort.swift"
[class]{}-[func]{bucketSort}
```
=== "JS"
```javascript title="bucket_sort.js"
[class]{}-[func]{bucketSort}
```
=== "TS"
```typescript title="bucket_sort.ts"
[class]{}-[func]{bucketSort}
```
=== "Dart"
```dart title="bucket_sort.dart"
[class]{}-[func]{bucketSort}
```
=== "Rust"
```rust title="bucket_sort.rs"
[class]{}-[func]{bucket_sort}
```
=== "C"
```c title="bucket_sort.c"
[class]{}-[func]{bucketSort}
```
=== "Kotlin"
```kotlin title="bucket_sort.kt"
[class]{}-[func]{bucketSort}
```
=== "Ruby"
```ruby title="bucket_sort.rb"
[class]{}-[func]{bucket_sort}
```
=== "Zig"
```zig title="bucket_sort.zig"
[class]{}-[func]{bucketSort}
```
## 11.8.2 &nbsp; アルゴリズムの特徴
バケットソートは非常に大きなデータセットの処理に適しています。例えば、入力データに100万個の要素が含まれ、システムメモリの制限によりすべてのデータを同時にロードできない場合、データを1,000個のバケットに分割し、各バケットを個別にソートしてから結果をマージできます。
- **時間計算量は $O(n + k)$**:要素がバケット間で均等に分散されていると仮定すると、各バケット内の要素数は $n/k$ です。単一のバケットのソートに $O(n/k \log(n/k))$ 時間がかかると仮定すると、すべてのバケットのソートに $O(n \log(n/k))$ 時間がかかります。**バケット数 $k$ が比較的大きいとき、時間計算量は $O(n)$ に近づきます**。結果のマージには、すべてのバケットと要素を走査する必要があり、$O(n + k)$ 時間がかかります。最悪の場合、すべてのデータが単一のバケットに分散され、そのバケットのソートには $O(n^2)$ 時間がかかります。
- **空間計算量は $O(n + k)$、非インプレースソート**$k$ 個のバケットと合計 $n$ 個の要素のための追加スペースが必要です。
- バケットソートが安定かどうかは、各バケット内で使用されるソートアルゴリズムが安定かどうかに依存します。
## 11.8.3 &nbsp; 均等分散を達成する方法
バケットソートの理論的時間計算量は $O(n)$ に達することができます。**重要なことは、すべてのバケットに要素を均等に分散させることです**。実世界のデータはしばしば均一に分散されていないからです。例えば、eBayのすべての商品を価格範囲で10個のバケットに均等に分散させたいとします。しかし、商品価格の分散は均等でない可能性があり、100ドル未満の商品が多く、500ドル以上の商品が少ないかもしれません。価格範囲を均等に10分割すると、各バケットの商品数の差が大きくなります。
均等分散を達成するために、最初におおよその境界を設定して、データを3つのバケットに大まかに分割できます。**分散が完了した後、より多くのアイテムを持つバケットをさらに3つのバケットに分割し、すべてのバケットの要素数がほぼ等しくなるまで続けます**。
以下の図に示すように、この方法は本質的に再帰木を構築し、葉ードの要素数ができるだけ均等になることを目指します。もちろん、各ラウンドでデータを3つのバケットに分割する必要はありません - 分割戦略はデータの独特な特性に適応的に調整できます。
![バケットの再帰的分割](bucket_sort.assets/scatter_in_buckets_recursively.png){ class="animation-figure" }
<p align="center"> 図 11-14 &nbsp; バケットの再帰的分割 </p>
商品価格の確率分布を事前に知っている場合、**データの確率分布に基づいて各バケットの価格境界を設定できます**。データ分布を具体的に計算する必要は必ずしもなく、代わりに確率モデルを使用してデータ特性に基づいて近似できることに注意してください。
以下の図に示すように、商品価格が正規分布に従うと仮定すると、バケット間でアイテムの分散のバランスを取るために合理的な価格区間を定義できます。
![確率分布に基づくバケット分割](bucket_sort.assets/scatter_in_buckets_distribution.png){ class="animation-figure" }
<p align="center"> 図 11-15 &nbsp; 確率分布に基づくバケット分割 </p>

View File

@@ -0,0 +1,397 @@
---
comments: true
---
# 11.9 &nbsp; 計数ソート
<u>計数ソート</u>は要素の数をカウントすることでソートを実現し、通常は整数配列に適用されます。
## 11.9.1 &nbsp; 簡単な実装
簡単な例から始めましょう。長さ $n$ の配列 `nums` が与えられ、すべての要素が「非負整数」である場合、計数ソートの全体的な過程は以下の図に示されています。
1. 配列を走査して最大数を見つけ、それを $m$ とし、長さ $m + 1$ の補助配列 `counter` を作成します。
2. **`counter` を使用して `nums` 内の各数の出現回数をカウントします**。ここで `counter[num]` は数 `num` の出現回数に対応します。カウント方法は簡単で、`nums` を走査し(現在の数を `num` とする)、各ラウンドで `counter[num]` を $1$ 増やします。
3. **`counter` のインデックスは自然に順序付けられているため、すべての数は本質的にすでにソートされています**。次に、`counter` を走査し、出現順に `nums` を昇順で埋めます。
![計数ソートの過程](counting_sort.assets/counting_sort_overview.png){ class="animation-figure" }
<p align="center"> 図 11-16 &nbsp; 計数ソートの過程 </p>
コードは以下の通りです:
=== "Python"
```python title="counting_sort.py"
def counting_sort_naive(nums: list[int]):
"""計数ソート"""
# シンプルな実装、オブジェクトのソートには使用できない
# 1. 配列内の最大要素 m を統計
m = 0
for num in nums:
m = max(m, num)
# 2. 各数字の出現回数を統計
# counter[num] は num の出現回数を表す
counter = [0] * (m + 1)
for num in nums:
counter[num] += 1
# 3. counter を走査し、各要素を元の配列 nums に埋め戻し
i = 0
for num in range(m + 1):
for _ in range(counter[num]):
nums[i] = num
i += 1
```
=== "C++"
```cpp title="counting_sort.cpp"
/* カウントソート */
// 簡単な実装、オブジェクトのソートには使用できない
void countingSortNaive(vector<int> &nums) {
// 1. 配列の最大要素mを統計
int m = 0;
for (int num : nums) {
m = max(m, num);
}
// 2. 各数字の出現回数を統計
// counter[num]はnumの出現回数を表す
vector<int> counter(m + 1, 0);
for (int num : nums) {
counter[num]++;
}
// 3. counterを走査し、各要素を元の配列numsに戻す
int i = 0;
for (int num = 0; num < m + 1; num++) {
for (int j = 0; j < counter[num]; j++, i++) {
nums[i] = num;
}
}
}
```
=== "Java"
```java title="counting_sort.java"
/* 計数ソート */
// 簡単な実装、オブジェクトのソートには使用できない
void countingSortNaive(int[] nums) {
// 1. 配列の最大要素 m を統計
int m = 0;
for (int num : nums) {
m = Math.max(m, num);
}
// 2. 各数字の出現回数を統計
// counter[num] は num の出現回数を表す
int[] counter = new int[m + 1];
for (int num : nums) {
counter[num]++;
}
// 3. counter を走査し、各要素を元の配列 nums に戻す
int i = 0;
for (int num = 0; num < m + 1; num++) {
for (int j = 0; j < counter[num]; j++, i++) {
nums[i] = num;
}
}
}
```
=== "C#"
```csharp title="counting_sort.cs"
[class]{counting_sort}-[func]{CountingSortNaive}
```
=== "Go"
```go title="counting_sort.go"
[class]{}-[func]{countingSortNaive}
```
=== "Swift"
```swift title="counting_sort.swift"
[class]{}-[func]{countingSortNaive}
```
=== "JS"
```javascript title="counting_sort.js"
[class]{}-[func]{countingSortNaive}
```
=== "TS"
```typescript title="counting_sort.ts"
[class]{}-[func]{countingSortNaive}
```
=== "Dart"
```dart title="counting_sort.dart"
[class]{}-[func]{countingSortNaive}
```
=== "Rust"
```rust title="counting_sort.rs"
[class]{}-[func]{counting_sort_naive}
```
=== "C"
```c title="counting_sort.c"
[class]{}-[func]{countingSortNaive}
```
=== "Kotlin"
```kotlin title="counting_sort.kt"
[class]{}-[func]{countingSortNaive}
```
=== "Ruby"
```ruby title="counting_sort.rb"
[class]{}-[func]{counting_sort_naive}
```
=== "Zig"
```zig title="counting_sort.zig"
[class]{}-[func]{countingSortNaive}
```
!!! note "計数ソートとバケットソートの関係"
バケットソートの観点から、計数ソートにおける計数配列 `counter` の各インデックスをバケットと考え、カウントの過程を要素を対応するバケットに分散させることと考えることができます。本質的に、計数ソートは整数データのためのバケットソートの特別なケースです。
## 11.9.2 &nbsp; 完全な実装
注意深い読者は気付くかもしれませんが、**入力データがオブジェクトの場合、上記の手順 `3.` は無効です**。入力データが商品オブジェクトで、価格(クラスメンバ変数)で商品をソートしたいとします。しかし、上記のアルゴリズムは結果としてソート済みの価格のみを提供できます。
では、元のデータのソート結果をどのように取得できるでしょうか?まず、`counter` の「前置和」を計算します。名前が示すように、インデックス `i` での前置和 `prefix[i]` は、配列の最初の `i` 個の要素の和に等しいです:
$$
\text{prefix}[i] = \sum_{j=0}^i \text{counter[j]}
$$
**前置和には明確な意味があります。`prefix[num] - 1` は結果配列 `res` における要素 `num` の最後の出現のインデックスを表します**。この情報は重要で、各要素が結果配列のどこに現れるべきかを教えてくれます。次に、元の配列 `nums` の各要素 `num` を逆順で走査し、各反復で以下の2つの手順を実行します。
1. インデックス `prefix[num] - 1` で配列 `res` に `num` を埋めます。
2. 前置和 `prefix[num]` を $1$ 減らして、`num` を配置する次のインデックスを取得します。
走査後、配列 `res` にはソートされた結果が含まれ、最後に `res` が元の配列 `nums` を置き換えます。完全な計数ソートの過程は以下の図に示されています。
=== "<1>"
![計数ソートの過程](counting_sort.assets/counting_sort_step1.png){ class="animation-figure" }
=== "<2>"
![counting_sort_step2](counting_sort.assets/counting_sort_step2.png){ class="animation-figure" }
=== "<3>"
![counting_sort_step3](counting_sort.assets/counting_sort_step3.png){ class="animation-figure" }
=== "<4>"
![counting_sort_step4](counting_sort.assets/counting_sort_step4.png){ class="animation-figure" }
=== "<5>"
![counting_sort_step5](counting_sort.assets/counting_sort_step5.png){ class="animation-figure" }
=== "<6>"
![counting_sort_step6](counting_sort.assets/counting_sort_step6.png){ class="animation-figure" }
=== "<7>"
![counting_sort_step7](counting_sort.assets/counting_sort_step7.png){ class="animation-figure" }
=== "<8>"
![counting_sort_step8](counting_sort.assets/counting_sort_step8.png){ class="animation-figure" }
<p align="center"> 図 11-17 &nbsp; 計数ソートの過程 </p>
計数ソートの実装コードは以下の通りです:
=== "Python"
```python title="counting_sort.py"
def counting_sort(nums: list[int]):
"""計数ソート"""
# 完全な実装、オブジェクトのソートが可能で、安定ソート
# 1. 配列内の最大要素 m を統計
m = max(nums)
# 2. 各数字の出現回数を統計
# counter[num] は num の出現回数を表す
counter = [0] * (m + 1)
for num in nums:
counter[num] += 1
# 3. counter の前置和を計算し、「出現回数」を「末尾インデックス」に変換
# counter[num]-1 は res において num が最後に出現するインデックス
for i in range(m):
counter[i + 1] += counter[i]
# 4. nums を逆順に走査し、各要素を結果配列 res に配置
# 結果を記録するための配列 res を初期化
n = len(nums)
res = [0] * n
for i in range(n - 1, -1, -1):
num = nums[i]
res[counter[num] - 1] = num # num を対応するインデックスに配置
counter[num] -= 1 # 前置和を1減らし、num を配置する次のインデックスを取得
# 結果配列 res を使用して元の配列 nums を上書き
for i in range(n):
nums[i] = res[i]
```
=== "C++"
```cpp title="counting_sort.cpp"
/* カウントソート */
// 完全な実装、オブジェクトのソートが可能で安定ソート
void countingSort(vector<int> &nums) {
// 1. 配列の最大要素mを統計
int m = 0;
for (int num : nums) {
m = max(m, num);
}
// 2. 各数字の出現回数を統計
// counter[num]はnumの出現回数を表す
vector<int> counter(m + 1, 0);
for (int num : nums) {
counter[num]++;
}
// 3. counterの前缀和を計算し、「出現回数」を「末尾インデックス」に変換
// counter[num]-1はnumがresで現れる最後のインデックス
for (int i = 0; i < m; i++) {
counter[i + 1] += counter[i];
}
// 4. numsを逆順で走査し、各要素を結果配列resに配置
// 結果を記録する配列resを初期化
int n = nums.size();
vector<int> res(n);
for (int i = n - 1; i >= 0; i--) {
int num = nums[i];
res[counter[num] - 1] = num; // numを対応するインデックスに配置
counter[num]--; // 前缀和を1減らし、numを配置する次のインデックスを取得
}
// 結果配列resで元の配列numsを上書き
nums = res;
}
```
=== "Java"
```java title="counting_sort.java"
/* 計数ソート */
// 完全な実装、オブジェクトをソートでき、安定ソート
void countingSort(int[] nums) {
// 1. 配列の最大要素 m を統計
int m = 0;
for (int num : nums) {
m = Math.max(m, num);
}
// 2. 各数字の出現回数を統計
// counter[num] は num の出現回数を表す
int[] counter = new int[m + 1];
for (int num : nums) {
counter[num]++;
}
// 3. counter の累積和を計算し、「出現回数」を「尻尾インデックス」に変換
// counter[num]-1 は res 内で num が出現する最後のインデックス
for (int i = 0; i < m; i++) {
counter[i + 1] += counter[i];
}
// 4. nums を逆順に走査し、各要素を結果配列 res に配置
// 結果を記録する配列 res を初期化
int n = nums.length;
int[] res = new int[n];
for (int i = n - 1; i >= 0; i--) {
int num = nums[i];
res[counter[num] - 1] = num; // num を対応するインデックスに配置
counter[num]--; // 累積和を 1 減算し、num を配置する次のインデックスを取得
}
// 結果配列 res を使って元の配列 nums を上書き
for (int i = 0; i < n; i++) {
nums[i] = res[i];
}
}
```
=== "C#"
```csharp title="counting_sort.cs"
[class]{counting_sort}-[func]{CountingSort}
```
=== "Go"
```go title="counting_sort.go"
[class]{}-[func]{countingSort}
```
=== "Swift"
```swift title="counting_sort.swift"
[class]{}-[func]{countingSort}
```
=== "JS"
```javascript title="counting_sort.js"
[class]{}-[func]{countingSort}
```
=== "TS"
```typescript title="counting_sort.ts"
[class]{}-[func]{countingSort}
```
=== "Dart"
```dart title="counting_sort.dart"
[class]{}-[func]{countingSort}
```
=== "Rust"
```rust title="counting_sort.rs"
[class]{}-[func]{counting_sort}
```
=== "C"
```c title="counting_sort.c"
[class]{}-[func]{countingSort}
```
=== "Kotlin"
```kotlin title="counting_sort.kt"
[class]{}-[func]{countingSort}
```
=== "Ruby"
```ruby title="counting_sort.rb"
[class]{}-[func]{counting_sort}
```
=== "Zig"
```zig title="counting_sort.zig"
[class]{}-[func]{countingSort}
```
## 11.9.3 &nbsp; アルゴリズムの特徴
- **時間計算量は $O(n + m)$、非適応ソート**`nums` と `counter` の走査が含まれ、どちらも線形時間を使用します。一般的に、$n \gg m$ であり、時間計算量は $O(n)$ に近づきます。
- **空間計算量は $O(n + m)$、非インプレースソート**:長さ $n$ の配列 `res` と長さ $m$ の配列 `counter` をそれぞれ使用します。
- **安定ソート**:要素が「右から左」の順序で `res` に埋められるため、`nums` の走査を逆順にすることで、等しい要素間の相対位置の変化を防ぎ、安定したソートを実現できます。実際、`nums` を順番に走査しても正しいソート結果を生成できますが、結果は不安定です。
## 11.9.4 &nbsp; 制限事項
今までに、計数ソートは非常に巧妙だと感じるかもしれません。単に量をカウントするだけで効率的なソートを実現できるからです。しかし、計数ソートを使用するための前提条件は比較的厳しいです。
**計数ソートは非負整数にのみ適用できます**。他のタイプのデータに適用したい場合、これらのデータが要素の元の順序を変更することなく非負整数に変換できることを保証する必要があります。例えば、負の整数を含む配列の場合、最初にすべての数に定数を加えて、すべてを正の数に変換し、ソート完了後に元に戻すことができます。
**計数ソートは値の範囲が小さい大きなデータセットに適しています**。例えば、上記の例では、$m$ は大きすぎるべきではありません。そうでなければ、あまりにも多くのスペースを占有してしまいます。そして $n \ll m$ の場合、計数ソートは $O(m)$ 時間を使用し、$O(n \log n)$ ソートアルゴリズムより遅い可能性があります。

View File

@@ -0,0 +1,283 @@
---
comments: true
---
# 11.7 &nbsp; ヒープソート
!!! tip
この節を読む前に、「ヒープ」の章を必ず完了させてください。
<u>ヒープソート</u>は、ヒープデータ構造に基づく効率的なソートアルゴリズムです。すでに学習した「ヒープの構築」と「要素の抽出」操作を使用してヒープソートを実装できます。
1. 配列を入力し、最小ヒープを構築します。ここで、最小要素がヒープの頂上に位置します。
2. 継続的に抽出操作を実行し、抽出された要素を順次記録して、最小から最大までのソート済みリストを取得します。
上記の方法は実現可能ですが、ポップされた要素を格納するための追加の配列が必要で、やや空間を消費します。実際には、通常、より優雅な実装を使用します。
## 11.7.1 &nbsp; アルゴリズムの流れ
配列の長さを $n$ とすると、ヒープソートの過程は以下の通りです。
1. 配列を入力し、最大ヒープを構築します。この手順の後、最大要素がヒープの頂上に位置します。
2. ヒープの頂上要素(最初の要素)とヒープの底部要素(最後の要素)を交換します。この交換の後、ヒープの長さを $1$ 減らし、ソート済み要素の数を $1$ 増やします。
3. ヒープの頂上から開始して、上から下へのsift-down操作を実行します。sift-downの後、ヒープの性質が復元されます。
4. 手順 `2.``3.` を繰り返します。$n - 1$ ラウンドループして、配列のソートを完了します。
!!! tip
実際、要素抽出操作も手順 `2.``3.` を含み、抽出された要素をヒープから削除する追加の手順があります。
=== "<1>"
![ヒープソートの過程](heap_sort.assets/heap_sort_step1.png){ class="animation-figure" }
=== "<2>"
![heap_sort_step2](heap_sort.assets/heap_sort_step2.png){ class="animation-figure" }
=== "<3>"
![heap_sort_step3](heap_sort.assets/heap_sort_step3.png){ class="animation-figure" }
=== "<4>"
![heap_sort_step4](heap_sort.assets/heap_sort_step4.png){ class="animation-figure" }
=== "<5>"
![heap_sort_step5](heap_sort.assets/heap_sort_step5.png){ class="animation-figure" }
=== "<6>"
![heap_sort_step6](heap_sort.assets/heap_sort_step6.png){ class="animation-figure" }
=== "<7>"
![heap_sort_step7](heap_sort.assets/heap_sort_step7.png){ class="animation-figure" }
=== "<8>"
![heap_sort_step8](heap_sort.assets/heap_sort_step8.png){ class="animation-figure" }
=== "<9>"
![heap_sort_step9](heap_sort.assets/heap_sort_step9.png){ class="animation-figure" }
=== "<10>"
![heap_sort_step10](heap_sort.assets/heap_sort_step10.png){ class="animation-figure" }
=== "<11>"
![heap_sort_step11](heap_sort.assets/heap_sort_step11.png){ class="animation-figure" }
=== "<12>"
![heap_sort_step12](heap_sort.assets/heap_sort_step12.png){ class="animation-figure" }
<p align="center"> 図 11-12 &nbsp; ヒープソートの過程 </p>
コードの実装では、「ヒープ」の章からのsift-down関数 `sift_down()` を使用しました。最大要素が抽出されるにつれてヒープの長さが減少するため、`sift_down()` 関数に長さパラメータ $n$ を追加して、ヒープの現在の有効長を指定する必要があることに注意することが重要です。コードは以下の通りです:
=== "Python"
```python title="heap_sort.py"
def sift_down(nums: list[int], n: int, i: int):
"""ヒープの長さが n、ード i から上から下へヒープ化を開始"""
while True:
# i、l、r の中で最大のードを判定し、ma とする
l = 2 * i + 1
r = 2 * i + 2
ma = i
if l < n and nums[l] > nums[ma]:
ma = l
if r < n and nums[r] > nums[ma]:
ma = r
# ノード i が最大または l、r のインデックスが範囲外の場合、さらなるヒープ化は不要、ループを抜ける
if ma == i:
break
# 2つのードを交換
nums[i], nums[ma] = nums[ma], nums[i]
# 下向きにヒープ化をループ
i = ma
def heap_sort(nums: list[int]):
"""ヒープソート"""
# ヒープ構築操作:葉ノード以外のすべてのノードをヒープ化
for i in range(len(nums) // 2 - 1, -1, -1):
sift_down(nums, len(nums), i)
# ヒープから最大要素を抽出し、n-1 回繰り返す
for i in range(len(nums) - 1, 0, -1):
# ルートノードと最も右の葉ノードを交換(最初の要素と最後の要素を交換)
nums[0], nums[i] = nums[i], nums[0]
# ルートノードから上から下へヒープ化を開始
sift_down(nums, i, 0)
```
=== "C++"
```cpp title="heap_sort.cpp"
/* ヒープの長さはn、ードiから上から下へヒープ化を開始 */
void siftDown(vector<int> &nums, int n, int i) {
while (true) {
// i、l、r の中で最大のードを決定し、maとして記録
int l = 2 * i + 1;
int r = 2 * i + 2;
int ma = i;
if (l < n && nums[l] > nums[ma])
ma = l;
if (r < n && nums[r] > nums[ma])
ma = r;
// ードiが最大か、インデックスl、rが境界外の場合、それ以上のヒープ化は不要で終了
if (ma == i) {
break;
}
// 二つのノードを交換
swap(nums[i], nums[ma]);
// 下向きにヒープ化をループ
i = ma;
}
}
/* ヒープソート */
void heapSort(vector<int> &nums) {
// ヒープ構築操作:葉以外のすべてのノードをヒープ化
for (int i = nums.size() / 2 - 1; i >= 0; --i) {
siftDown(nums, nums.size(), i);
}
// ヒープから最大要素を抽出し、n-1回繰り返す
for (int i = nums.size() - 1; i > 0; --i) {
// ルートノードを最右葉ノードと交換(最初の要素を最後の要素と交換)
swap(nums[0], nums[i]);
// ルートノードから上から下へヒープ化を開始
siftDown(nums, i, 0);
}
}
```
=== "Java"
```java title="heap_sort.java"
/* ヒープの長さは n、ード i から上から下へヒープ化開始 */
void siftDown(int[] nums, int n, int i) {
while (true) {
// i, l, r の中で最大のードを判定し、ma とする
int l = 2 * i + 1;
int r = 2 * i + 2;
int ma = i;
if (l < n && nums[l] > nums[ma])
ma = l;
if (r < n && nums[r] > nums[ma])
ma = r;
// ノード i が最大、またはインデックス l, r が範囲外の場合、さらなるヒープ化は不要、ブレーク
if (ma == i)
break;
// 2つのードを交換
int temp = nums[i];
nums[i] = nums[ma];
nums[ma] = temp;
// 下向きにヒープ化をループ
i = ma;
}
}
/* ヒープソート */
void heapSort(int[] nums) {
// ヒープ構築操作: 葉ノード以外のすべてのノードをヒープ化
for (int i = nums.length / 2 - 1; i >= 0; i--) {
siftDown(nums, nums.length, i);
}
// ヒープから最大要素を抽出し、n-1 回繰り返し
for (int i = nums.length - 1; i > 0; i--) {
// ルートノードと最も右の葉ノードを交換(最初の要素と最後の要素を交換)
int tmp = nums[0];
nums[0] = nums[i];
nums[i] = tmp;
// ルートノードから上から下へヒープ化開始
siftDown(nums, i, 0);
}
}
```
=== "C#"
```csharp title="heap_sort.cs"
[class]{heap_sort}-[func]{SiftDown}
[class]{heap_sort}-[func]{HeapSort}
```
=== "Go"
```go title="heap_sort.go"
[class]{}-[func]{siftDown}
[class]{}-[func]{heapSort}
```
=== "Swift"
```swift title="heap_sort.swift"
[class]{}-[func]{siftDown}
[class]{}-[func]{heapSort}
```
=== "JS"
```javascript title="heap_sort.js"
[class]{}-[func]{siftDown}
[class]{}-[func]{heapSort}
```
=== "TS"
```typescript title="heap_sort.ts"
[class]{}-[func]{siftDown}
[class]{}-[func]{heapSort}
```
=== "Dart"
```dart title="heap_sort.dart"
[class]{}-[func]{siftDown}
[class]{}-[func]{heapSort}
```
=== "Rust"
```rust title="heap_sort.rs"
[class]{}-[func]{sift_down}
[class]{}-[func]{heap_sort}
```
=== "C"
```c title="heap_sort.c"
[class]{}-[func]{siftDown}
[class]{}-[func]{heapSort}
```
=== "Kotlin"
```kotlin title="heap_sort.kt"
[class]{}-[func]{siftDown}
[class]{}-[func]{heapSort}
```
=== "Ruby"
```ruby title="heap_sort.rb"
[class]{}-[func]{sift_down}
[class]{}-[func]{heap_sort}
```
=== "Zig"
```zig title="heap_sort.zig"
[class]{}-[func]{siftDown}
[class]{}-[func]{heapSort}
```
## 11.7.2 &nbsp; アルゴリズムの特徴
- **時間計算量は $O(n \log n)$、非適応ソート**:ヒープの構築は $O(n)$ 時間を使用します。ヒープから最大要素を抽出するには $O(\log n)$ 時間がかかり、$n - 1$ ラウンドループします。
- **空間計算量は $O(1)$、インプレースソート**:いくつかのポインタ変数が $O(1)$ 空間を使用します。要素の交換とヒープ化操作は元の配列で実行されます。
- **非安定ソート**:ヒープの頂上と底部要素の交換中に、等しい要素の相対位置が変わる可能性があります。

View File

@@ -0,0 +1,28 @@
---
comments: true
icon: material/sort-ascending
---
# 第 11 章 &nbsp; ソート
![Sorting](../assets/covers/chapter_sorting.jpg){ class="cover-image" }
!!! abstract
ソートは混沌を秩序に変える魔法の鍵のようなもので、データをより効率的に理解し処理することを可能にします。
単純な昇順であろうと複雑なカテゴリ配列であろうと、ソートはデータの調和美を明らかにします。
## 章の内容
- [11.1 &nbsp; ソートアルゴリズム](sorting_algorithm.md)
- [11.2 &nbsp; 選択ソート](selection_sort.md)
- [11.3 &nbsp; バブルソート](bubble_sort.md)
- [11.4 &nbsp; 挿入ソート](insertion_sort.md)
- [11.5 &nbsp; クイックソート](quick_sort.md)
- [11.6 &nbsp; マージソート](merge_sort.md)
- [11.7 &nbsp; ヒープソート](heap_sort.md)
- [11.8 &nbsp; バケットソート](bucket_sort.md)
- [11.9 &nbsp; 計数ソート](counting_sort.md)
- [11.10 &nbsp; 基数ソート](radix_sort.md)
- [11.11 &nbsp; まとめ](summary.md)

View File

@@ -0,0 +1,168 @@
---
comments: true
---
# 11.4 &nbsp; 挿入ソート
<u>挿入ソート</u>は、トランプのデッキを手動でソートするプロセスによく似た動作をするシンプルなソートアルゴリズムです。
具体的には、未ソート区間からベース要素を選択し、その左側のソート済み区間の要素と比較して、要素を正しい位置に挿入します。
下図は、要素が配列に挿入される方法を示しています。ベース要素を`base`とすると、ターゲットインデックスから`base`までのすべての要素を右に1つずつシフトし、その後`base`をターゲットインデックスに割り当てる必要があります。
![Single insertion operation](insertion_sort.assets/insertion_operation.png){ class="animation-figure" }
<p align="center"> 図 11-6 &nbsp; Single insertion operation </p>
## 11.4.1 &nbsp; アルゴリズムプロセス
挿入ソートの全体的なプロセスは下図に示されます。
1. 配列の最初の要素をソート済みとみなします。
2. 2番目の要素を`base`として選択し、正しい位置に挿入して、**最初の2つの要素をソート済みにします**。
3. 3番目の要素を`base`として選択し、正しい位置に挿入して、**最初の3つの要素をソート済みにします**。
4. この方法で続行し、最後の反復では、最後の要素を`base`として取り、正しい位置に挿入した後、**すべての要素がソートされます**。
![Insertion sort process](insertion_sort.assets/insertion_sort_overview.png){ class="animation-figure" }
<p align="center"> 図 11-7 &nbsp; Insertion sort process </p>
コード例は以下の通りです:
=== "Python"
```python title="insertion_sort.py"
def insertion_sort(nums: list[int]):
"""挿入ソート"""
# 外側のループ:ソート済み範囲は [0, i-1]
for i in range(1, len(nums)):
base = nums[i]
j = i - 1
# 内側のループbase をソート済み範囲 [0, i-1] の正しい位置に挿入
while j >= 0 and nums[j] > base:
nums[j + 1] = nums[j] # nums[j] を右に1つ移動
j -= 1
nums[j + 1] = base # base を正しい位置に代入
```
=== "C++"
```cpp title="insertion_sort.cpp"
/* 挿入ソート */
void insertionSort(vector<int> &nums) {
// 外側ループ:ソート済み範囲は[0, i-1]
for (int i = 1; i < nums.size(); i++) {
int base = nums[i], j = i - 1;
// 内側ループbaseをソート済み範囲[0, i-1]内の正しい位置に挿入
while (j >= 0 && nums[j] > base) {
nums[j + 1] = nums[j]; // nums[j]を一つ右に移動
j--;
}
nums[j + 1] = base; // baseを正しい位置に代入
}
}
```
=== "Java"
```java title="insertion_sort.java"
/* 挿入ソート */
void insertionSort(int[] nums) {
// 外側ループ: ソート済み範囲は [0, i-1]
for (int i = 1; i < nums.length; i++) {
int base = nums[i], j = i - 1;
// 内側ループ: base をソート済み範囲 [0, i-1] の正しい位置に挿入
while (j >= 0 && nums[j] > base) {
nums[j + 1] = nums[j]; // nums[j] を右に1つ移動
j--;
}
nums[j + 1] = base; // base を正しい位置に代入
}
}
```
=== "C#"
```csharp title="insertion_sort.cs"
[class]{insertion_sort}-[func]{InsertionSort}
```
=== "Go"
```go title="insertion_sort.go"
[class]{}-[func]{insertionSort}
```
=== "Swift"
```swift title="insertion_sort.swift"
[class]{}-[func]{insertionSort}
```
=== "JS"
```javascript title="insertion_sort.js"
[class]{}-[func]{insertionSort}
```
=== "TS"
```typescript title="insertion_sort.ts"
[class]{}-[func]{insertionSort}
```
=== "Dart"
```dart title="insertion_sort.dart"
[class]{}-[func]{insertionSort}
```
=== "Rust"
```rust title="insertion_sort.rs"
[class]{}-[func]{insertion_sort}
```
=== "C"
```c title="insertion_sort.c"
[class]{}-[func]{insertionSort}
```
=== "Kotlin"
```kotlin title="insertion_sort.kt"
[class]{}-[func]{insertionSort}
```
=== "Ruby"
```ruby title="insertion_sort.rb"
[class]{}-[func]{insertion_sort}
```
=== "Zig"
```zig title="insertion_sort.zig"
[class]{}-[func]{insertionSort}
```
## 11.4.2 &nbsp; アルゴリズムの特性
- **時間計算量は$O(n^2)$、適応ソート**:最悪の場合、各挿入操作には$n - 1$、$n-2$、...、$2$、$1$のループが必要で、合計は$(n - 1) n / 2$となり、時間計算量は$O(n^2)$です。順序付きデータの場合、挿入操作は早期に終了します。入力配列が完全に順序付けられている場合、挿入ソートは最良時間計算量$O(n)$を実現します。
- **空間計算量は$O(1)$、インプレースソート**:ポインタ$i$と$j$は定数量の追加空間を使用します。
- **安定ソート**:挿入操作中、等しい要素の右側に要素を挿入し、順序を変更しません。
## 11.4.3 &nbsp; 挿入ソートの利点
挿入ソートの時間計算量は$O(n^2)$で、次に学習するクイックソートの時間計算量は$O(n \log n)$です。挿入ソートはより高い時間計算量を持ちますが、**小さな入力サイズでは通常より高速です**。
この結論は線形探索と二分探索の結論と似ています。時間計算量が$O(n \log n)$で分割統治戦略に基づくクイックソートなどのアルゴリズムは、多くの場合より多くの単位操作を含みます。小さな入力サイズでは、$n^2$と$n \log n$の数値は近く、計算量が支配的でなく、ラウンドあたりの単位操作数が決定的な役割を果たします。
実際、多くのプログラミング言語Javaなどは、組み込みソート関数内で挿入ソートを使用しています。一般的なアプローチは長い配列に対しては、クイックソートなどの分割統治戦略に基づくソートアルゴリズムを使用し、短い配列に対しては挿入ソートを直接使用します。
バブルソート、選択ソート、挿入ソートはすべて時間計算量$O(n^2)$を持ちますが、実際には、**挿入ソートはバブルソートや選択ソートよりも一般的に使用されます**。主な理由は以下の通りです。
- バブルソートは要素交換に基づき、一時変数の使用が必要で、3つの単位操作を含みます挿入ソートは要素代入に基づき、1つの単位操作のみが必要です。したがって、**バブルソートの計算オーバーヘッドは一般的に挿入ソートよりも高くなります**。
- 選択ソートの時間計算量は常に$O(n^2)$です。**部分的に順序付けられたデータのセットが与えられた場合、挿入ソートは通常選択ソートよりも効率的です**。
- 選択ソートは不安定で、マルチレベルソートに適用できません。

View File

@@ -0,0 +1,298 @@
---
comments: true
---
# 11.6 &nbsp; マージソート
<u>マージソート</u>は分割統治戦略に基づくソートアルゴリズムで、下図に示す「分割」と「マージ」フェーズを含みます。
1. **分割フェーズ**:中点から配列を再帰的に分割し、長い配列のソート問題をより短い配列に変換します。
2. **マージフェーズ**サブ配列の長さが1になったときに分割を停止し、その後マージを開始します。2つの短いソート済み配列を連続的により長いソート済み配列にマージし、プロセスが完了するまで続行します。
![The divide and merge phases of merge sort](merge_sort.assets/merge_sort_overview.png){ class="animation-figure" }
<p align="center"> 図 11-10 &nbsp; The divide and merge phases of merge sort </p>
## 11.6.1 &nbsp; アルゴリズムワークフロー
下図に示すように、「分割フェーズ」は中点から配列を上から下に2つのサブ配列に再帰的に分割します。
1. 中点`mid`を計算し、左サブ配列(区間`[left, mid]`)と右サブ配列(区間`[mid + 1, right]`)を再帰的に分割します。
2. サブ配列の長さが1になるまでステップ`1.`を再帰的に続行し、その後停止します。
「マージフェーズ」は左と右のサブ配列を下から上にソート済み配列に結合します。重要なのは、マージが長さ1のサブ配列から開始され、マージフェーズ中に各サブ配列がソートされることです。
=== "<1>"
![Merge sort process](merge_sort.assets/merge_sort_step1.png){ class="animation-figure" }
=== "<2>"
![merge_sort_step2](merge_sort.assets/merge_sort_step2.png){ class="animation-figure" }
=== "<3>"
![merge_sort_step3](merge_sort.assets/merge_sort_step3.png){ class="animation-figure" }
=== "<4>"
![merge_sort_step4](merge_sort.assets/merge_sort_step4.png){ class="animation-figure" }
=== "<5>"
![merge_sort_step5](merge_sort.assets/merge_sort_step5.png){ class="animation-figure" }
=== "<6>"
![merge_sort_step6](merge_sort.assets/merge_sort_step6.png){ class="animation-figure" }
=== "<7>"
![merge_sort_step7](merge_sort.assets/merge_sort_step7.png){ class="animation-figure" }
=== "<8>"
![merge_sort_step8](merge_sort.assets/merge_sort_step8.png){ class="animation-figure" }
=== "<9>"
![merge_sort_step9](merge_sort.assets/merge_sort_step9.png){ class="animation-figure" }
=== "<10>"
![merge_sort_step10](merge_sort.assets/merge_sort_step10.png){ class="animation-figure" }
<p align="center"> 図 11-11 &nbsp; Merge sort process </p>
マージソートの再帰順序は二分木の後順横断と一致することが観察できます。
- **後順横断**:まず左のサブツリーを再帰的に横断し、次に右のサブツリーを横断し、最後にルートノードを処理します。
- **マージソート**:まず左のサブ配列を再帰的に処理し、次に右のサブ配列を処理し、最後にマージを実行します。
マージソートの実装は以下のコードに示されます。`nums`でマージされる区間は`[left, right]`で、`tmp`の対応する区間は`[0, right - left]`であることに注意してください。
=== "Python"
```python title="merge_sort.py"
def merge(nums: list[int], left: int, mid: int, right: int):
"""左サブ配列と右サブ配列をマージ"""
# 左サブ配列区間は [left, mid]、右サブ配列区間は [mid+1, right]
# 一時配列 tmp を作成してマージ結果を格納
tmp = [0] * (right - left + 1)
# 左右サブ配列の開始インデックスを初期化
i, j, k = left, mid + 1, 0
# 両方のサブ配列に要素が残っている間、より小さい要素を一時配列にコピー
while i <= mid and j <= right:
if nums[i] <= nums[j]:
tmp[k] = nums[i]
i += 1
else:
tmp[k] = nums[j]
j += 1
k += 1
# 残った左右サブ配列の要素を一時配列にコピー
while i <= mid:
tmp[k] = nums[i]
i += 1
k += 1
while j <= right:
tmp[k] = nums[j]
j += 1
k += 1
# 一時配列 tmp の要素を元の配列 nums の対応する区間にコピーバック
for k in range(0, len(tmp)):
nums[left + k] = tmp[k]
def merge_sort(nums: list[int], left: int, right: int):
"""マージソート"""
# 終了条件
if left >= right:
return # サブ配列の長さが1のときに再帰を終了
# 分割段階
mid = left + (right - left) // 2 # 中点を計算
merge_sort(nums, left, mid) # 左サブ配列を再帰的に処理
merge_sort(nums, mid + 1, right) # 右サブ配列を再帰的に処理
# マージ段階
merge(nums, left, mid, right)
```
=== "C++"
```cpp title="merge_sort.cpp"
/* 左サブ配列と右サブ配列をマージ */
void merge(vector<int> &nums, int left, int mid, int right) {
// 左サブ配列の区間は[left, mid]、右サブ配列の区間は[mid+1, right]
// マージ結果を保存する一時配列tmpを作成
vector<int> tmp(right - left + 1);
// 左右サブ配列の開始インデックスを初期化
int i = left, j = mid + 1, k = 0;
// 両サブ配列に要素がある間、小さい方の要素を一時配列にコピー
while (i <= mid && j <= right) {
if (nums[i] <= nums[j])
tmp[k++] = nums[i++];
else
tmp[k++] = nums[j++];
}
// 左右サブ配列の残りの要素を一時配列にコピー
while (i <= mid) {
tmp[k++] = nums[i++];
}
while (j <= right) {
tmp[k++] = nums[j++];
}
// 一時配列tmpの要素を元の配列numsの対応する区間にコピー
for (k = 0; k < tmp.size(); k++) {
nums[left + k] = tmp[k];
}
}
/* マージソート */
void mergeSort(vector<int> &nums, int left, int right) {
// 終了条件
if (left >= right)
return; // サブ配列の長さが1の時、再帰を終了
// 分割段階
int mid = left + (right - left) / 2; // 中点を計算
mergeSort(nums, left, mid); // 左サブ配列を再帰的に処理
mergeSort(nums, mid + 1, right); // 右サブ配列を再帰的に処理
// マージ段階
merge(nums, left, mid, right);
}
```
=== "Java"
```java title="merge_sort.java"
/* 左部分配列と右部分配列をマージ */
void merge(int[] nums, int left, int mid, int right) {
// 左部分配列区間は [left, mid]、右部分配列区間は [mid+1, right]
// 一時配列 tmp を作成してマージ結果を格納
int[] tmp = new int[right - left + 1];
// 左右部分配列の開始インデックスを初期化
int i = left, j = mid + 1, k = 0;
// 両部分配列にまだ要素がある間、比較してより小さい要素を一時配列にコピー
while (i <= mid && j <= right) {
if (nums[i] <= nums[j])
tmp[k++] = nums[i++];
else
tmp[k++] = nums[j++];
}
// 左右部分配列の残りの要素を一時配列にコピー
while (i <= mid) {
tmp[k++] = nums[i++];
}
while (j <= right) {
tmp[k++] = nums[j++];
}
// 一時配列 tmp の要素を元の配列 nums の対応する区間にコピーバック
for (k = 0; k < tmp.length; k++) {
nums[left + k] = tmp[k];
}
}
/* マージソート */
void mergeSort(int[] nums, int left, int right) {
// 終了条件
if (left >= right)
return; // 部分配列の長さが 1 のとき再帰を終了
// 分割段階
int mid = left + (right - left) / 2; // 中点を計算
mergeSort(nums, left, mid); // 左部分配列を再帰的に処理
mergeSort(nums, mid + 1, right); // 右部分配列を再帰的に処理
// マージ段階
merge(nums, left, mid, right);
}
```
=== "C#"
```csharp title="merge_sort.cs"
[class]{merge_sort}-[func]{Merge}
[class]{merge_sort}-[func]{MergeSort}
```
=== "Go"
```go title="merge_sort.go"
[class]{}-[func]{merge}
[class]{}-[func]{mergeSort}
```
=== "Swift"
```swift title="merge_sort.swift"
[class]{}-[func]{merge}
[class]{}-[func]{mergeSort}
```
=== "JS"
```javascript title="merge_sort.js"
[class]{}-[func]{merge}
[class]{}-[func]{mergeSort}
```
=== "TS"
```typescript title="merge_sort.ts"
[class]{}-[func]{merge}
[class]{}-[func]{mergeSort}
```
=== "Dart"
```dart title="merge_sort.dart"
[class]{}-[func]{merge}
[class]{}-[func]{mergeSort}
```
=== "Rust"
```rust title="merge_sort.rs"
[class]{}-[func]{merge}
[class]{}-[func]{merge_sort}
```
=== "C"
```c title="merge_sort.c"
[class]{}-[func]{merge}
[class]{}-[func]{mergeSort}
```
=== "Kotlin"
```kotlin title="merge_sort.kt"
[class]{}-[func]{merge}
[class]{}-[func]{mergeSort}
```
=== "Ruby"
```ruby title="merge_sort.rb"
[class]{}-[func]{merge}
[class]{}-[func]{merge_sort}
```
=== "Zig"
```zig title="merge_sort.zig"
[class]{}-[func]{merge}
[class]{}-[func]{mergeSort}
```
## 11.6.2 &nbsp; アルゴリズムの特性
- **$O(n \log n)$の時間計算量、非適応ソート**:分割により高さ$\log n$の再帰ツリーが作成され、各層で合計$n$回の操作をマージし、全体的な時間計算量は$O(n \log n)$となります。
- **$O(n)$の空間計算量、非インプレースソート**:再帰深度は$\log n$で、$O(\log n)$のスタックフレーム空間を使用します。マージ操作には補助配列が必要で、追加の$O(n)$空間を使用します。
- **安定ソート**:マージプロセス中、等しい要素の順序は変更されません。
## 11.6.3 &nbsp; 連結リストのソート
連結リストの場合、マージソートは他のソートアルゴリズムよりも大きな利点があります。**連結リストソートタスクの空間計算量を$O(1)$に最適化できます**。
- **分割フェーズ**:「再帰」の代わりに「反復」を使用して連結リスト分割作業を実行できるため、再帰で使用されるスタックフレーム空間を節約できます。
- **マージフェーズ**連結リストでは、ードの挿入と削除操作は参照ポインタを変更することで実現できるため、マージフェーズ2つの短い順序付きリストを1つの長い順序付きリストに結合中に追加のリストを作成する必要がありません。
実装の詳細は比較的複雑で、興味のある読者は関連資料を参照して学習してください。

View File

@@ -0,0 +1,662 @@
---
comments: true
---
# 11.5 &nbsp; クイックソート
<u>クイックソート</u>は分割統治戦略に基づくソートアルゴリズムで、その効率性と幅広い応用で知られています。
クイックソートのコア操作は「ピボット分割」で、配列から要素を「ピボット」として選択し、ピボットより小さいすべての要素をその左側に移動し、ピボットより大きいすべての要素をその右側に移動することを目的としています。具体的に、ピボット分割のプロセスは下図に示されます。
1. 配列の最も左の要素をピボットとして選択し、2つのポインタ`i``j`を初期化して配列の両端をそれぞれ指すようにします。
2. 各ラウンドで`i``j`を使用してピボットより大きい小さい最初の要素を探索し、次にこれら2つの要素を交換するループを設定します。
3. `i``j`が出会うまでステップ`2.`を繰り返し、最後にピボットを2つのサブ配列の境界に交換します。
=== "<1>"
![Pivot division process](quick_sort.assets/pivot_division_step1.png){ class="animation-figure" }
=== "<2>"
![pivot_division_step2](quick_sort.assets/pivot_division_step2.png){ class="animation-figure" }
=== "<3>"
![pivot_division_step3](quick_sort.assets/pivot_division_step3.png){ class="animation-figure" }
=== "<4>"
![pivot_division_step4](quick_sort.assets/pivot_division_step4.png){ class="animation-figure" }
=== "<5>"
![pivot_division_step5](quick_sort.assets/pivot_division_step5.png){ class="animation-figure" }
=== "<6>"
![pivot_division_step6](quick_sort.assets/pivot_division_step6.png){ class="animation-figure" }
=== "<7>"
![pivot_division_step7](quick_sort.assets/pivot_division_step7.png){ class="animation-figure" }
=== "<8>"
![pivot_division_step8](quick_sort.assets/pivot_division_step8.png){ class="animation-figure" }
=== "<9>"
![pivot_division_step9](quick_sort.assets/pivot_division_step9.png){ class="animation-figure" }
<p align="center"> 図 11-8 &nbsp; Pivot division process </p>
ピボット分割後、元の配列は3つの部分に分割されます左サブ配列、ピボット、右サブ配列で、「左サブ配列の任意の要素 $\leq$ ピボット $\leq$ 右サブ配列の任意の要素」を満たします。したがって、これら2つのサブ配列のみをソートすればよいのです。
!!! note "クイックソートの分割統治戦略"
ピボット分割の本質は、より長い配列のソート問題をより短い2つの配列に簡素化することです。
=== "Python"
```python title="quick_sort.py"
def partition(self, nums: list[int], left: int, right: int) -> int:
"""分割"""
# nums[left] をピボットとして使用
i, j = left, right
while i < j:
while i < j and nums[j] >= nums[left]:
j -= 1 # 右から左へピボットより小さい最初の要素を探す
while i < j and nums[i] <= nums[left]:
i += 1 # 左から右へピボットより大きい最初の要素を探す
# 要素を交換
nums[i], nums[j] = nums[j], nums[i]
# ピボットを2つのサブ配列の境界に交換
nums[i], nums[left] = nums[left], nums[i]
return i # ピボットのインデックスを返す
```
=== "C++"
```cpp title="quick_sort.cpp"
/* 分割 */
int partition(vector<int> &nums, int left, int right) {
// nums[left]をピボットとして使用
int i = left, j = right;
while (i < j) {
while (i < j && nums[j] >= nums[left])
j--; // 右から左へピボットより小さい最初の要素を検索
while (i < j && nums[i] <= nums[left])
i++; // 左から右へピボットより大きい最初の要素を検索
swap(nums, i, j); // これら二つの要素を交換
}
swap(nums, i, left); // ピボットを二つのサブ配列の境界に交換
return i; // ピボットのインデックスを返す
}
```
=== "Java"
```java title="quick_sort.java"
/* 要素を交換 */
void swap(int[] nums, int i, int j) {
int tmp = nums[i];
nums[i] = nums[j];
nums[j] = tmp;
}
/* 分割 */
int partition(int[] nums, int left, int right) {
// nums[left] を基準値として使用
int i = left, j = right;
while (i < j) {
while (i < j && nums[j] >= nums[left])
j--; // 右から左へ、基準値より小さい最初の要素を検索
while (i < j && nums[i] <= nums[left])
i++; // 左から右へ、基準値より大きい最初の要素を検索
swap(nums, i, j); // これら2つの要素を交換
}
swap(nums, i, left); // 基準値を2つの部分配列の境界に交換
return i; // 基準値のインデックスを返す
}
```
=== "C#"
```csharp title="quick_sort.cs"
[class]{quickSort}-[func]{Swap}
[class]{quickSort}-[func]{Partition}
```
=== "Go"
```go title="quick_sort.go"
[class]{quickSort}-[func]{partition}
```
=== "Swift"
```swift title="quick_sort.swift"
[class]{}-[func]{partition}
```
=== "JS"
```javascript title="quick_sort.js"
[class]{QuickSort}-[func]{swap}
[class]{QuickSort}-[func]{partition}
```
=== "TS"
```typescript title="quick_sort.ts"
[class]{QuickSort}-[func]{swap}
[class]{QuickSort}-[func]{partition}
```
=== "Dart"
```dart title="quick_sort.dart"
[class]{QuickSort}-[func]{_swap}
[class]{QuickSort}-[func]{_partition}
```
=== "Rust"
```rust title="quick_sort.rs"
[class]{QuickSort}-[func]{partition}
```
=== "C"
```c title="quick_sort.c"
[class]{}-[func]{swap}
[class]{}-[func]{partition}
```
=== "Kotlin"
```kotlin title="quick_sort.kt"
[class]{}-[func]{swap}
[class]{}-[func]{partition}
```
=== "Ruby"
```ruby title="quick_sort.rb"
[class]{QuickSort}-[func]{partition}
```
=== "Zig"
```zig title="quick_sort.zig"
[class]{QuickSort}-[func]{swap}
[class]{QuickSort}-[func]{partition}
```
## 11.5.1 &nbsp; アルゴリズムプロセス
クイックソートの全体的なプロセスは下図に示されます。
1. まず、元の配列に対して「ピボット分割」を実行し、未ソートの左と右のサブ配列を取得します。
2. 次に、左と右のサブ配列に対してそれぞれ再帰的に「ピボット分割」を実行します。
3. サブ配列の長さが1になるまで再帰を続け、配列全体のソートを完了します。
![Quick sort process](quick_sort.assets/quick_sort_overview.png){ class="animation-figure" }
<p align="center"> 図 11-9 &nbsp; Quick sort process </p>
=== "Python"
```python title="quick_sort.py"
def quick_sort(self, nums: list[int], left: int, right: int):
"""クイックソート"""
# サブ配列の長さが1のときに再帰を終了
if left >= right:
return
# 分割
pivot = self.partition(nums, left, right)
# 左サブ配列と右サブ配列を再帰的に処理
self.quick_sort(nums, left, pivot - 1)
self.quick_sort(nums, pivot + 1, right)
```
=== "C++"
```cpp title="quick_sort.cpp"
/* クイックソート */
void quickSort(vector<int> &nums, int left, int right) {
// サブ配列の長さが1の時、再帰を終了
if (left >= right)
return;
// 分割
int pivot = partition(nums, left, right);
// 左サブ配列と右サブ配列を再帰的に処理
quickSort(nums, left, pivot - 1);
quickSort(nums, pivot + 1, right);
}
```
=== "Java"
```java title="quick_sort.java"
/* クイックソート */
void quickSort(int[] nums, int left, int right) {
// 部分配列の長さが 1 のとき再帰を終了
if (left >= right)
return;
// 分割
int pivot = partition(nums, left, right);
// 左部分配列と右部分配列を再帰的に処理
quickSort(nums, left, pivot - 1);
quickSort(nums, pivot + 1, right);
}
```
=== "C#"
```csharp title="quick_sort.cs"
[class]{quickSort}-[func]{QuickSort}
```
=== "Go"
```go title="quick_sort.go"
[class]{quickSort}-[func]{quickSort}
```
=== "Swift"
```swift title="quick_sort.swift"
[class]{}-[func]{quickSort}
```
=== "JS"
```javascript title="quick_sort.js"
[class]{QuickSort}-[func]{quickSort}
```
=== "TS"
```typescript title="quick_sort.ts"
[class]{QuickSort}-[func]{quickSort}
```
=== "Dart"
```dart title="quick_sort.dart"
[class]{QuickSort}-[func]{quickSort}
```
=== "Rust"
```rust title="quick_sort.rs"
[class]{QuickSort}-[func]{quick_sort}
```
=== "C"
```c title="quick_sort.c"
[class]{}-[func]{quickSort}
```
=== "Kotlin"
```kotlin title="quick_sort.kt"
[class]{}-[func]{quickSort}
```
=== "Ruby"
```ruby title="quick_sort.rb"
[class]{QuickSort}-[func]{quick_sort}
```
=== "Zig"
```zig title="quick_sort.zig"
[class]{QuickSort}-[func]{quickSort}
```
## 11.5.2 &nbsp; アルゴリズムの特徴
- **$O(n \log n)$の時間計算量、非適応ソート**:平均的なケースでは、ピボット分割の再帰レベルは$\log n$で、レベルあたりのループの総数は$n$であり、全体で$O(n \log n)$の時間を使用します。最悪の場合、各ラウンドのピボット分割は長さ$n$の配列を長さ$0$と$n - 1$の2つのサブ配列に分割し、再帰レベル数が$n$に達すると、各レベルのループ数は$n$で、使用される総時間は$O(n^2)$です。
- **$O(n)$の空間計算量、インプレースソート**:入力配列が完全に逆順の場合、最悪の再帰深度は$n$に達し、$O(n)$のスタックフレーム空間を使用します。ソート操作は追加の配列の助けなしに元の配列で実行されます。
- **非安定ソート**:ピボット分割の最終ステップで、ピボットは等しい要素の右側に交換される可能性があります。
## 11.5.3 &nbsp; なぜクイックソートは高速なのか
名前が示すように、クイックソートは効率性の面で一定の利点を持つべきです。クイックソートの平均時間計算量は「マージソート」や「ヒープソート」と同じですが、以下の理由で一般的により効率的です。
- **最悪ケースシナリオの低い確率**:クイックソートの最悪時間計算量は$O(n^2)$で、マージソートほど安定していませんが、ほとんどの場合、クイックソートは$O(n \log n)$の時間計算量で動作できます。
- **高いキャッシュ利用率**:ピボット分割操作中、システムはサブ配列全体をキャッシュにロードできるため、要素により効率的にアクセスできます。対照的に、「ヒープソート」などのアルゴリズムは要素にジャンプ方式でアクセスする必要があり、この特徴を欠いています。
- **計算量の小さな定数係数**上記3つのアルゴリズムの中で、クイックソートは比較、代入、交換などの操作の総数が最も少ないです。これは「挿入ソート」が「バブルソート」よりも高速な理由と似ています。
## 11.5.4 &nbsp; ピボット最適化
**クイックソートの時間効率は特定の入力で劣化する可能性があります**。例えば、入力配列が完全に逆順の場合、最も左の要素をピボットとして選択するため、ピボット分割後、ピボットは配列の右端に交換され、左サブ配列の長さが$n - 1$、右サブ配列の長さが$0$になります。この方法を続けると、各ラウンドのピボット分割でサブ配列の長さが$0$になり、分割統治戦略が失敗し、クイックソートは「バブルソート」に似た形に劣化します。
この状況を避けるため、**ピボット分割でピボット選択戦略を最適化できます**。例えば、要素をランダムに選択してピボットとすることができます。ただし、運が悪く、一貫して最適でないピボットを選択した場合、効率はまだ満足できません。
プログラミング言語は通常「疑似乱数」を生成することに注意することが重要です。疑似乱数シーケンスに対して特定のテストケースを構築すると、クイックソートの効率はまだ劣化する可能性があります。
さらなる改善のため、3つの候補要素通常は配列の最初、最後、中点の要素を選択し、**これら3つの候補要素の中央値をピボットとして使用**できます。この方法で、ピボットが「小さすぎず大きすぎない」確率が大幅に増加します。もちろん、さらに多くの候補要素を選択してアルゴリズムの堅牢性をさらに向上させることもできます。この方法により、時間計算量が$O(n^2)$に劣化する確率が大幅に削減されます。
サンプルコードは以下の通りです:
=== "Python"
```python title="quick_sort.py"
def median_three(self, nums: list[int], left: int, mid: int, right: int) -> int:
"""3つの候補要素の中央値を選択"""
l, m, r = nums[left], nums[mid], nums[right]
if (l <= m <= r) or (r <= m <= l):
return mid # m は l と r の間
if (m <= l <= r) or (r <= l <= m):
return left # l は m と r の間
return right
def partition(self, nums: list[int], left: int, right: int) -> int:
"""分割(三点中央値)"""
# nums[left] をピボットとして使用
med = self.median_three(nums, left, (left + right) // 2, right)
# 中央値を配列の最左端に交換
nums[left], nums[med] = nums[med], nums[left]
# nums[left] をピボットとして使用
i, j = left, right
while i < j:
while i < j and nums[j] >= nums[left]:
j -= 1 # 右から左へピボットより小さい最初の要素を探す
while i < j and nums[i] <= nums[left]:
i += 1 # 左から右へピボットより大きい最初の要素を探す
# 要素を交換
nums[i], nums[j] = nums[j], nums[i]
# ピボットを2つのサブ配列の境界に交換
nums[i], nums[left] = nums[left], nums[i]
return i # ピボットのインデックスを返す
```
=== "C++"
```cpp title="quick_sort.cpp"
/* 三つの候補要素の中央値を選択 */
int medianThree(vector<int> &nums, int left, int mid, int right) {
int l = nums[left], m = nums[mid], r = nums[right];
if ((l <= m && m <= r) || (r <= m && m <= l))
return mid; // mはlとrの間
if ((m <= l && l <= r) || (r <= l && l <= m))
return left; // lはmとrの間
return right;
}
/* 分割(三つの中央値) */
int partition(vector<int> &nums, int left, int right) {
// 三つの候補要素の中央値を選択
int med = medianThree(nums, left, (left + right) / 2, right);
// 中央値を配列の最左位置に交換
swap(nums, left, med);
// nums[left]をピボットとして使用
int i = left, j = right;
while (i < j) {
while (i < j && nums[j] >= nums[left])
j--; // 右から左へピボットより小さい最初の要素を検索
while (i < j && nums[i] <= nums[left])
i++; // 左から右へピボットより大きい最初の要素を検索
swap(nums, i, j); // これら二つの要素を交換
}
swap(nums, i, left); // ピボットを二つのサブ配列の境界に交換
return i; // ピボットのインデックスを返す
}
```
=== "Java"
```java title="quick_sort.java"
/* 3つの候補要素の中央値を選択 */
int medianThree(int[] nums, int left, int mid, int right) {
int l = nums[left], m = nums[mid], r = nums[right];
if ((l <= m && m <= r) || (r <= m && m <= l))
return mid; // m は l と r の間
if ((m <= l && l <= r) || (r <= l && l <= m))
return left; // l は m と r の間
return right;
}
/* 分割3つの中央値 */
int partition(int[] nums, int left, int right) {
// 3つの候補要素の中央値を選択
int med = medianThree(nums, left, (left + right) / 2, right);
// 中央値を配列の最左端の位置に交換
swap(nums, left, med);
// nums[left] を基準値として使用
int i = left, j = right;
while (i < j) {
while (i < j && nums[j] >= nums[left])
j--; // 右から左へ、基準値より小さい最初の要素を検索
while (i < j && nums[i] <= nums[left])
i++; // 左から右へ、基準値より大きい最初の要素を検索
swap(nums, i, j); // これら2つの要素を交換
}
swap(nums, i, left); // 基準値を2つの部分配列の境界に交換
return i; // 基準値のインデックスを返す
}
```
=== "C#"
```csharp title="quick_sort.cs"
[class]{QuickSortMedian}-[func]{MedianThree}
[class]{QuickSortMedian}-[func]{Partition}
```
=== "Go"
```go title="quick_sort.go"
[class]{quickSortMedian}-[func]{medianThree}
[class]{quickSortMedian}-[func]{partition}
```
=== "Swift"
```swift title="quick_sort.swift"
[class]{}-[func]{medianThree}
[class]{}-[func]{partitionMedian}
```
=== "JS"
```javascript title="quick_sort.js"
[class]{QuickSortMedian}-[func]{medianThree}
[class]{QuickSortMedian}-[func]{partition}
```
=== "TS"
```typescript title="quick_sort.ts"
[class]{QuickSortMedian}-[func]{medianThree}
[class]{QuickSortMedian}-[func]{partition}
```
=== "Dart"
```dart title="quick_sort.dart"
[class]{QuickSortMedian}-[func]{_medianThree}
[class]{QuickSortMedian}-[func]{_partition}
```
=== "Rust"
```rust title="quick_sort.rs"
[class]{QuickSortMedian}-[func]{median_three}
[class]{QuickSortMedian}-[func]{partition}
```
=== "C"
```c title="quick_sort.c"
[class]{}-[func]{medianThree}
[class]{}-[func]{partitionMedian}
```
=== "Kotlin"
```kotlin title="quick_sort.kt"
[class]{}-[func]{medianThree}
[class]{}-[func]{partitionMedian}
```
=== "Ruby"
```ruby title="quick_sort.rb"
[class]{QuickSortMedian}-[func]{median_three}
[class]{QuickSortMedian}-[func]{partition}
```
=== "Zig"
```zig title="quick_sort.zig"
[class]{QuickSortMedian}-[func]{medianThree}
[class]{QuickSortMedian}-[func]{partition}
```
## 11.5.5 &nbsp; 末尾再帰最適化
**特定の入力では、クイックソートはより多くの空間を占有する可能性があります**。例えば、完全に順序付けられた入力配列を考えてみましょう。再帰でのサブ配列の長さを$m$とします。各ラウンドのピボット分割で、長さ$0$の左サブ配列と長さ$m - 1$の右サブ配列が生成されます。これは、再帰呼び出しごとに問題サイズが1つの要素のみ減少することを意味し、各レベルの再帰での削減が非常に小さくなります。
結果として、再帰ツリーの高さは$n 1$に達する可能性があり、これには$O(n)$のスタックフレーム空間が必要です。
スタックフレーム空間の蓄積を防ぐため、各ラウンドのピボットソート後に2つのサブ配列の長さを比較し、**より短いサブ配列のみを再帰的にソート**できます。より短いサブ配列の長さは$n / 2$を超えないため、この方法は再帰深度が$\log n$を超えないことを保証し、最悪空間計算量を$O(\log n)$に最適化します。コードは以下の通りです:
=== "Python"
```python title="quick_sort.py"
def quick_sort(self, nums: list[int], left: int, right: int):
"""クイックソート(末尾再帰最適化)"""
# サブ配列の長さが1のときに終了
while left < right:
# 分割操作
pivot = self.partition(nums, left, right)
# 2つのサブ配列のうち短い方に対してクイックソートを実行
if pivot - left < right - pivot:
self.quick_sort(nums, left, pivot - 1) # 左サブ配列を再帰的にソート
left = pivot + 1 # 残りの未ソート区間は [pivot + 1, right]
else:
self.quick_sort(nums, pivot + 1, right) # 右サブ配列を再帰的にソート
right = pivot - 1 # 残りの未ソート区間は [left, pivot - 1]
```
=== "C++"
```cpp title="quick_sort.cpp"
/* クイックソート(末尾再帰最適化) */
void quickSort(vector<int> &nums, int left, int right) {
// サブ配列の長さが1の時終了
while (left < right) {
// 分割操作
int pivot = partition(nums, left, right);
// 二つのサブ配列のうち短い方でクイックソートを実行
if (pivot - left < right - pivot) {
quickSort(nums, left, pivot - 1); // 左サブ配列を再帰的にソート
left = pivot + 1; // 残りの未ソート区間は[pivot + 1, right]
} else {
quickSort(nums, pivot + 1, right); // 右サブ配列を再帰的にソート
right = pivot - 1; // 残りの未ソート区間は[left, pivot - 1]
}
}
}
```
=== "Java"
```java title="quick_sort.java"
/* クイックソート(末尾再帰最適化) */
void quickSort(int[] nums, int left, int right) {
// 部分配列の長さが 1 のとき終了
while (left < right) {
// 分割操作
int pivot = partition(nums, left, right);
// 2つの部分配列のうち短い方にクイックソートを実行
if (pivot - left < right - pivot) {
quickSort(nums, left, pivot - 1); // 左部分配列を再帰的にソート
left = pivot + 1; // 残りの未ソート区間は [pivot + 1, right]
} else {
quickSort(nums, pivot + 1, right); // 右部分配列を再帰的にソート
right = pivot - 1; // 残りの未ソート区間は [left, pivot - 1]
}
}
}
```
=== "C#"
```csharp title="quick_sort.cs"
[class]{QuickSortTailCall}-[func]{QuickSort}
```
=== "Go"
```go title="quick_sort.go"
[class]{quickSortTailCall}-[func]{quickSort}
```
=== "Swift"
```swift title="quick_sort.swift"
[class]{}-[func]{quickSortTailCall}
```
=== "JS"
```javascript title="quick_sort.js"
[class]{QuickSortTailCall}-[func]{quickSort}
```
=== "TS"
```typescript title="quick_sort.ts"
[class]{QuickSortTailCall}-[func]{quickSort}
```
=== "Dart"
```dart title="quick_sort.dart"
[class]{QuickSortTailCall}-[func]{quickSort}
```
=== "Rust"
```rust title="quick_sort.rs"
[class]{QuickSortTailCall}-[func]{quick_sort}
```
=== "C"
```c title="quick_sort.c"
[class]{}-[func]{quickSortTailCall}
```
=== "Kotlin"
```kotlin title="quick_sort.kt"
[class]{}-[func]{quickSortTailCall}
```
=== "Ruby"
```ruby title="quick_sort.rb"
[class]{QuickSortTailCall}-[func]{quick_sort}
```
=== "Zig"
```zig title="quick_sort.zig"
[class]{QuickSortTailCall}-[func]{quickSort}
```

View File

@@ -0,0 +1,303 @@
---
comments: true
---
# 11.10 &nbsp; 基数ソート
前の節では計数ソートを紹介しました。これは、データサイズ $n$ が大きいがデータ範囲 $m$ が小さいシナリオに適しています。$n = 10^6$ の学生IDをソートする必要があり、各IDが $8$ 桁の数字であるとします。これは、データ範囲 $m = 10^8$ が非常に大きいことを意味します。この場合、計数ソートを使用すると、大量のメモリスペースが必要になります。基数ソートはこの状況を回避できます。
<u>基数ソート</u>は計数ソートと同じ核心概念を共有し、要素の頻度をカウントすることでソートします。同時に、基数ソートは数字の桁間の漸進的関係を利用してこれを基盤としています。桁を一度に一つずつ処理してソートし、最終的なソート順序を達成します。
## 11.10.1 &nbsp; アルゴリズムの過程
学生IDデータを例として、最下位桁を $1$ 番目、最上位桁を $8$ 番目とすると、基数ソートの過程は以下の図に示されています。
1. 桁 $k = 1$ を初期化します。
2. 学生IDの $k$ 番目の桁に対して「計数ソート」を実行します。完了後、データは $k$ 番目の桁に基づいて最小から最大までソートされます。
3. $k$ を $1$ 増やし、手順 `2.` に戻って反復を続け、すべての桁がソートされるまで続けます。この時点で過程が終了します。
![基数ソートアルゴリズムの過程](radix_sort.assets/radix_sort_overview.png){ class="animation-figure" }
<p align="center"> 図 11-18 &nbsp; 基数ソートアルゴリズムの過程 </p>
以下、コード実装を詳しく見てみます。基数 $d$ での数 $x$ に対して、その $k$ 番目の桁 $x_k$ を取得するには、以下の計算式を使用できます:
$$
x_k = \lfloor\frac{x}{d^{k-1}}\rfloor \bmod d
$$
ここで $\lfloor a \rfloor$ は浮動小数点数 $a$ の切り捨てを表し、$\bmod \: d$ は $d$ による剰余を表します。学生IDデータの場合、$d = 10$ で $k \in [1, 8]$ です。
さらに、$k$ 番目の桁に基づいてソートできるように、計数ソートのコードを少し修正する必要があります:
=== "Python"
```python title="radix_sort.py"
def digit(num: int, exp: int) -> int:
"""要素 num の k 番目の桁を取得、exp = 10^(k-1)"""
# k の代わりに exp を渡すことで、ここでコストの高い累乗計算を避けることができる
return (num // exp) % 10
def counting_sort_digit(nums: list[int], exp: int):
"""計数ソートnums の k 番目の桁に基づく)"""
# 10進数の桁の範囲は 0~9、したがって長さ10のバケット配列が必要
counter = [0] * 10
n = len(nums)
# 数字 0~9 の出現回数を統計
for i in range(n):
d = digit(nums[i], exp) # nums[i] の k 番目の桁を取得、d とする
counter[d] += 1 # 数字 d の出現回数を統計
# 前置和を計算し、「出現回数」を「配列インデックス」に変換
for i in range(1, 10):
counter[i] += counter[i - 1]
# 逆順に走査し、バケット統計に基づいて各要素を res に配置
res = [0] * n
for i in range(n - 1, -1, -1):
d = digit(nums[i], exp)
j = counter[d] - 1 # 配列内の d のインデックス j を取得
res[j] = nums[i] # 現在の要素をインデックス j に配置
counter[d] -= 1 # d の数を1減らす
# 結果を使用して元の配列 nums を上書き
for i in range(n):
nums[i] = res[i]
def radix_sort(nums: list[int]):
"""基数ソート"""
# 配列の最大要素を取得し、最大桁数を判定するために使用
m = max(nums)
# 最下位桁から最上位桁まで走査
exp = 1
while exp <= m:
# 配列要素の k 番目の桁に対して計数ソートを実行
# k = 1 -> exp = 1
# k = 2 -> exp = 10
# つまり、exp = 10^(k-1)
counting_sort_digit(nums, exp)
exp *= 10
```
=== "C++"
```cpp title="radix_sort.cpp"
/* 要素numのk番目の桁を取得、exp = 10^(k-1) */
int digit(int num, int exp) {
// kの代わりにexpを渡すことで、ここで繰り返される高価な冪乗計算を避けることができる
return (num / exp) % 10;
}
/* カウントソートnumsのk番目の桁に基づく */
void countingSortDigit(vector<int> &nums, int exp) {
// 10進数の桁範囲は0~9なので、長さ10のバケット配列が必要
vector<int> counter(10, 0);
int n = nums.size();
// 数字0~9の出現回数を統計
for (int i = 0; i < n; i++) {
int d = digit(nums[i], exp); // nums[i]のk番目の桁を取得、dとして記録
counter[d]++; // 数字dの出現回数を統計
}
// 前缀和を計算し、「出現回数」を「配列インデックス」に変換
for (int i = 1; i < 10; i++) {
counter[i] += counter[i - 1];
}
// 逆順で走査し、バケット統計に基づいて各要素をresに配置
vector<int> res(n, 0);
for (int i = n - 1; i >= 0; i--) {
int d = digit(nums[i], exp);
int j = counter[d] - 1; // dが配列内にあるインデックスjを取得
res[j] = nums[i]; // 現在の要素をインデックスjに配置
counter[d]--; // dのカウントを1減らす
}
// 結果で元の配列numsを上書き
for (int i = 0; i < n; i++)
nums[i] = res[i];
}
/* 基数ソート */
void radixSort(vector<int> &nums) {
// 配列の最大要素を取得、最大桁数を判定するために使用
int m = *max_element(nums.begin(), nums.end());
// 最下位桁から最上位桁まで走査
for (int exp = 1; exp <= m; exp *= 10)
// 配列要素のk番目の桁でカウントソートを実行
// k = 1 -> exp = 1
// k = 2 -> exp = 10
// つまり、exp = 10^(k-1)
countingSortDigit(nums, exp);
}
```
=== "Java"
```java title="radix_sort.java"
/* 要素 num の k 番目の桁を取得、exp = 10^(k-1) */
int digit(int num, int exp) {
// k の代わりに exp を渡すことで、ここでコストの高い累乗計算の繰り返しを避けることができる
return (num / exp) % 10;
}
/* 計数ソートnums の k 番目の桁に基づく) */
void countingSortDigit(int[] nums, int exp) {
// 10進数の桁の範囲は 0~9、したがって長さ 10 のバケット配列が必要
int[] counter = new int[10];
int n = nums.length;
// 桁 0~9 の出現回数を統計
for (int i = 0; i < n; i++) {
int d = digit(nums[i], exp); // nums[i] の k 番目の桁を取得、d とする
counter[d]++; // 桁 d の出現回数を統計
}
// 累積和を計算し、「出現回数」を「配列インデックス」に変換
for (int i = 1; i < 10; i++) {
counter[i] += counter[i - 1];
}
// 逆順に走査し、バケット統計に基づいて各要素を res に配置
int[] res = new int[n];
for (int i = n - 1; i >= 0; i--) {
int d = digit(nums[i], exp);
int j = counter[d] - 1; // 配列内での d のインデックス j を取得
res[j] = nums[i]; // 現在の要素をインデックス j に配置
counter[d]--; // d のカウントを 1 減らす
}
// 結果で元の配列 nums を上書き
for (int i = 0; i < n; i++)
nums[i] = res[i];
}
/* 基数ソート */
void radixSort(int[] nums) {
// 配列の最大要素を取得し、最大桁数を判定するために使用
int m = Integer.MIN_VALUE;
for (int num : nums)
if (num > m)
m = num;
// 最下位桁から最上位桁まで走査
for (int exp = 1; exp <= m; exp *= 10) {
// 配列要素の k 番目の桁に対して計数ソートを実行
// k = 1 -> exp = 1
// k = 2 -> exp = 10
// すなわち exp = 10^(k-1)
countingSortDigit(nums, exp);
}
}
```
=== "C#"
```csharp title="radix_sort.cs"
[class]{radix_sort}-[func]{Digit}
[class]{radix_sort}-[func]{CountingSortDigit}
[class]{radix_sort}-[func]{RadixSort}
```
=== "Go"
```go title="radix_sort.go"
[class]{}-[func]{digit}
[class]{}-[func]{countingSortDigit}
[class]{}-[func]{radixSort}
```
=== "Swift"
```swift title="radix_sort.swift"
[class]{}-[func]{digit}
[class]{}-[func]{countingSortDigit}
[class]{}-[func]{radixSort}
```
=== "JS"
```javascript title="radix_sort.js"
[class]{}-[func]{digit}
[class]{}-[func]{countingSortDigit}
[class]{}-[func]{radixSort}
```
=== "TS"
```typescript title="radix_sort.ts"
[class]{}-[func]{digit}
[class]{}-[func]{countingSortDigit}
[class]{}-[func]{radixSort}
```
=== "Dart"
```dart title="radix_sort.dart"
[class]{}-[func]{digit}
[class]{}-[func]{countingSortDigit}
[class]{}-[func]{radixSort}
```
=== "Rust"
```rust title="radix_sort.rs"
[class]{}-[func]{digit}
[class]{}-[func]{counting_sort_digit}
[class]{}-[func]{radix_sort}
```
=== "C"
```c title="radix_sort.c"
[class]{}-[func]{digit}
[class]{}-[func]{countingSortDigit}
[class]{}-[func]{radixSort}
```
=== "Kotlin"
```kotlin title="radix_sort.kt"
[class]{}-[func]{digit}
[class]{}-[func]{countingSortDigit}
[class]{}-[func]{radixSort}
```
=== "Ruby"
```ruby title="radix_sort.rb"
[class]{}-[func]{digit}
[class]{}-[func]{counting_sort_digit}
[class]{}-[func]{radix_sort}
```
=== "Zig"
```zig title="radix_sort.zig"
[class]{}-[func]{digit}
[class]{}-[func]{countingSortDigit}
[class]{}-[func]{radixSort}
```
!!! question "なぜ最下位桁から開始するのか?"
連続するソートラウンドでは、後のラウンドの結果が前のラウンドの結果を上書きします。例えば、最初のラウンドの結果が $a < b$ で、2番目のラウンドが $a > b$ の場合、2番目のラウンドの結果が最初のラウンドの結果を置き換えます。上位桁は下位桁より優先されるため、上位桁の前に下位桁をソートすることが理にかなっています。
## 11.10.2 &nbsp; アルゴリズムの特徴
計数ソートと比較して、基数ソートはより大きな数値範囲に適していますが、**データが固定桁数で表現でき、桁数があまり大きくないことを前提としています**。例えば、浮動小数点数は桁数 $k$ が大きい可能性があり、時間計算量 $O(nk) \gg O(n^2)$ につながる可能性があるため、基数ソートには適していません。
- **時間計算量は $O(nk)$、非適応ソート**:データサイズを $n$、データが基数 $d$、最大桁数を $k$ とすると、単一桁のソートには $O(n + d)$ 時間がかかり、すべての $k$ 桁のソートには $O((n + d)k)$ 時間がかかります。一般的に、$d$ と $k$ はどちらも比較的小さく、時間計算量は $O(n)$ に近づきます。
- **空間計算量は $O(n + d)$、非インプレースソート**:計数ソートと同様に、基数ソートは長さ $n$ と $d$ の配列 `res` と `counter` にそれぞれ依存します。
- **安定ソート**:計数ソートが安定な場合、基数ソートも安定です。計数ソートが不安定な場合、基数ソートは正しいソート順序を保証できません。

View File

@@ -0,0 +1,187 @@
---
comments: true
---
# 11.2 &nbsp; 選択ソート
<u>選択ソート</u>は非常にシンプルな原理で動作します:各反復で未ソート区間から最小要素を選択し、ソート済みセクションの末尾に移動するループを使用します。
配列の長さを$n$とすると、選択ソートのステップは下図に示されます。
1. 最初に、すべての要素は未ソートで、つまり未ソート(インデックス)区間は$[0, n-1]$です。
2. 区間$[0, n-1]$の最小要素を選択し、インデックス$0$の要素と交換します。この後、配列の最初の要素がソートされます。
3. 区間$[1, n-1]$の最小要素を選択し、インデックス$1$の要素と交換します。この後、配列の最初の2つの要素がソートされます。
4. この方法で続行します。$n - 1$ラウンドの選択と交換の後、最初の$n - 1$個の要素がソートされます。
5. 残りの唯一の要素は結果的に最大要素であり、ソートする必要がないため、配列はソートされます。
=== "<1>"
![Selection sort process](selection_sort.assets/selection_sort_step1.png){ class="animation-figure" }
=== "<2>"
![selection_sort_step2](selection_sort.assets/selection_sort_step2.png){ class="animation-figure" }
=== "<3>"
![selection_sort_step3](selection_sort.assets/selection_sort_step3.png){ class="animation-figure" }
=== "<4>"
![selection_sort_step4](selection_sort.assets/selection_sort_step4.png){ class="animation-figure" }
=== "<5>"
![selection_sort_step5](selection_sort.assets/selection_sort_step5.png){ class="animation-figure" }
=== "<6>"
![selection_sort_step6](selection_sort.assets/selection_sort_step6.png){ class="animation-figure" }
=== "<7>"
![selection_sort_step7](selection_sort.assets/selection_sort_step7.png){ class="animation-figure" }
=== "<8>"
![selection_sort_step8](selection_sort.assets/selection_sort_step8.png){ class="animation-figure" }
=== "<9>"
![selection_sort_step9](selection_sort.assets/selection_sort_step9.png){ class="animation-figure" }
=== "<10>"
![selection_sort_step10](selection_sort.assets/selection_sort_step10.png){ class="animation-figure" }
=== "<11>"
![selection_sort_step11](selection_sort.assets/selection_sort_step11.png){ class="animation-figure" }
<p align="center"> 図 11-2 &nbsp; Selection sort process </p>
コードでは、$k$を使用して未ソート区間内の最小要素を記録します:
=== "Python"
```python title="selection_sort.py"
def selection_sort(nums: list[int]):
"""選択ソート"""
n = len(nums)
# 外側のループ:未ソート範囲は [i, n-1]
for i in range(n - 1):
# 内側のループ:未ソート範囲内で最小要素を見つける
k = i
for j in range(i + 1, n):
if nums[j] < nums[k]:
k = j # 最小要素のインデックスを記録
# 最小要素を未ソート範囲の先頭要素と交換
nums[i], nums[k] = nums[k], nums[i]
```
=== "C++"
```cpp title="selection_sort.cpp"
/* 選択ソート */
void selectionSort(vector<int> &nums) {
int n = nums.size();
// 外側ループ:未ソート範囲は[i, n-1]
for (int i = 0; i < n - 1; i++) {
// 内側ループ:未ソート範囲内で最小要素を見つける
int k = i;
for (int j = i + 1; j < n; j++) {
if (nums[j] < nums[k])
k = j; // 最小要素のインデックスを記録
}
// 最小要素を未ソート範囲の最初の要素と交換
swap(nums[i], nums[k]);
}
}
```
=== "Java"
```java title="selection_sort.java"
/* 選択ソート */
void selectionSort(int[] nums) {
int n = nums.length;
// 外側ループ: 未ソート範囲は [i, n-1]
for (int i = 0; i < n - 1; i++) {
// 内側ループ: 未ソート範囲内で最小要素を見つける
int k = i;
for (int j = i + 1; j < n; j++) {
if (nums[j] < nums[k])
k = j; // 最小要素のインデックスを記録
}
// 最小要素と未ソート範囲の最初の要素を交換
int temp = nums[i];
nums[i] = nums[k];
nums[k] = temp;
}
}
```
=== "C#"
```csharp title="selection_sort.cs"
[class]{selection_sort}-[func]{SelectionSort}
```
=== "Go"
```go title="selection_sort.go"
[class]{}-[func]{selectionSort}
```
=== "Swift"
```swift title="selection_sort.swift"
[class]{}-[func]{selectionSort}
```
=== "JS"
```javascript title="selection_sort.js"
[class]{}-[func]{selectionSort}
```
=== "TS"
```typescript title="selection_sort.ts"
[class]{}-[func]{selectionSort}
```
=== "Dart"
```dart title="selection_sort.dart"
[class]{}-[func]{selectionSort}
```
=== "Rust"
```rust title="selection_sort.rs"
[class]{}-[func]{selection_sort}
```
=== "C"
```c title="selection_sort.c"
[class]{}-[func]{selectionSort}
```
=== "Kotlin"
```kotlin title="selection_sort.kt"
[class]{}-[func]{selectionSort}
```
=== "Ruby"
```ruby title="selection_sort.rb"
[class]{}-[func]{selection_sort}
```
=== "Zig"
```zig title="selection_sort.zig"
[class]{}-[func]{selectionSort}
```
## 11.2.1 &nbsp; アルゴリズムの特性
- **$O(n^2)$の時間計算量、非適応ソート**:外側ループに$n - 1$回の反復があり、未ソートセクションの長さは最初の反復で$n$から始まり、最後の反復で$2$まで減少します。つまり、各外側ループ反復にはそれぞれ$n$、$n - 1$、$\dots$、$3$、$2$回の内側ループ反復が含まれ、合計は$\frac{(n - 1)(n + 2)}{2}$となります。
- **$O(1)$の空間計算量、インプレースソート**:ポインタ$i$と$j$で定数の追加空間を使用します。
- **非安定ソート**:下図に示すように、要素`nums[i]`は等しい要素の右側に交換される可能性があり、相対順序が変わる原因となります。
![Selection sort instability example](selection_sort.assets/selection_sort_instability.png){ class="animation-figure" }
<p align="center"> 図 11-3 &nbsp; Selection sort instability example </p>

View File

@@ -0,0 +1,52 @@
---
comments: true
---
# 11.1 &nbsp; ソートアルゴリズム
<u>ソートアルゴリズム</u>は、データセットを特定の順序で配列するために使用されます。ソートアルゴリズムは、順序付けられたデータは通常、より効率的に探索、分析、処理できるため、幅広い応用があります。
下図に示すように、ソートアルゴリズムのデータ型は整数、浮動小数点数、文字、文字列などです。ソート基準は、数値サイズ、文字ASCII順序、またはカスタム基準など、必要に応じて設定できます。
![Data types and comparator examples](sorting_algorithm.assets/sorting_examples.png){ class="animation-figure" }
<p align="center"> 図 11-1 &nbsp; Data types and comparator examples </p>
## 11.1.1 &nbsp; 評価次元
**実行効率**:ソートアルゴリズムの時間計算量ができるだけ低いことを期待し、全体的な操作数も少ないこと(時間計算量の定数項を下げる)を望みます。大容量データでは、実行効率が特に重要です。
**インプレース性**:名前が示すとおり、<u>インプレースソート</u>は元の配列を直接操作することで実現され、追加のヘルパー配列が不要であるため、メモリを節約します。一般的に、インプレースソートはデータ移動操作が少なく、高速です。
**安定性**<u>安定ソート</u>は、ソート後に配列内の等しい要素の相対順序が変わらないことを保証します。
安定ソートは、マルチキーソートシナリオにおいて必要条件です。学生情報を格納するテーブルがあり、第1列と第2列がそれぞれ名前と年齢であるとします。この場合、<u>不安定ソート</u>は入力データの順序を失う可能性があります:
```shell
# 入力データは名前でソート済み
# (名前, 年齢)
('A', 19)
('B', 18)
('C', 21)
('D', 19)
('E', 23)
# 不安定ソートアルゴリズムを使用してリストを年齢でソートすると仮定すると、
# 結果は('D', 19)と('A', 19)の相対位置を変更し、
# 入力データが名前でソート済みであるという性質が失われる
('B', 18)
('D', 19)
('A', 19)
('C', 21)
('E', 23)
```
**適応性**<u>適応ソート</u>は入力データ内の既存の順序情報を活用して計算負荷を削減し、より最適な時間効率を実現します。適応ソートアルゴリズムの最良ケース時間計算量は、通常平均ケース時間計算量よりも優れています。
**比較ベースまたは非比較ベース**<u>比較ベースソート</u>は比較演算子($<$、$=$、$>$)に依存して要素の相対順序を決定し、配列全体をソートします。理論的最適時間計算量は$O(n \log n)$です。一方、<u>非比較ソート</u>は比較演算子を使用せず、$O(n)$の時間計算量を実現できますが、汎用性は比較的劣ります。
## 11.1.2 &nbsp; 理想的なソートアルゴリズム
**高速実行、インプレース、安定、適応、汎用**。明らかに、これらのすべての特徴を組み合わせたソートアルゴリズムは今日まで見つかっていません。したがって、ソートアルゴリズムを選択する際は、データの特定の特徴と問題の要件に基づいて決定する必要があります。
次に、さまざまなソートアルゴリズムを一緒に学び、上記の評価次元に基づいてそれぞれの利点と欠点を分析します。

View File

@@ -0,0 +1,53 @@
---
comments: true
---
# 11.11 &nbsp; まとめ
### 1. &nbsp; 重要な復習
- バブルソートは隣接する要素を交換することで動作します。フラグを追加して早期リターンを可能にすることで、バブルソートの最良ケースの時間計算量を $O(n)$ に最適化できます。
- 挿入ソートは、未ソート区間から要素を取り出してソート済み区間の正しい位置に挿入することで各ラウンドをソートします。挿入ソートの時間計算量は $O(n^2)$ ですが、単位あたりの操作が比較的少ないため、少量のデータのソートでは非常に人気があります。
- クイックソートは歩哨分割操作に基づいています。歩哨分割では、常に最悪のピボットを選ぶ可能性があり、時間計算量が $O(n^2)$ に劣化する可能性があります。中央値やランダムピボットを導入することで、そのような劣化の確率を減らすことができます。末尾再帰は再帰の深さを効果的に減らし、空間計算量を $O(\log n)$ に最適化します。
- マージソートには分割とマージの2つの段階があり、通常分割統治戦略を体現しています。マージソートでは、配列のソートには補助配列の作成が必要で、空間計算量は $O(n)$ になります。しかし、リストのソートの空間計算量は $O(1)$ に最適化できます。
- バケットソートは3つの手順から構成されますデータをバケットに分散、各バケット内でのソート、バケット順での結果のマージ。これも分割統治戦略を体現し、非常に大きなデータセットに適しています。バケットソートの鍵はデータの均等分散です。
- 計数ソートはバケットソートの変形で、各データポイントの出現回数をカウントすることでソートします。計数ソートは限られた範囲のデータを持つ大きなデータセットに適しており、データを正の整数に変換する必要があります。
- 基数ソートは桁ごとにソートすることでデータを処理し、データが固定長の数値として表現される必要があります。
- 全体的に、私たちは高効率、安定性、インプレース操作、適応性を持つソートアルゴリズムを求めています。しかし、他のデータ構造やアルゴリズムと同様に、これらすべての条件を同時に満たすソートアルゴリズムは存在しません。実際の応用では、データの特性に基づいて適切なソートアルゴリズムを選択する必要があります。
- 以下の図は、効率性、安定性、インプレース性、適応性の観点から主流のソートアルゴリズムを比較しています。
![ソートアルゴリズムの比較](summary.assets/sorting_algorithms_comparison.png){ class="animation-figure" }
<p align="center"> 図 11-19 &nbsp; ソートアルゴリズムの比較 </p>
### 2. &nbsp; Q & A
**Q**: ソートアルゴリズムの安定性はいつ必要ですか?
実際には、オブジェクトの一つの属性に基づいてソートする場合があります。例えば、学生は名前と身長の属性を持ち、多段階ソートを実装することを目指します:最初に名前で `(A, 180) (B, 185) (C, 170) (D, 170)` を取得し、次に身長で。ソートアルゴリズムが不安定なため、`(D, 170) (C, 170) (A, 180) (B, 185)` になってしまう可能性があります。
学生DとCの位置が交換され、名前の順序性が破られているのが分かります。これは望ましくありません。
**Q**: 歩哨分割での「右から左への検索」と「左から右への検索」の順序を交換できますか?
いいえ、最左要素をピボットとして使用する場合、最初に「右から左への検索」を行い、次に「左から右への検索」を行う必要があります。この結論はやや直観に反するので、理由を分析してみましょう。
歩哨分割 `partition()` の最後のステップは `nums[left]``nums[i]` を交換することです。交換後、ピボットの左側の要素はすべてピボット以下になります。**これには最後の交換前に `nums[left] >= nums[i]` が成り立つ必要があります**。「左から右への検索」を最初に行い、ピボットより大きい要素が見つからない場合、**`i == j` でループを終了し、`nums[j] == nums[i] > nums[left]` となる可能性があります**。つまり、最終交換操作はピボットより大きい要素を配列の左端に交換し、歩哨分割を失敗させます。
例えば、配列 `[0, 0, 0, 0, 1]` が与えられた場合、最初に「左から右への検索」を行うと、歩哨分割後の配列は `[1, 0, 0, 0, 0]` となり、これは正しくありません。
さらに考えると、`nums[right]` をピボットとして選択する場合、まったく逆で、最初に「左から右への検索」を行う必要があります。
**Q**: 末尾再帰最適化について、短い配列を選択することで再帰の深さが $\log n$ を超えないことを保証するのはなぜですか?
再帰の深さは現在リターンしていない再帰メソッドの数です。歩哨分割の各ラウンドは元の配列を2つの副配列に分割します。末尾再帰最適化により、再帰的に続行する副配列の長さは最大でも元の配列長の半分です。最悪の場合常に長さを半分にすると仮定すると、最終的な再帰の深さは $\log n$ になります。
元のクイックソートを見直すと、より大きな配列を継続的に再帰処理する可能性があり、最悪の場合 $n$、$n - 1$、...、$2$、$1$ で、再帰の深さは $n$ になります。末尾再帰最適化はこのシナリオを回避できます。
**Q**: 配列のすべての要素が等しい場合、クイックソートの時間計算量は $O(n^2)$ ですか?この劣化ケースをどう処理すべきですか?
はい。この状況については、歩哨分割を使用して配列をピボットより小さい、等しい、大きいの3つの部分に分割することを検討してください。小さい部分と大きい部分のみを再帰的に進めます。この方法では、すべての入力要素が等しい配列を1ラウンドの歩哨分割だけでソートできます。
**Q**: なぜバケットソートの最悪ケース時間計算量は $O(n^2)$ ですか?
最悪の場合、すべての要素が同じバケットに配置されます。これらの要素をソートするために $O(n^2)$ アルゴリズムを使用する場合、時間計算量は $O(n^2)$ になります。