序文
この記事は、ステーションBのオープンクラス「MITアルゴリズム入門」の付随するメモです。メモの内容と形式について質問がある友人は、プライベートメッセージまたはコメントエリアで連絡できます〜
クイックソートアルゴリズムも分割統治アルゴリズムのアプリケーションの1つであり、その場でソートされ(元のデータ領域に要素を再配置)、クイックソートも非常に実用的です。
1.クイックソートの説明
- 高速キューで戦略を分割統治する
①分割する
キー要素を選択し、キー要素のサイズに応じて配列を2つのサブ配列部分に分割します。左側のサブ配列はキー要素よりも小さく、右側のサブ配列の要素≥キー要素です。
②征服
2つのサブ配列のソート問題を再帰的に処理します
③組み合わせる
高速ソートの問題では、分割が完了し、2つのサブ配列のソート問題が再帰的に処理されると、配列全体がすでに順序付けられているため、このステップを組み合わせることがそれほど重要ではありません。
高速ソートで最も重要なステップは、パーティションステップです。セグメンテーション要素を見つけて、配列全体を2つのサブ配列に分割します。
この観点から、配列を再帰的に連続的に分割する高速ソートの問題を理解できます。
- クイックソートの擬似コード表現
(1)パーティション()
int Partition(int A[],int p,int q){
x ← A[p]
i ← p
for j ← P+1 to q
do if A[j]≤x
then i ← i+1
exch(A[i],A[j])
exch(A[p],A[i])
return i
上記のコードの主なアイデア:
- 配列の最初の要素が分割要素として選択されるたびに、ポインターiとその前の要素は分割要素以下のすべての要素を保存します。ポインターiからjは、以下のすべての要素を格納します。分割要素;ポインタjの後の要素はまだ比較されていない要素です
- 要素がレビューされるたびに、要素がセグメント化された要素よりも大きい場合、ポインタjは後方に移動し続けることができます。現在の要素がセグメント化された要素よりも小さい場合、現在の要素とi +1が指す要素は次のようになります。交換され(最初のポイントによると、ポインターi +1が指す要素は、セグメント化された要素よりも大きい、現在比較されている最初の要素である必要があります)、ポインターiとjを1ビット戻します。
- 最後のループが完了すると、セグメント化された要素がポインタiが指す要素と交換され、セグメント化のラウンド後にインデックスiがインデックスとして返されます。
「アルゴリズムの概要」という本に記載されている擬似コードの考え方は同じですが、配列の最後の要素がセグメンテーション要素として選択されている点が異なります。
アルゴリズムクラスで接触したパーティションの例は、ほぼ同じ考えです。分割要素を選択し、分割要素と比較するためのポインターを維持し、サイズの関係に一致しない要素を交換します。しかし、その時は同時にスキャンしていました
int partition(int a[],int lo, int hi){
int i = lo,j = hi+1;
int v = a[lo];//切分元素
while(true){
while(less(a[++i],v) if(i == hi) break;
while(less(v,a[--j])) if( j == lo) break;
if( i >= j) break;//当指针i和j相遇时主循环就会退出
exch(a,i,j);
}
exch(a,lo,j);//将v = a[j]放入正确的位置
return j;
}
- ループでは、a [i]がvより小さい場合、iを増やし、a [j]がvより大きい場合、jを減らし、a [i]とa [j]を交換して、 iの左側の要素がvより大きい場合、jの右側の要素はv以上です。
- ポインタが出会ったら、a [lo]とa [j]を交換すると、分割が終了します。分割値がa [j]にとどまるようにします。
対称性を追求するために、アルゴリズムの本で抜粋されたパーティション実装コードには、特別な添え字の初期化が含まれているため、それに入る必要はありません。
実装に関係なく、プロセス全体が配列内のすべての要素をトラバースすることと同等であるため、セグメンテーションプロセスの時間計算量はおそらくθ(n)に維持されます。
(2)クイックソート()
(3)ヒントを改善する
①コード「if(p <q)」を調整します。
コードがif(p <q)の場合、処理された配列に0または1の要素がある場合、ワークロードがないことを意味します。
これを改善して、要素の数が少ない場合にソートの効率を改善するための適切なアルゴリズムを見つけることができます。
[この点は、マージアルゴリズム「マージソートアルゴリズムの実装と分析」の改善にも実装されています]
②高速ソートの擬似コードによると、アルゴリズムが末尾再帰的であることがわかるので、いくつかの末尾再帰的最適化手順を使用できます。
末尾再帰のいくつかの概念については、[Allen Chou]このブロガーの記事「末尾再帰とは何かを説明する(理解しやすい、説明の例)」を参照してください。
- 高速キューの例
注:上記のプロセスは、パーティションの実装に基づいています。つまり、分割要素の結果として、配列の最後の要素が選択されます。
2.高速ソートの複雑さの分析
まず、結論を出します。このセクションでは、複雑なものに対して多くの「興味深い」導出プロセスを実行します。
クイックソートの実行時間は、パーティションが対称であるかどうかに関係し、パーティションが大幅に対称であるかどうかは、選択されているパーティション要素によって異なります。
除算が対称の場合、高速ソートはプログレッシブの意味でのマージソートと同じ複雑さを持ちます。
除算が非対称の場合、高速ソートはプログレッシブの意味での挿入ソートと同じ複雑さを持ちます。
1.最悪の場合の分類
高速ソートの最悪の場合の除算動作は、除算プロセスによって生成された2つの領域にそれぞれn-1要素と10要素が含まれている場合に発生します(配列全体の観点から、つまり、配列全体が順序または逆の場合)注文)。
各除算が非対称除算を使用し、除算の時間コストがθ(n)であり、ゼロ要素配列T(0)への再帰呼び出しのコストがθ(1)に設定されていると仮定すると
、アルゴリズムは実行されます。時間の再帰的表現を次の図に示します。
直感的には、再帰の各層のコストを追加する場合は、等差数列を取得する必要があります。これにより、コストが2乗であると推定できます。
高速ソートの平均パフォーマンスは、最悪の場合の挿入ソートの平均パフォーマンスと一致していることがわかります。要素がすべて順序付けられている場合でも、挿入ソートに必要なのは線形時間計算量O(n)のみです。
再帰ツリーは次のようになります。
psグラフは少し高く、あいまいです。
上の図で得られた再帰ツリー(構造が非常に不均衡)を許してください。ツリーの高さはn(各レイヤーが1つ減っているため)、複雑さの合計です。すべてのルートノードのが追加されます。これは正方形レベルであり、すべてのリーフノードの複雑さ(合計n)はθ(1)であり、合計は線形レベルです。
総和がθであるように(N 2)+θ(N)=θ(N 2)。
2.ベストケース分析
(1)複雑さの漸化式
除算ステップで得られた最もバランスの取れた除算では、得られた2つのサブ問題のサイズをn / 2より大きくすることはできません。この場合、クイックソート操作速度ははるかに高速です。
この時点で複雑さの再帰式を導出するには、次の方法があります。
(2)バランスの取れた除算
高速ソートの最悪のシナリオが正方形レベルの複雑さであることがわかっている場合でも、平均的な状況から、高速ソートの効率も線形対数レベルであるため、高速ソートの効率を高く評価します。
高速ソートの平均的なケースは、最悪の場合の実行時間ではなく、最良の場合の実行時間に近いです。これを理解するには、実行を特徴付ける再帰式に除算のバランスがどのように反映されるかを理解する必要があります。時間。
除算プロセスが常に9:1の除算を生成すると仮定すると、
複雑さの再帰式は次のように記述されます
。T(n)= T(n / 10)+ T(9n / 10)+θ(n)≤T(n / 10))+ T(9n / 10)+ cn
上記の再帰式の再帰ツリーを構築するために、ツリーの高さと各レイヤーの計算コストを次の図に示します。
ツリーの構造に従って、全体の複雑さを分析できます。T(n)≤c・n・log10 / 9 n +θ(n)、漸近的な意味でのこの式の複雑さはO(nlogn)です。
数学計算の観点からは、対数演算の底が何であれ、対数演算の性質上、底を2に変換し、余剰項を係数として対数項全体に加算します。
3.平均的な状況分析
クイックソートの平均的な状況をより明確に理解するためには、さまざまな入力の発生頻度を想定する必要があります。
しかし、ランダム入力配列にクイックソートを適用する場合、各レイヤーが同じ分割と分布を持っていると想定することは不可能です。一部の部門はよりバランスが取れており、一部の部門は不均衡であると予想できます。
ツリーの各レベルで、適切なパーティションと不良なパーティションが交互に表示されるとします。
再帰ツリーと再帰ツリーの2つのセットを取得できます。
L(n)= 2U(n / 2)+θ(n)-------ラッキー
U(n)= L(n-1)+θ(n)-------不運
【使用置換法と主定理]
漸近的に最適な複雑さを常に持つようにするにはどうすればよいですか?
- 配列内の要素をランダムにシャッフルします
- ピボットを分割することをランダムに選択します
分析する方が便利で直感的であるため、通常は後者を検討します。
3.高速ソートのランダム化バージョン
「多くの場合、アルゴリズムにランダム化コンポーネントを追加して、すべての入力でより良い平均パフォーマンスを得ることができます。」
-高速ソートのランダム化バージョンは、十分な大きさの入力に最適です。
[ランダムサンプリング]
常にA [r]をピボットとして使用する代わりに、要素はサブ配列A [p ... r]からランダムに選択されます。つまり、A [r]とA [p ... rです。 ]はランダムに選択されます。1つの要素を交換してから、前のロジックに従ってクイックソートを実行します。
上記のランダム化操作では、サブ配列のr-p + 1要素の中で、ピボット要素x = A [r]などが確実になるように、p、... rの範囲からランダムにサンプリングします。 。それらのいずれかを取ります。
ピボット要素はランダムに選択されるため、平均して、入力配列の分割はより対称的になる可能性があります。
1.ランダム化高速ソートの利点:
①実行時間は入力シーケンスの順序に依存しません
。②入力シーケンスの分布を仮定する必要はありません。③
最悪の場合につながる特定の入力はありません。最悪の場合は、によってのみ決定されます。乱数ジェネレーター。
2.高速ソートの擬似コードをランダム化します