docs: add Japanese translate documents (#1812)
* docs: add Japanese documents (`ja/docs`) * docs: add Japanese documents (`ja/codes`) * docs: add Japanese documents * Remove pythontutor blocks in ja/ * Add an empty at the end of each markdown file. * Add the missing figures (use the English version temporarily). * Add index.md for Japanese version. * Add index.html for Japanese version. * Add missing index.assets * Fix backtracking_algorithm.md for Japanese version. * Add avatar_eltociear.jpg. Fix image links on the Japanese landing page. * Add the Japanese banner. --------- Co-authored-by: krahets <krahets@163.com>
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 34 KiB |
@@ -0,0 +1,59 @@
|
||||
# バブルソート
|
||||
|
||||
<u>バブルソート</u>は、隣接する要素を継続的に比較し交換することで動作します。このプロセスは泡が底から上に上昇するようなものなので、「バブルソート」と名付けられました。
|
||||
|
||||
下図に示すように、バブリングプロセスは要素交換を使用してシミュレートできます:配列の左端から開始して右に移動し、隣接する要素の各ペアを比較します。左の要素が右の要素より大きい場合は、それらを交換します。横断後、最大要素は配列の右端にバブルアップします。
|
||||
|
||||
=== "<1>"
|
||||

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

|
||||
|
||||
=== "<3>"
|
||||

|
||||
|
||||
=== "<4>"
|
||||

|
||||
|
||||
=== "<5>"
|
||||

|
||||
|
||||
=== "<6>"
|
||||

|
||||
|
||||
=== "<7>"
|
||||

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

|
||||
|
||||
コード例は以下の通りです:
|
||||
|
||||
```src
|
||||
[file]{bubble_sort}-[class]{}-[func]{bubble_sort}
|
||||
```
|
||||
|
||||
## 効率の最適化
|
||||
|
||||
「バブリング」のラウンド中に交換が発生しない場合、配列はすでにソートされているため、すぐに戻ることができます。これを検出するために、`flag`変数を追加できます;パスで交換が行われない場合は、フラグを設定して早期に戻ります。
|
||||
|
||||
この最適化があっても、バブルソートの最悪時間計算量と平均時間計算量は$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$によって定数量の追加空間のみが使用されます。
|
||||
- **安定ソート。** 等しい要素は「バブリング」中に交換されないため、元の順序が保持され、これは安定ソートになります。
|
||||
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 33 KiB |
@@ -0,0 +1,45 @@
|
||||
# バケットソート
|
||||
|
||||
前述のソートアルゴリズムはすべて「比較ベースのソートアルゴリズム」で、値を比較することで要素をソートします。このようなソートアルゴリズムは $O(n \log n)$ より良い時間計算量を持つことはできません。次に、線形時間計算量を達成できるいくつかの「非比較ソートアルゴリズム」について議論します。
|
||||
|
||||
<u>バケットソート</u>は分割統治戦略の典型的な応用です。一連の順序付けられたバケットを設定し、各バケットがデータの範囲を含み、入力データをこれらのバケットに均等に分散させることで動作します。そして、各バケット内のデータを個別にソートします。最後に、すべてのバケットからのソート済みデータを順次マージして最終結果を生成します。
|
||||
|
||||
## アルゴリズムの過程
|
||||
|
||||
長さ $n$ の配列で、$[0, 1)$ の範囲の浮動小数点数を考えてみます。バケットソートの過程は以下の図に示されています。
|
||||
|
||||
1. $k$ 個のバケットを初期化し、$n$ 個の要素をこれらの $k$ 個のバケットに分散させます。
|
||||
2. 各バケットを個別にソートします(プログラミング言語の組み込みソート関数を使用)。
|
||||
3. 最小から最大のバケットの順序で結果をマージします。
|
||||
|
||||

|
||||
|
||||
コードは以下の通りです:
|
||||
|
||||
```src
|
||||
[file]{bucket_sort}-[class]{}-[func]{bucket_sort}
|
||||
```
|
||||
|
||||
## アルゴリズムの特徴
|
||||
|
||||
バケットソートは非常に大きなデータセットの処理に適しています。例えば、入力データに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$ 個の要素のための追加スペースが必要です。
|
||||
- バケットソートが安定かどうかは、各バケット内で使用されるソートアルゴリズムが安定かどうかに依存します。
|
||||
|
||||
## 均等分散を達成する方法
|
||||
|
||||
バケットソートの理論的時間計算量は $O(n)$ に達することができます。**重要なことは、すべてのバケットに要素を均等に分散させることです**。実世界のデータはしばしば均一に分散されていないからです。例えば、eBayのすべての商品を価格範囲で10個のバケットに均等に分散させたいとします。しかし、商品価格の分散は均等でない可能性があり、100ドル未満の商品が多く、500ドル以上の商品が少ないかもしれません。価格範囲を均等に10分割すると、各バケットの商品数の差が大きくなります。
|
||||
|
||||
均等分散を達成するために、最初におおよその境界を設定して、データを3つのバケットに大まかに分割できます。**分散が完了した後、より多くのアイテムを持つバケットをさらに3つのバケットに分割し、すべてのバケットの要素数がほぼ等しくなるまで続けます**。
|
||||
|
||||
以下の図に示すように、この方法は本質的に再帰木を構築し、葉ノードの要素数ができるだけ均等になることを目指します。もちろん、各ラウンドでデータを3つのバケットに分割する必要はありません - 分割戦略はデータの独特な特性に適応的に調整できます。
|
||||
|
||||

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

|
||||
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 16 KiB |
@@ -0,0 +1,84 @@
|
||||
# 計数ソート
|
||||
|
||||
<u>計数ソート</u>は要素の数をカウントすることでソートを実現し、通常は整数配列に適用されます。
|
||||
|
||||
## 簡単な実装
|
||||
|
||||
簡単な例から始めましょう。長さ $n$ の配列 `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}
|
||||
```
|
||||
|
||||
!!! note "計数ソートとバケットソートの関係"
|
||||
|
||||
バケットソートの観点から、計数ソートにおける計数配列 `counter` の各インデックスをバケットと考え、カウントの過程を要素を対応するバケットに分散させることと考えることができます。本質的に、計数ソートは整数データのためのバケットソートの特別なケースです。
|
||||
|
||||
## 完全な実装
|
||||
|
||||
注意深い読者は気付くかもしれませんが、**入力データがオブジェクトの場合、上記の手順 `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>"
|
||||

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

|
||||
|
||||
=== "<3>"
|
||||

|
||||
|
||||
=== "<4>"
|
||||

|
||||
|
||||
=== "<5>"
|
||||

|
||||
|
||||
=== "<6>"
|
||||

|
||||
|
||||
=== "<7>"
|
||||

|
||||
|
||||
=== "<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` を順番に走査しても正しいソート結果を生成できますが、結果は不安定です。
|
||||
|
||||
## 制限事項
|
||||
|
||||
今までに、計数ソートは非常に巧妙だと感じるかもしれません。単に量をカウントするだけで効率的なソートを実現できるからです。しかし、計数ソートを使用するための前提条件は比較的厳しいです。
|
||||
|
||||
**計数ソートは非負整数にのみ適用できます**。他のタイプのデータに適用したい場合、これらのデータが要素の元の順序を変更することなく非負整数に変換できることを保証する必要があります。例えば、負の整数を含む配列の場合、最初にすべての数に定数を加えて、すべてを正の数に変換し、ソート完了後に元に戻すことができます。
|
||||
|
||||
**計数ソートは値の範囲が小さい大きなデータセットに適しています**。例えば、上記の例では、$m$ は大きすぎるべきではありません。そうでなければ、あまりにも多くのスペースを占有してしまいます。そして $n \ll m$ の場合、計数ソートは $O(m)$ 時間を使用し、$O(n \log n)$ ソートアルゴリズムより遅い可能性があります。
|
||||
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 23 KiB |
@@ -0,0 +1,73 @@
|
||||
# ヒープソート
|
||||
|
||||
!!! tip
|
||||
|
||||
この節を読む前に、「ヒープ」の章を必ず完了させてください。
|
||||
|
||||
<u>ヒープソート</u>は、ヒープデータ構造に基づく効率的なソートアルゴリズムです。すでに学習した「ヒープの構築」と「要素の抽出」操作を使用してヒープソートを実装できます。
|
||||
|
||||
1. 配列を入力し、最小ヒープを構築します。ここで、最小要素がヒープの頂上に位置します。
|
||||
2. 継続的に抽出操作を実行し、抽出された要素を順次記録して、最小から最大までのソート済みリストを取得します。
|
||||
|
||||
上記の方法は実現可能ですが、ポップされた要素を格納するための追加の配列が必要で、やや空間を消費します。実際には、通常、より優雅な実装を使用します。
|
||||
|
||||
## アルゴリズムの流れ
|
||||
|
||||
配列の長さを $n$ とすると、ヒープソートの過程は以下の通りです。
|
||||
|
||||
1. 配列を入力し、最大ヒープを構築します。この手順の後、最大要素がヒープの頂上に位置します。
|
||||
2. ヒープの頂上要素(最初の要素)とヒープの底部要素(最後の要素)を交換します。この交換の後、ヒープの長さを $1$ 減らし、ソート済み要素の数を $1$ 増やします。
|
||||
3. ヒープの頂上から開始して、上から下へのsift-down操作を実行します。sift-downの後、ヒープの性質が復元されます。
|
||||
4. 手順 `2.` と `3.` を繰り返します。$n - 1$ ラウンドループして、配列のソートを完了します。
|
||||
|
||||
!!! tip
|
||||
|
||||
実際、要素抽出操作も手順 `2.` と `3.` を含み、抽出された要素をヒープから削除する追加の手順があります。
|
||||
|
||||
=== "<1>"
|
||||

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

|
||||
|
||||
=== "<3>"
|
||||

|
||||
|
||||
=== "<4>"
|
||||

|
||||
|
||||
=== "<5>"
|
||||

|
||||
|
||||
=== "<6>"
|
||||

|
||||
|
||||
=== "<7>"
|
||||

|
||||
|
||||
=== "<8>"
|
||||

|
||||
|
||||
=== "<9>"
|
||||

|
||||
|
||||
=== "<10>"
|
||||

|
||||
|
||||
=== "<11>"
|
||||

|
||||
|
||||
=== "<12>"
|
||||

|
||||
|
||||
コードの実装では、「ヒープ」の章からのsift-down関数 `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)$ 空間を使用します。要素の交換とヒープ化操作は元の配列で実行されます。
|
||||
- **非安定ソート**:ヒープの頂上と底部要素の交換中に、等しい要素の相対位置が変わる可能性があります。
|
||||
@@ -0,0 +1,9 @@
|
||||
# ソート
|
||||
|
||||

|
||||
|
||||
!!! abstract
|
||||
|
||||
ソートは混沌を秩序に変える魔法の鍵のようなもので、データをより効率的に理解し処理することを可能にします。
|
||||
|
||||
単純な昇順であろうと複雑なカテゴリ配列であろうと、ソートはデータの調和美を明らかにします。
|
||||
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 30 KiB |
@@ -0,0 +1,46 @@
|
||||
# 挿入ソート
|
||||
|
||||
<u>挿入ソート</u>は、トランプのデッキを手動でソートするプロセスによく似た動作をするシンプルなソートアルゴリズムです。
|
||||
|
||||
具体的には、未ソート区間からベース要素を選択し、その左側のソート済み区間の要素と比較して、要素を正しい位置に挿入します。
|
||||
|
||||
下図は、要素が配列に挿入される方法を示しています。ベース要素を`base`とすると、ターゲットインデックスから`base`までのすべての要素を右に1つずつシフトし、その後`base`をターゲットインデックスに割り当てる必要があります。
|
||||
|
||||

|
||||
|
||||
## アルゴリズムプロセス
|
||||
|
||||
挿入ソートの全体的なプロセスは下図に示されます。
|
||||
|
||||
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)$で、次に学習するクイックソートの時間計算量は$O(n \log n)$です。挿入ソートはより高い時間計算量を持ちますが、**小さな入力サイズでは通常より高速です**。
|
||||
|
||||
この結論は線形探索と二分探索の結論と似ています。時間計算量が$O(n \log n)$で分割統治戦略に基づくクイックソートなどのアルゴリズムは、多くの場合より多くの単位操作を含みます。小さな入力サイズでは、$n^2$と$n \log n$の数値は近く、計算量が支配的でなく、ラウンドあたりの単位操作数が決定的な役割を果たします。
|
||||
|
||||
実際、多くのプログラミング言語(Javaなど)は、組み込みソート関数内で挿入ソートを使用しています。一般的なアプローチは:長い配列に対しては、クイックソートなどの分割統治戦略に基づくソートアルゴリズムを使用し、短い配列に対しては挿入ソートを直接使用します。
|
||||
|
||||
バブルソート、選択ソート、挿入ソートはすべて時間計算量$O(n^2)$を持ちますが、実際には、**挿入ソートはバブルソートや選択ソートよりも一般的に使用されます**。主な理由は以下の通りです。
|
||||
|
||||
- バブルソートは要素交換に基づき、一時変数の使用が必要で、3つの単位操作を含みます;挿入ソートは要素代入に基づき、1つの単位操作のみが必要です。したがって、**バブルソートの計算オーバーヘッドは一般的に挿入ソートよりも高くなります**。
|
||||
- 選択ソートの時間計算量は常に$O(n^2)$です。**部分的に順序付けられたデータのセットが与えられた場合、挿入ソートは通常選択ソートよりも効率的です**。
|
||||
- 選択ソートは不安定で、マルチレベルソートに適用できません。
|
||||
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 16 KiB |
@@ -0,0 +1,73 @@
|
||||
# マージソート
|
||||
|
||||
<u>マージソート</u>は分割統治戦略に基づくソートアルゴリズムで、下図に示す「分割」と「マージ」フェーズを含みます。
|
||||
|
||||
1. **分割フェーズ**:中点から配列を再帰的に分割し、長い配列のソート問題をより短い配列に変換します。
|
||||
2. **マージフェーズ**:サブ配列の長さが1になったときに分割を停止し、その後マージを開始します。2つの短いソート済み配列を連続的により長いソート済み配列にマージし、プロセスが完了するまで続行します。
|
||||
|
||||

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

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

|
||||
|
||||
=== "<3>"
|
||||

|
||||
|
||||
=== "<4>"
|
||||

|
||||
|
||||
=== "<5>"
|
||||

|
||||
|
||||
=== "<6>"
|
||||

|
||||
|
||||
=== "<7>"
|
||||

|
||||
|
||||
=== "<8>"
|
||||

|
||||
|
||||
=== "<9>"
|
||||

|
||||
|
||||
=== "<10>"
|
||||

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

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

|
||||
|
||||
=== "<3>"
|
||||

|
||||
|
||||
=== "<4>"
|
||||

|
||||
|
||||
=== "<5>"
|
||||

|
||||
|
||||
=== "<6>"
|
||||

|
||||
|
||||
=== "<7>"
|
||||

|
||||
|
||||
=== "<8>"
|
||||

|
||||
|
||||
=== "<9>"
|
||||

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

|
||||
|
||||
以下、コード実装を詳しく見てみます。基数 $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$ 番目の桁に基づいてソートできるように、計数ソートのコードを少し修正する必要があります:
|
||||
|
||||
```src
|
||||
[file]{radix_sort}-[class]{}-[func]{radix_sort}
|
||||
```
|
||||
|
||||
!!! question "なぜ最下位桁から開始するのか?"
|
||||
|
||||
連続するソートラウンドでは、後のラウンドの結果が前のラウンドの結果を上書きします。例えば、最初のラウンドの結果が $a < b$ で、2番目のラウンドが $a > b$ の場合、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` にそれぞれ依存します。
|
||||
- **安定ソート**:計数ソートが安定な場合、基数ソートも安定です。計数ソートが不安定な場合、基数ソートは正しいソート順序を保証できません。
|
||||
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 13 KiB |
@@ -0,0 +1,58 @@
|
||||
# 選択ソート
|
||||
|
||||
<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>"
|
||||

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

|
||||
|
||||
=== "<3>"
|
||||

|
||||
|
||||
=== "<4>"
|
||||

|
||||
|
||||
=== "<5>"
|
||||

|
||||
|
||||
=== "<6>"
|
||||

|
||||
|
||||
=== "<7>"
|
||||

|
||||
|
||||
=== "<8>"
|
||||

|
||||
|
||||
=== "<9>"
|
||||

|
||||
|
||||
=== "<10>"
|
||||

|
||||
|
||||
=== "<11>"
|
||||

|
||||
|
||||
コードでは、$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]`は等しい要素の右側に交換される可能性があり、相対順序が変わる原因となります。
|
||||
|
||||

|
||||
|
After Width: | Height: | Size: 18 KiB |
@@ -0,0 +1,46 @@
|
||||
# ソートアルゴリズム
|
||||
|
||||
<u>ソートアルゴリズム</u>は、データセットを特定の順序で配列するために使用されます。ソートアルゴリズムは、順序付けられたデータは通常、より効率的に探索、分析、処理できるため、幅広い応用があります。
|
||||
|
||||
下図に示すように、ソートアルゴリズムのデータ型は整数、浮動小数点数、文字、文字列などです。ソート基準は、数値サイズ、文字ASCII順序、またはカスタム基準など、必要に応じて設定できます。
|
||||
|
||||

|
||||
|
||||
## 評価次元
|
||||
|
||||
**実行効率**:ソートアルゴリズムの時間計算量ができるだけ低いことを期待し、全体的な操作数も少ないこと(時間計算量の定数項を下げる)を望みます。大容量データでは、実行効率が特に重要です。
|
||||
|
||||
**インプレース性**:名前が示すとおり、<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)$の時間計算量を実現できますが、汎用性は比較的劣ります。
|
||||
|
||||
## 理想的なソートアルゴリズム
|
||||
|
||||
**高速実行、インプレース、安定、適応、汎用**。明らかに、これらのすべての特徴を組み合わせたソートアルゴリズムは今日まで見つかっていません。したがって、ソートアルゴリズムを選択する際は、データの特定の特徴と問題の要件に基づいて決定する必要があります。
|
||||
|
||||
次に、さまざまなソートアルゴリズムを一緒に学び、上記の評価次元に基づいてそれぞれの利点と欠点を分析します。
|
||||
|
After Width: | Height: | Size: 58 KiB |
@@ -0,0 +1,47 @@
|
||||
# まとめ
|
||||
|
||||
### 重要な復習
|
||||
|
||||
- バブルソートは隣接する要素を交換することで動作します。フラグを追加して早期リターンを可能にすることで、バブルソートの最良ケースの時間計算量を $O(n)$ に最適化できます。
|
||||
- 挿入ソートは、未ソート区間から要素を取り出してソート済み区間の正しい位置に挿入することで各ラウンドをソートします。挿入ソートの時間計算量は $O(n^2)$ ですが、単位あたりの操作が比較的少ないため、少量のデータのソートでは非常に人気があります。
|
||||
- クイックソートは歩哨分割操作に基づいています。歩哨分割では、常に最悪のピボットを選ぶ可能性があり、時間計算量が $O(n^2)$ に劣化する可能性があります。中央値やランダムピボットを導入することで、そのような劣化の確率を減らすことができます。末尾再帰は再帰の深さを効果的に減らし、空間計算量を $O(\log n)$ に最適化します。
|
||||
- マージソートには分割とマージの2つの段階があり、通常分割統治戦略を体現しています。マージソートでは、配列のソートには補助配列の作成が必要で、空間計算量は $O(n)$ になります。しかし、リストのソートの空間計算量は $O(1)$ に最適化できます。
|
||||
- バケットソートは3つの手順から構成されます:データをバケットに分散、各バケット内でのソート、バケット順での結果のマージ。これも分割統治戦略を体現し、非常に大きなデータセットに適しています。バケットソートの鍵はデータの均等分散です。
|
||||
- 計数ソートはバケットソートの変形で、各データポイントの出現回数をカウントすることでソートします。計数ソートは限られた範囲のデータを持つ大きなデータセットに適しており、データを正の整数に変換する必要があります。
|
||||
- 基数ソートは桁ごとにソートすることでデータを処理し、データが固定長の数値として表現される必要があります。
|
||||
- 全体的に、私たちは高効率、安定性、インプレース操作、適応性を持つソートアルゴリズムを求めています。しかし、他のデータ構造やアルゴリズムと同様に、これらすべての条件を同時に満たすソートアルゴリズムは存在しません。実際の応用では、データの特性に基づいて適切なソートアルゴリズムを選択する必要があります。
|
||||
- 以下の図は、効率性、安定性、インプレース性、適応性の観点から主流のソートアルゴリズムを比較しています。
|
||||
|
||||

|
||||
|
||||
### 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)$ になります。
|
||||