Retranslate the complete Japanese online edition from Chinese (#2008)
* Retranslate the complete Japanese online edition from Chinese * Fit Japanese homepage labels and localize heading permalinks * Point Japanese contribution instructions to the Japanese preview * Replace locale conditionals with multilingual UI tables
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 8.1 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 8.7 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 8.6 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 8.8 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 8.5 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 9.2 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 23 KiB |
@@ -1,38 +1,38 @@
|
||||
# バブルソート
|
||||
|
||||
<u>バブルソート(bubble sort)</u>は、隣接する要素を繰り返し比較して交換することで整列を行います。この過程が泡のように下から上へ浮かび上がることから、バブルソートと呼ばれます。
|
||||
<u>バブルソート(bubble sort)</u>は、隣り合う要素の比較と交換を繰り返してソートします。この過程が泡が下から上へ浮かぶ様子に似ていることから、この名前が付いています。
|
||||
|
||||
次の図に示すように、バブル処理は要素の交換操作によってシミュレートできます。配列の最も左の端から右へ走査し、隣接する要素の大小を順に比較して、「左要素 > 右要素」であれば両者を交換します。走査が終わると、最大の要素は配列の最も右端へ移動します。
|
||||
下図のように、要素の交換で泡の動きを再現できます。配列の左端から右へ走査し、隣り合う要素を順に比較して、「左の要素 > 右の要素」なら交換します。走査が終わると、最大の要素が配列の右端に移ります。
|
||||
|
||||
=== "<1>"
|
||||

|
||||

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

|
||||

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

|
||||

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

|
||||

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

|
||||

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

|
||||

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

|
||||

|
||||
|
||||
## アルゴリズムの流れ
|
||||
|
||||
配列の長さを $n$ とすると、バブルソートの手順は次の図のとおりです。
|
||||
配列の長さを $n$ とすると、バブルソートの手順は下図のとおりです。
|
||||
|
||||
1. まず、$n$ 個の要素に対して「バブル処理」を行い、**配列中の最大要素を正しい位置へ交換します**。
|
||||
2. 次に、残りの $n - 1$ 個の要素に対して「バブル処理」を行い、**2 番目に大きい要素を正しい位置へ交換します**。
|
||||
3. このようにして、$n - 1$ 回の「バブル処理」を終えると、**大きいほうから $n - 1$ 個の要素がすべて正しい位置へ交換されます**。
|
||||
4. 残った 1 つの要素は必ず最小要素なので、並べ替える必要はなく、これで配列のソートが完了します。
|
||||
1. まず、$n$ 個の要素に対して泡を浮かべる操作を行い、**最大の要素を正しい位置へ移します**。
|
||||
2. 続いて、残りの $n - 1$ 個の要素に同じ操作を行い、**2 番目に大きい要素を正しい位置へ移します**。
|
||||
3. 同様に $n - 1$ 回繰り返すと、**大きいほうから $n - 1$ 個の要素がすべて正しい位置へ移ります**。
|
||||
4. 残った一つの要素は必ず最小要素なので、ソートは不要です。これで配列全体のソートが完了します。
|
||||
|
||||

|
||||
|
||||
@@ -42,11 +42,11 @@
|
||||
[file]{bubble_sort}-[class]{}-[func]{bubble_sort}
|
||||
```
|
||||
|
||||
## 効率の最適化
|
||||
## 効率の改善
|
||||
|
||||
ある回の「バブル処理」で交換操作が一度も行われなければ、配列はすでにソート済みであり、結果をそのまま返せることがわかります。したがって、この状況を検出するためのフラグ `flag` を追加し、発生した時点で直ちに返すようにできます。
|
||||
ある反復で一度も交換しなかった場合、配列はすでに整列済みなので、そのまま結果を返せます。そこで、フラグ `flag` を追加してこの状況を検知し、該当したら直ちに返すようにします。
|
||||
|
||||
最適化後も、バブルソートの最悪時間計算量と平均時間計算量は依然として $O(n^2)$ です。ただし、入力配列が完全に整列済みであれば、最良時間計算量は $O(n)$ に達します。
|
||||
この改善後も、最悪時間計算量と平均時間計算量は $O(n^2)$ のままです。ただし、入力配列が完全に整列済みなら、最良時間計算量 $O(n)$ を達成できます。
|
||||
|
||||
```src
|
||||
[file]{bubble_sort}-[class]{}-[func]{bubble_sort_with_flag}
|
||||
@@ -54,6 +54,6 @@
|
||||
|
||||
## アルゴリズムの特徴
|
||||
|
||||
- **時間計算量は $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$ は定数サイズの追加領域を使います。
|
||||
- **安定ソート**:泡を浮かべる操作では、等しい要素を交換しないためです。
|
||||
|
||||
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 30 KiB |
@@ -1,45 +1,45 @@
|
||||
# バケットソート
|
||||
|
||||
前述のいくつかのソートアルゴリズムは、いずれも「比較ベースのソートアルゴリズム」に属し、要素間の大小を比較することで整列を実現します。この種のソートアルゴリズムの最悪時間計算量の下界は $\Omega(n \log n)$ です。続いて、時間計算量が線形オーダーに達しうる「非比較ソートアルゴリズム」をいくつか見ていきます。
|
||||
ここまでのソートアルゴリズムは、要素の大小を比較する「比較ソート」です。この種類の最悪時間計算量の下界は $\Omega(n \log n)$ です。ここからは、時間計算量を線形にできる「非比較ソート」をいくつか見ていきます。
|
||||
|
||||
<u>バケットソート(bucket sort)</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 万個の要素が含まれ、空間の制約によりシステムメモリへすべてのデータを一度に読み込めない場合です。このとき、データを 1000 個のバケットに分け、それぞれのバケットを個別にソートしてから、最後に結果を結合できます。
|
||||
バケットソートは大量のデータを扱うのに適しています。例えば、100 万個の入力要素があり、容量の制約で一度にすべてをメモリに読み込めないとします。この場合、1000 個のバケットに分けて個別にソートし、最後に結果を結合できます。
|
||||
|
||||
- **時間計算量は $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 + k)$**:要素が各バケットに均等に分かれるとすると、一つのバケットの要素数は $\frac{n}{k}$ です。一つをソートする時間を $O(\frac{n}{k} \log\frac{n}{k})$ とすると、すべてのソートには $O(n \log\frac{n}{k})$ 時間かかります。**バケット数 $k$ が十分大きいと、時間計算量は $O(n)$ に近づきます**。結果の結合ではすべてのバケットと要素を走査するため、$O(n + k)$ 時間かかります。最悪の場合、すべてのデータが一つのバケットに入り、そのソートに $O(n^2)$ 時間かかります。
|
||||
- **空間計算量は $O(n + k)$、非インプレースソート**:$k$ 個のバケットと、合計 $n$ 個の要素を格納する追加領域が必要です。
|
||||
- バケットソートの安定性は、各バケット内で使うソートアルゴリズムの安定性に依存します。
|
||||
|
||||
## 均等な分配を実現するには
|
||||
## 均等に振り分けるには
|
||||
|
||||
バケットソートの時間計算量は理論上 $O(n)$ に達しますが、**鍵は要素を各バケットへ均等に分配すること** にあります。実際のデータは均一に分布していないことが多いからです。たとえば、Taobao 上のすべての商品を価格帯ごとに 10 個のバケットへ均等に分けたいとしても、商品の価格分布は偏っており、100 元未満は非常に多く、1000 元超は非常に少ないかもしれません。価格区間を単純に 10 等分すると、各バケットの商品数には大きな差が生じます。
|
||||
バケットソートの時間計算量は理論上 $O(n)$ にできますが、**要素を各バケットへ均等に振り分けることが鍵です**。実際のデータは均等に分布しているとは限らないためです。例えば、淘宝の全商品を価格帯で 10 個のバケットに均等に分けたいとしても、100 元未満の商品は多く、1000 元超は少ないなど、価格分布には偏りがあります。価格帯を等間隔に 10 分割すると、バケットごとの商品数は大きく異なります。
|
||||
|
||||
均等な分配を実現するために、まず大まかな境界線を設定し、データをひとまず 3 個のバケットに粗く振り分けます。**分配後は、商品数の多いバケットをさらに 3 個のバケットに分割し、すべてのバケット内の要素数がおおむね等しくなるまでこれを続けます**。
|
||||
均等に振り分けるには、まずおおよその境界を決め、データを大まかに 3 個のバケットへ分けます。**その後、商品が多いバケットをさらに 3 分割し、すべてのバケットの要素数がおおむね等しくなるまで続けます**。
|
||||
|
||||
以下の図に示すように、この方法の本質は再帰木を構築することにあり、目標は葉ノードの値をできるだけ均等にすることです。もちろん、毎回データを 3 個のバケットに分割する必要はなく、具体的な分け方はデータの特徴に応じて柔軟に選べます。
|
||||
下図のように、この方法は本質的に再帰木を作っており、葉ノードの値をできるだけ均等にすることを目指しています。毎回 3 個に分ける必要はなく、データの特徴に応じて柔軟に選べます。
|
||||
|
||||

|
||||

|
||||
|
||||
商品価格の確率分布をあらかじめ把握しているなら、**データの確率分布に基づいて各バケットの価格境界を設定できます**。なお、データ分布は必ずしも特別に統計を取る必要はなく、データの特徴に応じて何らかの確率モデルで近似することもできます。
|
||||
商品価格の確率分布があらかじめ分かっていれば、**その分布に合わせてバケットごとの価格の境界を設定できます**。分布は必ずしも実測する必要はなく、データの特徴に応じた確率モデルで近似することもできます。
|
||||
|
||||
以下の図に示すように、商品価格が正規分布に従うと仮定すれば、価格区間を合理的に設定でき、それによって商品を各バケットへ均等に分配できます。
|
||||
下図では、商品価格が正規分布に従うと仮定しています。これにより価格帯を適切に設定し、商品を各バケットへ均等に振り分けられます。
|
||||
|
||||

|
||||

|
||||
|
||||
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 11 KiB |
@@ -1,84 +1,84 @@
|
||||
# 計数ソート
|
||||
# カウントソート
|
||||
|
||||
<u>計数ソート(counting sort)</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}
|
||||
```
|
||||
|
||||
!!! note "計数ソートとバケットソートの関係"
|
||||
!!! note "カウントソートとバケットソートの関係"
|
||||
|
||||
バケットソートの観点から見ると、計数ソートにおける計数配列 `counter` の各インデックスを 1 つのバケットとみなし、個数を数える過程を各要素を対応するバケットへ振り分ける操作とみなせます。本質的には、計数ソートは整数データにおけるバケットソートの特殊な一例です。
|
||||
バケットソートの視点では、カウント配列 `counter` の各インデックスを一つのバケット、個数を数える過程を各要素を対応するバケットに入れる過程と見なせます。本質的に、カウントソートは整数データにおけるバケットソートの特殊な場合です。
|
||||
|
||||
## 完全な実装
|
||||
|
||||
注意深い読者なら、**入力データがオブジェクトである場合、上記の手順 `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` は要素 `num` が結果配列 `res` に最後に現れるインデックスを表します**。この情報は非常に重要で、各要素が結果配列のどの位置に現れるべきかを示してくれます。続いて元の配列 `nums` を逆順に走査し、各要素 `num` に対して各反復で次の 2 つの手順を行います。
|
||||
**累積和には明確な意味があります。`prefix[num] - 1` は、結果配列 `res` に要素 `num` が最後に現れるインデックスを表します**。これは各要素を結果配列のどこに置くかを示す重要な情報です。続いて、元の配列 `nums` の各要素 `num` を逆順に走査し、各反復で次の二つを行います。
|
||||
|
||||
1. `num` を配列 `res` のインデックス `prefix[num] - 1` に格納します。
|
||||
2. 累積和 `prefix[num]` を $1$ 減らし、次に `num` を配置するインデックスを得ます。
|
||||
1. 配列 `res` のインデックス `prefix[num] - 1` に `num` を入れます。
|
||||
2. 累積和 `prefix[num]` を $1$ 減らし、次に `num` を置くインデックスを得ます。
|
||||
|
||||
走査が完了すると、配列 `res` にソート済みの結果が格納されます。最後に `res` で元の配列 `nums` を上書きすれば完了です。以下の図は完全な計数ソートの流れを示しています。
|
||||
走査が終わると、配列 `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$ と $m$ の配列 `res` と `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)$ のソートより遅くなる可能性があります。
|
||||
|
||||
@@ -1,94 +1,94 @@
|
||||
# 演習
|
||||
# 練習問題
|
||||
|
||||
## 確認問題
|
||||
## 知識の確認
|
||||
|
||||
### 選択ソートとバブルソートの最初の数回
|
||||
### 選択ソートとバブルソートの最初の反復
|
||||
|
||||
配列 `[4, 2, 5, 1, 3]` が与えられます。次の操作では、いずれも小さい順にソートします。
|
||||
配列 `[4, 2, 5, 1, 3]` が与えられます。以下では、いずれも昇順にソートします。
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 選択ソートの最初の 2 回を行い、各回の終了時の配列を書いてください。また、この時点で位置が確定した要素を示してください。
|
||||
2. バブルソートの最初の 1 回を行い、配列の状態と交換回数を書いてください。また、どの位置が確定したかを示してください。
|
||||
1. 選択ソートの最初の二回の反復をたどり、各反復後の配列と、その時点で確定した位置を書いてください。
|
||||
2. バブルソートの最初の一回の反復をたどり、配列の状態、交換回数、確定した位置を書いてください。
|
||||
|
||||
??? success "解答"
|
||||
??? success "解答例"
|
||||
|
||||
1. 最初の 2 回は次のとおりです。
|
||||
1. 最初の二回の反復は、次のとおりです。
|
||||
|
||||
| 回 | 配列の状態 | 説明 |
|
||||
| 反復 | 配列の状態 | 説明 |
|
||||
| --- | --- | --- |
|
||||
| 1 | `[1, 2, 5, 4, 3]` | 最小要素 1 を先頭の要素と交換する |
|
||||
| 2 | `[1, 2, 5, 4, 3]` | 値 2 はすでにインデックス 1 にあるため、交換は不要 |
|
||||
| 1 | `[1, 2, 5, 4, 3]` | 最小要素 1 と先頭を交換します |
|
||||
| 2 | `[1, 2, 5, 4, 3]` | 2 はすでにインデックス 1 にあるため、交換は不要です |
|
||||
|
||||
この時点で、先頭の 2 つの位置が確定しています。以後は `[5, 4, 3]` から最小要素を選び続ければよいです。
|
||||
これで最初の二つの位置が確定し、以降は `[5, 4, 3]` から最小要素を選び続けます。
|
||||
|
||||
2. 隣り合う要素を順に比較します。4 と 2 は交換し、4 と 5 は交換せず、5 と 1、5 と 3 はそれぞれ交換します。
|
||||
その結果は `[2, 4, 1, 3, 5]` で、交換回数は 3 回です。最大要素 5 が配列の末尾へ移動したため、最後の位置が確定しています。
|
||||
2. 隣り合う要素を順に比較します。4 と 2 は交換し、4 と 5 は交換せず、5 と 1、5 と 3 を交換すると、
|
||||
`[2, 4, 1, 3, 5]` になります。交換は計 3 回です。最大要素 5 が末尾に移動したので、最後の位置が確定します。
|
||||
|
||||
### 等しい要素の順序は変わるか
|
||||
|
||||
配列 $[2_a, 2_b, 1]$ で、$2_a$ と $2_b$ の値は等しいものの、添字によって元の順序を区別しています。
|
||||
配列 $[2_a, 2_b, 1]$ の $2_a$ と $2_b$ は値が等しく、添字で元の順序を示しています。
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 選択ソートの 1 回目が終わった後の配列を書いてください。$2_a$ と $2_b$ の相対的な順序は変わりますか?
|
||||
2. バブルソートの 1 回目が終わった後の配列を書いてください。$2_a$ と $2_b$ の相対的な順序は変わりますか?
|
||||
3. 問い 1、2 の結果から、等しい要素の元の順序を保つという点で、2 つのソートにはどのような違いがあるかを説明してください。
|
||||
1. 選択ソートの一回目の反復後の配列を書いてください。$2_a$ と $2_b$ の相対的な順序は変わりますか。
|
||||
2. バブルソートの一回目の反復後の配列を書いてください。$2_a$ と $2_b$ の相対的な順序は変わりますか。
|
||||
3. 前の二問から、等しい要素の元の順序を保つという点で、二つのソートがどう違うかを説明してください。
|
||||
|
||||
??? success "解答"
|
||||
??? success "解答例"
|
||||
|
||||
1. 選択ソートの 1 回目では最小要素 1 を選び、先頭の $2_a$ と交換するため、
|
||||
$[1, 2_b, 2_a]$ となります。$2_a$ は $2_b$ の後ろへ移動し、相対的な順序が変わっています。
|
||||
1. 選択ソートの一回目では、最小要素 1 を選び、先頭の $2_a$ と交換するので、
|
||||
$[1, 2_b, 2_a]$ となります。$2_a$ が $2_b$ の後ろに移り、相対的な順序が変わります。
|
||||
|
||||
2. バブルソートでは、まず $2_a$ と $2_b$ を比較します。両者は等しいので交換しません。次に $2_b$ と 1 を比較して交換すると、
|
||||
1 回目の終了時は $[2_a, 1, 2_b]$ となります。$2_a$ は引き続き $2_b$ より前にあり、相対的な順序は変わっていません。
|
||||
2. バブルソートでは、まず $2_a$ と $2_b$ を比較します。等しいので交換せず、次に $2_b$ と 1 を比較して交換するため、
|
||||
一回目の反復後は $[2_a, 1, 2_b]$ です。$2_a$ は引き続き $2_b$ より前にあり、相対的な順序は変わりません。
|
||||
|
||||
3. この例では、選択ソートは等しい要素の元の順序を変えます。バブルソートは、左側の要素が右側の要素より大きい場合にだけ
|
||||
3. この例では、選択ソートは等しい要素の元の順序を変えます。一方、バブルソートは、左の要素が右の要素より大きい場合にだけ
|
||||
隣り合う要素を交換します。等しい要素同士は交換しないため、元の順序を保てます。
|
||||
|
||||
### 計数ソートと基数ソートの比較
|
||||
### カウントソートと基数ソートの比較
|
||||
|
||||
学校で、8 桁に固定された多数の学籍番号をソートします。次の問いに答えてください。
|
||||
学校で、桁数が 8 桁に固定された多数の学籍番号をソートします。次の問いに答えてください。
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 最下位桁から始める基数ソートでは、何回の処理が必要ですか?
|
||||
2. 学籍番号をそのまま整数として計数ソートを行うと、使われないカウント位置を大量に用意することになるのはなぜですか?
|
||||
3. 問い 1、2 の結果から、8 桁に固定された多数の学籍番号を計数ソートと基数ソートのどちらかで処理するなら、どちらを選びますか?その理由も説明してください。
|
||||
1. 最下位桁から始める基数ソートでは、何回の反復が必要ですか。
|
||||
2. 学籍番号をそのまま整数としてカウントソートすると、使われないカウント位置を大量に用意することになるのはなぜですか。
|
||||
3. 前の二問から、多数の 8 桁の学籍番号を処理するためにカウントソートと基数ソートのどちらかを選ぶなら、どちらにしますか。理由も説明してください。
|
||||
|
||||
??? success "解答"
|
||||
??? success "解答例"
|
||||
|
||||
1. 学籍番号には 8 個の桁があるため、最下位桁から最上位桁まで、合計 8 回処理します。各回では 0~9 だけを使って分類します。
|
||||
1. 学籍番号は 8 桁なので、最下位桁から最上位桁まで計 8 回処理します。各回は 0~9 による分類だけを行います。
|
||||
|
||||
2. そのまま数えるには、取り得るすべての 8 桁の値に対して位置を用意する必要があります。しかし、実際に生徒が使うのはそのうちのごく一部なので、
|
||||
ほとんどのカウント位置は常に 0 のままです。
|
||||
2. 直接カウントするには、8 桁で取り得るすべての値に位置を用意する必要があります。しかし、実際の生徒が使うのはそのごく一部で、
|
||||
ほとんどのカウント位置は 0 のままです。
|
||||
|
||||
3. 基数ソートを選びます。「桁数が固定され、各桁が 10 種類の値だけを取る」という特徴を利用し、安定した分類を 8 回繰り返すだけで済むためです。
|
||||
8 桁の学籍番号全体を整数のインデックスとして計数ソートを行うと、実際には現れない多数の値にもカウント位置を用意する必要があります。
|
||||
3. 基数ソートを選びます。「桁数が固定され、各桁に 10 種類の値しかない」という特徴を利用し、安定な分類を 8 回繰り返すだけで済みます。
|
||||
カウントソートで 8 桁の番号全体を整数のインデックスとして使うと、実際には現れない多数の値にカウント位置を用意しなければなりません。
|
||||
|
||||
## プログラミング演習
|
||||
|
||||
### マージソートで配列を並べる
|
||||
|
||||
整数配列 `nums` が与えられます。マージソートを自分で実装し、要素を非減少順に並べて返してください。言語に組み込まれたソート関数を呼び出してはいけません。
|
||||
整数配列 `nums` が与えられます。マージソートを自分で実装し、要素を非減少順に並べて返してください。言語に組み込まれたソート関数は呼び出さないでください。
|
||||
|
||||
??? tip "解法のヒント"
|
||||
??? tip "解くためのヒント"
|
||||
|
||||
1. 区間の長さが 1 以下なら、すでにソートされています
|
||||
2. 中点で区間を 2 つに分け、それぞれを再帰的にソートします
|
||||
3. 2 つのポインタでソート済みの左右の区間をマージし、結果を元の配列へ書き戻します
|
||||
1. 区間の長さが 1 以下なら、すでに整列済みです
|
||||
2. 中点で区間を二つに分け、それぞれを再帰的にソートします
|
||||
3. 二つのポインタで整列済みの半区間をマージし、結果を元の配列に書き戻します
|
||||
|
||||
[LeetCode](https://leetcode.com/problems/sort-an-array/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
|
||||
### 計数ソートで整数配列を並べる
|
||||
### カウントソートで整数配列を並べる
|
||||
|
||||
整数配列 `nums` と 0 以上の整数 $K$ が与えられ、配列の各要素は $0$ 以上 $K$ 以下です。
|
||||
整数配列 `nums` と非負整数 $K$ が与えられます。配列の各要素は $0$ 以上 $K$ 以下です。
|
||||
|
||||
計数ソートを実装し、要素を非減少順に `nums` へ書き戻して、`nums` を返してください。
|
||||
要素同士の大小比較によって順序を決めたり、言語に組み込まれたソート関数を呼び出したりしてはいけません。
|
||||
カウントソートを実装し、結果を非減少順に `nums` へ書き戻して、`nums` を返してください。
|
||||
要素間の大小比較で順序を決めたり、言語に組み込まれたソート関数を呼び出したりしてはいけません。
|
||||
|
||||
??? tip "解法のヒント"
|
||||
??? tip "解くためのヒント"
|
||||
|
||||
1. 各要素は 0 以上 K 以下なので、その値をカウント配列のインデックスとして直接使えます
|
||||
2. 1 回目に nums を走査し、対応する位置のカウントに 1 を足します
|
||||
3. 次にカウント配列を 0 から K まで走査します。値 x が現れた回数だけ、nums に x を続けて書き込みます
|
||||
1. 各要素は 0 以上 K 以下なので、値をそのままカウント配列のインデックスに使えます
|
||||
2. 一回目の走査では nums を調べ、対応する位置の個数を 1 増やします
|
||||
3. 次にカウント配列を 0 から K まで走査します。値 x が出現した回数だけ、nums に x を連続して書き込みます
|
||||
|
||||
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 16 KiB |
@@ -2,72 +2,72 @@
|
||||
|
||||
!!! tip
|
||||
|
||||
本節を読む前に、「ヒープ」の章を学習済みであることを確認してください。
|
||||
この節を読む前に、「ヒープ」の章を学習しておいてください。
|
||||
|
||||
<u>ヒープソート(heap sort)</u>は、ヒープデータ構造に基づいて実装される効率的なソートアルゴリズムです。すでに学んだ「ヒープ構築操作」と「要素の取り出し操作」を利用してヒープソートを実現できます。
|
||||
<u>ヒープソート(heap sort)</u>は、ヒープというデータ構造に基づく効率的なソートアルゴリズムです。すでに学んだヒープの構築と要素の取り出しを使って実装できます。
|
||||
|
||||
1. 配列を入力して最小ヒープを構築すると、このとき最小要素はヒープの頂点にあります。
|
||||
2. 取り出し操作を繰り返し実行し、取り出された要素を順に記録すれば、昇順に並んだ列が得られます。
|
||||
1. 配列を入力して最小ヒープを構築します。この時点で最小要素がヒープの先頭にあります。
|
||||
2. 取り出しを繰り返し、取り出した要素を順に記録すると、昇順に並んだ列が得られます。
|
||||
|
||||
以上の方法でも実行できますが、取り出した要素を保存するために追加の配列が必要となり、空間をやや無駄にします。実際には、通常はより洗練された実装方法を用います。
|
||||
この方法でも実現できますが、取り出した要素を保存する追加の配列が必要で、領域を多く使います。実際には、通常、よりすっきりした実装方法を使います。
|
||||
|
||||
## アルゴリズムの流れ
|
||||
|
||||
配列の長さを $n$ とすると、ヒープソートの流れは次図のとおりです。
|
||||
配列の長さを $n$ とすると、ヒープソートの流れは下図のとおりです。
|
||||
|
||||
1. 配列を入力して最大ヒープを構築します。完了後、最大要素はヒープの頂点にあります。
|
||||
2. ヒープ頂点の要素(最初の要素)とヒープ末尾の要素(最後の要素)を交換します。交換後、ヒープの長さは $1$ 減少し、整列済み要素数は $1$ 増加します。
|
||||
3. ヒープ頂点の要素から始めて、上から下へヒープ化操作(sift down)を実行します。ヒープ化が完了すると、ヒープの性質が回復します。
|
||||
4. 第 `2.` ステップと第 `3.` ステップを繰り返し実行します。これを $n - 1$ 回繰り返すと、配列の整列が完了します。
|
||||
1. 配列を入力して最大ヒープを構築します。すると、最大要素がヒープの先頭にあります。
|
||||
2. ヒープの先頭要素(最初の要素)と末尾要素(最後の要素)を交換します。その後、ヒープの長さを $1$ 減らし、整列済み要素数を $1$ 増やします。
|
||||
3. 先頭要素から下向きにヒープ化(sift down)します。これにより、ヒープの性質が復元されます。
|
||||
4. 手順 `2.` と `3.` を繰り返します。$n - 1$ 回反復すると、配列のソートが完了します。
|
||||
|
||||
!!! tip
|
||||
|
||||
実際には、要素の取り出し操作にも第 `2.` ステップと第 `3.` ステップが含まれており、要素を取り出す処理が 1 つ加わるだけです。
|
||||
実は、ヒープから要素を取り出す操作にも手順 `2.` と `3.` が含まれています。違いは、要素を取り除く手順が追加されるだけです。
|
||||
|
||||
=== "<1>"
|
||||

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

|
||||

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

|
||||

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

|
||||

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

|
||||

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

|
||||

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

|
||||

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

|
||||

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

|
||||

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

|
||||

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

|
||||

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

|
||||

|
||||
|
||||
コード実装では、「ヒープ」の章と同じ上から下へのヒープ化 `sift_down()` 関数を使用します。注意すべき点として、ヒープの長さは最大要素を取り出すたびに短くなるため、`sift_down()` 関数に長さパラメータ $n$ を追加し、ヒープの現在の有効な長さを指定する必要があります。コードは以下のとおりです。
|
||||
実装では「ヒープ」の章と同じ、下向きにヒープ化する関数 `sift_down()` を使います。ただし、最大要素を取り出すたびにヒープの長さが縮むので、現在の有効な長さを指定する引数 $n$ を `sift_down()` に追加する必要があります。コードは次のとおりです。
|
||||
|
||||
```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)$ の領域を使います。要素の交換もヒープ化も、元の配列上で行います。
|
||||
- **不安定ソート**:先頭要素と末尾要素を交換する際、等しい要素の相対的な位置が変わる可能性があります。
|
||||
|
||||
@@ -4,6 +4,6 @@
|
||||
|
||||
!!! abstract
|
||||
|
||||
ソートは、混沌を秩序へと変える魔法の鍵のようなものであり、データをより効率的に理解し処理することを可能にします。
|
||||
ソートは混沌を秩序へと変える魔法の鍵のようなものであり、データをより効率的に理解し、処理できるようにします。
|
||||
|
||||
単純な昇順であれ、複雑な分類配列であれ、ソートはデータの調和のとれた美しさを私たちに示してくれます。
|
||||
単純な昇順であれ、複雑な分類と並べ替えであれ、ソートはデータの調和の美しさを見せてくれます。
|
||||
|
||||
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 19 KiB |
@@ -1,25 +1,25 @@
|
||||
# 挿入ソート
|
||||
|
||||
<u>挿入ソート(insertion sort)</u>は単純なソートアルゴリズムであり、その動作原理は手作業でトランプの山を整える過程と非常によく似ています。
|
||||
<u>挿入ソート(insertion sort)</u>は単純なソートアルゴリズムで、その仕組みは手作業でトランプを並べる過程によく似ています。
|
||||
|
||||
具体的には、未ソート区間から基準要素を 1 つ選び、その要素を左側の整列済み区間の要素と 1 つずつ比較し、正しい位置に挿入します。
|
||||
具体的には、未整列区間から基準となる要素を一つ選び、左側の整列済み区間の要素と順に比較して、正しい位置へ挿入します。
|
||||
|
||||
以下の図は、配列に要素を挿入する操作の流れを示しています。基準要素を `base` とすると、目的のインデックスから `base` までのすべての要素を 1 つずつ右に移動し、その後 `base` を目的のインデックスに代入する必要があります。
|
||||
下図は配列に要素を挿入する操作の流れです。基準の要素を `base` とすると、挿入先のインデックスから `base` までの要素をすべて一つ右へ移動し、挿入先のインデックスに `base` を代入します。
|
||||
|
||||

|
||||

|
||||
|
||||
## アルゴリズムの流れ
|
||||
|
||||
挿入ソート全体の流れを以下の図に示します。
|
||||
挿入ソート全体の流れを下図に示します。
|
||||
|
||||
1. 初期状態では、配列の 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}
|
||||
@@ -27,20 +27,20 @@
|
||||
|
||||
## アルゴリズムの特徴
|
||||
|
||||
- **計算量は $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)$、適応的ソート**:最悪の場合、各挿入操作で必要な反復は $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)$ ですが、実際には**挿入ソートがほかの二つよりはるかによく使われます**。主な理由は次のとおりです。
|
||||
|
||||
- バブルソートは要素の交換によって実装され、1 つの一時変数を必要とするため、合計で 3 回の基本演算が関わります。これに対して、挿入ソートは要素の代入に基づいており、必要な基本演算は 1 回だけです。したがって、**バブルソートの計算コストは通常、挿入ソートより高くなります**。
|
||||
- 選択ソートの時間計算量はどのような場合でも $O(n^2)$ です。**部分的に整列されたデータが与えられた場合、挿入ソートは通常、選択ソートより効率的**です。
|
||||
- 選択ソートは安定ではないため、多段ソートには適用できません。
|
||||
- バブルソートは要素の交換を使うため、一時変数が必要で、基本操作を 3 回行います。一方、挿入ソートは要素への代入を使うため、基本操作は 1 回だけです。したがって、**通常、バブルソートの計算負荷は挿入ソートより大きくなります**。
|
||||
- 選択ソートの時間計算量は、どのような場合も $O(n^2)$ です。**一部が整列済みのデータでは、通常、挿入ソートのほうが選択ソートより効率的です**。
|
||||
- 選択ソートは不安定なので、複数の基準で段階的にソートする用途には使えません。
|
||||
|
||||
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 8.1 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 8.4 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 8.7 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 9.5 KiB |
@@ -1,73 +1,73 @@
|
||||
# マージソート
|
||||
|
||||
<u>マージソート(merge sort)</u>は分割統治戦略に基づくソートアルゴリズムであり、以下の図に示す「分割」と「マージ」の段階から構成されます。
|
||||
<u>マージソート(merge sort)</u>は分割統治法に基づくソートアルゴリズムで、下図の「分割」と「結合」の段階からなります。
|
||||
|
||||
1. **分割段階**:再帰によって配列を中点で繰り返し分割し、長い配列のソート問題を短い配列のソート問題へ変換します。
|
||||
2. **マージ段階**:部分配列の長さが 1 になったら分割を終了し、マージを開始して、左右 2 つの短いソート済み配列をより長いソート済み配列へと繰り返しマージしていきます。
|
||||
1. **分割の段階**:再帰的に配列を中点で分け続け、長い配列のソート問題を短い配列のソート問題へ変換します。
|
||||
2. **結合の段階**:部分配列の長さが 1 になったら分割を終了して結合を始めます。左右の短い整列済み配列を一つの長い整列済み配列へ結合し、最後まで続けます。
|
||||
|
||||

|
||||

|
||||
|
||||
## アルゴリズムの流れ
|
||||
|
||||
以下の図に示すように、「分割段階」では配列を上から下へ再帰的に中点で 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>"
|
||||

|
||||

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

|
||||

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

|
||||

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

|
||||

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

|
||||

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

|
||||

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

|
||||

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

|
||||

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

|
||||

|
||||
|
||||
観察すると、マージソートの再帰順序は二分木の後順走査と一致していることがわかります。
|
||||
マージソートの再帰の順序は、二分木の帰りがけ順走査と同じです。
|
||||
|
||||
- **後順走査**:まず左部分木を再帰し、次に右部分木を再帰し、最後に根ノードを処理します。
|
||||
- **マージソート**:まず左部分配列を再帰し、次に右部分配列を再帰し、最後にマージを処理します。
|
||||
- **帰りがけ順走査**:まず左の部分木を再帰的に処理し、次に右の部分木を処理して、最後に根ノードを処理します。
|
||||
- **マージソート**:まず左の部分配列を再帰的に処理し、次に右の部分配列を処理して、最後に結合します。
|
||||
|
||||
マージソートの実装を以下のコードに示します。注意として、`nums` のマージ対象区間は `[left, right]` であり、`tmp` の対応区間は `[0, right - left]` です。
|
||||
実装は次のとおりです。結合する `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(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 つの長いソート済み連結リストにマージすること)では追加の連結リストを作成する必要がありません。
|
||||
- **分割の段階**:再帰の代わりに反復を使って連結リストを分割すれば、再帰のスタックフレーム領域を省けます。
|
||||
- **結合の段階**:連結リストのノードの追加や削除は、参照(ポインタ)の変更だけで行えます。そのため、二つの短い整列済みリストを一つの長い整列済みリストに結合する際、追加のリストを作る必要はありません。
|
||||
|
||||
具体的な実装の詳細は比較的複雑なので、興味のある読者は関連資料を参照して学習してください。
|
||||
具体的な実装はやや複雑なので、興味があれば関連資料で学んでみてください。
|
||||
|
||||
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 19 KiB |
@@ -1,45 +1,45 @@
|
||||
# クイックソート
|
||||
|
||||
<u>クイックソート(quick sort)</u>は分割統治戦略に基づくソートアルゴリズムであり、実行効率が高く、広く利用されています。
|
||||
<u>クイックソート(quick sort)</u>は分割統治法に基づくソートアルゴリズムで、実行効率が高く、広く使われています。
|
||||
|
||||
クイックソートの中核操作は「パーティション」であり、その目的は、配列内のある要素を「基準数」として選び、基準数より小さいすべての要素を左側へ、大きい要素を右側へ移動することです。具体的には、パーティションの流れを下図に示します。
|
||||
中心となる操作は「番兵を用いた分割」です。配列の要素を一つ「ピボット」として選び、それより小さい要素を左へ、大きい要素を右へ移すことを目指します。具体的な流れを下図に示します。
|
||||
|
||||
1. 配列の最左端の要素を基準数として選び、2 つのポインタ `i` と `j` を初期化して、それぞれ配列の両端を指すようにします。
|
||||
2. ループを設定し、各ラウンドで `i`(`j`)を使ってそれぞれ基準数より大きい(小さい)最初の要素を探し、その後この 2 つの要素を交換します。
|
||||
3. `i` と `j` が出会うまでステップ `2.` を繰り返し、最後に基準数を 2 つの部分配列の境界へ交換します。
|
||||
1. 配列の左端の要素をピボットとして選び、ポインタ `i` と `j` を配列の両端に初期化します。
|
||||
2. ループの各反復で、`i`(`j`)を使ってピボットより大きい(小さい)最初の要素を探し、その二つを交換します。
|
||||
3. 手順 `2.` を繰り返し、`i` と `j` が出会ったら停止します。最後に、ピボットを二つの部分配列の境目へ交換して移します。
|
||||
|
||||
=== "<1>"
|
||||

|
||||

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

|
||||

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

|
||||

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

|
||||

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

|
||||

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

|
||||

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

|
||||

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

|
||||

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

|
||||

|
||||
|
||||
パーティションが完了すると、元の配列は左部分配列、基準数、右部分配列の 3 つに分けられ、「左部分配列の任意の要素 $\leq$ 基準数 $\leq$ 右部分配列の任意の要素」を満たします。したがって、次はこの 2 つの部分配列だけをソートすれば済みます。
|
||||
分割後、元の配列は左の部分配列、ピボット、右の部分配列の三つに分かれ、「左の部分配列の任意の要素 $\leq$ ピボット $\leq$ 右の部分配列の任意の要素」を満たします。したがって、あとは二つの部分配列をソートするだけです。
|
||||
|
||||
!!! note "クイックソートの分割統治戦略"
|
||||
!!! note "クイックソートの分割統治法"
|
||||
|
||||
パーティションの本質は、長い配列のソート問題を 2 つの短い配列のソート問題へ簡略化することです。
|
||||
番兵を用いた分割は、長い配列のソート問題を、二つの短い配列のソート問題へと単純化します。
|
||||
|
||||
```src
|
||||
[file]{quick_sort}-[class]{quick_sort}-[func]{partition}
|
||||
@@ -49,9 +49,9 @@
|
||||
|
||||
クイックソート全体の流れを下図に示します。
|
||||
|
||||
1. まず、元の配列に対して 1 回「パーティション」を実行し、未ソートの左部分配列と右部分配列を得ます。
|
||||
2. 次に、左部分配列と右部分配列に対してそれぞれ再帰的に「パーティション」を実行します。
|
||||
3. 部分配列の長さが 1 になるまで再帰を続け、配列全体のソートを完了します。
|
||||
1. まず、元の配列に番兵を用いた分割を一回行い、未整列の左右の部分配列を得ます。
|
||||
2. 続いて、左右それぞれの部分配列に対して、再帰的に同じ分割を行います。
|
||||
3. 部分配列の長さが 1 になるまで再帰を続けると、配列全体のソートが完了します。
|
||||
|
||||

|
||||
|
||||
@@ -59,41 +59,41 @@
|
||||
[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$ の部分配列に分かれます。このとき再帰の層数は $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)$ 時間で実行できます。
|
||||
- **キャッシュを効率よく使える**:分割時には部分配列全体をキャッシュに読み込めるため、要素へのアクセスが効率的です。ヒープソートのように要素へ飛び飛びにアクセスするアルゴリズムには、この特徴がありません。
|
||||
- **計算量の定数係数が小さい**:この三つのアルゴリズムの中で、比較、代入、交換などの操作の総数が最も少なくなります。挿入ソートがバブルソートより速い理由と似ています。
|
||||
|
||||
## 基準数の最適化
|
||||
## ピボットの選び方の改善
|
||||
|
||||
**クイックソートは、入力によっては時間効率が低下する可能性があります**。極端な例として、入力配列が完全な逆順であるとします。最左端の要素を基準数として選ぶため、パーティション完了後には基準数が配列の最右端へ交換され、左部分配列の長さが $n - 1$、右部分配列の長さが $0$ になります。この再帰を続けると、各回のパーティション後に必ず一方の部分配列の長さが $0$ となり、分割統治戦略が機能せず、クイックソートは「バブルソート」に近い形へ退化します。
|
||||
**入力によっては、クイックソートの時間効率が下がります**。極端な例として、完全に逆順の配列を考えます。左端をピボットにすると、分割後にはピボットが右端に移り、左の部分配列の長さは $n - 1$、右は $0$ になります。このまま再帰を続けると、毎回どちらかの部分配列の長さが $0$ となり、分割統治法が機能せず、バブルソートに近い形へと退化します。
|
||||
|
||||
この状況をできるだけ避けるため、**パーティションにおける基準数の選び方を最適化できます**。たとえば、ランダムに 1 つの要素を選んで基準数にできます。しかし、運が悪く毎回望ましくない基準数を選んでしまうと、効率は依然として十分ではありません。
|
||||
この状況をできるだけ避けるため、**分割時のピボットの選択方法を改善できます**。例えば、要素をランダムに選ぶ方法があります。ただし、運悪く毎回不適切なピボットを選べば、効率は依然としてよくありません。
|
||||
|
||||
注意すべき点として、プログラミング言語が通常生成するのは「疑似乱数」です。疑似乱数列に合わせて特定のテストケースを構築すると、クイックソートの効率はやはり劣化する可能性があります。
|
||||
プログラミング言語が通常生成するのは「疑似乱数」であることにも注意が必要です。その疑似乱数列に合わせた特定のテストケースを作れば、クイックソートの効率はやはり悪化する可能性があります。
|
||||
|
||||
さらに改善するために、配列から 3 つの候補要素(通常は先頭、末尾、中間の要素)を選び、**その 3 つの候補要素の中央値を基準数とする**ことができます。こうすると、基準数が「小さすぎず大きすぎもしない」確率が大幅に上がります。もちろん、候補要素をさらに増やして、アルゴリズムの頑健性をいっそう高めることも可能です。この方法を採用すると、時間計算量が $O(n^2)$ まで劣化する確率は大きく下がります。
|
||||
さらに改善するには、配列から三つの候補(通常は先頭、末尾、中点の要素)を選び、**それらの中央値をピボットにします**。これにより、ピボットが小さすぎず大きすぎない確率が大幅に上がります。候補をさらに増やして、頑健性を高めることもできます。この方法では、時間計算量が $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)$ まで最適化できます。コードを以下に示します。
|
||||
スタックフレームの蓄積を防ぐには、毎回の分割後に二つの部分配列の長さを比べ、**短いほうだけを再帰的に処理します**。短いほうの長さは $n / 2$ を超えないため、再帰の深さを $\log n$ 以下に抑えられ、最悪空間計算量を $O(\log n)$ に改善できます。コードは次のとおりです。
|
||||
|
||||
```src
|
||||
[file]{quick_sort}-[class]{quick_sort_tail_call}-[func]{quick_sort}
|
||||
|
||||
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 33 KiB |
@@ -1,41 +1,41 @@
|
||||
# 基数ソート
|
||||
|
||||
前節では計数ソートを紹介しました。これは、データ量 $n$ が大きく、データ範囲 $m$ が小さい場合に適しています。$n = 10^6$ 個の学籍番号をソートすると仮定すると、学籍番号は $8$ 桁の数字なので、データ範囲 $m = 10^8$ は非常に大きくなります。計数ソートでは大量のメモリ空間を確保する必要がありますが、基数ソートではこの問題を回避できます。
|
||||
前節で紹介したカウントソートは、データ量 $n$ が多く、値の範囲 $m$ が狭い場合に適しています。$n = 10^6$ 個の学籍番号をソートし、各番号が $8$ 桁だとすると、値の範囲は $m = 10^8$ と非常に大きく、カウントソートでは大量のメモリが必要です。基数ソートなら、この状況を避けられます。
|
||||
|
||||
<u>基数ソート(radix sort)</u>の基本的な考え方は計数ソートと同じで、個数を数えることによってソートを実現します。そのうえで、基数ソートは各桁の段階的な関係を利用し、各桁を順にソートすることで、最終的なソート結果を得ます。
|
||||
<u>基数ソート(radix sort)</u>の中心的な考え方はカウントソートと同じで、個数を数えてソートします。さらに、数の各桁の関係を利用して一桁ずつ順にソートし、最終的な結果を得ます。
|
||||
|
||||
## アルゴリズムの流れ
|
||||
|
||||
学籍番号データを例にすると、数字の最下位桁を第 $1$ 位、最上位桁を第 $8$ 位としたとき、基数ソートの流れは次図のようになります。
|
||||
学籍番号を例に、最下位を第 $1$ 桁、最上位を第 $8$ 桁とすると、基数ソートの流れは下図のとおりです。
|
||||
|
||||
1. 桁番号 $k = 1$ を初期化します。
|
||||
2. 学籍番号の第 $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$ による剰余を表します。学籍番号データでは、$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 "なぜ最下位の桁からソートするのか?"
|
||||
|
||||
連続するソートの各ラウンドでは、後のラウンドの結果が前のラウンドの結果を上書きします。たとえば、第1ラウンドで $a < b$ となっていても、第2ラウンドで $a > b$ となれば、第2ラウンドの結果が優先されます。数字では高位の優先度が低位より高いため、先に低位をソートし、その後で高位をソートする必要があります。
|
||||
ソートを繰り返すと、後の回の結果が前の回の結果に優先します。例えば、1 回目の結果が $a < b$ で、2 回目が $a > b$ なら、1 回目の結果が 2 回目の結果に置き換わります。数では上位の桁が下位の桁より優先されるため、下位の桁を先に、上位の桁を後にソートします。
|
||||
|
||||
## アルゴリズムの特徴
|
||||
|
||||
計数ソートと比べると、基数ソートは値の範囲が大きい場合に適しています。**ただし、データが固定桁数の形式で表せること、かつ桁数が大きすぎないことが前提です**。たとえば、浮動小数点数は基数ソートに適していません。桁数 $k$ が大きすぎて、時間計算量が $O(nk) \gg O(n^2)$ になる可能性があるためです。
|
||||
カウントソートと比べ、基数ソートは値の範囲が広い場合に適しています。**ただし、データを固定桁数の形式で表せて、桁数が大きすぎないことが前提です**。例えば、浮動小数点数は桁数 $k$ が大きすぎて時間計算量が $O(nk) \gg O(n^2)$ となる可能性があり、基数ソートには適していません。
|
||||
|
||||
- **時間計算量は $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` を補助的に用います。
|
||||
- **安定ソート**:計数ソートが安定であれば基数ソートも安定です。計数ソートが不安定な場合、基数ソートでは正しいソート結果を保証できません。
|
||||
- **時間計算量は $O(nk)$、非適応的ソート**:データ量を $n$、基数を $d$、最大桁数を $k$ とすると、一桁のカウントソートには $O(n + d)$ 時間、全 $k$ 桁には $O((n + d)k)$ 時間かかります。通常、$d$ と $k$ は比較的小さいので、時間計算量は $O(n)$ に近づきます。
|
||||
- **空間計算量は $O(n + d)$、非インプレースソート**:カウントソートと同様に、長さ $n$ と $d$ の配列 `res` と `counter` を使います。
|
||||
- **安定ソート**:カウントソートが安定なら、基数ソートも安定です。カウントソートが不安定な場合、基数ソートで正しい結果を得られる保証はありません。
|
||||
|
||||
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 7.8 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 8.0 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 7.8 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 8.1 KiB |
@@ -1,49 +1,49 @@
|
||||
# 選択ソート
|
||||
|
||||
<u>選択ソート(selection sort)</u>の仕組みは非常に単純です。ループを開始し、各ラウンドで未ソート区間から最小の要素を選び、整列済み区間の末尾に配置します。
|
||||
<u>選択ソート(selection sort)</u>の仕組みはとても簡単です。ループの各反復で未整列の区間から最小の要素を選び、整列済み区間の末尾に置きます。
|
||||
|
||||
配列の長さを $n$ とすると、選択ソートの手順は次の図のようになります。
|
||||
配列の長さを $n$ とすると、選択ソートの流れは下図のとおりです。
|
||||
|
||||
1. 初期状態では、すべての要素が未ソートであり、未ソートな(インデックス)区間は $[0, n-1]$ です。
|
||||
2. 区間 $[0, n-1]$ 内の最小要素を選び、インデックス $0$ の要素と交換します。これにより、配列の先頭 1 要素が整列済みになります。
|
||||
3. 区間 $[1, n-1]$ 内の最小要素を選び、インデックス $1$ の要素と交換します。これにより、配列の先頭 2 要素が整列済みになります。
|
||||
4. これを繰り返します。$n - 1$ 回の選択と交換を経ると、配列の先頭 $n - 1$ 要素が整列済みになります。
|
||||
5. 残った 1 つの要素は必ず最大要素なので、ソートは不要です。これで配列のソートは完了します。
|
||||
1. 初期状態ではすべての要素が未整列で、未整列のインデックス区間は $[0, n-1]$ です。
|
||||
2. 区間 $[0, n-1]$ の最小要素を選び、インデックス $0$ の要素と交換します。これで先頭の 1 要素が整列済みになります。
|
||||
3. 区間 $[1, n-1]$ の最小要素を選び、インデックス $1$ の要素と交換します。これで先頭の 2 要素が整列済みになります。
|
||||
4. 同様に、選択と交換を $n - 1$ 回繰り返すと、先頭の $n - 1$ 要素が整列済みになります。
|
||||
5. 残った一つの要素は必ず最大要素なので、ソートは不要です。これで配列全体のソートが完了します。
|
||||
|
||||
=== "<1>"
|
||||

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

|
||||

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

|
||||

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

|
||||

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

|
||||

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

|
||||

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

|
||||

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

|
||||

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

|
||||

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

|
||||

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

|
||||

|
||||
|
||||
コードでは、$k$ を用いて未ソート区間内の最小要素を記録します。
|
||||
コードでは、未整列区間の最小要素を記録するために $k$ を使います。
|
||||
|
||||
```src
|
||||
[file]{selection_sort}-[class]{}-[func]{selection_sort}
|
||||
@@ -51,8 +51,8 @@
|
||||
|
||||
## アルゴリズムの特徴
|
||||
|
||||
- **時間計算量は $O(n^2)$、非適応ソート**:外側のループは合計 $n - 1$ 回です。内側のループは最初のラウンドで $n - 1$ 回、最後のラウンドで $1$ 回実行されます。各ラウンドの実行回数はそれぞれ $n - 1$、$n - 2$、$\dots$、$2$、$1$ であり、総和は $\frac{n(n - 1)}{2}$ です。
|
||||
- **空間計算量は $O(1)$、インプレースソート**:ポインタ $i$ と $j$ は定数サイズの追加領域しか使用しません。
|
||||
- **不安定ソート**:次の図のように、要素 `nums[i]` がそれと等しい要素の右側へ交換され、両者の相対的な順序が変わる可能性があります。
|
||||
- **時間計算量は $O(n^2)$、非適応的ソート**:外側のループは $n - 1$ 回です。内側のループは最初が $n - 1$ 回、最後が $1$ 回で、順に $n - 1$、$n - 2$、$\dots$、$2$、$1$ 回実行されます。その合計は $\frac{n(n - 1)}{2}$ です。
|
||||
- **空間計算量は $O(1)$、インプレースソート**:ポインタ $i$ と $j$ は定数サイズの追加領域を使います。
|
||||
- **不安定ソート**:下図のように、要素 `nums[i]` が等しい要素の右側に移され、両者の相対的な順序が変わる可能性があります。
|
||||
|
||||

|
||||

|
||||
|
||||
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 12 KiB |
@@ -1,23 +1,23 @@
|
||||
# ソートアルゴリズム
|
||||
|
||||
<u>ソートアルゴリズム(sorting algorithm)</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)
|
||||
@@ -25,9 +25,9 @@
|
||||
('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>は、比較演算子($<$、$=$、$>$)に依存して要素の相対順序を判定し、それによって配列全体をソートします。その最悪時間計算量の下界は $\Omega(n \log n)$ です。一方、<u>非比較ソート</u>は比較演算子を使用せず、時間計算量は $O(n)$ に達しますが、汎用性は相対的に低くなります。
|
||||
**比較に基づくかどうか**:<u>比較ソート</u>は、比較演算子($<$、$=$、$>$)で要素間の相対的な順序を判定し、配列全体をソートします。最悪時間計算量の下界は $\Omega(n \log n)$ です。一方、<u>非比較ソート</u>は比較演算子を使わず、時間計算量が $O(n)$ にもなりますが、汎用性は比較的低くなります。
|
||||
|
||||
## 理想的なソートアルゴリズム
|
||||
|
||||
**高速、インプレース、安定、適応的、高い汎用性**。明らかに、これまでのところ、以上のすべての特性を兼ね備えたソートアルゴリズムはまだ見つかっていません。そのため、ソートアルゴリズムを選択する際には、具体的なデータの特徴と問題の要件に応じて判断する必要があります。
|
||||
**速く、インプレースで、安定で、適応的で、汎用性が高いこと**が理想です。しかし、これらをすべて兼ね備えたソートアルゴリズムは、これまで見つかっていません。そのため、具体的なデータの特徴と問題の要件に応じて選ぶ必要があります。
|
||||
|
||||
次に、さまざまなソートアルゴリズムを一緒に学び、上記の評価軸に基づいて各ソートアルゴリズムの長所と短所を分析していきます。
|
||||
これからさまざまなソートアルゴリズムを学び、これらの観点からそれぞれの長所と短所を分析していきます。
|
||||
|
||||
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 41 KiB |
@@ -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)$ に改善できます。
|
||||
- マージソートは分割と結合の二段階からなり、分割統治法の典型です。配列のソートには補助配列が必要で空間計算量は $O(n)$ ですが、連結リストなら $O(1)$ に改善できます。
|
||||
- バケットソートは、データの振り分け、バケット内のソート、結果の結合という三段階からなります。これも分割統治法を体現しており、大量のデータに適しています。均等に振り分けることが鍵です。
|
||||
- カウントソートはバケットソートの特殊な場合で、出現回数を数えてソートします。データ量が多く、値の範囲が限られる場合に適し、データを正の整数に変換できることが必要です。
|
||||
- 基数ソートは一桁ずつソートする方法で、データを固定桁数の数値として表せることが必要です。
|
||||
- 理想的には、高効率、安定、インプレース、適応性などの利点を備えたソートが望まれます。しかし、ほかのデータ構造やアルゴリズムと同じく、すべてを満たす方法はありません。実際には、データの特徴に応じて選ぶ必要があります。
|
||||
- 下図で、主なソートアルゴリズムの効率、安定性、インプレース性、適応性などを比較します。
|
||||
|
||||

|
||||
|
||||
### Q & A
|
||||
|
||||
**Q**:ソートアルゴリズムの安定性は、どのような場合に必須ですか?
|
||||
**Q**:ソートの安定性は、どのような場合に必要ですか?
|
||||
|
||||
現実には、オブジェクトのある属性に基づいて整列することがあります。たとえば、学生には氏名と身長という 2 つの属性があり、多段階のソートを行いたいとします。まず氏名で整列して `(A, 180) (B, 185) (C, 170) (D, 170)` を得て、その後に身長で整列します。ソートアルゴリズムが不安定である場合、結果は `(D, 170) (C, 170) (A, 180) (B, 185)` になる可能性があります。
|
||||
実際には、オブジェクトの特定の属性でソートすることがあります。例えば、名前と身長を持つ学生を、まず名前順に並べて `(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$ になります。
|
||||
再帰の深さは、まだ戻っていない再帰呼び出しの数です。各分割で元の配列は二つに分かれます。改善後に再帰する部分配列の長さは、最大でも元の半分です。最悪の場合に毎回ちょうど半分でも、最終的な深さは $\log n$ です。
|
||||
|
||||
元のクイックソートを振り返ると、長いほうの配列に対して連続して再帰してしまう可能性があり、最悪の場合は $n$、$n - 1$、$\dots$、$2$、$1$ と続き、再帰深度は $n$ になります。再帰深度の最適化により、このような状況を避けられます。
|
||||
元のクイックソートでは、長いほうを続けて再帰する可能性があります。最悪の場合、長さが $n$、$n - 1$、$\dots$、$2$、$1$ となり、深さは $n$ です。改善によってこれを避けられます。
|
||||
|
||||
**Q**:配列内のすべての要素が等しい場合、クイックソートの時間計算量は $O(n^2)$ になりますか?このような退化はどう処理すべきですか?
|
||||
**Q**:要素がすべて等しいと、クイックソートは $O(n^2)$ 時間になりますか? どう対処できますか?
|
||||
|
||||
はい。この場合は、番兵分割によって配列を「基準値より小さい」「基準値に等しい」「基準値より大きい」の 3 つの部分に分ける方法を検討できます。下方向に再帰するのは、小さい部分と大きい部分だけです。この方法では、入力要素がすべて等しい配列は、1 回の番兵分割だけで整列を完了できます。
|
||||
はい。その場合、分割で配列をピボットより小さい部分、等しい部分、大きい部分の三つに分け、小さい部分と大きい部分だけを再帰的に処理できます。この方法なら、全要素が等しい配列は一回の分割でソートが完了します。
|
||||
|
||||
**Q**:バケットソートの最悪時間計算量が $O(n^2)$ なのはなぜですか?
|
||||
**Q**:バケットソートの最悪時間計算量は、なぜ $O(n^2)$ ですか?
|
||||
|
||||
最悪の場合、すべての要素が同じバケットに振り分けられます。その要素群を整列するのに $O(n^2)$ のアルゴリズムを使えば、時間計算量は $O(n^2)$ になります。
|
||||
最悪の場合、すべての要素が同じバケットに入ります。それを $O(n^2)$ のアルゴリズムでソートすると、時間計算量は $O(n^2)$ になります。
|
||||
|
||||