Java で実装されたいくつかの一般的なソートアルゴリズムの詳細な解釈

Java で実装されたいくつかの一般的なソートアルゴリズムの詳細な解釈

ソートアルゴリズムはさまざまな場所で使用されています。最近、そのアルゴリズムを読み直し、自分で簡単に実装しました。将来のレビューのために資料として残しておくために、ここに記録しておきます。

では、これ以上長々とせずに、古典的なソートアルゴリズムを 1 つずつ見ていきましょう。

1. 選択ソート

選択ソートの基本的な考え方は、配列をトラバースすることです。i はソートする必要がある現在のシーケンス番号を表します。残りの [i...n-1] で最小値を見つけ、見つかった最小値を i が指す値と交換する必要があります。要素を決定するたびに、最適な値を選択するサブプロセスがあるため、比喩的に選択ソートと呼ばれます。

例を見てみましょう:

  1. 初期: [ 38 , 17 , 16 , 16 , 7 , 31 , 39 , 32 , 2 , 11 ]
  2.  
  3. i = 0 : [ 2 , 17 , 16 , 16 , 7 , 31 , 39 , 32 , 38 , 11 ] (0番目 [ 38 ]<->8番目 [ 2 ])
  4.  
  5. i = 1 : [ 2 , 7 , 16 , 16 , 17 , 31 , 39 , 32 , 38 , 11 ] (1番目 [ 38 ]<->4番目 [ 17 ])
  6.  
  7. i = 2 : [ 2 , 7 , 11 , 16 , 17 , 31 , 39 , 32 , 38 , 16 ] (2番目 [ 11 ]<->9番目 [ 16 ])
  8.  
  9. i = 3 : [ 2 , 7 , 11 , 16 , 17 , 31 , 39 , 32 , 38 , 16 ] ( スワップは不要 )
  10.  
  11. i = 4 : [ 2 , 7 , 11 , 16 , 16 , 31 , 39 , 32 , 38 , 17 ] (4番目 [ 17 ]<->9番目 [ 16 ])
  12.  
  13. i = 5 : [ 2 , 7 , 11 , 16 , 16 , 17 , 39 , 32 , 38 , 31 ] (5番目 [ 31 ]<->9番目 [ 17 ])
  14.  
  15. i = 6 : [ 2 , 7 , 11 , 16 , 16 , 17 , 31 , 32 , 38 , 39 ] (6番目 [ 39 ]<->9番目 [ 31 ])
  16.  
  17. i = 7 : [ 2 , 7 , 11 , 16 , 16 , 17 , 31 , 32 , 38 , 39 ] ( スワップは不要 )
  18.  
  19. i = 8 : [ 2 , 7 , 11 , 16 , 16 , 17 , 31 , 32 , 38 , 39 ] ( スワップは不要 )
  20.  
  21. i = 9 : [ 2 , 7 , 11 , 16 , 16 , 17 , 31 , 32 , 38 , 39 ] (スワップは不要)

例からわかるように、選択ソートが進むにつれて(i が徐々に増加する)、比較の回数はどんどん少なくなりますが、配列が最初に順序付けられているかどうかに関係なく、選択ソートは i から配列の最後まで選択比較を実行するため、特定の長さの配列の場合、選択ソートの比較回数は固定です:1 + 2 + 3 + …. + n = n * (n + 1) / 2、交換回数は初期配列の順序によって異なります。初期配列順序がランダムである場合、最悪の場合、配列要素は n 回交換され、最良の場合(配列自体は順序付けられている)は 0 回になる可能性があります。

このことから、選択ソートの時間計算量と空間計算量はそれぞれ O(n2) と O(1) であると推測できます (選択ソートでは、配列要素を交換するための追加のスペースのみが必要です)。

実装コード:

  1. /**
  2. * 選択ソート
  3. */  
  4. 選択(新しいソート可能() {
  5.     パブリック<T はComparable<T>>を拡張しますvoid sort(T[] 配列、ブール値の昇順) {
  6.          int len ​​= 配列.長さ;
  7.          ( int i = 0 ; i < len; i++) {
  8.             選択されたint = i;
  9.              ( int j = i + 1 ; j < len; j++) {
  10.                  int compare = 配列[j].compareTo(配列[選択]);
  11.                  if (比較!= 0 && 比較< 0 == 昇順) {
  12. 選択された = j;
  13. }
  14. }
  15.  
  16. exchange(配列、i、選択されたもの);
  17. }
  18. }
  19. })

2. 挿入ソート

挿入ソートの基本的な考え方は、配列をトラバースする過程で、シリアル番号 i の前の要素、つまり [0..i-1] がソートされていると想定することです。このトリップでは、i に対応する要素 x の正しい位置 k を見つける必要があり、この位置 k を見つける過程で、比較する要素を 1 つずつ 1 つずつ後ろに移動して要素 x のための「スペースを確保」し、最後に k に対応する要素の値を x に割り当てます。挿入ソートは、ソートの特性に応じて名前が付けられることもあります。

以下は例です。赤でマークされた数字は挿入された数字、取り消し線が引かれた数字は今回のソートに参加しない要素、赤でマークされた数字と取り消し線が引かれた数字の間の要素は一つずつ後ろに移動される要素です。例えば、2回目のソートに参加している要素は[11, 31, 12]で、挿入される要素は12ですが、12は現在正しい位置にないので、前の要素31と11と順番に比較し、比較する要素を移動しながら比較し、12より小さい最初の要素11を見つけて比較を停止する必要があります。このとき、31に対応するインデックス1は、12を挿入する必要がある位置です。

  1. 初期: [ 11 , 31 , 12 , 5 , 34 , 30 , 26 , 38 , 36 , 18 ]
  2.  
  3. 1回目のパス: [ 11 , 31 , 12 , 5 , 34 , 30 , 26 , 38 , 36 , 18 ] (移動する要素はありません)
  4.  
  5. 2回目のパス: [ 11 , 12 , 31 , 5 , 34 , 30 , 26 , 38 , 36 , 18 ] ( 31は後方に移動する)
  6.  
  7. 第3ラウンド: [ 5 11 12 31 34 30 26 38 36 18 ] ( 11 12 31 はすべて後ろに移動します)
  8.  
  9. 4回目のパス: [ 5 11 12 31 34 30 26 38 36 18 ] (要素は移動されません)
  10.  
  11. 5回目のパス: [ 5 11 12 30 31 34 26 38 36 18 ] ( 31 34 は後方に移動)
  12.  
  13. 6回目のパス: [ 5 11 12 26 30 31 34 38 36 18 ] ( 30 31 34 は後ろへ移動)
  14.  
  15. 7回目のパス: [ 5 11 12 26 30 31 34 38 36 18 ] (要素は移動されません)
  16.  
  17. 第8ラウンド: [ 5 11 12 26 30 31 34 36 38 18 ] ( 38 は後戻り)
  18.  
  19. 9 回目の移動: [ 5 11 12 18 26 30 31 34 36 38 ] ( 26 30 31 34 36 38 は後方へ移動)

挿入ソートは選択ソートよりも優れています。これは、配列要素の最初の部分がソート処理中にすでにソートされているという事実を利用できるため、比較回数を効果的に減らすことができます。もちろん、この利点は配列の初期順序に依存します。最悪の場合 (指定された配列が逆順になっている場合)、挿入ソートに必要な比較と移動の回数は 1 + 2 + 3… + n = n * (n + 1) / 2 になります。この極端な場合、挿入ソートの効率は選択ソートよりもさらに悪くなります。したがって、挿入ソートは不安定なソート方法であり、挿入効率は配列の初期順序に密接に関係しています。一般に、挿入ソートの時間計算量と空間計算量はそれぞれ O(n2) と O(1) です。

実装コード:

  1. /**
  2. * 挿入ソート
  3. */  
  4. 挿入(新しいSortable() {
  5.     パブリック<T はComparable<T>>を拡張しますvoid sort(T[] 配列、ブール値の昇順) {
  6.          int len ​​= 配列.長さ;
  7.          ( int i = 1 ; i < len; i++) {
  8. T toInsert = 配列[i];
  9.             整数j = i;
  10.              (; j > 0 ; j--)の場合{
  11.                  int compare = 配列[j - 1 ].compareTo(toInsert);
  12.                  if (比較 == 0 || 比較 < 0 == 昇順) {
  13.                     壊す;
  14. }
  15. 配列[j] = 配列[j - 1 ];
  16. }
  17.  
  18. 配列[j] = toInsert;
  19. }
  20. }
  21. })

3. バブルソート

バブルソートは、最も古典的なソートアルゴリズムとみなすことができます。これは、学生時代に初めて触れたアルゴリズムだったことを覚えています。実装方法が最も単純で、2層のforループを使用しているためです。内側のループでは、隣接する2つの要素が逆順になっているかどうかを判断します。逆順になっている場合は、2つの要素を交換します。外側のループを1回実行すると、配列の残りの要素のうち最小の要素を「フロート」して前に配置できるため、バブルソートと呼ばれています。

簡単な例を見てみましょう。

  1.  
  2.  
  3. 初期状態: [ 24 , 19 , 26 , 39 , 36 , 7 , 31 , 29 , 38 , 23 ]
  4.  
  5. 内側の第1パス: [ 24 , 19 , 26 , 39 , 36 , 7 , 31 , 29 , 23 , 38 ] ( 9番目 [ 23 ] <-> 8番目 [ 38 ] )
  6.  
  7. 2番目の内側パス: [ 24 , 19 , 26 , 39 , 36 , 7 , 31 , 23 , 29 , 38 ] ( 8番目 [ 23 ] <-> 7番目 [ 29 ] )
  8.  
  9. 内側の3番目のパス:[ 24 19 26 39 36 7 23 31 29 38 ] (7番目 [ 23 ] <-> 6番目 [ 31 ] )
  10.  
  11. 4番目の内側パス: [ 24 19 26 39 36 7 23 31 29 38 ] ( 723 は正しい順序なので、入れ替える必要はありません)
  12.  
  13. 5番目の内側パス: [ 24 , 19 , 26 , 39 , 7 , 36 , 23 , 31 , 29 , 38 ] ( 5番目 [ 7 ] <-> 4番目 [ 36 ] )
  14.  
  15. 内側の6番目のパス: [ 24 19 26 7 39 36 23 31 29 38 ] ( 4番目 [ 7 ] <-> 3番目 [ 39 ] )
  16.  
  17. 7番目の内側パス: [ 24 , 19 , 7 , 26 , 39 , 36 , 23 , 31 , 29 , 38 ] ( 3番目 [ 7 ] <-> 2番目 [ 26 ] )
  18.  
  19. 内側の8番目のパス:[ 24 7 19 26 39 36 23 31 29 38 ] (2番目 [ 7 ] <-> 1番目 [ 19 ] )
  20.  
  21. 9番目の内側のトリップ: [ 7 24 19 26 39 36 23 31 29 38 ] ( 1番目 [ 7 ] <-> 0番目 [ 24 ] )
  22.  
  23. ………

実際、バブルソートは選択ソートと非常によく似ています。比較回数は同じで、n * (n + 1) / 2です。ただし、バブルソートは最小値を選択する過程で追加の交換を実行します(バブルソートは、ソート中に隣接する要素の順序が間違っていることが判明した場合、隣接する要素を交換します。対応する選択ソートは、内部ループの比較が完了した後の状況に応じてのみ、交換するかどうかを決定します)。したがって、私の意見では、選択ソートはバブルソートの改良版です。

実装コード:

  1. /**
  2. * バブルソートは挿入ソートに非常に似ています
  3. */  
  4. バブル(新しいソート可能() {
  5.     パブリック<T はComparable<T>>を拡張しますvoid sort(T[] 配列、ブール値の昇順) {
  6.          int長さ = 配列.長さ;
  7.         最後の交換ID = 0 ;
  8.          ( int i = 0 ; i < 長さ; i++) {
  9.              // 交換が行われたかどうかを識別するためのフラグを false にマークします 
  10.             ブール値isExchanged = false ;
  11.              // 最後の比較と交換はインデックス i に到達する前に発生しました 
  12.              int currOrderedIdx = lastExchangedIdx > i ? lastExchangedIdx : i;
  13.              ( int j = length - 1 ; j > currOrderedIdx; j--) {
  14.                  int compare = 配列[j - 1 ].compareTo(配列[j]);
  15.                  if (比較!= 0 && 比較> 0 == 昇順) {
  16. 交換(配列、j - 1 、j);
  17. isExchanged = true ;
  18. 最後の交換ID = j;
  19. }
  20. }
  21.              // 交換が行われない場合は、配列がすでに整列していることを意味します 
  22.              if (isExchanged == false ) {
  23.                 壊す;
  24. }
  25. }
  26. }
  27. })

4. シェルソート

シェル ソートが誕生したのは、挿入ソートでは大きな配列を処理するときに移動しなければならない要素が多すぎるという問題に遭遇したためです。ヒルソートの考え方は、大きな配列をギャップで分割されたいくつかの小さな配列に「分割して征服する」ことです。たとえば、配列[1、2、3、4、5、6、7、8]は、ギャップ= 2で分割すると、[1、3、5、7]と[2、4、6、8]の2つの配列に分割できます(同様に、ギャップ= 3の場合、分割された配列は[1、4、7]、[2、5、8]、[3、6]です)。次に、分割された配列に対してそれぞれ挿入ソートを実行します。各サブ配列がソートされた後、ギャップ値が減らされ、ギャップ= 1になるまで、つまり配列全体が挿入されてソートされるまで、前の手順が繰り返されます。この時点で、配列は基本的にソートされているため、移動する必要がある要素は非常に小さくなり、挿入ソートが大きな配列を処理するときに移動回数が多いという問題が解決されます。

具体的な例については、挿入ソートを参照してください。

シェルソートは挿入ソートの改良版です。データ量が多い場合の効率向上に非常に役立ちます。データ量が少ない場合は、挿入ソートを直接使用することをお勧めします。

実装コード:

  1. /**
  2. * 貝殻の選別
  3. */  
  4. SHELL(新しいソート可能() {
  5.     パブリック<T はComparable<T>>を拡張しますvoid sort(T[] 配列、ブール値の昇順) {
  6.          int長さ = 配列.長さ;
  7.         ギャップ= 1 ;
  8.  
  9.          // 長さ/3の次に大きいものを最初のギャップとして使用します 
  10.         ギャップ<長さ/ 3 ){
  11. ギャップ = ギャップ * 3 + 1 ;
  12. }
  13.  
  14.         ギャップ >= 1場合
  15.              ( int i = ギャップ; i < 長さ; i++) {
  16. T next = 配列[i];
  17.                 整数j = i;
  18.                  (j >= ギャップ) {
  19.                      int compare = 配列[j - ギャップ].compareTo(next);
  20.                      // すでに位置が見つかりました 
  21.                      if (比較 == 0 || 比較 < 0 == 昇順) {
  22.                         壊す;
  23. }
  24.  
  25. 配列[j] = 配列[j - ギャップ];
  26. j -= ギャップ;
  27. }
  28.                 もし(j != i) {
  29. 配列[j] = 次;
  30. }
  31. }
  32. ギャップ /= 3 ;
  33. }
  34.  
  35. }
  36. })

5. マージソート

マージは、「分裂と征服」のカテゴリに属し、2つの並べ替えが完了した後、2つの並べ替えのように並べ替えます[3、6、8、11]および[1、3、12、15]は指定されていますが、これらの要素は実際には元の配列に分割されていますが、元のアレイは[3、8、11、1、3、12、15を除いて、3つのポインターを設定します必要な配列は4 + 4 = 8です。マージプロセスは、次のように簡単にまとめることができます。

1) 2 つのサブ配列の要素を新しい配列 copyedArray にコピーします。上記の例では、copiedArray = [3, 6, 8, 11, 1, 3, 12, 15] となります。

2) アトミック配列の最初の要素を指すように2つのポインタを設定します。2つのポインタの名前がleftIdxとrightIdxであると仮定すると、leftIdx = 0(copytedArray [3]の最初の要素に対応)、rightIdx = 4(copytedArray [1]の5番目の要素に対応)となります。

3) leftIdx と rightIdx が指す配列要素の値を比較し、小さい方を選択して、その値を元の配列の対応する位置 i に割り当てます。割り当てが完了すると、割り当てに関係する 2 つのインデックスが 1 ずつ増加します。leftIdx または rightIdx の値が対応する配列の末尾に達した場合は、配列の残りの要素を残りの位置に順番にコピーするだけです。

マージの具体的な例を以下に示します。

  1. ***旅行:
  2.  
  3. 補助配列 [ 21 , 28 , 39 | 35 , 38 ] (配列は|で区切られた左と右の2つのサブ配列に分割されます)
  4.  
  5. [ 21 、 、 、 ] (最初に2135を比較すると、左のサブ配列が勝ち、 leftIdx = 0 、 i = 0
  6.  
  7. 2回目の旅行:
  8.  
  9. 補助配列 [ 21 , 28 , 39 | 35 , 38 ]
  10.  
  11. [ 21 , 28 , , , ] (2回目2835を比較すると、左のサブ配列が勝ち、 leftIdx = 1 、 i = 1
  12.  
  13. 3回目の旅行: [ 21 28 39 | 35 38 ]
  14.  
  15. [ 21 28 35 、 、 ] (3回目に3935を比較すると、右のサブ配列が勝ち、 rightIdx = 0 、 i = 2
  16.  
  17. 4回目の旅行: [ 21 28 39 | 35 38 ]
  18.  
  19. [ 21 28 35 38 、 ] (4回目に3938を比較すると、右のサブ配列が勝ち、 rightIdx = 1 、 i = 3
  20.  
  21. 5回目の旅行: [ 21 28 39 | 35 38 ]
  22.  
  23. [ 21 28 35 38 39 ] (5回目は右のサブ配列がコピーされているので、比較する必要はありません。leftIdx = 2 、i = 4

上記はマージ プロセスです。ソートする配列全体を有限回数 (1 回につき 2 つ) 分割して、長さ 1 の小さな配列に分割することができます。長さが 1 になると、配列をソートする必要がなくなります。その後、配列は逆の順序で(再帰の使用により)マージされ、長さ n/2 の最後のサブ配列がマージされます。マージが完了すると、配列のソートも完了します。

マージ ソートは、すべてのソートの中で最も多くの追加スペースを必要とします。各マージには、マージに関係する 2 つの配列の長さの合計と同じ数の要素が必要です (補助配列を提供するため)。マージソートの空間計算量は 1 + 2 + 4 + … + n = n * ( n + 2) / 4 (n の偶奇性を無視) であると推測できます。時間計算量は見積もるのが難しく、それが何だったか忘れてしまいました (囧)。

実装コード:

  1. /**
  2. * マージソート
  3. */  
  4. MERGE(新しいソート可能() {
  5.     パブリック<T はComparable<T>>を拡張しますvoid sort(T[] 配列、ブール値の昇順) {
  6.          this .sort(配列、 0 、配列の長さ - 1 、昇順);
  7. }
  8.  
  9.     プライベート<T extends Comparable<T>> void sort(T[] array, int lo, int hi, boolean ascend) {
  10.          // 1つを最適化 
  11.          // 部分文字列の長さが20未満の場合、  
  12.          // 挿入ソートを使用して再帰呼び出しを減らす 
  13.          (高 - 低 < 20 )の場合{
  14.              ( int i = lo + 1 ; i <= hi; i++) {
  15. T toInsert = 配列[i];
  16.                 整数j = i;
  17.                  (; j > lo; j--)の場合{
  18.                      int compare = 配列[j - 1 ].compareTo(toInsert);
  19.                      if (比較 == 0 || 比較 < 0 == 昇順) {
  20.                         壊す;
  21. }
  22. 配列[j] = 配列[j - 1 ];
  23. }
  24.  
  25. 配列[j] = toInsert;
  26. }
  27.  
  28.             戻る;
  29. }
  30.  
  31.          int mid = lo + (hi - lo) / 2 ;
  32. ソート(配列、lo、mid、ascend);
  33. ソート(配列、mid + 1 、hi、ascend);
  34. マージ(配列、lo、mid、hi、ascend);
  35. }
  36.  
  37.     プライベート<T extends Comparable<T>> void merge(T[] array, int lo, int mid, int hi, boolean ascend) {
  38.          // 2つを最適化 
  39.          // すでに正しい順序になっている場合は、このマージをスキップします 
  40.          // そうする必要はないので 
  41.          int leftEndCompareToRigthStart = array[mid].compareTo(array[mid + 1 ]);
  42.          (leftEndCompareToRigthStart == 0 || leftEndCompareToRigthStart < 0 == ascend)の場合{
  43.             戻る;
  44. }
  45.  
  46.          @SuppressWarnings ( "チェックなし" )
  47. T[]配列コピー = (T[])新しいComparable[hi - lo + 1 ];
  48. System.arraycopy(配列、lo、arrayCopy、 0 、arrayCopy.length);
  49.  
  50.         整数lowIdx = 0 ;
  51.          int highIdx = mid - lo + 1 ;
  52.  
  53.          ( int i = lo; i <= hi; i++) {
  54.              (lowIdx > mid - lo)の場合{
  55.                  // 左のサブ配列が使い果たされました 
  56. 配列[i] = 配列コピー[highIdx++];
  57. }それ以外  (highIdx > hi - lo)の場合{
  58.                  // 右サブ配列が使い果たされました 
  59. 配列[i] = 配列コピー[lowIdx++];
  60. }それ以外  (arrayCopy[lowIdx].compareTo(arrayCopy[highIdx]) < 0 == ascend)の場合{
  61. 配列[i] = 配列コピー[lowIdx++];
  62. }それ以外{
  63. 配列[i] = 配列コピー[highIdx++];
  64. }
  65. }
  66. }
  67. })

6. クイックソート

クイックソートも、マージメソッドによって実装された「分割統治」ソートアルゴリズムです。その魅力は、各パーティション内の配列要素の最終的な正しいソート位置を決定できることにあります(ソートアルゴリズムの核心)(一度正確に配置できれば、この要素は次のループでは考慮されません)。

オリジナルリンク: http://easense2009.iteye.com/blog/1568614

<<:  再帰アルゴリズムにおけるリンクリスト操作

>>:  App Storeが検索アルゴリズムを大幅に変更:名前よりも人気に重点を置く

ブログ    
ブログ    

推薦する

AIコミック: 人工知能の3つの主要分野とその産業応用について1つの記事で学ぶ

音声認識 「音声認識」は、私たちが日常生活で使える iPhone の Siri など、コンピューター...

...

RLHF にはもう人間は必要ありません! Googleチームの研究により、AIによる注釈が人間のレベルに達したことが証明される

たとえば、RLHF の「人間」が入れ替わった場合、それは実現可能でしょうか? Google チームの...

3つの興味深い写真: 負荷分散アルゴリズムの改善が必要

図1: 負荷分散アルゴリズムの改善が必要[[91541]]図2: 開発者対テスター、非常に奇妙な図[...

...

次回の組み込み設計に人工知能を使用する4つの理由

次のプロジェクトに機械学習を取り入れるべき 4 つの理由をご紹介します。 理由その1 – マーケティ...

人工知能は私たちに取って代わるのでしょうか?科学者たちは十分な証拠を提示しているが、その日が来るのはまだ遠い。

人工知能といえば、これは現代社会の最新の産物であり、この産物もまた最速のスピードで人間を駆逐していま...

社会的関心の強化に基づくビデオ推奨アルゴリズム

1. 推奨ステータスまず、レコメンデーションシステムの現状について簡単に紹介します。推薦システムは、...

ドローンは将来のスマートシティで重要な役割を果たすだろう

「スマートシティ」という概念は何十年も前から存在していたが、その最新版では、住民の生活を向上させるた...

メタは自社の弁護士の警告を無視し、海賊版書籍を使用してAIモデルを訓練したと報じられている。

ロイター通信は12月13日、著作権侵害訴訟の新たな文書によると、メタ・プラットフォームズは何千冊もの...

機械学習のための3つの主要な学習リソースを丁寧に整理

機械学習はここしばらく話題になっていますが、それには十分な理由があります。機械学習は、将来の行動を予...

AI を使って AI を修正しますか?これらの検出ツールを理解する

生成型AI作成ロボットの登場以来、各界はロボットを使って記事や学術論文を書くようになりました。この状...