banner
genkaim

genkaim

Darkmode,yes \q(≧▽≦q) 主题来自@birdgg

十大ソートアルゴリズムのマージソート、クイックソート、ヒープソート

カバー
原文を見る

33238e2b7a5d476195dfc19af728bbaf

マージソート#

紹介#

マージソートは、マージの考え方を使用して実現されるソート方法であり、このアルゴリズムは古典的な分割統治法を採用しています。

具体的には、ソート中に配列を異なる小さな配列(境界では 1 つの要素のみ)に分割し、マージ時にソートします。

img

複雑度 / 安定性#

時間複雑度

上記のコードを例にすると、この時点で n=8n = 8 です。マージソートのセクションは、インデックスを 2 分割する方法を使用しています。

次の図のように、各 2 つのセクションをマージするとき、各レベルで nn 回比較が行われ、合計で log2nlog_2n レベルあります。したがって、時間複雑度は O(nlog2n)O(nlog_2n) です。

image-20230814181555918

安定性

各 2 つのセクションは隣接しているため、同じ数値をマージするとき、相対的な位置は変わりません。したがって、マージソートは安定です。

コードの実装#

以下は C++ での実装です。コメントを参照してください:

const int N = 1e4+5;
int n, tmp[N], a[N];
void fsort(int l, int r){
    //まず再帰的にグループ分けを行います
    if(l >= r) return ;//境界条件、分割される配列の最小値は1です
    int mid = (l+r)/2; //中央値を取得します
    fsort(l, mid); //左側
    fsort(mid+1, r); //右側

    //バックトラック時にマージしてソートします。元の配列を直接操作することはできないため、マージ結果を一時的にtmp配列に保存します
    int i = l, j = mid+1, k = l;
    while(i <= mid && j <= r){ //2つのシーケンスの各要素を比較し、順序に従って一時的な配列に格納します
        if(a[i] <= a[j]){
            tmp[k] = a[i];//左側の範囲の数値が小さいので、格納します
            i++;
            k++;
        }
        else{
            tmp[k] = a[j];//右側の範囲の数値が小さいので、格納します
            j++;
            k++;
        }
    }
    //余分な数値が1つの配列に残っている場合、追加して配置します
    while(i<=mid){
        tmp[k]=a[i];
        i++;
        k++;
    }
    while(j<=r){
        tmp[k]=a[j];
        j++;
        k++;
    }
    //一時的な領域の数値を元の配列に格納します
    for(int i = l; i <= r; i++)
        a[i] = tmp[i];  
}
signed main(){
    fsort(1, n);
    return 0;
}

クイックソート#

紹介#

クイックソートは、二分法を使用して配列のソートを行うアルゴリズムです。

クイックソートは、ソートする配列からピボット値を選択し、配列をピボット値の位置を境に小さい方と大きい方に調整し、再帰的に両側をソートすることでソートを完了します。

図 1 を参照してください:

img

具体的な操作については、以下の図 2 を参照してください:

3

複雑度 / 安定性の分析#

時間複雑度

最良の場合、各分割は均等になるため、マージソートと同様に各レベルで nn 個の要素をソートします。合計で log2nlog_2n レベルありますので、時間複雑度は O(nlog2n)O(nlog_2n) です。

最悪の場合、ピボット値が最大または最小の場合、空のシーケンスが発生し、nn 回の分割が必要です。したがって、時間複雑度は O(n2)O(n^2) です。

同様に、ピボット値がランダム性を失った場合、クイックソートの時間複雑度も O(n2)O(n^2) に低下します。

安定性

クイックソートは、各ピボット値の分類時に、上記の図のように、2 つの要素の値が等しい場合、相対的な位置が変わるため、安定ではありません。

コードの実装#

以下は C++ での実装です。コメントを参照してください:

const int N = 1e4+5;
int n, a[N];
void quicksort(int l, int r){//図2を参照してください
    if(l >= r) return ;//範囲外にならないようにする
    int t = a[l]; //ピボット値を選択します。図のように、最初の位置を選択します
    int i = l, j = r;
    while(i < j){ //i != jの場合、ループに入ります
        while( a[j] > t ) j--; //後ろから前に向かって、ピボット値より小さい値を順番に探します
        while( a[i] <= t && i<j) i++;//前から後ろに向かって、ピボット値より大きい値を順番に探します
        if(i < j) swap(a[i], a[j]);//i < jの場合にのみ交換できます。同じ位置を交換することはできません
    }
    swap(a[l],a[i]); //ピボット値を適切な位置に配置します。この時点でi = jです
    quicksort(l,i-1);//下に分割を続けます
    quicksort(i+1,r);
}
signed main(){
    quicksort(1, n);
    return 0;
}

ヒープソート#

ヒープソート (Heapsort)(Heap-sort) は、ヒープというデータ構造を使用してソートを行います。選択ソートとは異なり、ツリーというデータ構造を使用して要素を検索します。

事前チーズ:ヒープ#

図を参照してください。ヒープは特殊な完全二分木であり、大きなヒープと小さなヒープの 2 種類に分かれます:

  1. 大きなヒープ:完全二分木の任意の非葉子ノードの値は、その子ノードの値以上です。
  2. 小さなヒープ:完全二分木の任意の非葉子ノードの値は、その子ノードの値以下です。

img

紹介#

選択ソートの原理は理解しやすいため、この部分はスキップします(類推できます)。ヒープで要素を検索する方法に重点を置きます。

配列要素を単調に減少させる場合、最終的に大きなヒープを作成する必要があります。下から上に非葉子ノード(子ノードのないノード)を逆順に走査し、交換することで親子ノードの順序を調整します。

例:次の図のような配列がある場合、この操作を行うだけで、ソートされた配列が得られます。

image-20230818112757861

複雑度 / 安定性の分析#

時間複雑度

選択ソートと同様に、nn 個の要素を操作する必要がありますが、違いは、選択ソートでは 1 つの要素を操作するのに nn 回かかりますが、ヒープソートでは 1 つの要素を検索するのに log2nlog_2n 回しかかかりません。したがって、ヒープソートの時間複雑度は O(nlog2n)O(nlog_2n) です。

安定性

ヒープソートは、各要素の分類時に、上記の図のように、2 つの要素の値が等しい場合、相対的な位置が変わるため、安定ではありません。

コードの実装#

以下は C++ での実装です。コメントを参照してください:

const int N = 1e4+5;
int m, a[N]
//ヒープソート
//ヒープ部分
void adjust(int i, int parent)
{
  int child = parent * 2;//まず子ノードを確認します
  // 範囲がiを超えないようにする
  if(child + 1 <= i && a[child] < a[child + 1]){
    child++;//2つの子ノードのうち、大きい方を選択して上に移動します(大きなヒープを維持する)
  }
  if(a[child] > a[parent]){
    swap(a[child], a[parent]);//子ノードの方が大きい場合、上に交換します
    if(child <= i / 2){//iの親ノードを超えないようにする
      adjust(i, child);//現在のノードが更新された場合、現在のノードの子ノードも更新する必要があります
    }
  }
}
//メインのソート部分
void heapSort(){
  for(int i = n; i > 1; i--){//右から左に維持するため、左から右にすることはできません。子ノードがすでに維持されていることを保証できません
    for(int j = i / 2; j >= 1; j--){//iの親ノードから開始して維持します
      adjust(i, j);//j番目のノードを根ノードとして維持します。iは境界を判断するための補助です
    }
    swap(a[1], a[i]);//この時点でツリーの根は[1, i]範囲内で最大の数です。現在の位置と交換します(選択ソートを参照)
  }
}
signed main(){
    heapSort();
    return 0;
}
読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。