データ構造とアルゴリズム (5) ソートアルゴリズム

画像-20220821224607720

ソートアルゴリズム

最後のパート「並べ替えアルゴリズム」に到達した友人の皆さん、おめでとうございます。データ構造とアルゴリズムの研究は終わりに近づいています。継続は勝利です!

配列内のデータはもともと乱雑ですが、必要に応じて配列する必要があります。配列を並べ替えるには、以前にC 言語プログラミングの記事でバブル ソートとクイック ソート (オプション) について説明しました。パートでは、さらに多くの種類の並べ替えアルゴリズムについて説明していきます。

始める前に、バブルソートから始めましょう。

基本的な並べ替え

バブルソート

バブル ソートについては C 言語プログラミングの章で説明しました。バブル ソートの核心は交換です。継続的な交換により、大きな要素が少しずつ端に押し出されます。各ラウンドで、最大の要素が対応する位置に配置されます。 、そして最後に注文を形成します。アルゴリズムのデモ Web サイト: https://visualgo.net/zh/sorting?slide=2-2

配列の長さを N とすると、詳細なプロセスは次のようになります。

  • 合計 N ラウンドのソートが実行されます。
  • 並べ替えの各ラウンドは配列の左端の要素から開始され、2 つの要素を比較します。左側の要素が右側の要素より大きい場合、2 つの要素の位置は交換され、そうでない場合は変更されません。
  • 並べ替えの各ラウンドでは、残りの要素のうち最大の要素が右端に移動され、次の並べ替えでは、すでに対応する位置にあるこれらの要素は考慮されなくなります。

たとえば、次の配列:

画像-20220904212453328

次に、並べ替えの最初のラウンドで、最初の 2 つの要素を比較します。

画像-20220904212608834

前者の方が大きいことが判明したため、この時点で交換する必要があります。交換後、次の 2 つの要素を後方比較し続けます。

画像-20220904212637156

後者の方が大きく、変化していないことがわかります。引き続き最後の 2 つを見てみましょう。

画像-20220904212720898

現時点では、前者の方が大きいため、交換し、後続の要素の比較を続けます。

画像-20220904212855292

後者の方が大きい場合は、交換を続けてから逆方向に比較します。

画像-20220904212942212

後者のほうがさらに大きく、それが最大の要素である限り、すべての比較で元に戻されることがわかります。

画像-20220904213034375

最後に、現在の配列の最大の要素が最前面にスローされ、このラウンドの並べ替えは終了します。最大の要素が対応する位置に配置されているため、2 番目のラウンドでは、その前の要素のみを考慮する必要があります。できること:

画像-20220904213115671

このようにして、最も大きいものを右端に投げ続けることができ、最後の N ラウンドのソートの後、順序付けされた配列が得られます。

プログラムコードは次のとおりです。

void bubbleSort(int arr[], int size){
    
    
    for (int i = 0; i < size; ++i) {
    
    
        for (int j = 0; j < size - i - 1; ++j) {
    
    
            //注意需要到N-1的位置就停止,因为要比较j和j+1
            //这里减去的i也就是已经排好的不需要考虑了
            if(arr[j] > arr[j + 1]) {
    
       //如果后面比前面的小,那么就交换
                int tmp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = tmp;
            }
        }
    }
}

ただ、このコードは依然として最も原始的なバブル ソートであり、最適化することができます。

  1. 実際、ソートには N ラウンドではなく N-1 ラウンドが必要ですが、最後のラウンドで 1 つの要素だけがソートされていないため、ソートされているのと同じになり、再検討する必要はありません。
  2. ソートの全ラウンドで交換がない場合は、配列がすでに整っていて、前の配列が最後の配列より大きいという状況がないことを意味します。

それで、それを改善しましょう:

void bubbleSort(int arr[], int size){
    
    
    for (int i = 0; i < size - 1; ++i) {
    
       //只需要size-1次即可
        _Bool flag = 1;   //这里使用一个标记,默认为1表示数组是有序的
        for (int j = 0; j < size - i - 1; ++j) {
    
    
            if(arr[j] > arr[j + 1]) {
    
    
                flag = 0;    //如果发生交换,说明不是有序的,把标记变成0
                int tmp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = tmp;
            }
        }
        if(flag) break;   //如果没有发生任何交换,flag一定是1,数组已经有序,所以说直接结束战斗
    }
}

このようにして、バブル ソートの最適化されたバージョンの作成が完了しました。

もちろん、最終的には、ソートの安定性という追加の概念を導入する必要があります。では、安定性とは何でしょうか? 同じサイズの 2 つの要素の順序が並べ替えの前後で変わらない場合、並べ替えアルゴリズムは安定しています。今回導入したバブルソートは、前者が後者より大きい場合にのみ交換を行うため、元々等しい2つの要素の順序には影響を与えないため、安定したソートアルゴリズムとなります

挿入ソート

新しいソート アルゴリズム、正確には直接挿入ソートと呼ぶべき挿入ソートを導入しましょう。その中心となるアイデアは、Landlord をプレイするときとまったく同じです。

画像-20220904214541199

皆さんもプレイしたことがあると思いますが、ゲームの各ラウンドが始まる前に、カードの山からカードを引く必要がありますが、カードを引いた後、手札の順番がバラバラになることがあります。これでは絶対にうまくいきません。カードがありません。簡単に言えば、どのカードに何枚あるかを知るにはどうすればよいでしょうか? 順番を整えるために、新しく引いたカードをカードの順序に従って対応する位置に挿入します。これにより、後で手札にあるカードを並べ替える必要がなくなります。

挿入ソートも実際には同じ原理です。デフォルトでは、前のカードはすでにソートされており (最初は最初のカードだけが順番に並んでいます)、残りの部分を隣り合わせて走査してから挿入します。対応位置前、アニメーションデモアドレス:https://visualgo.net/zh/sorting

配列の長さを N とすると、詳細なプロセスは次のようになります。

  • 合計 N ラウンドのソートが実行されます。
  • 並べ替えの各ラウンドでは、後ろから要素を選択し、現在の要素よりも大きくない要素が見つかるまで、前に並べ替えられた要素と後ろから前に比較し、現在の要素がこの要素の前に挿入されます。
  • 要素が挿入されると、後続のすべての要素は 1 つ前の位置に移動されます。
  • 後続のすべての要素がトラバースされ、対応する位置に挿入されると、並べ替えが完了します。

たとえば、次の配列:

画像-20220904212453328

この時点で、最初の要素はすでに適切であると想定し、2 番目の要素を見ていきます。

画像-20220904221510897

それを取り出して、前の順序付けられたシーケンスと後ろから前に比較します。最初の比較は 4 で、4 よりも小さいことがわかります。さらに前進し続けると、最後に到達したことがわかります。注: 前面に移動する前に、後続の要素を後ろに移動してスペースを空けてください。

画像-20220904221648492

次に、それを挿入します。

画像-20220904221904359

最初の 2 つの要素が順序付けられた状態になったので、引き続き 3 番目の要素を見てみましょう。

画像-20220904221938583

まだ後ろから前に向かって見ていて、上がってきたときに 7 と 4 に遭遇したことがわかったので、この位置に直接配置しました。

画像-20220904222022949

最初の 3 つの要素がすべて整ったので、引き続き 4 番目の要素を見てみましょう。

画像-20220904222105375

順に前方に比較すると、最後に 1 より小さい要素が見つからなかったため、最初の 3 つの要素をすべて元に戻しました。

画像-20220904222145903

対応する位置に 1 を挿入します。

画像-20220904222207544

最初の 4 つの要素がすべて順序付けされた状態になったので、同じ方法で後続の要素の走査を完了するだけです。最終結果は順序付けされた配列になります。コードを書いてみましょう:

void insertSort(int arr[], int size){
    
    
    for (int i = 1; i < size; ++i) {
    
       //从第二个元素开始看
        int j = i, tmp = arr[i];   //j直接变成i,因为前面的都是有序的了,tmp相当于是抽出来的牌暂存一下
        while (j > 0 && arr[j - 1] > tmp) {
    
       //只要j>0并且前一个还大于当前待插入元素,就一直往前找
            arr[j] = arr[j - 1];   //找的过程中需要不断进行后移操作,把位置腾出来
            j--;
        }
        arr[j] = tmp;  //j最后在哪个位置,就是是哪个位置插入
    }
}

もちろん、このコードも改善できます。挿入位置を見つけるために各要素を比較するのに時間がかかりすぎるためです。要素の前の部分はすでに順序付けされた状態にあるため、二分探索アルゴリズムを使用して要素を見つけることを検討できます。対応する挿入位置。これにより、挿入ポイントを見つける時間が節約されます。

int binarySearch(int arr[], int left, int right, int target){
    
    
    int mid;
    while (left <= right) {
    
    
        mid = (left + right) / 2;
        if(target == arr[mid]) return mid + 1;   //如果插入元素跟中间元素相等,直接返回后一位
        else if (target < arr[mid])  //如果大于待插入元素,说明插入位置肯定在左边
            right = mid - 1;   //范围划到左边
        else   
            left = mid + 1;   //范围划到右边
    }
    return left;   //不断划分范围,left也就是待插入位置了
}

void insertSort(int arr[], int size){
    
    
    for (int i = 1; i < size; ++i) {
    
    
        int tmp = arr[i];
        int j = binarySearch(arr, 0, i - 1, tmp);   //由二分搜索来确定插入位置
        for (int k = i; k > j; k--) arr[k] = arr[k - 1];   //依然是将后面的元素后移
        arr[j] = tmp;
    }
}

最後に、挿入ソートアルゴリズムの安定性について説明します。その後、最適化を行わない挿入ソートは実際には、挿入される要素よりも大きくない要素を見つけることを楽しみに続けます。そのため、等しい要素が見つかった場合、その要素はその後にのみ挿入され、同じ要素の元の順序が維持されます。したがって、挿入ソートも安定したソート アルゴリズムであると言われています(ただし、後で二分探索最適化を使用すると不安定になります。たとえば、順序付けされた配列に 2 つの連続した等しい要素があり、別の等しい要素があったとします)要素が来ると、真ん中の要素が見つかりました。これは 1 位にランクされた等しい要素であり、次の位置に戻ります。新しく挿入された要素は、元々 2 番目にランクされていた等しい要素を後ろに押し込みます。)

選択ソート

最後の選択ソート (正確には直接選択ソートです) を見てみましょう。このソートもわかりやすく、毎回後ろに行って最小のものを見つけて前に置くだけです。アルゴリズムデモWebサイト:https://visualgo.net/en/sorting

配列の長さを N とすると、詳細なプロセスは次のようになります。

  • 合計 N ラウンドのソートが実行されます。
  • ソートの各ラウンドでは、後続のすべての要素から最小の要素が検索され、それがソートされた次の位置と交換されます。
  • N ラウンドの交換の後、順序付けられた配列が得られます。

たとえば、次の配列:

画像-20220904212453328

最初の並べ替えでは、配列全体で最小の要素を見つけて、それを最初の要素と交換する必要があります。

画像-20220905141347927

交換後、最初の要素はすでに整っていて、残りの要素から最小のものを探し続けます。

画像-20220905141426011

このとき、たまたま 2 が 2 番目の位置にあります。最初の 2 つの要素がすでに順番に並んでいるように、それを入れ替えるふりをしてみましょう。残りを見てみましょう:

画像-20220905141527050

この時点で、3 が最小であることが判明したため、3 番目の要素の位置に直接置き換えられます。

画像-20220905141629207

このようにして、最初の 3 つの要素はすべて順序付けされています。このように継続的に交換することで、最終的に取得する配列は順序付けられた配列になります。コードを書いてみましょう:

void selectSort(int arr[], int size){
    
    
    for (int i = 0; i < size - 1; ++i) {
    
       //因为最后一个元素一定是在对应位置上的,所以只需要进行N - 1轮排序
        int min = i;   //记录一下当前最小的元素,默认是剩余元素中的第一个元素
        for (int j = i + 1; j < size; ++j)   //挨个遍历剩余的元素,如果遇到比当前记录的最小元素还小的元素,就更新
            if(arr[min] > arr[j])
                min = j;
        int tmp = arr[i];    //找出最小的元素之后,开始交换
        arr[i] = arr[min];
        arr[min] = tmp;
    }
}

もちろん、選択ソートを最適化することもできます。なぜなら、最小のものを選択する必要があるたびに、最大のものを選択し、小さなものを左に投げ、大きなものを右に投げたほうがよいからです。 2倍の効率を実現できます。

void swap(int * a, int * b){
    
    
    int tmp = *a;
    *a = *b;
    *b = tmp;
}

void selectSort(int arr[], int size){
    
    
    int left = 0, right = size - 1;   //相当于左端和右端都是已经排好序的,中间是待排序的,所以说范围不断缩小
    while (left < right) {
    
    
        int min = left, max = right;
        for (int i = left; i <= right; i++) {
    
    
            if (arr[i] < arr[min]) min = i;   //同时找最小的和最大的
            if (arr[i] > arr[max]) max = i;
        }
        swap(&arr[max], &arr[right]);   //这里先把大的换到右边
        //注意大的换到右边之后,有可能被换出来的这个就是最小的,所以说需要判断一下
        //如果遍历完发现最小的就是当前右边排序的第一个元素
        //此时因为已经被换出来了,所以说需要将min改到换出来的那个位置
        if (min == right) min = max;
        swap(&arr[min], &arr[left]);   //接着把小的换到左边
        left++;    //这一轮完事之后,缩小范围
        right--;
    }
}

最後に、選択ソートの安定性を分析してみましょう. まず、選択ソートは毎回最小のものを選択します. 前方に挿入する場合、交換操作は直接実行されます. たとえば、元のシーケンスは 3,3,1 であり、1 が選択されますこのとき、最小の要素であり、最初の3つと入れ替えられますが、入れ替え後は元々1位だった3つが最後に行き、元の順序が崩れてしまうため、選択ソートは不安定なソートアルゴリズムです

上で学んだ 3 つの並べ替えアルゴリズムを要約しましょう。並べ替えられる配列の長さが次であると仮定しますn

  • バブルソート (最適化バージョン):
    • 最良の場合の時間計算量: O ( n ) O(n)O ( n )、順序付けされている場合、トラバースは 1 回だけ必要です。マークが交換が発生していないことを検出すると、直接終了するため、一度実行できます。
    • 最悪の場合の時間計算量: O ( n 2 ) O(n^2)O ( n2 )、つまり、完全に反転した配列のように、すべてのラウンドが突然埋められます。
    • **空間複雑度:** 交換する必要がある変数を一時的に格納するために必要な変数は 1 つだけであるため、空間複雑度はO ( 1 ) O (1)です。
    • **安定性: **安定
  • 挿入ソート:
    • 最良の場合の時間計算量: O ( n ) O(n)O ( n )が順序付けされている場合、配列自体が順序付けされている場合、挿入位置も同じ位置になるため、各ラウンドで他の要素を変更する必要はありません。
    • 最悪の場合の時間計算量: O ( n 2 ) O(n^2)O ( n2 )たとえば、完全に反転した配列は次のようになり、ラウンドごとに、前方の挿入を完全に見つける必要があります。
    • 空間複雑度: 抽出された要素を格納するために必要な変数は 1 つだけなので、空間複雑度はO ( 1 ) O (1)です。
    • **安定性: **安定
  • 並べ替えを選択します:
    • 最良の場合の時間計算量: O ( n 2 ) O(n^2)O ( n2 )、配列自体が順序付けされている場合でも、最小要素を決定する前に、各ラウンドで残りの部分を 1 つずつ見つける必要があるため、依然として平方順序が必要です。
    • 最悪の場合の時間計算量: O ( n 2 ) O(n^2)O ( n2 )、これ以上言う必要はありません。
    • 空間複雑度: 各ラウンドは最小の要素位置を記録するだけでよいため、空間複雑度はO ( 1 ) O (1)です。
    • **安定性: **不安定

表は次のとおりですので、覚えておいてください。

ソートアルゴリズム 最良のシナリオ 最悪のシナリオ 空間の複雑さ 安定性
バブルソート O ( n ) O( n )O ( n ) O ( n 2 ) O(n^2)O ( n2 ) O ( 1 ) O (1) 安定させる
挿入ソート O ( n ) O( n )O ( n ) O ( n 2 ) O(n^2)O ( n2 ) O ( 1 ) O (1) 安定させる
選択ソート O ( n 2 ) O(n^2)O ( n2 ) O ( n 2 ) O(n^2)O ( n2 ) O ( 1 ) O (1) 不安定な

高度な並べ替え

先ほど、3 つの基本的な並べ替えアルゴリズムを紹介しましたが、それらの平均時間計算量はO ( n 2 ) O(n^2)に達しました。O ( n2 )では、より高速な並べ替えアルゴリズムを見つけることができるでしょうか? このパートでは、引き続き、前の 3 つの並べ替えアルゴリズムの高度なバージョンを紹介します。

クイックソート

C 言語プログラミングの章では、クイック ソートも紹介しました。クイック ソートはバブル ソートの発展版です。バブル ソートでは、隣接する要素間で要素の比較と交換が行われます。各要素の交換では 1 つの位置しか移動できないため、比較と移動の回数が多く、効率は比較的低くなります。クイックソートでは、要素の比較・交換が両端から中間に向かって行われ、大きい要素は1周で後ろの位置に、小さい要素は1周で前の位置に入れ替えることができ、1手ごとに遠くに移動します。 , そのため、比較や移動が少なくなり、その名前のように高速になります。

実際、クイック ソートの各ラウンドの目的は、大きいものをベンチマークの右側に投げ、小さいものをベンチマークの左側に投げることです。

配列の長さを N とすると、詳細なプロセスは次のようになります。

  • 初期状態ではソート範囲は配列全体です
  • 並べ替える前に、並べ替え範囲全体の最初の要素を基準として選択し、並べ替え範囲内の要素をすばやく並べ替えます。
  • まず右端から左に見て、各要素と基準要素を比較し、基準要素より小さい場合は左の横断位置(基準要素の位置)の要素と交換します。初めに)、この時点ではそれを保持します。 現在の横断位置が右側に表示されます。
  • 交換後、要素は左から右にトラバースされ、ベース要素よりも大きいことが判明した場合は、以前に予約された右トラバース位置の要素と交換されます。左側の現在の位置も保持され、前のステップはループ内で実行されます。
  • 左右のトラバースが衝突すると、このラウンドのクイック ソートが完了し、中央の最終位置がベース要素の位置になります。
  • 基準位置を中心として左右に分けて同様にクイックソートを行います。

たとえば、次の配列:

画像-20220904212453328

まず、最初の要素 4 をベース要素として選択します。最初は、左右のポインタが両端に配置されています。

画像-20220905210056432

この時点で、4 より小さい要素が見つかるまで右から左に見ていきます。最初の要素は 6 ですが、これは間違いなく当てはまりません。ポインタを後ろに移動します。

画像-20220905210625181

この時点で、引き続き 3 と 4 を比較し、それが 4 より小さいことがわかります。次に、3 を左ポインタが指す要素の位置に直接交換します (実際には、直接上書きするだけです)。

画像-20220905210730105

この時点で、左から右に見ていきます。4 より大きい要素が見つかった場合は、それを右側のポインタに交換します。3 は速度が低下しただけなので、もう存在しません。次に 2 があります。

画像-20220905210851474

2 は 4 ほど大きくないので、さらに遡ってみると、現時点では 7 が 4 より大きいため、交換を続けます。

画像-20220905211300102

それから、彼は再び右から左に目を向け始めました。

画像-20220905211344027

この時点では、5 は 4 より大きいです。さらに先に進むと、1 は 4 より小さいことがわかり、次の交換を続けます。

画像-20220905211427939

このとき、2 つのポインターが衝突し、並べ替えが終了し、最後の 2 つのポインターが指す位置が基本要素の位置になります。

画像-20220905211543845

このラウンドのクイック ソートの後、左側はすべて整っていない可能性がありますが、左側は基本要素より小さく、右側は基本要素より大きくなければなりません。次に、ベンチマークを中心として 2 つの部分に分割し、再度簡単に並べ替えます。

画像-20220905211741787

このようにして、最終的に配列全体を順番に並べることができます。もちろん、クイックソートという言い方は他にもあります。左右の部分を両方見つけて交換する方法もあります。ここでは、それらを捨てることです。見つかり次第。アイデアは明確になったので、クイック ソートを実装してみましょう。

void quickSort(int arr[], int start, int end){
    
    
    if(start >= end) return;    //范围不可能无限制的划分下去,要是范围划得都没了,肯定要结束了
    int left = start, right = end, pivot = arr[left];   //这里我们定义两个指向左右两个端点的指针,以及取出基准
    while (left < right) {
    
         //只要两个指针没相遇,就一直循环进行下面的操作
        while (left < right && arr[right] >= pivot) right--;   //从右向左看,直到遇到比基准小的
        arr[left] = arr[right];    //遇到比基准小的,就丢到左边去
        while (left < right && arr[left] <= pivot) left++;   //从左往右看,直到遇到比基准大的
        arr[right] = arr[left];    //遇到比基准大的,就丢到右边去
    }
    arr[left] = pivot;    //最后相遇的位置就是基准存放的位置了
    quickSort(arr, start, left - 1);   //不包含基准,划分左右两边,再次进行快速排序
    quickSort(arr, left + 1, end);
}

このようにして、クイックソートを実装します。クイック ソートの安定性を分析してみましょう。クイック ソートは、ベンチマークよりも小さい要素または大きい要素を直接交換します。たとえば、元の配列は 2, 2, 1 です。このとき、最初の要素がベンチマークとして使用さますまず、右側の 1 が投げられて 1、2、1 になり、次に左から右へ、参照 2 より大きい要素に遭遇した場合にのみ変更されるため、最後の参照が配置されます。このとき、前にあるべき2が後ろに行ってしまっているため、クイックソートアルゴリズムは不安定ソートアルゴリズムである。

2軸クイックソート(オプション)

ここでは、クイック ソート、デュアル軸クイック ソートのアップグレード バージョンを追加する必要があります。Java 言語の配列ツール クラスは、このソート方法を使用して大きな配列をソートします。クイックソートと比較してどのような改善が加えられたかを見てみましょう。まず、極端な状況に遭遇した場合、通常のクイック ソート アルゴリズムは次のようになります。

画像-20220906131959909

配列全体がたまたま逆の順序になっているため、最初に配列全体を検索し、その後、最後の位置に 8 を入れることと同じになります。この時点で、最初のラウンドは終了します。

画像-20220906132112592

8 は右端に直接移動するため、この時点では右半分はなく、左半分のみです。このとき、左半分は引き続き迅速にソートされます。

画像-20220906132244369

このとき、再び 1 が最小要素となるため、トラバースが終了しても 1 はその位置に残りますが、この時点では左半分はなく、右半分のみです。

画像-20220906132344525

このときのベンチマークは最大の7ですが、非常に運が悪いのですが、配置した結果、7が一番左に行ってしまって、まだ右半分がありません。

画像-20220906132437765

この極端なケースでは、各ラウンドが範囲全体を完全に横断する必要があり、各ラウンドで最大または最小の要素が両側に押し出されることになることがわかりました。これはバブル ソートではありませんか? したがって、極端な場合には、クイック ソートはバブル ソートに退化するため、クイック ソートによって参照要素がランダムに選択されます。この極端な場合に発生する問題を解決するには、別の基底要素を追加します。これにより、極端な状況が発生した場合でも、両側が最小要素または最大要素でない限り、少なくとも 1 つの基底は正常に分割でき、極端な状況は発生します。発生する確率も大幅に減少します。

画像-20220906132945691

このとき、最初の要素と最後の要素は両方とも参照要素として使用され、リターン全体が 3 つのセグメントに分割されます。ベースライン 1 がベースライン 2 より小さいと仮定すると、最初のセグメントに格納されるすべての要素は、ベースライン 2 よりも小さくなければなりません。ベースライン 1 であり、2 番目のセグメントに格納されるすべての要素はベースライン 1 より小さくなければなりません。ベースライン 1 以上、ベース 2 以下である必要があります。3 番目のセクションに格納されるすべての要素はベース 2 より大きくなければなりません。

画像-20220906133219853

したがって、3 つのセグメントに分割した後、2 軸クイック ソートの各ラウンドの後、3 つのセグメントを 2 軸クイック ソートで継続する必要があります。最終的に、配列全体を並べ替えることができます。もちろん、このソート アルゴリズムはどの数量ですか比較的大きな配列の場合、量が比較的少ない場合、二軸クイック ソートは非常に多くの操作を実行する必要があることを考慮すると、実際には挿入ソートほど高速ではありません。

2 軸クイック ソートがどのように機能するかをシミュレートしてみましょう。

画像-20220906140255444

まず、最初の要素と最後の要素を 2 つのベンチマークとして取り出し、それらを比較する必要があります。ベンチマーク 1 がベンチマーク 2 より大きい場合、最初に 2 つのベンチマークを交換する必要がありますが、ここでは 4 が 6 より小さいため、 、交換する必要はありません。

この時点で、3 つのポインターを作成する必要があります。

画像-20220906140538076

領域が 3 つあるため、青色のポインターの位置とその左側の領域は両方ともベンチマーク 1 より小さく、オレンジ色のポインターの左から青色のポインターまでの領域はベンチマーク 1 より小さくなく、ベンチマーク 2 より大きくありません。緑色のポインターの位置と右側の領域がベンチマーク 2 より大きく、オレンジ色のポインターと緑色のポインターの間の領域がソート対象の領域です。

まずはオレンジ色のポインタが指す要素から判断していきますが、これは以下の3つの状況に分けられます。

  • 基数 1 より小さい場合は、まず青色のポインタを後方に移動し、要素を青色のポインタに交換してから、オレンジ色のポインタを後方に移動する必要があります。
  • 基数 1 以上、基数 2 以下の場合は、この範囲内にあるため、何もする必要はありません。オレンジ色のポインターを前に動かすだけです。
  • ベンチマーク2より大きい場合は右に投げる必要があるので、まず右ポインタを左に動かし、そのまま進んでベンチマーク2より大きくないものを探すとスムーズに交換できます。

まず見てみましょう。このとき、オレンジ色のポインタは 2 を指しているので、2 は基底の 1 より小さいです。まず青いポインタを元に戻してから、オレンジと青のポインタの要素を交換する必要がありますが、ここでは、それらは同じ One であるため、変更されません。この時点で、両方のポインターは 1 つ前の位置に移動しています。

画像-20220906141556398

同様に、オレンジ色のポインタが指す要素を見てみましょう。この時点では、基数 2 よりも大きい 7 です。次に、基数 2 以下の右側の要素を見つける必要があります。

画像-20220906141653453

緑色のポインターは右から左に検索し、この時点で 3 を見つけ、オレンジ色のポインター要素と青色のポインター要素を直接交換します。

画像-20220906141758610

次のラウンドでは、引き続きオレンジ色のポインタ要素を見ますが、この時点では参照 1 よりも小さいことがわかります。このとき、両方のポインタは 1 つ前の位置に移動されます。

画像-20220906141926006

新しいラウンドでは、オレンジ色のポインタが指す要素を引き続き調べます。この時点で、1 もベースライン 1 よりも小さいことがわかります。最初に青いポインタを移動し、次に交換し、次にオレンジ色のポインタを移動します。上記と同じ、孤独を交換します。

画像-20220906142041202

この時点で、オレンジ色のポインタは、基数 2 より大きい 8 を指しています。次に、交換のために、基数 2 以下の右側の別のポインタも見つける必要があります。

画像-20220906142134949

このとき、5つ見つけて条件を満たして交換します。

画像-20220906142205055

引き続きオレンジ色のポインタを確認すると、オレンジ色のポインタ要素が基数 1 以上、基数 2 以下であることがわかります。その後、前のルールに従って、オレンジ色のポインタを前方に移動するだけで済みます。

画像-20220906142303329

このとき、オレンジ色のポインタと緑色のポインタが衝突し、並べ替えられる要素がなくなりました。最後に、両端点にある 2 つの参照要素を対応するポインタに交換します。参照 1 は青色のポインタに交換され、参照 2 は緑色のポインタと交換されます。

画像-20220906142445417

今回分けた3つのエリアはちょうど条件を満たしているので、もちろんここで運が良ければ全体が整いますが、正規のルートだと残りのエリアも引き続き2軸クイックソートを行う必要があります。 3つのエリアが完成し、ようやく仕分けが完了しました。

次に、二軸クイックソートのコードを書いてみましょう。

void dualPivotQuickSort(int arr[], int start, int end) {
    
    
    if(start >= end) return;     //首先结束条件还是跟之前快速排序一样,因为不可能无限制地分下去,分到只剩一个或零个元素时该停止了
    if(arr[start] > arr[end])    //先把首尾两个基准进行比较,看看谁更大
        swap(&arr[start], &arr[end]);    //把大的换到后面去
    int pivot1 = arr[start], pivot2 = arr[end];    //取出两个基准元素
    int left = start, right = end, mid = left + 1;   //因为分了三块区域,此时需要三个指针来存放
    while (mid < right) {
    
        //因为左边冲在最前面的是mid指针,所以说跟之前一样,只要小于right说明mid到right之间还有没排序的元素
        if(arr[mid] < pivot1)     //如果mid所指向的元素小于基准1,说明需要放到最左边
            swap(&arr[++left], &arr[mid++]);   //直接跟最左边交换,然后left和mid都向前移动
        else if (arr[mid] <= pivot2) {
    
        //在如果不小于基准1但是小于基准2,说明在中间
            mid++;   //因为mid本身就是在中间的,所以说只需要向前缩小范围就行
        } else {
    
        //最后就是在右边的情况了
            while (arr[--right] > pivot2 && right > mid);  //此时我们需要找一个右边的位置来存放需要换过来的元素,注意先移动右边指针
            if(mid >= right) break;   //要是把剩余元素找完了都还没找到一个比基准2小的,那么就直接结束,本轮排序已经完成了
            swap(&arr[mid], &arr[right]);   //如果还有剩余元素,说明找到了,直接交换right指针和mid指针所指元素
        }
    }
    swap(&arr[start], &arr[left]);    //最后基准1跟left交换位置,正好左边的全部比基准1小
    swap(&arr[end], &arr[right]);     //最后基准2跟right交换位置,正好右边的全部比基准2大
    dualPivotQuickSort(arr, start, left - 1);    //继续对三个区域再次进行双轴快速排序
    dualPivotQuickSort(arr, left + 1, right - 1);
    dualPivotQuickSort(arr, right + 1, end);
}

この部分はオプションのみであり、必須ではありません。

ヒルソート

ヒル ソートは、直接挿入ソートの高度なバージョンです (ヒル ソートは、縮小増分ソートとも呼ばれます)。挿入ソートは理解するのが簡単ですが、極端なケースでは、ソートされたすべての要素が元に戻されてしまいます (たとえば、単に必要な場合)この問題を解決するために、Hill ソートは挿入ソートを改善し、ステップ サイズに従って配列全体をグループ化し、遠くの要素を最初に比較します。

このステップ サイズは、インクリメント シーケンスによって決定されます。このインクリメント シーケンスは非常に重要です。多くの研究で、インクリメント シーケンスdlta[k] = 2^(t-k+1)-1(0<=k<=t<=(log2(n+1)))。 } {2}2n 4 \frac {n} {4}4n 8 \frac {n} {8}8、...、1 つのこのような増分シーケンス。

配列の長さを N とすると、詳細なプロセスは次のようになります。

  • まず、初期ステップ サイズ (n/2) を見つけます。
  • 配列全体をステップ サイズに従って、つまり 2 つずつグループ化します (n が奇数の場合、最初のグループには 3 つの要素が含まれます)。
  • これらのグループ内でそれぞれ挿入ソートを実行します。
  • ソートが完了したら、ステップを /2 で再グループ化し、ステップが 1 になり、挿入ソートの最後のパスが完了するまで上記のステップを繰り返します。

この場合、一度グループ内の順序を調整しているため、小さな要素ができるだけ早く配置されるため、最後のソート時に小さな要素が挿入されたとしても、後ろに戻す必要のある要素はそれほど多くありません。 . .

例として次の配列を考えてみましょう。

画像-20220905223505975

まず、配列の長さは 8 です。2 を直接除算して 34 を取得します。すると、ステップ サイズは 4 になります。4 のステップ サイズに従ってグループ化します。

画像-20220905223609936

このうち 4 と 8 が第 1 グループ、2 と 5 が第 2 グループ、7 と 3 が第 3 グループ、1 と 6 が第 4 グループとなり、それぞれこの 4 つのグループ内で挿入ソートを実行します。グループ、結果は次のようになります。

画像-20220905223659584

現在の小さな要素は、まだ順序が整っていませんが、できるだけ前に進んでいることがわかります。次に、ステップ サイズを 4/2=2 に減らし、このステップ サイズに従って分割します。

画像-20220905223804907

この時点では、4、3、8、7 がグループ、2、1、5、6 がグループとなっており、これら 2 つのグループ内でソートを続けると、次の結果が得られます。

画像-20220905224111803

最後に、ステップ サイズ / 2 を増やし続けて 2/2=1 を取得します。このとき、ステップ サイズは 1 になり、配列全体がグループであることになります。再度挿入ソートを実行します。このとき、ステップ サイズは 1 になります。 、小さな要素が左側にあることがわかり、この時点で挿入ソートを実行するのは非常に簡単になります。

では、コードを書いてみましょう。

void shellSort(int arr[], int size){
    
    
    int delta = size / 2;
    while (delta >= 1) {
    
    
        //这里依然是使用之前的插入排序,不过此时需要考虑分组了
        for (int i = delta; i < size; ++i) {
    
       //我们需要从delta开始,因为前delta个组的第一个元素默认是有序状态
            int j = i, tmp = arr[i];   //这里依然是把待插入的先抽出来
            while (j >= delta && arr[j - delta] > tmp) {
    
       
              	//注意这里比较需要按步长往回走,所以说是j - delta,此时j必须大于等于delta才可以,如果j - delta小于0说明前面没有元素了
                arr[j] = arr[j - delta];
                j -= delta;
            }
            arr[j] = tmp;
        }
        delta /= 2;    //分组插排完事之后,重新计算步长
    }
}

ここでは 3 レベルのループの入れ子が使用されていますが、実際の時間計算量は O ( n 2 ) O(n^2)を超える可能性があります。O ( n2 )はまだ小さいですが、小さい要素を左に移動する必要があるため、実際のソート数は想像したほど多くありません。証明プロセスが複雑すぎるため、ここではリストしません。

では、ヒルソートは安定しているのでしょうか?ステップ サイズでグループ化しているため、隣接する 2 つの同一の要素がそれぞれのグループ内で前に移動する可能性があるため、ヒル ソートは不安定なソート アルゴリズムです

ヒープソート

ヒープ ソートも選択ソートの一種ですが、直接選択ソートよりも高速な場合があります。先ほど説明した大きなトップパイルと小さなトップパイルを覚えていますか? 確認してみましょう:

完全なバイナリ ツリーの場合、ツリー内のすべての父ノードが子ノードより小さい場合、それを小さなルート ヒープ(小さなトップ ヒープ)と呼び、ツリー内のすべての父ノードが子ノードより大きい場合、それは大きなルートヒープです。

ヒープが完全なバイナリ ツリーであるという事実のおかげで、配列を使用してそれを簡単に表すことができます。

画像-20220818110224673

ヒープを構築することで, 順序のない配列を順番に入力することができ, 最終的に格納されるシーケンスは, 順番に並べられたシーケンスになります. この特性を利用して, ソートにヒープを簡単に使用できます. まず小さなトップパイルを書きましょう:

typedef int E;
typedef struct MinHeap {
    
    
    E * arr;
    int size;
    int capacity;
} * Heap;

_Bool initHeap(Heap heap){
    
    
    heap->size = 0;
    heap->capacity = 10;
    heap->arr = malloc(sizeof (E) * heap->capacity);
    return heap->arr != NULL;
}

_Bool insert(Heap heap, E element){
    
    
    if(heap->size == heap->capacity) return 0;
    int index = ++heap->size;
    while (index > 1 && element < heap->arr[index / 2]) {
    
    
        heap->arr[index] = heap->arr[index / 2];
        index /= 2;
    }
    heap->arr[index] = element;
    return 1;
}

E delete(Heap heap){
    
    
    E max = heap->arr[1], e = heap->arr[heap->size--];
    int index = 1;
    while (index * 2 <= heap->size) {
    
    
        int child = index * 2;
        if(child < heap->size && heap->arr[child] > heap->arr[child + 1])
            child += 1;
        if(e <= heap->arr[child]) break;
        else heap->arr[index] = heap->arr[child];
        index = child;
    }
    heap->arr[index] = e;
    return max;
}

次に、これらの要素を 1 つずつヒープに挿入し、1 つずつ取り出すだけで済み、順序付けられたシーケンスが得られます。

int main(){
    
    
    int arr[] = {
    
    3, 5, 7, 2, 9, 0, 6, 1, 8, 4};

    struct MinHeap heap;    //先创建堆
    initHeap(&heap);
    for (int i = 0; i < 10; ++i)
        insert(&heap, arr[i]);   //直接把乱序的数组元素挨个插入
    for (int i = 0; i < 10; ++i)
        arr[i] = delete(&heap);    //然后再一个一个拿出来,就是按顺序的了

    for (int i = 0; i < 10; ++i)
        printf("%d ", arr[i]);
}

最終結果は次のとおりです。

画像-20220906001134488

これは使い方が簡単ですが、追加のO ( n ) O(n)が必要です。O ( n )スペースはヒープとして使用されるため、さらに最適化してスペース占有を減らすことができます。では、どのように最適化すればよいのでしょうか? 考え方を変えて、指定された配列のヒープを直接構築することも考えられます。

配列の長さを N とすると、詳細なプロセスは次のようになります。

  • まず、指定された配列のサイズを変更して大きな上部ヒープにします
  • N ラウンドの選択を実行し、毎回、大きなトップ ヒープの先頭にある要素を選択し、それを配列の末尾から前方に格納します (ヒープの先頭とヒープの最後の要素を交換します)。
  • 交換が完了したら、ヒープのルート ノードが大きなトップ ヒープのプロパティを引き続き満たすように再調整し、上記の操作を繰り返します。
  • N ラウンドが終了すると、小さいものから大きいものへと並べられた配列が得られます。

まず、次の配列を例として、指定された配列を完全なバイナリ ツリーに変換します。

画像-20220906220020172

この時点では、このバイナリ ツリーはまだヒープではないため、私たちの主な目標は、これを大きな上部ヒープに変えることです。では、このバイナリ ツリーを大きなトップ ヒープに変えるにはどうすればよいでしょうか? 最後の非リーフ ノードから (上から下の順に) 調整するだけで済みます。たとえば、この時点では 1 が最後の非リーフ ノードなので、1 から比較する必要があります。子ノードがそれよりも大きい場合は、最大の子ノードを交換する必要があります。このとき、その子ノード 6 は 1 より大きいため、交換する必要があります。

画像-20220906221306519

次に、最後から 2 番目の非リーフ ノード 7 を見てみましょう。この時点では、両方の子はそれよりも小さいため、調整は必要ありません。次に最後から 2 番目の非リーフ ノード 2 を見てみましょう。 2 の 2 つの子、6 と 8 が両方とも 2 より大きい場合、交換する 2 つの子の中で最大のものを選択します。

画像-20220906221504364

最後に、リーフ以外のノードが残っているのはルート ノードだけです。この時点では、4 の左右の子は 4 より大きいため、まだ調整が必要です。

[外部リンク画像の転送に失敗しました。ソース サイトにはリーチ防止メカニズムがある可能性があります。画像を保存して直接アップロードすることをお勧めします (img-87a7s1F4-1662545089947)(/Users/nagocoler/Library/Application Support/typora-user) -images/image-20220906221657599 .png)]

この時点で 4 を置き換えても、まだ大きなトップ ヒープの特性を満たしていないため、調整後はまだ終わっていません。この時点では、4 の左の子は 4 より大きく、続行する必要があります。見下ろす:

画像-20220906221833012

交換後、バイナリ ツリー全体が大きなトップ ヒープの特性を満たし、最初の初期調整が完了しました。

この時点で、2 番目のステップが開始されます。ヒープの先頭要素を 1 つずつ交換する必要があります。これは、完了するまで毎回最大の要素を取り出すことと同じです。まず、ヒープの先頭要素を交換し、最後の要素:

画像-20220906222327297

このとき、配列全体の最大の要素が対応する位置に配置されたので、最後の要素は考慮しなくなり、その前にある残りの要素は引き続き完全な二分木とみなされ、ルート ノードは再びヒープ化されます (他の非リーフ ノードは変更されていないため、ルート ノードのみを調整する必要があります)。これにより、引き続き大きな上部ヒープのプロパティが満たされます。

画像-20220906222819554

まだ終わっていないので、調整を続けてください。

画像-20220906222858752

この時点では、最初のラウンドが終了し、次の 2 ラウンドが繰り返されます。上記の操作が繰り返されます。最初に、ヒープの先頭要素は依然として最後から 2 番目の位置にスローされます。これは、2 番目の要素を最後から 2 番目の位置に配置することと同じです。最後に最大の要素を対応する位置に移動します。

画像-20220906222934602

この時点で、2 つの要素がソートされています。同様に、残りの要素を引き続き完全なバイナリ ツリーと見なし、ルート ノードでヒープ操作を実行し続けて、大きなトップ ヒープ プロパティを満たし続けるようにします。

画像-20220906223110734

3 番目のラウンドでも同じアイデアが使用され、最大のアイデアが後ろに交換されます。

画像-20220906223326135

N 回のソートの後、最終的に各要素を対応する位置に配置することができます。上記の考えに基づいて、いくつかのコードを書いてみましょう:

//这个函数就是对start顶点位置的子树进行堆化
void makeHeap(int* arr, int start, int end) {
    
    
    while (start * 2 + 1 <= end) {
    
        //如果有子树,就一直往下,因为调整之后有可能子树又不满足性质了
        int child = start * 2 + 1;    //因为下标是从0开始,所以左孩子下标就是i * 2 + 1,右孩子下标就是i * 2 + 2
        if(child + 1 <= end && arr[child] < arr[child + 1])   //如果存在右孩子且右孩子比左孩子大
            child++;    //那就直接看右孩子
        if(arr[child] > arr[start])   //如果上面选出来的孩子,比父结点大,那么就需要交换,大的换上去,小的换下来
            swap(&arr[child], &arr[start]);
        start = child;   //继续按照同样的方式前往孩子结点进行调整
    }
}

void heapSort(int arr[], int size) {
    
    
    for(int i= size/2 - 1; i >= 0; i--)   //我们首选需要对所有非叶子结点进行一次堆化操作,需要从最后一个到第一个,这里size/2计算的位置刚好是最后一个非叶子结点
        makeHeap(arr, i, size - 1);
    for (int i = size - 1; i > 0; i--) {
    
       //接着我们需要一个一个把堆顶元素搬到后面,有序排列
        swap(&arr[i], &arr[0]);    //搬运实际上就是直接跟倒数第i个元素交换,这样,每次都能从堆顶取一个最大的过来
        makeHeap(arr, 0, i - 1);   //每次搬运完成后,因为堆底元素被换到堆顶了,所以需要再次对根结点重新进行堆化
    }
}

最後に、ヒープ ソートの安定性を分析してみます。実際、ヒープ ソート自体も選択を行っています。毎回、ヒープの先頭の要素が選択されて最後尾に配置されますが、ヒープは常に動的に維持されます。実際、要素がヒープの最上部から取り出されるとき、要素は下のリーフと交換され、次のようなことが発生する可能性があります。

画像-20220906223706019

したがって、ヒープ ソートは不安定なソート アルゴリズムです。

最後に、上記の 3 つの並べ替えアルゴリズムの関連するプロパティをまとめてみましょう。

ソートアルゴリズム 最良のシナリオ 最悪のシナリオ 空間の複雑さ 安定性
クイックソート O ( nlogn ) O(nlogn)O ( nログn ) _ _ O ( n 2 ) O(n^2)O ( n2 ) O (ログン) O(ログン)O (ログオン) _ _ _ 不安定な
ヒルソート O ( n 1.3 ) O(n^{1.3})O ( n1.3 ) O ( n 2 ) O(n^2)O ( n2 ) O ( 1 ) O (1) 不安定な
ヒープソート O ( nlogn ) O(nlogn)O ( nログn ) _ _ O ( nlogn ) O(nlogn)O ( nログn ) _ _ O ( 1 ) O (1) 不安定な

その他の並べ替えオプション

先ほど紹介したいくつかの並べ替えアルゴリズムに加えて、他の種類の並べ替えアルゴリズムもありますので、それらをすべて見てみましょう。

マージソート

マージ ソートは、再帰的分割統治の考え方を使用して元の配列を分割し、次に分割された小さな配列を最初に並べ替え、最後にそれらを順序付けられた大きな配列にマージします。

画像-20220906232451040

例として次の配列を考えてみましょう。

画像-20220905223505975

最初は急いで並べ替えずに、半分と半分に分けてみましょう。

画像-20220907135544173

分割を続けます:

画像-20220907135744253

最終的にはこんな感じで一つずつ要素になります。

画像-20220907135927289

この時点で、マージと並べ替えを開始できます。ここでのマージは単純なマージではないことに注意してください。各要素を小さいものから大きいものへ順番にマージする必要があります。ツリーの最初のグループ 4 と 2。この時点で、次のことが必要です。これら 2 つの配列から小さい方を選択し、前に移動します。

画像-20220907140219455

並べ替えが完了した後、上向きにマージを続けます。

画像-20220907141217008

最後に、2 つの配列をマージして元のサイズに戻します。

画像-20220907141442229

最後に、順序付けられた配列が得られます。

実際、この並べ替えアルゴリズムも非常に効率的ですが、分解されたデータを並べ替えるために元の配列のサイズのスペースを犠牲にする必要があります。コードは次のとおりです。

void merge(int arr[], int tmp[], int left, int leftEnd, int right, int rightEnd){
    
    
    int i = left, size = rightEnd - left + 1;   //这里需要保存一下当前范围长度,后面使用
    while (left <= leftEnd && right <= rightEnd) {
    
       //如果两边都还有,那么就看哪边小,下一个就存哪一边的
        if(arr[left] <= arr[right])   //如果左边的小,那么就将左边的存到下一个位置(这里i是从left开始的)
            tmp[i++] = arr[left++];   //操作完后记得对i和left都进行自增
        else
            tmp[i++] = arr[right++];
    }
    while (left <= leftEnd)    //如果右边看完了,只剩左边,直接把左边的存进去
        tmp[i++] = arr[left++];
    while (right <= rightEnd)   //同上
        tmp[i++] = arr[right++];
    for (int j = 0; j < size; ++j, rightEnd--)   //全部存到暂存空间中之后,暂存空间中的内容都是有序的了,此时挨个搬回原数组中(注意只能搬运范围内的)
        arr[rightEnd] = tmp[rightEnd];
}

void mergeSort(int arr[], int tmp[], int start, int end){
    
       //要进行归并排序需要提供数组和原数组大小的辅助空间
    if(start >= end) return;   //依然是使用递归,所以说如果范围太小,就不用看了
    int mid = (start + end) / 2;   //先找到中心位置,一会分两半
    mergeSort(arr, tmp, start, mid);   //对左半和右半分别进行归并排序
    mergeSort(arr, tmp, mid + 1, end);
    merge(arr, tmp, start, mid, mid + 1, end);  
  	//上面完事之后,左边和右边都是有序状态了,此时再对整个范围进行一次归并排序即可
}

マージソートも最終的には小さい優先度に従ってマージされるので、等価になった場合は最初にあるものが先に元の配列に戻されるので、最初のものが1位のままなので、マージソートも安定したソートですアルゴリズム

バケットソートと基数ソート

バケット ソートの説明を始める前に、まずカウント ソートを見てみましょう。配列の長さが N で、配列内の要素の値の範囲が 0 から M-1 の間である必要があります (M は以下です)。 Nに等しい)

アルゴリズムのデモ Web サイト: https://visualgo.net/zh/sorting?slide=1

たとえば、次の配列では、すべての要素の範囲は 1 ~ 6 です。

画像-20220907142933725

まずそれを走査し、各要素の出現数を数えます。統計が完了すると、並べ替え後にどの値を持つ要素をどこに保存するかを知ることができます。

画像-20220907145336855

それを分析しましょう。まず、1 は 1 つだけあるので、1 つの位置だけを占めます。2 は 1 つしかないので、1 つの位置だけを占めます。以下同様です。

画像-20220907145437992

したがって、統計結果に基づいてこれらの値を 1 つずつ直接入力することができ、依然として安定しています。いくつかの値を順番に入力するだけです。

画像-20220907145649061

とてもシンプルで、統計のために一度走査するだけで済むと思いませんか?

もちろん、間違いなく欠点もあります。

  1. 配列内の最大値と最小値の差が大きすぎる場合、カウントするためにより多くのスペースを申請する必要があるため、カウントの並べ替えには適していません。
  2. 配列内の要素の値が離散的でない (つまり、整数でない) 場合、それらをカウントする方法はありません。

次にバケット ソートを見てみましょう, これはカウンティング ソートの拡張であり、アイデアは比較的単純です。また、配列の長さが N であり、配列内の要素の値の範囲が 0 ~ M-1 である必要があります。 (M は N 以下です) たとえば、現在 1,000 人の生徒がおり、スコアに従ってこれらの生徒を並べ替える必要があります。スコアの範囲は 0 ~ 100 であるため、分類されたストレージ用に 101 個のバケットを作成できます。 。

たとえば、次の配列:

画像-20220907142933725

この配列には要素 1 ~ 6 が含まれているため、統計用に 6 つのバケットを作成できます。

画像-20220907143715938

このように、一度トラバースするだけですべての要素を分類し、これらのバケットに投入します。最後に、これらのバケットを順番にトラバースし、要素を取り出して格納し直すだけで、順序付けされたデータを取得できます。配列:

画像-20220907144255326

ただし、バケット ソートも非常に高速ですが、上記のカウント ソートと同じ制限があり、各バケットで特定の範囲内の要素を受け入れることでバケットの数を減らすことができますが、追加の時間オーバーヘッドが発生します。

基数ソートは依然として統計に依存するソート アルゴリズムですが、範囲が大きすぎるため補助空間に無制限に適用されるわけではありません。アイデアは、10 の基数 (0 ~ 9) を分離することです。トラバースする必要があるのはまだ 1 回だけです。各要素の一の位の数字に従って分類します。これは、10 の基数があるため、つまり 10 A ですバケツ。1 つが終わったら、何十、何百ものものを見てください。

アルゴリズムのデモ Web サイト: https://visualgo.net/zh/sorting

画像-20220907152403435

最初に 1 桁ずつ数えてから並べ替え、次に 10 桁ずつ数えてから並べ替えると、最終結果は次のようになります。

画像-20220907152903020

次に、10の位が続きます。

画像-20220907153005797

最後に、次の順序で再度取り出します。
画像-20220907153139536

順序付けされた配列の取得に成功しました。

最後に、すべての並べ替えアルゴリズムの関連プロパティをまとめてみましょう。

ソートアルゴリズム 最良のシナリオ 最悪のシナリオ 空間の複雑さ 安定性
バブルソート O ( n ) O( n )O ( n ) O ( n 2 ) O(n^2)O ( n2 ) O ( 1 ) O (1) 安定させる
挿入ソート O ( n ) O( n )O ( n ) O ( n 2 ) O(n^2)O ( n2 ) O ( 1 ) O (1) 安定させる
選択ソート O ( n 2 ) O(n^2)O ( n2 ) O ( n 2 ) O(n^2)O ( n2 ) O ( 1 ) O (1) 不安定な
クイックソート O ( nlogn ) O(nlogn)O ( nログn ) _ _ O ( n 2 ) O(n^2)O ( n2 ) O (ログン) O(ログン)O (ログオン) _ _ _ 不安定な
ヒルソート O ( n 1.3 ) O(n^{1.3})O ( n1.3 ) O ( n 2 ) O(n^2)O ( n2 ) O ( 1 ) O (1) 不安定な
ヒープソート O ( nlogn ) O(nlogn)O ( nログn ) _ _ O ( nlogn ) O(nlogn)O ( nログn ) _ _ O ( 1 ) O (1) 不安定な
マージソート O ( nlogn ) O(nlogn)O ( nログn ) _ _ O ( nlogn ) O(nlogn)O ( nログn ) _ _ O ( n ) O( n )O ( n ) 安定させる
カウントソート O ( n + k ) O(n + k)O ( n+k ) O ( n + k ) O(n + k)O ( n+k ) はいはい)( k ) 安定させる
バケットソート O ( n + k ) O(n + k)O ( n+k ) O ( n 2 ) O(n^2)O ( n2 ) O ( k + n ) O(k + n)( k+n ) 安定させる
基数ソート O ( n × k ) O(n \times k)O ( n×k ) O ( n × k ) O(n \times k)O ( n×k ) O ( k + n ) O(k+n)( k+n ) 安定させる

猿の仕分け

猿の仕分けは、いつ仕分けを完了できるかはすべて運次第なので、より仏教的です。

無限猿定理は、1909 年に出版された確率に関する本の中でエミール ボレルによって初めて言及され、「猿の入力」の概念が紹介されました。無限モンキー定理は、確率論におけるコルモゴロフのゼロ一様性命題の一例です。一般的な意味は、猿にタイプライターのキーをランダムに押させて、このようにキーを押し続けた場合、時間が無限に達する限り、猿はほぼ確実にどんなテキストでも入力できるようになるということです。完全な作品セットもタイプできます。

長さ N の配列があるとします。

画像-20220907154254943

毎回、配列から要素をランダムに選択し、それをランダムな要素と交換します。

画像-20220907154428792

運が良ければ数回で終わるかもしれませんが、運が悪いと孫が結婚するまで結婚できないかもしれません。

コードは以下のように表示されます。

_Bool checkOrder(int arr[], int size){
    
    
    for (int i = 0; i < size - 1; ++i)
        if(arr[i] > arr[i + 1]) return 0;
    return 1;
}

int main(){
    
    
    int arr[] = {
    
    3,5, 7,2, 9, 0, 6,1, 8, 4}, size = 10;

    int counter = 0;
    while (1) {
    
    
        int a = rand() % size, b = rand() % size;
        swap(&arr[a], &arr[b]);
        if(checkOrder(arr, size)) break;
        counter++;
    }
    printf("在第 %d 次排序完成!", counter);
}

要素が 10 個ある場合、7485618 回目の並べ替えが成功したことがわかります。

画像-20220907160219493

しかし、ソートの結果が毎回同じになる理由はわかりません。乱数が十分にランダムではないのかもしれません。

ソートアルゴリズム 最良のシナリオ 最悪のシナリオ 空間の複雑さ 安定性
猿の仕分け O ( 1 ) O (1) O ( 1 ) O (1) 不安定な

おすすめ

転載: blog.csdn.net/qq_25928447/article/details/126751213