mirror of
https://github.com/krahets/hello-algo.git
synced 2026-07-08 21:46:06 +00:00
Re-translate the Japanese version (#1871)
* Retranslate Japanese docs with GPT-5.4 * Retranslate Japanese code with GPT-5.4
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
# バブルソート
|
||||
|
||||
<u>バブルソート</u>は、隣接する要素を継続的に比較し交換することで動作します。このプロセスは泡が底から上に上昇するようなものなので、「バブルソート」と名付けられました。
|
||||
<u>バブルソート(bubble sort)</u>は、隣接する要素を繰り返し比較して交換することで整列を行います。この過程が泡のように下から上へ浮かび上がることから、バブルソートと呼ばれます。
|
||||
|
||||
下図に示すように、バブリングプロセスは要素交換を使用してシミュレートできます:配列の左端から開始して右に移動し、隣接する要素の各ペアを比較します。左の要素が右の要素より大きい場合は、それらを交換します。横断後、最大要素は配列の右端にバブルアップします。
|
||||
次の図に示すように、バブル処理は要素の交換操作によってシミュレートできます。配列の最も左の端から右へ走査し、隣接する要素の大小を順に比較して、「左要素 > 右要素」であれば両者を交換します。走査が終わると、最大の要素は配列の最も右端へ移動します。
|
||||
|
||||
=== "<1>"
|
||||

|
||||

|
||||
|
||||
=== "<2>"
|
||||

|
||||
@@ -25,18 +25,18 @@
|
||||
=== "<7>"
|
||||

|
||||
|
||||
## アルゴリズムプロセス
|
||||
## アルゴリズムの流れ
|
||||
|
||||
配列の長さを$n$とします。バブルソートのステップは下図に示されます:
|
||||
配列の長さを $n$ とすると、バブルソートの手順は次の図のとおりです。
|
||||
|
||||
1. まず、$n$個の要素に対して1回の「バブル」パスを実行し、**最大要素を正しい位置に交換します**。
|
||||
2. 次に、残りの$n - 1$個の要素に対して「バブル」パスを実行し、**2番目に大きい要素を正しい位置に交換します**。
|
||||
3. この方法で続行します;$n - 1$回のパスの後、**最大$n - 1$個の要素が正しい位置に移動されます**。
|
||||
4. 残りの唯一の要素は**必ず**最小であるため、**さらなる**ソートは必要ありません。この時点で、配列はソートされます。
|
||||
1. まず、$n$ 個の要素に対して「バブル処理」を行い、**配列中の最大要素を正しい位置へ交換します**。
|
||||
2. 次に、残りの $n - 1$ 個の要素に対して「バブル処理」を行い、**2 番目に大きい要素を正しい位置へ交換します**。
|
||||
3. このようにして、$n - 1$ 回の「バブル処理」を終えると、**大きいほうから $n - 1$ 個の要素がすべて正しい位置へ交換されます**。
|
||||
4. 残った 1 つの要素は必ず最小要素なので、並べ替える必要はなく、これで配列のソートが完了します。
|
||||
|
||||

|
||||

|
||||
|
||||
コード例は以下の通りです:
|
||||
コード例は次のとおりです。
|
||||
|
||||
```src
|
||||
[file]{bubble_sort}-[class]{}-[func]{bubble_sort}
|
||||
@@ -44,16 +44,16 @@
|
||||
|
||||
## 効率の最適化
|
||||
|
||||
「バブリング」のラウンド中に交換が発生しない場合、配列はすでにソートされているため、すぐに戻ることができます。これを検出するために、`flag`変数を追加できます;パスで交換が行われない場合は、フラグを設定して早期に戻ります。
|
||||
ある回の「バブル処理」で交換操作が一度も行われなければ、配列はすでにソート済みであり、結果をそのまま返せることがわかります。したがって、この状況を検出するためのフラグ `flag` を追加し、発生した時点で直ちに返すようにできます。
|
||||
|
||||
この最適化があっても、バブルソートの最悪時間計算量と平均時間計算量は$O(n^2)$のままです。ただし、入力配列がすでにソートされている場合、最良ケース時間計算量は$O(n)$まで低くなる可能性があります。
|
||||
最適化後も、バブルソートの最悪時間計算量と平均時間計算量は依然として $O(n^2)$ です。ただし、入力配列が完全に整列済みであれば、最良時間計算量は $O(n)$ に達します。
|
||||
|
||||
```src
|
||||
[file]{bubble_sort}-[class]{}-[func]{bubble_sort_with_flag}
|
||||
```
|
||||
|
||||
## アルゴリズムの特性
|
||||
## アルゴリズムの特徴
|
||||
|
||||
- **$O(n^2)$の時間計算量、適応ソート。** 各「バブリング」ラウンドは長さ$n - 1$、$n - 2$、$\dots$、$2$、$1$の配列セグメントを横断し、合計は$(n - 1) n / 2$となります。`flag`最適化により、配列がすでにソートされている場合、最良ケース時間計算量は$O(n)$に達する可能性があります。
|
||||
- **$O(1)$の空間計算量、インプレースソート。** ポインタ$i$と$j$によって定数量の追加空間のみが使用されます。
|
||||
- **安定ソート。** 等しい要素は「バブリング」中に交換されないため、元の順序が保持され、これは安定ソートになります。
|
||||
- **時間計算量は $O(n^2)$、適応的ソート**:各回の「バブル処理」で走査する配列の長さは順に $n - 1$、$n - 2$、$\dots$、$2$、$1$ であり、その総和は $(n - 1) n / 2$ です。`flag` による最適化を導入すると、最良時間計算量は $O(n)$ に達します。
|
||||
- **空間計算量は $O(1)$、インプレースソート**:ポインタ $i$ と $j$ は定数サイズの追加領域しか使用しません。
|
||||
- **安定ソート**:「バブル処理」では等しい要素に出会っても交換しないためです。
|
||||
|
||||
@@ -1,45 +1,45 @@
|
||||
# バケットソート
|
||||
|
||||
前述のソートアルゴリズムはすべて「比較ベースのソートアルゴリズム」で、値を比較することで要素をソートします。このようなソートアルゴリズムは $O(n \log n)$ より良い時間計算量を持つことはできません。次に、線形時間計算量を達成できるいくつかの「非比較ソートアルゴリズム」について議論します。
|
||||
前述のいくつかのソートアルゴリズムは、いずれも「比較ベースのソートアルゴリズム」に属し、要素間の大小を比較することで整列を実現します。この種のソートアルゴリズムの時間計算量は $O(n \log n)$ を超えられません。続いて、時間計算量が線形オーダーに達しうる「非比較ソートアルゴリズム」をいくつか見ていきます。
|
||||
|
||||
<u>バケットソート</u>は分割統治戦略の典型的な応用です。一連の順序付けられたバケットを設定し、各バケットがデータの範囲を含み、入力データをこれらのバケットに均等に分散させることで動作します。そして、各バケット内のデータを個別にソートします。最後に、すべてのバケットからのソート済みデータを順次マージして最終結果を生成します。
|
||||
<u>バケットソート(bucket sort)</u>は分割統治戦略の典型的な応用です。大小関係をもつ複数のバケットを用意し、各バケットがあるデータ範囲に対応するようにして、データを各バケットへ均等に分配します。その後、各バケット内でそれぞれソートを行い、最後にバケットの順序に従ってすべてのデータを結合します。
|
||||
|
||||
## アルゴリズムの過程
|
||||
## アルゴリズムの流れ
|
||||
|
||||
長さ $n$ の配列で、$[0, 1)$ の範囲の浮動小数点数を考えてみます。バケットソートの過程は以下の図に示されています。
|
||||
長さ $n$ の配列を考え、その要素は範囲 $[0, 1)$ の浮動小数点数であるとします。バケットソートの流れを以下の図に示します。
|
||||
|
||||
1. $k$ 個のバケットを初期化し、$n$ 個の要素をこれらの $k$ 個のバケットに分散させます。
|
||||
2. 各バケットを個別にソートします(プログラミング言語の組み込みソート関数を使用)。
|
||||
3. 最小から最大のバケットの順序で結果をマージします。
|
||||
1. $k$ 個のバケットを初期化し、$n$ 個の要素を $k$ 個のバケットに分配します。
|
||||
2. 各バケットに対してそれぞれソートを実行します(ここではプログラミング言語の組み込みソート関数を用います)。
|
||||
3. バケットを小さい順にたどって結果を結合します。
|
||||
|
||||

|
||||

|
||||
|
||||
コードは以下の通りです:
|
||||
コードは以下のとおりです:
|
||||
|
||||
```src
|
||||
[file]{bucket_sort}-[class]{}-[func]{bucket_sort}
|
||||
```
|
||||
|
||||
## アルゴリズムの特徴
|
||||
## アルゴリズムの特性
|
||||
|
||||
バケットソートは非常に大きなデータセットの処理に適しています。例えば、入力データに100万個の要素が含まれ、システムメモリの制限によりすべてのデータを同時にロードできない場合、データを1,000個のバケットに分割し、各バケットを個別にソートしてから結果をマージできます。
|
||||
バケットソートは、非常に大規模なデータの処理に適しています。たとえば、入力データに 100 万個の要素が含まれ、空間の制約によりシステムメモリへすべてのデータを一度に読み込めない場合です。このとき、データを 1000 個のバケットに分け、それぞれのバケットを個別にソートしてから、最後に結果を結合できます。
|
||||
|
||||
- **時間計算量は $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$ 個の要素のための追加スペースが必要です。
|
||||
- バケットソートが安定かどうかは、各バケット内で使用されるソートアルゴリズムが安定かどうかに依存します。
|
||||
- **時間計算量は $O(n + k)$** :要素が各バケット内に平均的に分布していると仮定すると、各バケット内の要素数は $\frac{n}{k}$ です。1 つのバケットをソートするのに $O(\frac{n}{k} \log\frac{n}{k})$ の時間がかかるなら、すべてのバケットのソートには $O(n \log\frac{n}{k})$ の時間がかかります。**バケット数 $k$ が十分大きいとき、時間計算量は $O(n)$ に近づきます**。結果を結合する際には、すべてのバケットと要素を走査する必要があり、$O(n + k)$ の時間を要します。最悪の場合、すべてのデータが 1 つのバケットに割り当てられ、そのバケットのソートに $O(n^2)$ の時間がかかります。
|
||||
- **空間計算量は $O(n + k)$、非インプレースソート**:$k$ 個のバケットと合計 $n$ 個の要素ぶんの追加領域が必要です。
|
||||
- バケットソートが安定かどうかは、バケット内要素のソートに用いるアルゴリズムが安定かどうかに依存します。
|
||||
|
||||
## 均等分散を達成する方法
|
||||
## 均等な分配を実現するには
|
||||
|
||||
バケットソートの理論的時間計算量は $O(n)$ に達することができます。**重要なことは、すべてのバケットに要素を均等に分散させることです**。実世界のデータはしばしば均一に分散されていないからです。例えば、eBayのすべての商品を価格範囲で10個のバケットに均等に分散させたいとします。しかし、商品価格の分散は均等でない可能性があり、100ドル未満の商品が多く、500ドル以上の商品が少ないかもしれません。価格範囲を均等に10分割すると、各バケットの商品数の差が大きくなります。
|
||||
バケットソートの時間計算量は理論上 $O(n)$ に達しますが、**鍵は要素を各バケットへ均等に分配すること** にあります。実際のデータは均一に分布していないことが多いからです。たとえば、Taobao 上のすべての商品を価格帯ごとに 10 個のバケットへ均等に分けたいとしても、商品の価格分布は偏っており、100 元未満は非常に多く、1000 元超は非常に少ないかもしれません。価格区間を単純に 10 等分すると、各バケットの商品数には大きな差が生じます。
|
||||
|
||||
均等分散を達成するために、最初におおよその境界を設定して、データを3つのバケットに大まかに分割できます。**分散が完了した後、より多くのアイテムを持つバケットをさらに3つのバケットに分割し、すべてのバケットの要素数がほぼ等しくなるまで続けます**。
|
||||
均等な分配を実現するために、まず大まかな境界線を設定し、データをひとまず 3 個のバケットに粗く振り分けます。**分配後は、商品数の多いバケットをさらに 3 個のバケットに分割し、すべてのバケット内の要素数がおおむね等しくなるまでこれを続けます**。
|
||||
|
||||
以下の図に示すように、この方法は本質的に再帰木を構築し、葉ノードの要素数ができるだけ均等になることを目指します。もちろん、各ラウンドでデータを3つのバケットに分割する必要はありません - 分割戦略はデータの独特な特性に適応的に調整できます。
|
||||
以下の図に示すように、この方法の本質は再帰木を構築することにあり、目標は葉ノードの値をできるだけ均等にすることです。もちろん、毎回データを 3 個のバケットに分割する必要はなく、具体的な分け方はデータの特徴に応じて柔軟に選べます。
|
||||
|
||||

|
||||

|
||||
|
||||
商品価格の確率分布を事前に知っている場合、**データの確率分布に基づいて各バケットの価格境界を設定できます**。データ分布を具体的に計算する必要は必ずしもなく、代わりに確率モデルを使用してデータ特性に基づいて近似できることに注意してください。
|
||||
商品価格の確率分布をあらかじめ把握しているなら、**データの確率分布に基づいて各バケットの価格境界を設定できます**。なお、データ分布は必ずしも特別に統計を取る必要はなく、データの特徴に応じて何らかの確率モデルで近似することもできます。
|
||||
|
||||
以下の図に示すように、商品価格が正規分布に従うと仮定すると、バケット間でアイテムの分散のバランスを取るために合理的な価格区間を定義できます。
|
||||
以下の図に示すように、商品価格が正規分布に従うと仮定すれば、価格区間を合理的に設定でき、それによって商品を各バケットへ均等に分配できます。
|
||||
|
||||

|
||||

|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
# 計数ソート
|
||||
|
||||
<u>計数ソート</u>は要素の数をカウントすることでソートを実現し、通常は整数配列に適用されます。
|
||||
<u>計数ソート(counting sort)</u>は要素数を集計することでソートを実現し、通常は整数配列に適用されます。
|
||||
|
||||
## 簡単な実装
|
||||
## 単純な実装
|
||||
|
||||
簡単な例から始めましょう。長さ $n$ の配列 `nums` が与えられ、すべての要素が「非負整数」である場合、計数ソートの全体的な過程は以下の図に示されています。
|
||||
まず簡単な例を見てみましょう。長さ $n$ の配列 `nums` が与えられ、その要素はすべて「非負整数」であるとします。計数ソートの全体的な流れを以下の図に示します。
|
||||
|
||||
1. 配列を走査して最大数を見つけ、それを $m$ とし、長さ $m + 1$ の補助配列 `counter` を作成します。
|
||||
2. **`counter` を使用して `nums` 内の各数の出現回数をカウントします**。ここで `counter[num]` は数 `num` の出現回数に対応します。カウント方法は簡単で、`nums` を走査し(現在の数を `num` とする)、各ラウンドで `counter[num]` を $1$ 増やします。
|
||||
3. **`counter` のインデックスは自然に順序付けられているため、すべての数は本質的にすでにソートされています**。次に、`counter` を走査し、出現順に `nums` を昇順で埋めます。
|
||||
1. 配列を走査し、その中の最大値を見つけて $m$ とし、続いて長さ $m + 1$ の補助配列 `counter` を作成します。
|
||||
2. **`counter` を用いて `nums` 内の各数値の出現回数を集計します**。ここで `counter[num]` は数値 `num` の出現回数に対応します。集計方法は非常に簡単で、`nums` を走査し(現在の数値を `num` とする)、各回で `counter[num]` を $1$ 増やせばよいです。
|
||||
3. **`counter` の各インデックスは自然に順序づけられているため、すべての数値はすでに整列された状態とみなせます**。続いて `counter` を走査し、各数値の出現回数に応じて小さい順に `nums` へ書き戻せば完了です。
|
||||
|
||||

|
||||

|
||||
|
||||
コードは以下の通りです:
|
||||
コードは以下のとおりです:
|
||||
|
||||
```src
|
||||
[file]{counting_sort}-[class]{}-[func]{counting_sort_naive}
|
||||
@@ -20,27 +20,27 @@
|
||||
|
||||
!!! note "計数ソートとバケットソートの関係"
|
||||
|
||||
バケットソートの観点から、計数ソートにおける計数配列 `counter` の各インデックスをバケットと考え、カウントの過程を要素を対応するバケットに分散させることと考えることができます。本質的に、計数ソートは整数データのためのバケットソートの特別なケースです。
|
||||
バケットソートの観点から見ると、計数ソートにおける計数配列 `counter` の各インデックスを 1 つのバケットとみなし、個数を数える過程を各要素を対応するバケットへ振り分ける操作とみなせます。本質的には、計数ソートは整数データにおけるバケットソートの特殊な一例です。
|
||||
|
||||
## 完全な実装
|
||||
|
||||
注意深い読者は気付くかもしれませんが、**入力データがオブジェクトの場合、上記の手順 `3.` は無効です**。入力データが商品オブジェクトで、価格(クラスメンバ変数)で商品をソートしたいとします。しかし、上記のアルゴリズムは結果としてソート済みの価格のみを提供できます。
|
||||
注意深い読者なら、**入力データがオブジェクトである場合、上記の手順 `3.` は機能しない**ことに気づくかもしれません。入力データが商品オブジェクトであり、商品価格(クラスのメンバ変数)に基づいて商品をソートしたいとします。しかし上記のアルゴリズムが返せるのは価格のソート結果だけです。
|
||||
|
||||
では、元のデータのソート結果をどのように取得できるでしょうか?まず、`counter` の「前置和」を計算します。名前が示すように、インデックス `i` での前置和 `prefix[i]` は、配列の最初の `i` 個の要素の和に等しいです:
|
||||
では、元のデータのソート結果を得るにはどうすればよいのでしょうか。まず `counter` の「累積和」を計算します。名前のとおり、インデックス `i` における累積和 `prefix[i]` は、配列の先頭から `i` 番目までの要素の総和に等しくなります:
|
||||
|
||||
$$
|
||||
\text{prefix}[i] = \sum_{j=0}^i \text{counter[j]}
|
||||
$$
|
||||
|
||||
**前置和には明確な意味があります。`prefix[num] - 1` は結果配列 `res` における要素 `num` の最後の出現のインデックスを表します**。この情報は重要で、各要素が結果配列のどこに現れるべきかを教えてくれます。次に、元の配列 `nums` の各要素 `num` を逆順で走査し、各反復で以下の2つの手順を実行します。
|
||||
**累積和には明確な意味があり、`prefix[num] - 1` は要素 `num` が結果配列 `res` に最後に現れるインデックスを表します**。この情報は非常に重要で、各要素が結果配列のどの位置に現れるべきかを示してくれます。続いて元の配列 `nums` を逆順に走査し、各要素 `num` に対して各反復で次の 2 つの手順を行います。
|
||||
|
||||
1. インデックス `prefix[num] - 1` で配列 `res` に `num` を埋めます。
|
||||
2. 前置和 `prefix[num]` を $1$ 減らして、`num` を配置する次のインデックスを取得します。
|
||||
1. `num` を配列 `res` のインデックス `prefix[num] - 1` に格納します。
|
||||
2. 累積和 `prefix[num]` を $1$ 減らし、次に `num` を配置するインデックスを得ます。
|
||||
|
||||
走査後、配列 `res` にはソートされた結果が含まれ、最後に `res` が元の配列 `nums` を置き換えます。完全な計数ソートの過程は以下の図に示されています。
|
||||
走査が完了すると、配列 `res` にソート済みの結果が格納されます。最後に `res` で元の配列 `nums` を上書きすれば完了です。以下の図は完全な計数ソートの流れを示しています。
|
||||
|
||||
=== "<1>"
|
||||

|
||||

|
||||
|
||||
=== "<2>"
|
||||

|
||||
@@ -63,22 +63,22 @@ $$
|
||||
=== "<8>"
|
||||

|
||||
|
||||
計数ソートの実装コードは以下の通りです:
|
||||
計数ソートの実装コードは以下のとおりです:
|
||||
|
||||
```src
|
||||
[file]{counting_sort}-[class]{}-[func]{counting_sort}
|
||||
```
|
||||
|
||||
## アルゴリズムの特徴
|
||||
## アルゴリズムの特性
|
||||
|
||||
- **時間計算量は $O(n + m)$、非適応ソート**:`nums` と `counter` の走査が含まれ、どちらも線形時間を使用します。一般的に、$n \gg m$ であり、時間計算量は $O(n)$ に近づきます。
|
||||
- **空間計算量は $O(n + m)$、非インプレースソート**:長さ $n$ の配列 `res` と長さ $m$ の配列 `counter` をそれぞれ使用します。
|
||||
- **安定ソート**:要素が「右から左」の順序で `res` に埋められるため、`nums` の走査を逆順にすることで、等しい要素間の相対位置の変化を防ぎ、安定したソートを実現できます。実際、`nums` を順番に走査しても正しいソート結果を生成できますが、結果は不安定です。
|
||||
- **時間計算量は $O(n + m)$、非適応ソート** :`nums` の走査と `counter` の走査が含まれ、いずれも線形時間です。一般には $n \gg m$ であり、時間計算量は $O(n)$ に近づきます。
|
||||
- **空間計算量は $O(n + m)$、非インプレースソート**:長さがそれぞれ $n$ と $m$ の配列 `res` と `counter` を利用します。
|
||||
- **安定ソート**:`res` に要素を埋める順序が「右から左」であるため、`nums` を逆順に走査することで等しい要素どうしの相対位置が変化するのを防ぎ、安定ソートを実現できます。実際には、`nums` を順方向に走査しても正しいソート結果は得られますが、その結果は安定ではありません。
|
||||
|
||||
## 制限事項
|
||||
## 制約
|
||||
|
||||
今までに、計数ソートは非常に巧妙だと感じるかもしれません。単に量をカウントするだけで効率的なソートを実現できるからです。しかし、計数ソートを使用するための前提条件は比較的厳しいです。
|
||||
ここまで読むと、計数ソートは非常に巧妙で、個数を数えるだけで効率的なソートを実現できると感じるかもしれません。しかし、計数ソートを利用するための前提条件は比較的厳格です。
|
||||
|
||||
**計数ソートは非負整数にのみ適用できます**。他のタイプのデータに適用したい場合、これらのデータが要素の元の順序を変更することなく非負整数に変換できることを保証する必要があります。例えば、負の整数を含む配列の場合、最初にすべての数に定数を加えて、すべてを正の数に変換し、ソート完了後に元に戻すことができます。
|
||||
**計数ソートは非負整数にしか適用できません**。ほかの型のデータに適用したい場合は、それらのデータを非負整数に変換でき、かつ変換の過程で要素間の相対的な大小関係が変わらないことを保証する必要があります。たとえば、負数を含む整数配列に対しては、すべての数値に定数を加えて正数へ変換し、ソート後に元へ戻すことができます。
|
||||
|
||||
**計数ソートは値の範囲が小さい大きなデータセットに適しています**。例えば、上記の例では、$m$ は大きすぎるべきではありません。そうでなければ、あまりにも多くのスペースを占有してしまいます。そして $n \ll m$ の場合、計数ソートは $O(m)$ 時間を使用し、$O(n \log n)$ ソートアルゴリズムより遅い可能性があります。
|
||||
**計数ソートはデータ量が多く、値域が小さい場合に適しています**。たとえば上記の例では $m$ が大きすぎてはならず、そうでないと過剰な空間を消費します。また、$n \ll m$ のとき、計数ソートは $O(m)$ 時間を要するため、$O(n \log n)$ のソートアルゴリズムより遅くなる可能性があります。
|
||||
|
||||
@@ -2,30 +2,30 @@
|
||||
|
||||
!!! tip
|
||||
|
||||
この節を読む前に、「ヒープ」の章を必ず完了させてください。
|
||||
本節を読む前に、「ヒープ」の章を学習済みであることを確認してください。
|
||||
|
||||
<u>ヒープソート</u>は、ヒープデータ構造に基づく効率的なソートアルゴリズムです。すでに学習した「ヒープの構築」と「要素の抽出」操作を使用してヒープソートを実装できます。
|
||||
<u>ヒープソート(heap sort)</u>は、ヒープデータ構造に基づいて実装される効率的なソートアルゴリズムです。すでに学んだ「ヒープ構築操作」と「要素の取り出し操作」を利用してヒープソートを実現できます。
|
||||
|
||||
1. 配列を入力し、最小ヒープを構築します。ここで、最小要素がヒープの頂上に位置します。
|
||||
2. 継続的に抽出操作を実行し、抽出された要素を順次記録して、最小から最大までのソート済みリストを取得します。
|
||||
1. 配列を入力して最小ヒープを構築すると、このとき最小要素はヒープの頂点にあります。
|
||||
2. 取り出し操作を繰り返し実行し、取り出された要素を順に記録すれば、昇順に並んだ列が得られます。
|
||||
|
||||
上記の方法は実現可能ですが、ポップされた要素を格納するための追加の配列が必要で、やや空間を消費します。実際には、通常、より優雅な実装を使用します。
|
||||
以上の方法でも実行できますが、取り出した要素を保存するために追加の配列が必要となり、空間をやや無駄にします。実際には、通常はより洗練された実装方法を用います。
|
||||
|
||||
## アルゴリズムの流れ
|
||||
|
||||
配列の長さを $n$ とすると、ヒープソートの過程は以下の通りです。
|
||||
配列の長さを $n$ とすると、ヒープソートの流れは次図のとおりです。
|
||||
|
||||
1. 配列を入力し、最大ヒープを構築します。この手順の後、最大要素がヒープの頂上に位置します。
|
||||
2. ヒープの頂上要素(最初の要素)とヒープの底部要素(最後の要素)を交換します。この交換の後、ヒープの長さを $1$ 減らし、ソート済み要素の数を $1$ 増やします。
|
||||
3. ヒープの頂上から開始して、上から下へのsift-down操作を実行します。sift-downの後、ヒープの性質が復元されます。
|
||||
4. 手順 `2.` と `3.` を繰り返します。$n - 1$ ラウンドループして、配列のソートを完了します。
|
||||
1. 配列を入力して最大ヒープを構築します。完了後、最大要素はヒープの頂点にあります。
|
||||
2. ヒープ頂点の要素(最初の要素)とヒープ末尾の要素(最後の要素)を交換します。交換後、ヒープの長さは $1$ 減少し、整列済み要素数は $1$ 増加します。
|
||||
3. ヒープ頂点の要素から始めて、上から下へヒープ化操作(sift down)を実行します。ヒープ化が完了すると、ヒープの性質が回復します。
|
||||
4. 第 `2.` ステップと第 `3.` ステップを繰り返し実行します。これを $n - 1$ 回繰り返すと、配列の整列が完了します。
|
||||
|
||||
!!! tip
|
||||
|
||||
実際、要素抽出操作も手順 `2.` と `3.` を含み、抽出された要素をヒープから削除する追加の手順があります。
|
||||
実際には、要素の取り出し操作にも第 `2.` ステップと第 `3.` ステップが含まれており、要素を取り出す処理が 1 つ加わるだけです。
|
||||
|
||||
=== "<1>"
|
||||

|
||||

|
||||
|
||||
=== "<2>"
|
||||

|
||||
@@ -60,14 +60,14 @@
|
||||
=== "<12>"
|
||||

|
||||
|
||||
コードの実装では、「ヒープ」の章からのsift-down関数 `sift_down()` を使用しました。最大要素が抽出されるにつれてヒープの長さが減少するため、`sift_down()` 関数に長さパラメータ $n$ を追加して、ヒープの現在の有効長を指定する必要があることに注意することが重要です。コードは以下の通りです:
|
||||
コード実装では、「ヒープ」の章と同じ上から下へのヒープ化 `sift_down()` 関数を使用します。注意すべき点として、ヒープの長さは最大要素を取り出すたびに短くなるため、`sift_down()` 関数に長さパラメータ $n$ を追加し、ヒープの現在の有効な長さを指定する必要があります。コードは以下のとおりです。
|
||||
|
||||
```src
|
||||
[file]{heap_sort}-[class]{}-[func]{heap_sort}
|
||||
```
|
||||
|
||||
## アルゴリズムの特徴
|
||||
## アルゴリズムの特性
|
||||
|
||||
- **時間計算量は $O(n \log n)$、非適応ソート**:ヒープの構築は $O(n)$ 時間を使用します。ヒープから最大要素を抽出するには $O(\log n)$ 時間がかかり、$n - 1$ ラウンドループします。
|
||||
- **空間計算量は $O(1)$、インプレースソート**:いくつかのポインタ変数が $O(1)$ 空間を使用します。要素の交換とヒープ化操作は元の配列で実行されます。
|
||||
- **非安定ソート**:ヒープの頂上と底部要素の交換中に、等しい要素の相対位置が変わる可能性があります。
|
||||
- **時間計算量は $O(n \log n)$、非適応ソート**:ヒープ構築操作には $O(n)$ の時間がかかります。ヒープから最大要素を取り出す時間計算量は $O(\log n)$ であり、これを合計 $n - 1$ 回繰り返します。
|
||||
- **空間計算量は $O(1)$、インプレースソート**:いくつかのポインタ変数が使う空間は $O(1)$ です。要素の交換とヒープ化操作はいずれも元の配列上で行われます。
|
||||
- **非安定ソート**:ヒープ頂点の要素とヒープ末尾の要素を交換する際、等しい要素どうしの相対位置が変化する可能性があります。
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# ソート
|
||||
|
||||

|
||||

|
||||
|
||||
!!! abstract
|
||||
|
||||
ソートは混沌を秩序に変える魔法の鍵のようなもので、データをより効率的に理解し処理することを可能にします。
|
||||
ソートは、混沌を秩序へと変える魔法の鍵のようなものであり、データをより効率的に理解し処理することを可能にします。
|
||||
|
||||
単純な昇順であろうと複雑なカテゴリ配列であろうと、ソートはデータの調和美を明らかにします。
|
||||
単純な昇順であれ、複雑な分類配列であれ、ソートはデータの調和のとれた美しさを私たちに示してくれます。
|
||||
|
||||
@@ -1,46 +1,46 @@
|
||||
# 挿入ソート
|
||||
|
||||
<u>挿入ソート</u>は、トランプのデッキを手動でソートするプロセスによく似た動作をするシンプルなソートアルゴリズムです。
|
||||
<u>挿入ソート(insertion sort)</u>は単純なソートアルゴリズムであり、その動作原理は手作業でトランプの山を整える過程と非常によく似ています。
|
||||
|
||||
具体的には、未ソート区間からベース要素を選択し、その左側のソート済み区間の要素と比較して、要素を正しい位置に挿入します。
|
||||
具体的には、未ソート区間から基準要素を 1 つ選び、その要素を左側の整列済み区間の要素と 1 つずつ比較し、正しい位置に挿入します。
|
||||
|
||||
下図は、要素が配列に挿入される方法を示しています。ベース要素を`base`とすると、ターゲットインデックスから`base`までのすべての要素を右に1つずつシフトし、その後`base`をターゲットインデックスに割り当てる必要があります。
|
||||
以下の図は、配列に要素を挿入する操作の流れを示しています。基準要素を `base` とすると、目的のインデックスから `base` までのすべての要素を 1 つずつ右に移動し、その後 `base` を目的のインデックスに代入する必要があります。
|
||||
|
||||

|
||||

|
||||
|
||||
## アルゴリズムプロセス
|
||||
## アルゴリズムの流れ
|
||||
|
||||
挿入ソートの全体的なプロセスは下図に示されます。
|
||||
挿入ソート全体の流れを以下の図に示します。
|
||||
|
||||
1. 配列の最初の要素をソート済みとみなします。
|
||||
2. 2番目の要素を`base`として選択し、正しい位置に挿入して、**最初の2つの要素をソート済みにします**。
|
||||
3. 3番目の要素を`base`として選択し、正しい位置に挿入して、**最初の3つの要素をソート済みにします**。
|
||||
4. この方法で続行し、最後の反復では、最後の要素を`base`として取り、正しい位置に挿入した後、**すべての要素がソートされます**。
|
||||
1. 初期状態では、配列の 1 番目の要素はすでに整列済みです。
|
||||
2. 配列の 2 番目の要素を `base` として選び、正しい位置に挿入すると、**配列の先頭 2 要素が整列済み**になります。
|
||||
3. 3 番目の要素を `base` として選び、正しい位置に挿入すると、**配列の先頭 3 要素が整列済み**になります。
|
||||
4. このように繰り返し、最後のラウンドで最後の要素を `base` として選んで正しい位置に挿入すると、**すべての要素が整列済み**になります。
|
||||
|
||||

|
||||

|
||||
|
||||
コード例は以下の通りです:
|
||||
コード例は以下のとおりです。
|
||||
|
||||
```src
|
||||
[file]{insertion_sort}-[class]{}-[func]{insertion_sort}
|
||||
```
|
||||
|
||||
## アルゴリズムの特性
|
||||
## アルゴリズムの特徴
|
||||
|
||||
- **時間計算量は$O(n^2)$、適応ソート**:最悪の場合、各挿入操作には$n - 1$、$n-2$、...、$2$、$1$のループが必要で、合計は$(n - 1) n / 2$となり、時間計算量は$O(n^2)$です。順序付きデータの場合、挿入操作は早期に終了します。入力配列が完全に順序付けられている場合、挿入ソートは最良時間計算量$O(n)$を実現します。
|
||||
- **空間計算量は$O(1)$、インプレースソート**:ポインタ$i$と$j$は定数量の追加空間を使用します。
|
||||
- **安定ソート**:挿入操作中、等しい要素の右側に要素を挿入し、順序を変更しません。
|
||||
- **計算量は $O(n^2)$、適応的ソート**:最悪の場合、各挿入操作ではそれぞれ $n - 1$、$n-2$、$\dots$、$2$、$1$ 回のループが必要であり、合計は $(n - 1) n / 2$ となるため、時間計算量は $O(n^2)$ です。データが整列済みであれば、挿入操作は早期に終了します。入力配列が完全に整列済みである場合、挿入ソートは最良の時間計算量 $O(n)$ に達します。
|
||||
- **空間計算量は $O(1)$、インプレースソート**:ポインタ $i$ と $j$ は定数サイズの追加領域しか使用しません。
|
||||
- **安定ソート**:挿入操作の過程では、要素を等しい要素の右側に挿入するため、それらの順序は変化しません。
|
||||
|
||||
## 挿入ソートの利点
|
||||
|
||||
挿入ソートの時間計算量は$O(n^2)$で、次に学習するクイックソートの時間計算量は$O(n \log n)$です。挿入ソートはより高い時間計算量を持ちますが、**小さな入力サイズでは通常より高速です**。
|
||||
挿入ソートの時間計算量は $O(n^2)$ であり、これから学ぶクイックソートの時間計算量は $O(n \log n)$ です。挿入ソートの時間計算量のほうが大きいにもかかわらず、**データ量が小さい場合には、挿入ソートのほうが通常は高速**です。
|
||||
|
||||
この結論は線形探索と二分探索の結論と似ています。時間計算量が$O(n \log n)$で分割統治戦略に基づくクイックソートなどのアルゴリズムは、多くの場合より多くの単位操作を含みます。小さな入力サイズでは、$n^2$と$n \log n$の数値は近く、計算量が支配的でなく、ラウンドあたりの単位操作数が決定的な役割を果たします。
|
||||
この結論は、線形探索と二分探索の適用条件に関する結論と似ています。クイックソートのような $O(n \log n)$ のアルゴリズムは分割統治法に基づくソートアルゴリズムであり、一般により多くの基本演算を含みます。一方、データ量が小さい場合は、$n^2$ と $n \log n$ の値は比較的近く、計算量が支配的ではなくなり、各ラウンドにおける基本演算の回数が決定的な役割を果たします。
|
||||
|
||||
実際、多くのプログラミング言語(Javaなど)は、組み込みソート関数内で挿入ソートを使用しています。一般的なアプローチは:長い配列に対しては、クイックソートなどの分割統治戦略に基づくソートアルゴリズムを使用し、短い配列に対しては挿入ソートを直接使用します。
|
||||
実際、多くのプログラミング言語(たとえば Java)の組み込みソート関数では挿入ソートが採用されており、その大まかな考え方は次のとおりです。長い配列にはクイックソートなどの分割統治法に基づくソートアルゴリズムを使い、短い配列には直接挿入ソートを使います。
|
||||
|
||||
バブルソート、選択ソート、挿入ソートはすべて時間計算量$O(n^2)$を持ちますが、実際には、**挿入ソートはバブルソートや選択ソートよりも一般的に使用されます**。主な理由は以下の通りです。
|
||||
バブルソート、選択ソート、挿入ソートはいずれも時間計算量が $O(n^2)$ ですが、実際には、**挿入ソートはバブルソートや選択ソートよりもはるかに高い頻度で使われます**。主な理由は次のとおりです。
|
||||
|
||||
- バブルソートは要素交換に基づき、一時変数の使用が必要で、3つの単位操作を含みます;挿入ソートは要素代入に基づき、1つの単位操作のみが必要です。したがって、**バブルソートの計算オーバーヘッドは一般的に挿入ソートよりも高くなります**。
|
||||
- 選択ソートの時間計算量は常に$O(n^2)$です。**部分的に順序付けられたデータのセットが与えられた場合、挿入ソートは通常選択ソートよりも効率的です**。
|
||||
- 選択ソートは不安定で、マルチレベルソートに適用できません。
|
||||
- バブルソートは要素の交換によって実装され、1 つの一時変数を必要とするため、合計で 3 回の基本演算が関わります。これに対して、挿入ソートは要素の代入に基づいており、必要な基本演算は 1 回だけです。したがって、**バブルソートの計算コストは通常、挿入ソートより高くなります**。
|
||||
- 選択ソートの時間計算量はどのような場合でも $O(n^2)$ です。**部分的に整列されたデータが与えられた場合、挿入ソートは通常、選択ソートより効率的**です。
|
||||
- 選択ソートは安定ではないため、多段ソートには適用できません。
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
# マージソート
|
||||
|
||||
<u>マージソート</u>は分割統治戦略に基づくソートアルゴリズムで、下図に示す「分割」と「マージ」フェーズを含みます。
|
||||
<u>マージソート(merge sort)</u>は分割統治戦略に基づくソートアルゴリズムであり、以下の図に示す「分割」と「マージ」の段階から構成されます。
|
||||
|
||||
1. **分割フェーズ**:中点から配列を再帰的に分割し、長い配列のソート問題をより短い配列に変換します。
|
||||
2. **マージフェーズ**:サブ配列の長さが1になったときに分割を停止し、その後マージを開始します。2つの短いソート済み配列を連続的により長いソート済み配列にマージし、プロセスが完了するまで続行します。
|
||||
1. **分割段階**:再帰によって配列を中点で繰り返し分割し、長い配列のソート問題を短い配列のソート問題へ変換します。
|
||||
2. **マージ段階**:部分配列の長さが 1 になったら分割を終了し、マージを開始して、左右 2 つの短いソート済み配列をより長いソート済み配列へと繰り返しマージしていきます。
|
||||
|
||||

|
||||

|
||||
|
||||
## アルゴリズムワークフロー
|
||||
## アルゴリズムの流れ
|
||||
|
||||
下図に示すように、「分割フェーズ」は中点から配列を上から下に2つのサブ配列に再帰的に分割します。
|
||||
以下の図に示すように、「分割段階」では配列を上から下へ再帰的に中点で 2 つの部分配列へ分割します。
|
||||
|
||||
1. 中点`mid`を計算し、左サブ配列(区間`[left, mid]`)と右サブ配列(区間`[mid + 1, right]`)を再帰的に分割します。
|
||||
2. サブ配列の長さが1になるまでステップ`1.`を再帰的に続行し、その後停止します。
|
||||
1. 配列の中点 `mid` を計算し、左部分配列(区間 `[left, mid]` )と右部分配列(区間 `[mid + 1, right]` )を再帰的に分割します。
|
||||
2. 手順 `1.` を再帰的に実行し、部分配列区間の長さが 1 になった時点で終了します。
|
||||
|
||||
「マージフェーズ」は左と右のサブ配列を下から上にソート済み配列に結合します。重要なのは、マージが長さ1のサブ配列から開始され、マージフェーズ中に各サブ配列がソートされることです。
|
||||
「マージ段階」では左部分配列と右部分配列を下から上へとマージし、1 つのソート済み配列にします。長さ 1 の部分配列からマージを始めるため、この段階の各部分配列はすでに整列されています。
|
||||
|
||||
=== "<1>"
|
||||

|
||||

|
||||
|
||||
=== "<2>"
|
||||

|
||||
@@ -46,12 +46,12 @@
|
||||
=== "<10>"
|
||||

|
||||
|
||||
マージソートの再帰順序は二分木の後順横断と一致することが観察できます。
|
||||
観察すると、マージソートの再帰順序は二分木の後順走査と一致していることがわかります。
|
||||
|
||||
- **後順横断**:まず左のサブツリーを再帰的に横断し、次に右のサブツリーを横断し、最後にルートノードを処理します。
|
||||
- **マージソート**:まず左のサブ配列を再帰的に処理し、次に右のサブ配列を処理し、最後にマージを実行します。
|
||||
- **後順走査**:まず左部分木を再帰し、次に右部分木を再帰し、最後に根ノードを処理します。
|
||||
- **マージソート**:まず左部分配列を再帰し、次に右部分配列を再帰し、最後にマージを処理します。
|
||||
|
||||
マージソートの実装は以下のコードに示されます。`nums`でマージされる区間は`[left, right]`で、`tmp`の対応する区間は`[0, right - left]`であることに注意してください。
|
||||
マージソートの実装を以下のコードに示します。注意として、`nums` のマージ対象区間は `[left, right]` であり、`tmp` の対応区間は `[0, right - left]` です。
|
||||
|
||||
```src
|
||||
[file]{merge_sort}-[class]{}-[func]{merge_sort}
|
||||
@@ -59,15 +59,15 @@
|
||||
|
||||
## アルゴリズムの特性
|
||||
|
||||
- **$O(n \log n)$の時間計算量、非適応ソート**:分割により高さ$\log n$の再帰ツリーが作成され、各層で合計$n$回の操作をマージし、全体的な時間計算量は$O(n \log n)$となります。
|
||||
- **$O(n)$の空間計算量、非インプレースソート**:再帰深度は$\log n$で、$O(\log n)$のスタックフレーム空間を使用します。マージ操作には補助配列が必要で、追加の$O(n)$空間を使用します。
|
||||
- **安定ソート**:マージプロセス中、等しい要素の順序は変更されません。
|
||||
- **時間計算量は $O(n \log n)$、非適応型ソート**:分割によって高さ $\log n$ の再帰木が生成され、各層でのマージ操作の総数は $n$ であるため、全体の時間計算量は $O(n \log n)$ です。
|
||||
- **空間計算量は $O(n)$、インプレースではないソート**:再帰の深さは $\log n$ であり、サイズ $O(\log n)$ のスタックフレーム領域を使用します。マージ操作は補助配列を用いて実装する必要があり、サイズ $O(n)$ の追加領域を使用します。
|
||||
- **安定ソート**:マージの過程では、等しい要素の順序は変化しません。
|
||||
|
||||
## 連結リストのソート
|
||||
|
||||
連結リストの場合、マージソートは他のソートアルゴリズムよりも大きな利点があります。**連結リストソートタスクの空間計算量を$O(1)$に最適化できます**。
|
||||
連結リストに対しては、マージソートは他のソートアルゴリズムと比べて顕著な利点があり、**連結リストのソート問題の空間計算量を $O(1)$ まで最適化できます** 。
|
||||
|
||||
- **分割フェーズ**:「再帰」の代わりに「反復」を使用して連結リスト分割作業を実行できるため、再帰で使用されるスタックフレーム空間を節約できます。
|
||||
- **マージフェーズ**:連結リストでは、ノードの挿入と削除操作は参照(ポインタ)を変更することで実現できるため、マージフェーズ(2つの短い順序付きリストを1つの長い順序付きリストに結合)中に追加のリストを作成する必要がありません。
|
||||
- **分割段階**:連結リストの分割は「再帰」の代わりに「反復」で実装できるため、再帰で使用するスタックフレーム領域を省けます。
|
||||
- **マージ段階**:連結リストでは、ノードの追加や削除は参照(ポインタ)を変更するだけで実現できるため、マージ段階(2 つの短いソート済み連結リストを 1 つの長いソート済み連結リストにマージすること)では追加の連結リストを作成する必要がありません。
|
||||
|
||||
実装の詳細は比較的複雑で、興味のある読者は関連資料を参照して学習してください。
|
||||
具体的な実装の詳細は比較的複雑なので、興味のある読者は関連資料を参照して学習してください。
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
# クイックソート
|
||||
|
||||
<u>クイックソート</u>は分割統治戦略に基づくソートアルゴリズムで、その効率性と幅広い応用で知られています。
|
||||
<u>クイックソート(quick sort)</u>は分割統治戦略に基づくソートアルゴリズムであり、実行効率が高く、広く利用されています。
|
||||
|
||||
クイックソートのコア操作は「ピボット分割」で、配列から要素を「ピボット」として選択し、ピボットより小さいすべての要素をその左側に移動し、ピボットより大きいすべての要素をその右側に移動することを目的としています。具体的に、ピボット分割のプロセスは下図に示されます。
|
||||
クイックソートの中核操作は「パーティション」であり、その目的は、配列内のある要素を「基準数」として選び、基準数より小さいすべての要素を左側へ、大きい要素を右側へ移動することです。具体的には、パーティションの流れを下図に示します。
|
||||
|
||||
1. 配列の最も左の要素をピボットとして選択し、2つのポインタ`i`と`j`を初期化して配列の両端をそれぞれ指すようにします。
|
||||
2. 各ラウンドで`i`(`j`)を使用してピボットより大きい(小さい)最初の要素を探索し、次にこれら2つの要素を交換するループを設定します。
|
||||
3. `i`と`j`が出会うまでステップ`2.`を繰り返し、最後にピボットを2つのサブ配列の境界に交換します。
|
||||
1. 配列の最左端の要素を基準数として選び、2 つのポインタ `i` と `j` を初期化して、それぞれ配列の両端を指すようにします。
|
||||
2. ループを設定し、各ラウンドで `i`(`j`)を使ってそれぞれ基準数より大きい(小さい)最初の要素を探し、その後この 2 つの要素を交換します。
|
||||
3. `i` と `j` が出会うまでステップ `2.` を繰り返し、最後に基準数を 2 つの部分配列の境界へ交換します。
|
||||
|
||||
=== "<1>"
|
||||

|
||||

|
||||
|
||||
=== "<2>"
|
||||

|
||||
@@ -35,66 +35,65 @@
|
||||
=== "<9>"
|
||||

|
||||
|
||||
ピボット分割後、元の配列は3つの部分に分割されます:左サブ配列、ピボット、右サブ配列で、「左サブ配列の任意の要素 $\leq$ ピボット $\leq$ 右サブ配列の任意の要素」を満たします。したがって、これら2つのサブ配列のみをソートすればよいのです。
|
||||
パーティションが完了すると、元の配列は左部分配列、基準数、右部分配列の 3 つに分けられ、「左部分配列の任意の要素 $\leq$ 基準数 $\leq$ 右部分配列の任意の要素」を満たします。したがって、次はこの 2 つの部分配列だけをソートすれば済みます。
|
||||
|
||||
!!! note "クイックソートの分割統治戦略"
|
||||
|
||||
ピボット分割の本質は、より長い配列のソート問題をより短い2つの配列に簡素化することです。
|
||||
パーティションの本質は、長い配列のソート問題を 2 つの短い配列のソート問題へ簡略化することです。
|
||||
|
||||
```src
|
||||
[file]{quick_sort}-[class]{quick_sort}-[func]{partition}
|
||||
```
|
||||
|
||||
## アルゴリズムプロセス
|
||||
## アルゴリズムの流れ
|
||||
|
||||
クイックソートの全体的なプロセスは下図に示されます。
|
||||
クイックソート全体の流れを下図に示します。
|
||||
|
||||
1. まず、元の配列に対して「ピボット分割」を実行し、未ソートの左と右のサブ配列を取得します。
|
||||
2. 次に、左と右のサブ配列に対してそれぞれ再帰的に「ピボット分割」を実行します。
|
||||
3. サブ配列の長さが1になるまで再帰を続け、配列全体のソートを完了します。
|
||||
1. まず、元の配列に対して 1 回「パーティション」を実行し、未ソートの左部分配列と右部分配列を得ます。
|
||||
2. 次に、左部分配列と右部分配列に対してそれぞれ再帰的に「パーティション」を実行します。
|
||||
3. 部分配列の長さが 1 になるまで再帰を続け、配列全体のソートを完了します。
|
||||
|
||||

|
||||

|
||||
|
||||
```src
|
||||
[file]{quick_sort}-[class]{quick_sort}-[func]{quick_sort}
|
||||
```
|
||||
|
||||
## アルゴリズムの特徴
|
||||
## アルゴリズムの特性
|
||||
|
||||
- **$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)$のスタックフレーム空間を使用します。ソート操作は追加の配列の助けなしに元の配列で実行されます。
|
||||
- **非安定ソート**:ピボット分割の最終ステップで、ピボットは等しい要素の右側に交換される可能性があります。
|
||||
- **時間計算量は $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)$ のスタックフレーム空間を使用します。ソート操作は元の配列上で行われ、追加の配列は用いません。
|
||||
- **非安定ソート**:パーティションの最後のステップで、基準数が等しい要素の右側へ交換される可能性があります。
|
||||
|
||||
## なぜクイックソートは高速なのか
|
||||
## クイックソートが速い理由
|
||||
|
||||
名前が示すように、クイックソートは効率性の面で一定の利点を持つべきです。クイックソートの平均時間計算量は「マージソート」や「ヒープソート」と同じですが、以下の理由で一般的により効率的です。
|
||||
名前からも分かるように、クイックソートは効率面で一定の優位性を持っています。クイックソートの平均時間計算量は「マージソート」や「ヒープソート」と同じですが、通常はクイックソートのほうが高効率であり、主な理由は次のとおりです。
|
||||
|
||||
- **最悪ケースシナリオの低い確率**:クイックソートの最悪時間計算量は$O(n^2)$で、マージソートほど安定していませんが、ほとんどの場合、クイックソートは$O(n \log n)$の時間計算量で動作できます。
|
||||
- **高いキャッシュ利用率**:ピボット分割操作中、システムはサブ配列全体をキャッシュにロードできるため、要素により効率的にアクセスできます。対照的に、「ヒープソート」などのアルゴリズムは要素にジャンプ方式でアクセスする必要があり、この特徴を欠いています。
|
||||
- **計算量の小さな定数係数**:上記3つのアルゴリズムの中で、クイックソートは比較、代入、交換などの操作の総数が最も少ないです。これは「挿入ソート」が「バブルソート」よりも高速な理由と似ています。
|
||||
- **最悪ケースが起こる確率が低い**:クイックソートの最悪時間計算量は $O(n^2)$ で、「マージソート」ほど安定ではありませんが、大半のケースでは $O(n \log n)$ の時間計算量で動作します。
|
||||
- **キャッシュ利用効率が高い**:パーティション操作の実行時には、システムが部分配列全体をキャッシュに読み込めるため、要素アクセスの効率が高くなります。一方、「ヒープソート」のようなアルゴリズムは要素へ飛び飛びにアクセスする必要があり、この性質を持ちません。
|
||||
- **計算量の定数係数が小さい**:上記 3 つのアルゴリズムの中で、クイックソートは比較、代入、交換などの操作総数が最も少なくなります。これは「挿入ソート」が「バブルソート」より速い理由と似ています。
|
||||
|
||||
## ピボット最適化
|
||||
## 基準数の最適化
|
||||
|
||||
**クイックソートの時間効率は特定の入力で劣化する可能性があります**。例えば、入力配列が完全に逆順の場合、最も左の要素をピボットとして選択するため、ピボット分割後、ピボットは配列の右端に交換され、左サブ配列の長さが$n - 1$、右サブ配列の長さが$0$になります。この方法を続けると、各ラウンドのピボット分割でサブ配列の長さが$0$になり、分割統治戦略が失敗し、クイックソートは「バブルソート」に似た形に劣化します。
|
||||
**クイックソートは、入力によっては時間効率が低下する可能性があります**。極端な例として、入力配列が完全な逆順であるとします。最左端の要素を基準数として選ぶため、パーティション完了後には基準数が配列の最右端へ交換され、左部分配列の長さが $n - 1$、右部分配列の長さが $0$ になります。この再帰を続けると、各回のパーティション後に必ず一方の部分配列の長さが $0$ となり、分割統治戦略が機能せず、クイックソートは「バブルソート」に近い形へ退化します。
|
||||
|
||||
この状況を避けるため、**ピボット分割でピボット選択戦略を最適化できます**。例えば、要素をランダムに選択してピボットとすることができます。ただし、運が悪く、一貫して最適でないピボットを選択した場合、効率はまだ満足できません。
|
||||
この状況をできるだけ避けるため、**パーティションにおける基準数の選び方を最適化できます**。たとえば、ランダムに 1 つの要素を選んで基準数にできます。しかし、運が悪く毎回望ましくない基準数を選んでしまうと、効率は依然として十分ではありません。
|
||||
|
||||
プログラミング言語は通常「疑似乱数」を生成することに注意することが重要です。疑似乱数シーケンスに対して特定のテストケースを構築すると、クイックソートの効率はまだ劣化する可能性があります。
|
||||
注意すべき点として、プログラミング言語が通常生成するのは「疑似乱数」です。疑似乱数列に合わせて特定のテストケースを構築すると、クイックソートの効率はやはり劣化する可能性があります。
|
||||
|
||||
さらなる改善のため、3つの候補要素(通常は配列の最初、最後、中点の要素)を選択し、**これら3つの候補要素の中央値をピボットとして使用**できます。この方法で、ピボットが「小さすぎず大きすぎない」確率が大幅に増加します。もちろん、さらに多くの候補要素を選択してアルゴリズムの堅牢性をさらに向上させることもできます。この方法により、時間計算量が$O(n^2)$に劣化する確率が大幅に削減されます。
|
||||
さらに改善するために、配列から 3 つの候補要素(通常は先頭、末尾、中間の要素)を選び、**その 3 つの候補要素の中央値を基準数とする**ことができます。こうすると、基準数が「小さすぎず大きすぎもしない」確率が大幅に上がります。もちろん、候補要素をさらに増やして、アルゴリズムの頑健性をいっそう高めることも可能です。この方法を採用すると、時間計算量が $O(n^2)$ まで劣化する確率は大きく下がります。
|
||||
|
||||
サンプルコードは以下の通りです:
|
||||
コード例を以下に示します。
|
||||
|
||||
```src
|
||||
[file]{quick_sort}-[class]{quick_sort_median}-[func]{partition}
|
||||
```
|
||||
|
||||
## 末尾再帰最適化
|
||||
## 再帰の深さの最適化
|
||||
|
||||
**特定の入力では、クイックソートはより多くの空間を占有する可能性があります**。例えば、完全に順序付けられた入力配列を考えてみましょう。再帰でのサブ配列の長さを$m$とします。各ラウンドのピボット分割で、長さ$0$の左サブ配列と長さ$m - 1$の右サブ配列が生成されます。これは、再帰呼び出しごとに問題サイズが1つの要素のみ減少することを意味し、各レベルの再帰での削減が非常に小さくなります。
|
||||
結果として、再帰ツリーの高さは$n − 1$に達する可能性があり、これには$O(n)$のスタックフレーム空間が必要です。
|
||||
**一部の入力では、クイックソートは多くの空間を消費する可能性があります**。完全に整列済みの入力配列を例にとり、再帰中の部分配列の長さを $m$ とします。各回のパーティション操作では長さ $0$ の左部分配列と長さ $m - 1$ の右部分配列が生成されます。これは、各再帰呼び出しで減る問題サイズが非常に小さいこと(要素が 1 つ減るだけ)を意味し、再帰木の高さは $n - 1$ に達するため、このとき $O(n)$ のスタックフレーム空間を占有します。
|
||||
|
||||
スタックフレーム空間の蓄積を防ぐため、各ラウンドのピボットソート後に2つのサブ配列の長さを比較し、**より短いサブ配列のみを再帰的にソート**できます。より短いサブ配列の長さは$n / 2$を超えないため、この方法は再帰深度が$\log n$を超えないことを保証し、最悪空間計算量を$O(\log n)$に最適化します。コードは以下の通りです:
|
||||
スタックフレーム空間の蓄積を防ぐために、各回のパーティション完了後に 2 つの部分配列の長さを比較し、**短いほうの部分配列に対してのみ再帰**を行えます。短い部分配列の長さは $n / 2$ を超えないため、この方法なら再帰の深さを $\log n$ 以下に抑えられ、最悪時の空間計算量を $O(\log n)$ まで最適化できます。コードを以下に示します。
|
||||
|
||||
```src
|
||||
[file]{quick_sort}-[class]{quick_sort_tail_call}-[func]{quick_sort}
|
||||
|
||||
@@ -1,41 +1,41 @@
|
||||
# 基数ソート
|
||||
|
||||
前の節では計数ソートを紹介しました。これは、データサイズ $n$ が大きいがデータ範囲 $m$ が小さいシナリオに適しています。$n = 10^6$ の学生IDをソートする必要があり、各IDが $8$ 桁の数字であるとします。これは、データ範囲 $m = 10^8$ が非常に大きいことを意味します。この場合、計数ソートを使用すると、大量のメモリスペースが必要になります。基数ソートはこの状況を回避できます。
|
||||
前節では計数ソートを紹介しました。これは、データ量 $n$ が大きく、データ範囲 $m$ が小さい場合に適しています。$n = 10^6$ 個の学籍番号をソートすると仮定すると、学籍番号は $8$ 桁の数字なので、データ範囲 $m = 10^8$ は非常に大きくなります。計数ソートでは大量のメモリ空間を確保する必要がありますが、基数ソートではこの問題を回避できます。
|
||||
|
||||
<u>基数ソート</u>は計数ソートと同じ核心概念を共有し、要素の頻度をカウントすることでソートします。同時に、基数ソートは数字の桁間の漸進的関係を利用してこれを基盤としています。桁を一度に一つずつ処理してソートし、最終的なソート順序を達成します。
|
||||
<u>基数ソート(radix sort)</u>の基本的な考え方は計数ソートと同じで、個数を数えることによってソートを実現します。そのうえで、基数ソートは各桁の段階的な関係を利用し、各桁を順にソートすることで、最終的なソート結果を得ます。
|
||||
|
||||
## アルゴリズムの過程
|
||||
## アルゴリズムの流れ
|
||||
|
||||
学生IDデータを例として、最下位桁を $1$ 番目、最上位桁を $8$ 番目とすると、基数ソートの過程は以下の図に示されています。
|
||||
学籍番号データを例にすると、数字の最下位桁を第 $1$ 位、最上位桁を第 $8$ 位としたとき、基数ソートの流れは次図のようになります。
|
||||
|
||||
1. 桁 $k = 1$ を初期化します。
|
||||
2. 学生IDの $k$ 番目の桁に対して「計数ソート」を実行します。完了後、データは $k$ 番目の桁に基づいて最小から最大までソートされます。
|
||||
3. $k$ を $1$ 増やし、手順 `2.` に戻って反復を続け、すべての桁がソートされるまで続けます。この時点で過程が終了します。
|
||||
1. 桁番号 $k = 1$ を初期化します。
|
||||
2. 学籍番号の第 $k$ 位に対して「計数ソート」を実行します。完了すると、データは第 $k$ 位に従って昇順に並びます。
|
||||
3. $k$ を $1$ 増やし、手順 `2.` に戻って反復を続けます。すべての桁のソートが完了したら終了します。
|
||||
|
||||

|
||||

|
||||
|
||||
以下、コード実装を詳しく見てみます。基数 $d$ での数 $x$ に対して、その $k$ 番目の桁 $x_k$ を取得するには、以下の計算式を使用できます:
|
||||
以下ではコード実装を分解して見ていきます。$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]$ です。
|
||||
ここで、$\lfloor a \rfloor$ は浮動小数点数 $a$ の切り捨てを表し、$\bmod \: d$ は $d$ による剰余を表します。学籍番号データでは、$d = 10$ かつ $k \in [1, 8]$ です。
|
||||
|
||||
さらに、$k$ 番目の桁に基づいてソートできるように、計数ソートのコードを少し修正する必要があります:
|
||||
さらに、数字の第 $k$ 位に基づいてソートできるように、計数ソートのコードを少し変更する必要があります。
|
||||
|
||||
```src
|
||||
[file]{radix_sort}-[class]{}-[func]{radix_sort}
|
||||
```
|
||||
|
||||
!!! question "なぜ最下位桁から開始するのか?"
|
||||
!!! question "なぜ最下位桁からソートするのですか?"
|
||||
|
||||
連続するソートラウンドでは、後のラウンドの結果が前のラウンドの結果を上書きします。例えば、最初のラウンドの結果が $a < b$ で、2番目のラウンドが $a > b$ の場合、2番目のラウンドの結果が最初のラウンドの結果を置き換えます。上位桁は下位桁より優先されるため、上位桁の前に下位桁をソートすることが理にかなっています。
|
||||
連続するソートの各ラウンドでは、後のラウンドの結果が前のラウンドの結果を上書きします。たとえば、第1ラウンドで $a < b$ となっていても、第2ラウンドで $a > b$ となれば、第2ラウンドの結果が優先されます。数字では高位の優先度が低位より高いため、先に低位をソートし、その後で高位をソートする必要があります。
|
||||
|
||||
## アルゴリズムの特徴
|
||||
|
||||
計数ソートと比較して、基数ソートはより大きな数値範囲に適していますが、**データが固定桁数で表現でき、桁数があまり大きくないことを前提としています**。例えば、浮動小数点数は桁数 $k$ が大きい可能性があり、時間計算量 $O(nk) \gg O(n^2)$ につながる可能性があるため、基数ソートには適していません。
|
||||
計数ソートと比べると、基数ソートは値の範囲が大きい場合に適しています。**ただし、データが固定桁数の形式で表せること、かつ桁数が大きすぎないことが前提です**。たとえば、浮動小数点数は基数ソートに適していません。桁数 $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` にそれぞれ依存します。
|
||||
- **安定ソート**:計数ソートが安定な場合、基数ソートも安定です。計数ソートが不安定な場合、基数ソートは正しいソート順序を保証できません。
|
||||
- **時間計算量は $O(nk)$、非適応ソート**:データ量を $n$、データが $d$ 進数、最大桁数を $k$ とすると、ある1桁に対して計数ソートを実行する時間は $O(n + d)$ であり、全 $k$ 桁をソートする時間は $O((n + d)k)$ です。通常、$d$ と $k$ はどちらも比較的小さいため、時間計算量は $O(n)$ に近づきます。
|
||||
- **空間計算量は $O(n + d)$、非原地ソート**:計数ソートと同様に、基数ソートでは長さ $n$ と $d$ の配列 `res` と `counter` を補助的に用います。
|
||||
- **安定ソート**:計数ソートが安定であれば基数ソートも安定です。計数ソートが不安定な場合、基数ソートでは正しいソート結果を保証できません。
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
# 選択ソート
|
||||
|
||||
<u>選択ソート</u>は非常にシンプルな原理で動作します:各反復で未ソート区間から最小要素を選択し、ソート済みセクションの末尾に移動するループを使用します。
|
||||
<u>選択ソート(selection sort)</u>の仕組みは非常に単純です。ループを開始し、各ラウンドで未ソート区間から最小の要素を選び、整列済み区間の末尾に配置します。
|
||||
|
||||
配列の長さを$n$とすると、選択ソートのステップは下図に示されます。
|
||||
配列の長さを $n$ とすると、選択ソートの手順は次の図のようになります。
|
||||
|
||||
1. 最初に、すべての要素は未ソートで、つまり未ソート(インデックス)区間は$[0, n-1]$です。
|
||||
2. 区間$[0, n-1]$の最小要素を選択し、インデックス$0$の要素と交換します。この後、配列の最初の要素がソートされます。
|
||||
3. 区間$[1, n-1]$の最小要素を選択し、インデックス$1$の要素と交換します。この後、配列の最初の2つの要素がソートされます。
|
||||
4. この方法で続行します。$n - 1$ラウンドの選択と交換の後、最初の$n - 1$個の要素がソートされます。
|
||||
5. 残りの唯一の要素は結果的に最大要素であり、ソートする必要がないため、配列はソートされます。
|
||||
1. 初期状態では、すべての要素が未ソートであり、未ソートな(インデックス)区間は $[0, n-1]$ です。
|
||||
2. 区間 $[0, n-1]$ 内の最小要素を選び、インデックス $0$ の要素と交換します。これにより、配列の先頭 1 要素が整列済みになります。
|
||||
3. 区間 $[1, n-1]$ 内の最小要素を選び、インデックス $1$ の要素と交換します。これにより、配列の先頭 2 要素が整列済みになります。
|
||||
4. これを繰り返します。$n - 1$ 回の選択と交換を経ると、配列の先頭 $n - 1$ 要素が整列済みになります。
|
||||
5. 残った 1 つの要素は必ず最大要素なので、ソートは不要です。これで配列のソートは完了します。
|
||||
|
||||
=== "<1>"
|
||||

|
||||

|
||||
|
||||
=== "<2>"
|
||||

|
||||
@@ -43,16 +43,16 @@
|
||||
=== "<11>"
|
||||

|
||||
|
||||
コードでは、$k$を使用して未ソート区間内の最小要素を記録します:
|
||||
コードでは、$k$ を用いて未ソート区間内の最小要素を記録します。
|
||||
|
||||
```src
|
||||
[file]{selection_sort}-[class]{}-[func]{selection_sort}
|
||||
```
|
||||
|
||||
## アルゴリズムの特性
|
||||
## アルゴリズムの特徴
|
||||
|
||||
- **$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]`は等しい要素の右側に交換される可能性があり、相対順序が変わる原因となります。
|
||||
- **時間計算量は $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]` がそれと等しい要素の右側へ交換され、両者の相対的な順序が変わる可能性があります。
|
||||
|
||||

|
||||

|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
# ソートアルゴリズム
|
||||
|
||||
<u>ソートアルゴリズム</u>は、データセットを特定の順序で配列するために使用されます。ソートアルゴリズムは、順序付けられたデータは通常、より効率的に探索、分析、処理できるため、幅広い応用があります。
|
||||
<u>ソートアルゴリズム(sorting algorithm)</u>は、データの集合を特定の順序に従って並べ替えるために用いられます。ソートアルゴリズムは幅広く応用されており、整列済みデータは通常、より効率的に検索、分析、処理できるためです。
|
||||
|
||||
下図に示すように、ソートアルゴリズムのデータ型は整数、浮動小数点数、文字、文字列などです。ソート基準は、数値サイズ、文字ASCII順序、またはカスタム基準など、必要に応じて設定できます。
|
||||
下図に示すように、ソートアルゴリズムにおけるデータ型は整数、浮動小数点数、文字、文字列などです。ソートの判定規則は、数値の大小、文字の ASCII コード順、またはカスタムルールなど、要件に応じて設定できます。
|
||||
|
||||

|
||||

|
||||
|
||||
## 評価次元
|
||||
## 評価軸
|
||||
|
||||
**実行効率**:ソートアルゴリズムの時間計算量ができるだけ低いことを期待し、全体的な操作数も少ないこと(時間計算量の定数項を下げる)を望みます。大容量データでは、実行効率が特に重要です。
|
||||
**実行効率**:ソートアルゴリズムの時間計算量はできるだけ低く、かつ全体の操作回数も少ないこと(時間計算量における定数項が小さいこと)が望まれます。大量データの場合、実行効率はとりわけ重要です。
|
||||
|
||||
**インプレース性**:名前が示すとおり、<u>インプレースソート</u>は元の配列を直接操作することで実現され、追加のヘルパー配列が不要であるため、メモリを節約します。一般的に、インプレースソートはデータ移動操作が少なく、高速です。
|
||||
**インプレース性**:その名のとおり、<u>インプレースソート</u>は元の配列を直接操作して並べ替えを行うため、追加の補助配列を必要とせず、メモリを節約できます。通常、インプレースソートはデータの移動操作が少なく、実行速度もより高速です。
|
||||
|
||||
**安定性**:<u>安定ソート</u>は、ソート後に配列内の等しい要素の相対順序が変わらないことを保証します。
|
||||
**安定性**:<u>安定ソート</u>は、並べ替え完了後も、等しい要素の配列内での相対順序が変化しません。
|
||||
|
||||
安定ソートは、マルチキーソートシナリオにおいて必要条件です。学生情報を格納するテーブルがあり、第1列と第2列がそれぞれ名前と年齢であるとします。この場合、<u>不安定ソート</u>は入力データの順序を失う可能性があります:
|
||||
安定ソートは多段ソートの場面で必要条件となります。学生情報を保存した表があり、第 1 列と第 2 列がそれぞれ氏名と年齢であると仮定します。この場合、<u>不安定ソート</u>によって入力データの順序性が失われる可能性があります。
|
||||
|
||||
```shell
|
||||
# 入力データは名前でソート済み
|
||||
# (名前, 年齢)
|
||||
# 入力データは氏名順にソートされている
|
||||
# (name, age)
|
||||
('A', 19)
|
||||
('B', 18)
|
||||
('C', 21)
|
||||
('D', 19)
|
||||
('E', 23)
|
||||
|
||||
# 不安定ソートアルゴリズムを使用してリストを年齢でソートすると仮定すると、
|
||||
# 結果は('D', 19)と('A', 19)の相対位置を変更し、
|
||||
# 入力データが名前でソート済みであるという性質が失われる
|
||||
# 不安定ソートアルゴリズムで年齢順にリストを並べ替えると仮定すると、
|
||||
# 結果では ('D', 19) と ('A', 19) の相対位置が変わり、
|
||||
# 入力データが氏名順である性質が失われる
|
||||
('B', 18)
|
||||
('D', 19)
|
||||
('A', 19)
|
||||
@@ -35,12 +35,12 @@
|
||||
('E', 23)
|
||||
```
|
||||
|
||||
**適応性**:<u>適応ソート</u>は入力データ内の既存の順序情報を活用して計算負荷を削減し、より最適な時間効率を実現します。適応ソートアルゴリズムの最良ケース時間計算量は、通常平均ケース時間計算量よりも優れています。
|
||||
**適応性**:<u>適応的ソート</u>は、入力データに既に存在する順序情報を利用して計算量を減らし、より優れた時間効率を実現できます。適応的ソートアルゴリズムの最良時間計算量は、通常、平均時間計算量より優れています。
|
||||
|
||||
**比較ベースまたは非比較ベース**:<u>比較ベースソート</u>は比較演算子($<$、$=$、$>$)に依存して要素の相対順序を決定し、配列全体をソートします。理論的最適時間計算量は$O(n \log n)$です。一方、<u>非比較ソート</u>は比較演算子を使用せず、$O(n)$の時間計算量を実現できますが、汎用性は比較的劣ります。
|
||||
**比較ベースかどうか**:<u>比較ベースのソート</u>は、比較演算子($<$、$=$、$>$)に依存して要素の相対順序を判定し、それによって配列全体をソートします。理論上の最良時間計算量は $O(n \log n)$ です。一方、<u>非比較ソート</u>は比較演算子を使用せず、時間計算量は $O(n)$ に達しますが、汎用性は相対的に低くなります。
|
||||
|
||||
## 理想的なソートアルゴリズム
|
||||
|
||||
**高速実行、インプレース、安定、適応、汎用**。明らかに、これらのすべての特徴を組み合わせたソートアルゴリズムは今日まで見つかっていません。したがって、ソートアルゴリズムを選択する際は、データの特定の特徴と問題の要件に基づいて決定する必要があります。
|
||||
**高速、インプレース、安定、適応的、高い汎用性**。明らかに、これまでのところ、以上のすべての特性を兼ね備えたソートアルゴリズムはまだ見つかっていません。そのため、ソートアルゴリズムを選択する際には、具体的なデータの特徴と問題の要件に応じて判断する必要があります。
|
||||
|
||||
次に、さまざまなソートアルゴリズムを一緒に学び、上記の評価次元に基づいてそれぞれの利点と欠点を分析します。
|
||||
次に、さまざまなソートアルゴリズムを一緒に学び、上記の評価軸に基づいて各ソートアルゴリズムの長所と短所を分析していきます。
|
||||
|
||||
@@ -1,47 +1,47 @@
|
||||
# まとめ
|
||||
|
||||
### 重要な復習
|
||||
### 重要なポイントの振り返り
|
||||
|
||||
- バブルソートは隣接する要素を交換することで動作します。フラグを追加して早期リターンを可能にすることで、バブルソートの最良ケースの時間計算量を $O(n)$ に最適化できます。
|
||||
- 挿入ソートは、未ソート区間から要素を取り出してソート済み区間の正しい位置に挿入することで各ラウンドをソートします。挿入ソートの時間計算量は $O(n^2)$ ですが、単位あたりの操作が比較的少ないため、少量のデータのソートでは非常に人気があります。
|
||||
- クイックソートは歩哨分割操作に基づいています。歩哨分割では、常に最悪のピボットを選ぶ可能性があり、時間計算量が $O(n^2)$ に劣化する可能性があります。中央値やランダムピボットを導入することで、そのような劣化の確率を減らすことができます。末尾再帰は再帰の深さを効果的に減らし、空間計算量を $O(\log n)$ に最適化します。
|
||||
- マージソートには分割とマージの2つの段階があり、通常分割統治戦略を体現しています。マージソートでは、配列のソートには補助配列の作成が必要で、空間計算量は $O(n)$ になります。しかし、リストのソートの空間計算量は $O(1)$ に最適化できます。
|
||||
- バケットソートは3つの手順から構成されます:データをバケットに分散、各バケット内でのソート、バケット順での結果のマージ。これも分割統治戦略を体現し、非常に大きなデータセットに適しています。バケットソートの鍵はデータの均等分散です。
|
||||
- 計数ソートはバケットソートの変形で、各データポイントの出現回数をカウントすることでソートします。計数ソートは限られた範囲のデータを持つ大きなデータセットに適しており、データを正の整数に変換する必要があります。
|
||||
- 基数ソートは桁ごとにソートすることでデータを処理し、データが固定長の数値として表現される必要があります。
|
||||
- 全体的に、私たちは高効率、安定性、インプレース操作、適応性を持つソートアルゴリズムを求めています。しかし、他のデータ構造やアルゴリズムと同様に、これらすべての条件を同時に満たすソートアルゴリズムは存在しません。実際の応用では、データの特性に基づいて適切なソートアルゴリズムを選択する必要があります。
|
||||
- 以下の図は、効率性、安定性、インプレース性、適応性の観点から主流のソートアルゴリズムを比較しています。
|
||||
- バブルソートは隣接する要素を交換することで整列を行います。フラグを追加して早期リターンを可能にすると、バブルソートの最良時間計算量を $O(n)$ に最適化できます。
|
||||
- 挿入ソートは各ラウンドで未整列区間の要素を整列済み区間の正しい位置に挿入することで整列を完了します。挿入ソートの時間計算量は $O(n^2)$ ですが、基本操作が比較的少ないため、小規模データのソート処理で非常に人気があります。
|
||||
- クイックソートは番兵分割操作に基づいて整列を行います。番兵分割では毎回最悪の基準値を選んでしまう可能性があり、その結果、時間計算量は $O(n^2)$ まで劣化することがあります。中央値の基準値やランダムな基準値を導入すると、この劣化の確率を下げられます。短い部分配列を優先して再帰すれば、再帰の深さを効果的に抑え、空間計算量を $O(\log n)$ に最適化できます。
|
||||
- マージソートは分割とマージという 2 つの段階からなり、分割統治戦略を典型的に体現しています。マージソートでは配列を整列する際に補助配列の作成が必要で、空間計算量は $O(n)$ です。一方、連結リストを整列する場合の空間計算量は $O(1)$ まで最適化できます。
|
||||
- バケットソートはデータのバケット分配、バケット内ソート、結果の結合という 3 つの手順を含みます。これも分割統治戦略を体現しており、データ量が非常に大きい場合に適しています。バケットソートの鍵は、データを平均的に分配することにあります。
|
||||
- カウントソートはバケットソートの特例であり、データの出現回数を数えることで整列を行います。カウントソートはデータ量が大きく、かつデータ範囲が限られている場合に適しており、データを正の整数に変換できることが前提です。
|
||||
- 基数ソートは各桁ごとの整列によってデータを整列し、データが固定桁数の数値として表せることを前提とします。
|
||||
- 総じて言えば、私たちは高効率で、安定で、インプレースで、さらに適応的であるといった利点を備えたソートアルゴリズムを見つけたいと考えます。しかし、ほかのデータ構造やアルゴリズムと同様に、これらすべての条件を同時に満たせるソートアルゴリズムは存在しません。実際の応用では、データの特性に応じて適切なソートアルゴリズムを選ぶ必要があります。
|
||||
- 下図では、主流のソートアルゴリズムについて、効率、安定性、インプレース性、適応性などを比較しています。
|
||||
|
||||

|
||||
|
||||
### Q & A
|
||||
|
||||
**Q**: ソートアルゴリズムの安定性はいつ必要ですか?
|
||||
**Q**:ソートアルゴリズムの安定性は、どのような場合に必須ですか?
|
||||
|
||||
実際には、オブジェクトの一つの属性に基づいてソートする場合があります。例えば、学生は名前と身長の属性を持ち、多段階ソートを実装することを目指します:最初に名前で `(A, 180) (B, 185) (C, 170) (D, 170)` を取得し、次に身長で。ソートアルゴリズムが不安定なため、`(D, 170) (C, 170) (A, 180) (B, 185)` になってしまう可能性があります。
|
||||
現実には、オブジェクトのある属性に基づいて整列することがあります。たとえば、学生には氏名と身長という 2 つの属性があり、多段階のソートを行いたいとします。まず氏名で整列して `(A, 180) (B, 185) (C, 170) (D, 170)` を得て、その後に身長で整列します。ソートアルゴリズムが不安定である場合、結果は `(D, 170) (C, 170) (A, 180) (B, 185)` になる可能性があります。
|
||||
|
||||
学生DとCの位置が交換され、名前の順序性が破られているのが分かります。これは望ましくありません。
|
||||
このように、学生 D と C の位置が入れ替わり、氏名に関する順序性が壊れてしまいます。これは望ましくありません。
|
||||
|
||||
**Q**: 歩哨分割での「右から左への検索」と「左から右への検索」の順序を交換できますか?
|
||||
**Q**:番兵分割において、「右から左へ探索する」順序と「左から右へ探索する」順序は入れ替えられますか?
|
||||
|
||||
いいえ、最左要素をピボットとして使用する場合、最初に「右から左への検索」を行い、次に「左から右への検索」を行う必要があります。この結論はやや直観に反するので、理由を分析してみましょう。
|
||||
できません。最も左端の要素を基準値とする場合は、必ず先に「右から左へ探索する」を行い、その後に「左から右へ探索する」を行う必要があります。この結論はやや直感に反するので、理由を分析してみましょう。
|
||||
|
||||
歩哨分割 `partition()` の最後のステップは `nums[left]` と `nums[i]` を交換することです。交換後、ピボットの左側の要素はすべてピボット以下になります。**これには最後の交換前に `nums[left] >= nums[i]` が成り立つ必要があります**。「左から右への検索」を最初に行い、ピボットより大きい要素が見つからない場合、**`i == j` でループを終了し、`nums[j] == nums[i] > nums[left]` となる可能性があります**。つまり、最終交換操作はピボットより大きい要素を配列の左端に交換し、歩哨分割を失敗させます。
|
||||
番兵分割 `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]` となり、これは正しくありません。
|
||||
たとえば、配列 `[0, 0, 0, 0, 1]` が与えられたとき、先に「左から右へ探索する」を行うと、番兵分割後の配列は `[1, 0, 0, 0, 0]` になります。これは誤った結果です。
|
||||
|
||||
さらに考えると、`nums[right]` をピボットとして選択する場合、まったく逆で、最初に「左から右への検索」を行う必要があります。
|
||||
さらに考えると、`nums[right]` を基準値に選ぶ場合はちょうど逆になり、必ず先に「左から右へ探索する」を行う必要があります。
|
||||
|
||||
**Q**: 末尾再帰最適化について、短い配列を選択することで再帰の深さが $\log n$ を超えないことを保証するのはなぜですか?
|
||||
**Q**:クイックソートの再帰深度最適化について、短い配列を選ぶとなぜ再帰深度が $\log n$ を超えないと保証できるのですか?
|
||||
|
||||
再帰の深さは現在リターンしていない再帰メソッドの数です。歩哨分割の各ラウンドは元の配列を2つの副配列に分割します。末尾再帰最適化により、再帰的に続行する副配列の長さは最大でも元の配列長の半分です。最悪の場合常に長さを半分にすると仮定すると、最終的な再帰の深さは $\log n$ になります。
|
||||
再帰深度とは、現在まだ戻っていない再帰呼び出しの数のことです。各ラウンドの番兵分割では、元の配列を 2 つの部分配列に分けます。再帰深度の最適化後は、下方向に再帰する部分配列の長さは最大でも元の配列長の半分です。最悪の場合でも毎回半分の長さになると仮定すれば、最終的な再帰深度は $\log n$ になります。
|
||||
|
||||
元のクイックソートを見直すと、より大きな配列を継続的に再帰処理する可能性があり、最悪の場合 $n$、$n - 1$、...、$2$、$1$ で、再帰の深さは $n$ になります。末尾再帰最適化はこのシナリオを回避できます。
|
||||
元のクイックソートを振り返ると、長いほうの配列に対して連続して再帰してしまう可能性があり、最悪の場合は $n$、$n - 1$、$\dots$、$2$、$1$ と続き、再帰深度は $n$ になります。再帰深度の最適化により、このような状況を避けられます。
|
||||
|
||||
**Q**: 配列のすべての要素が等しい場合、クイックソートの時間計算量は $O(n^2)$ ですか?この劣化ケースをどう処理すべきですか?
|
||||
**Q**:配列内のすべての要素が等しい場合、クイックソートの時間計算量は $O(n^2)$ になりますか?このような退化はどう処理すべきですか?
|
||||
|
||||
はい。この状況については、歩哨分割を使用して配列をピボットより小さい、等しい、大きいの3つの部分に分割することを検討してください。小さい部分と大きい部分のみを再帰的に進めます。この方法では、すべての入力要素が等しい配列を1ラウンドの歩哨分割だけでソートできます。
|
||||
はい。この場合は、番兵分割によって配列を「基準値より小さい」「基準値に等しい」「基準値より大きい」の 3 つの部分に分ける方法を検討できます。下方向に再帰するのは、小さい部分と大きい部分だけです。この方法では、入力要素がすべて等しい配列は、1 回の番兵分割だけで整列を完了できます。
|
||||
|
||||
**Q**: なぜバケットソートの最悪ケース時間計算量は $O(n^2)$ ですか?
|
||||
**Q**:バケットソートの最悪時間計算量が $O(n^2)$ なのはなぜですか?
|
||||
|
||||
最悪の場合、すべての要素が同じバケットに配置されます。これらの要素をソートするために $O(n^2)$ アルゴリズムを使用する場合、時間計算量は $O(n^2)$ になります。
|
||||
最悪の場合、すべての要素が同じバケットに振り分けられます。その要素群を整列するのに $O(n^2)$ のアルゴリズムを使えば、時間計算量は $O(n^2)$ になります。
|
||||
|
||||
Reference in New Issue
Block a user