図 | 武術の観点から STL ソート アルゴリズムの秘密を探る

図 | 武術の観点から STL ソート アルゴリズムの秘密を探る

[[410325]]

この記事はWeChatの公開アカウント「Backend Research Institute」から転載したもので、著者はDabaiskiです。この記事を転載する場合は、バックエンド研究所の公式アカウントまでご連絡ください。

序文

今日は、STL でのソート アルゴリズムの基礎となる実装とコーディング手法を見てみましょう。

ご存知のとおり、STL はデータ構造とアルゴリズムの一般化をサポートするためにテンプレートに依存しています。一般化は C++ ユーザーにとってすでに驚きですが、STL 開発者の強力なラインナップを見ると、STL がもたらす驚きは一般化にとどまらないことがわかります。強力なパフォーマンスと効率性により、STL はさらに素晴らしいものになっています。

STL の究極のパフォーマンスの背後には、熟練プログラマーの優れたプログラミング スキルと完璧さを追求する職人精神がまさに体現されています。

私の能力の限界により、先人たちの肩を追って STL のソート アルゴリズムの背後に何が隠されているかを調べることしかできません。まるで「Into Science」のワンシーンのようですね。では、ソート アルゴリズムの旅を始めましょう。

内省的哲学

ソートアルゴリズムの実装を理解する前に、イントロスペクティブソートという概念を見てみましょう。正直に言うと、私の中国語レベルは本当に平均的で、ソートアルゴリズムで使用されるこの用語は明確ではないといつも感じているので、勉強しましょう。

イントロスペクティブソートは英語で Introspective Sort と呼ばれます。introspective という言葉は内省的な意味です。まだよくわかりません。検索を続け、この用語の Baidu Encyclopedia の説明を見てみます。

内省は心理学における基本的な研究方法の一つです。内省は自己観察とも呼ばれます。それは内部で発生し、私たち自身も認識している主観的な現象です。それは、自分自身の主観的な経験とその変化の観察であるとも言えます。

内省はまさにその主観性ゆえに、心理学の分野では古代から長年の論争の的となってきました。また、内省は自己反省とも言え、儒教が重視する自己思考でもあります。この観点から、Java イントロスペクション メカニズムや Cocoa イントロスペクション メカニズムなどのコンピュータ分野に応用できます。

わあ、内省は心理学用語だということがわかりました。これで少し理解できました。内省とは、外の世界に希望を託すのではなく、自分自身の経験と能力に基づいて、自己を振り返り、自分で考え、変化を観察し、自分の主観的な経験に基づいて調整することを意味します。

簡単に言えば、イントロスペクション アルゴリズムはデータ セットを選り好みせず、ソートが時間内に保証されるように、各データ セットに対応する処理方法を提供しようとします。

これを書いていると、「天剣龍驤」で張無忌が光明山で六大宗派と戦う場面が頭に浮かびます。敵がどんなに強くても弱くても、私は自分のやり方で対処します。

彼は強いので強い、そよ風が丘を越えて吹き渡る。

彼が望むままにさせておけば、明るい月が川面を照らします。

彼は残酷で邪悪だが、私には十分なエネルギーがある。

--- 達磨の『九陽経』

哲学、その通りです。分類の視点に切り替えて、内省のプロセスを見てみましょう。

私が理解する内省的ソート アルゴリズムは、外部データの品質や量に依存せず、それぞれの極端なシナリオに対する独自の対応に基づいて対応する判断と決定の調整を行い、さまざまなデータ セットに適応して優れたパフォーマンスを発揮するアルゴリズムです。

内省的ソート

諺にあるように、英雄はナイフ、銃、剣、戟、斧、手斧、フック、フォークなど、多くの武器を使用します。これは、無敵の武器はなく、特定のシナリオでのみ明らかな利点があることを示しています。これは、ソフトウェア エンジニアリングに特効薬がないのと同じです。

ソートアルゴリズムに戻ると、バブルソート、選択ソート、挿入ソート、クイックソート、ヒープソート、バケットソートなど、ソートアルゴリズムは多数あります。

古いソートアルゴリズムの多くは O(n^2) ですが、優れたアルゴリズムは O(nlogn) に到達できます。ただし、nlogn であるクイックソートやヒープソートにも、それぞれ長所と短所があります。挿入ソートは、データがほぼ順序付けられている場合に O(n) のパフォーマンスを達成できます。場合によっては、競合比較ではなく、統合と革新を行う必要があります。

イントロスペクティブソートは、1997 年に David Musser によって設計されたソートアルゴリズムです。このソートアルゴリズムはクイックソートから始まり、再帰の深さが一定の深さを超えるとヒープソートに切り替わります (深さはソートされた要素の数の対数です)。David Musser は STL 分野ではよく知られた人物です。

文脈を考慮せずに、盲目的にどちらが良いか悪いかを比較するのは意味がありません。イントロスペクティブソートは、それらすべてを集大成したものです。ソートアルゴリズムが総合的に優れたパフォーマンスを実現できるように、イントロスペクティブソートアルゴリズムは、クイックソート、ヒープソート、挿入ソートを組み合わせ、現在のデータセットの特性に基づいてどのソートアルゴリズムを使用するかを選択し、各アルゴリズムが独自の長所を発揮できるようにします。このアイデアは確かに非常に刺激的です。

デプロイメントの内省的なソート

前述のように、イントロスペクティブ ソートは主にクイック ソート、ヒープ ソート、挿入ソートを組み合わせたものです。では、これら 3 つのソートはどのように配置されているのでしょうか。

自分と敵を知ることで、あらゆる戦いで勝利が保証されます。まずは、3 つの分類の長所と短所を見てみましょう。

クイックソート

大量のデータの場合、順序付きか繰り返しかに関係なく、最適化されたアルゴリズムはほとんどの場合 O(nlogn) に到達できます。ヒープソートも O(nlogn) ですが、クイックソートの方がいくつかの理由で高速です。再帰が深すぎてセグメンテーションが著しく不均一な場合、O(n^2) の複雑さに退化し、パフォーマンスが低下します。これがクイックソートの欠点です。

ヒープソート

ヒープソートはクイックソートの強力な競合相手です。その最大の特徴は、O(nlogn) に到達でき、その複雑性が非常に安定していることです。クイックソートのように O(n^2) に低下することはありません。ただし、ヒープソートプロセスには多くのヒープ調整が含まれ、要素の比較はジャンプ方式で行われるため、キャッシュの局所性特性が十分に活用されません。その他の理由と同様に、ヒープソートはクイックソートよりも少し遅くなりますが、ビッグ O の複雑性は依然として同じレベルです。

挿入ソート

挿入ソートの特徴の 1 つは、トランプに似ていることです。手札のカードをソートする場合、カードがすでに整列していれば、調整はほとんど必要ありません。したがって、データ量が多くなく、ほぼ整列している場合は、挿入ソートの複雑さは O(n) にまで削減できるため、適用する価値があります。

利点と欠点は大体明らかなので、イントロスペクティブ ソートが実際にこれら 3 つのソート アルゴリズムをどのようにスケジュールするかを推測できます。

  • 起動フェーズでは、ソートする要素が多数ある場合、最初にクイックソートを使用して大幅なソートを実行し、その複雑さは O(nlogn) で実行できます。
  • ディープステージでは、クイックソートで再帰を使用すると、スタックフレームの保存や切り替えなどの再帰操作が多くなります。パーティションの切り方が不適切で再帰が深すぎると、スタックオーバーフローによりプログラムが終了する可能性があります。そのため、クイックソート処理がO(n^2)に退化した場合、ヒープソートは劣化がなく、O(nlogn)で安定できるため、この時点で自動的に検出してヒープソートに切り替えます。
  • 最終段階では、クイックソートとヒープソートの後、データパーティション内のソートする要素の数が特定の経験的設定値(再帰の最初の数回の呼​​び出しが終了しようとしていると考えることができる)よりも少ない場合、データは実際にほぼ順序付けられています。この時点で、挿入ソートを使用して効率を向上させ、複雑さをさらに O(n) に減らすことができます。

これを書き終えて、著者は次のような場面を思いつきました。

2005年春節祝賀会のスケッチ「装飾」で、黄紅と龔翰林が演じた。装飾役の黄紅は、大小2本のハンマーを持っていた。大ハンマーは80ポンド、小ハンマーは40ポンドで、大小のハンマーを切り替えることができた。

実際、ソートアルゴリズムを内省的ソートに切り替えるのと同じです。つまり、テクノロジーは生命から生まれ、生命よりも高いのです。以下は、みんなで一緒に体験できる画像です。

イントロスペクションとイントロスペクティブソートについて長々と説明してきました。皆さんはもう理解していると思いますので、実装の詳細を詳しく見ていきましょう。これがこの記事の焦点です。一緒に分析を続けていきましょう!

ソートアルゴリズムの実装の詳細

この記事で紹介したソートアルゴリズムは、SGI STLバージョンに基づいており、主に侯潔先生の著書「STLソースコード分析」に基づいています。そのため、関数のないバージョンが使用されています。専門家の傑作を一緒に鑑賞しましょう!写真は、著者がずっと前に購入したが、常に箱の底に保管していたSTLマジックブックです。

ソート機能の応用シナリオ

SGI STL のソートのパラメータは 2 つのランダム アクセス イテレータ RandomAccessIterator であり、ソート テンプレートもこの種のイテレータに基づいています。したがって、コンテナーがランダム アクセス イテレータでない場合は、一般的なソート関数を使用できない可能性があります。

  • 基礎となる連想コンテナマップとセットはRBツリーに基づいており、すでに独自の順序を持​​っているため、ソートアルゴリズムを使用する必要はありません。
  • シーケンス コンテナ リストは双方向反復子であり、ランダム アクセス反復子ではありません。Vector と deque は、ソート アルゴリズムに適したランダム アクセス反復子です。
  • コンテナ アダプタのスタック、キュー、および優先度キューは、要素の順序を制限するコンテナであるため、ソート アルゴリズムは適用できません。

要約すると、ソート アルゴリズムは、ベクター コンテナーとデキュー コンテナーの両方に適用できることがわかります。

並べ替え 概要

先ほどイントロスペクティブ ソートを紹介しましたので、sort が introsort をどのように使用するかをステップごとに見ていきましょう。前回のエントリ コードは次のとおりです。

  1. テンプレート <class RandomAccessIterator>
  2. インライン void sort(RandomAccessIterator first 、 RandomAccessIterator last ) {
  3. if (最初!=最後) {
  4. __introsort_loop( first last 、 value_type( first )、 __lg( last - first ) * 2);
  5. __final_insertion_sort(最初最後);
  6. }
  7. }

コードから、sort はソートするシーケンスの開始と終了として 2 つのランダム アクセス イテレータ (first と last) を使用し、さらに 2 つの関数 __introsort_loop と __final_insertion_sort を呼び出すことがわかります。文字通り、前者はイントロスペクティブ ソート ループであり、後者は挿入ソートです。 __introsort_loop の 3 番目のパラメータは __lg(last - first)*2 であることに注意してください。経験に基づいて、これが再帰の深さの限界であると推測します。コード実装を見てみましょう。

  1. テンプレート <クラスSize >
  2. インラインサイズ__lg(サイズn){
  3. サイズk;
  4. (k = 0;n > 1;n >>= 1)の場合++k;
  5. kを返します
  6. }

このコードは、n=last-first、つまり 2^k<=n の最大の整数 k 値を意味します。

したがって、全体として、last-first=20、k=4 と仮定すると、最大セグメンテーション深度 depth_max=4*2=8 となり、first と last に基づいて再帰の最大深度を決定できます。

クイック ソートとヒープ ソートの調整 __introsort_loop 関数は、主にクイック ソートとヒープ ソートをカプセル化します。この関数の実装の詳細を見てみましょう。

  1. //ソート関数のエントリ
  2. テンプレート <クラス RandomAccessIterator、クラス T、クラスSize >
  3. void __introsort_loop(RandomAccessIterator最初
  4. ランダムアクセスイテレータ最後, T*,
  5. サイズdepth_limit) {
  6. while (最後-最初> __stl_threshold) {
  7. 深さ制限 == 0 の場合
  8. partial_sort( first , last , last ); //ヒープソートを使用する
  9. 戻る;
  10. }
  11. --depth_limit; //分割残高を減らす 
  12. RandomAccessIterator カット = __unguarded_pa​​rtition
  13. (最初最後、 T(__median(*最初、 *(最初+ (最後-最初)/2 )、
  14. *( last - 1)))); //3点中央値分割プロセス
  15. __introsort_loop(cut, last , value_type( first ), depth_limit); // サブシーケンスの再帰呼び出し
  16. last = cut; // 左のシーケンスへのイテレータ交換スイッチ
  17. }
  18. }
  19. //3点中央値法に基づく分割アルゴリズム
  20. テンプレート <クラス RandomAccessIterator、クラス T>
  21. RandomAccessIterator __unguarded_pa​​rtition(RandomAccessIterator最初
  22. RandomAccessIterator最後
  23. Tピボット)
  24. )の間{
  25. (* first < pivot) の場合 ++ first ;
  26. - 最後;  
  27. while (pivot < * last ) --last;  
  28. if (!(最初<最後))戻り値 初め;
  29. iter_swap(最初最後);
  30. ++最初;
  31. }

めまいや混乱を感じないでください。少し分析すれば必ず理解できます。

  • まず、2 つのランダム アクセス イテレータのパラメータ (first と last) を確認します。3 番目のパラメータは、__lg によって計算されたセグメンテーション深度です。
  • このとき、while を入力して last-first の間隔サイズを決定します。__stl_threshold は 16 です。Hou Jie は、__stl_threshold のサイズは 5 ~ 20 で、具体的なサイズは自分で設定できると指摘しています。__stl_threshold より大きい場合は実行を継続し、そうでない場合は飛び出します。間隔サイズが __stl_threshold より大きい場合は、3 番目のパラメーター depth_limit が 0 かどうか、つまりセグメンテーションが深すぎるかどうかを判断します。これは、初期最大値を指定してから、各セグメンテーションごとに 1 を減算し、depth_limit=0 になるまで、partial_sort を呼び出すことと同じです。「STL ソースコード分析」の他の章から、partial_sort はヒープソートのカプセル化であることがわかります。ここで、主役の 1 つである heapsort が登場するのは少し興味深いです。
  • 下を見続けると、depth_limit>0 なので、まだ分割の余地があるので、ワクワクしましょう。こうして __unguarded_pa​​rtition にたどり着きます。この関数は、文字通りクイックソートのパーティション処理であり、カットランダムアクセスイテレータを返します。__unguarded_pa​​rtition の 3 番目のパラメータ __median は、3 点中央値法を使用して基準値ピボットを取得します。この時点で、クイックソートパーティションの 3 つの要素が集められ、最後に新しいカットポイントの位置が返されます。
  • 読み進めていけば、すぐに終わります。__introsort_loop が現れます。確かに再帰的です。ここでは再帰が 1 つだけであること、および cut と last が渡されることに特に注意してください。これらは右サブシーケンスに相当します。左サブシーケンスはどうでしょうか。読み進めようと急がないでください。last=cut は反転し、cut は左サブシーケンスの右境界になります。このようにして、左サブシーケンスの処理が始まります。

クイックソートの実装の比較

前述のように、sort でのクイック ソートの記述は、これまで見てきたものとは多少異なります。「STL ソース コード分析」でクイック ソートの左シーケンスの処理を見た後、Hou Jie 先生は次のように書いています。「記述が読みにくく、効率も良くありません。」これを見て、私はさらに混乱しましたが、分析してみましょう。

写真: STLソースコード解析におけるこの記述方法についてのHou Jie先生のコメント

一般的な書き方:

  1. // クイックソート用の一般的な疑似コード
  2. クイックソート(arr,,){
  3. pivoit = ​​func(arr); //ベンチマーク値を取得するには何らかの方法を使用する
  4. cut = partition( left , right ,pivot); // 左と右の境界と参照値を組み合わせて分割点の位置を決定します。
  5. quicksort(arr, left ,cut-1); // 左のシーケンスを再帰的に処理する
  6. quicksort(arr,cut+1, right ); // 右のシーケンスを再帰的に処理する
  7. }

SGI STL で記述する方法:

  1. stl_quicksort(最初最後){
  2. // 外部制御構造としてのループ
  3. while(ok){
  4. cut = stl_partition( first , last ,_median( first , last )); // パーティションを分割
  5. stl_quicksort(cut, last ); // 右の部分列を処理するための再帰呼び出し
  6. last = cut; // cut をlastに割り当てることは、左の部分列に切り替えてループを継続することと同じです。
  7. }
  8. }

インターネット上の大物による記事によると、SGI STL のクイック ソートの記述方法では、while ループを使用することで再帰呼び出しが半分に削減されるとのことですが、これは典型的な末尾再帰の最適化のアイデアです。

ここでは比較のためのテスト コードをまだ書いていません。まずピットを取り上げて、後で比較テストを書いてからコメントします。ただし、sgi のこの書き方を見ることはできます。

ヒープソートの詳細

  1. //注: これはカスタム比較関数を使用したヒープソートバージョンです
  2. //ヒープとトップの操作
  3. テンプレート <クラス RandomAccessIterator、クラス T、クラス Compare>
  4. void __partial_sort(RandomAccessIterator first 、 RandomAccessIterator middle 、
  5. RandomAccessIterator last 、 T*、 比較 comp) {
  6. make_heap(最初、真ん中、comp );
  7. (RandomAccessIterator i = 中間; i <最後; ++i)の場合
  8. if (comp(*i, * first ))
  9. __pop_heap( first 、 middle 、 i 、 T(*i )、 comp 、 distance_type( first ));
  10. sort_heap(最初、 真ん中 、 comp );
  11. }
  12. //ヒープソートの入り口
  13. テンプレート <クラス RandomAccessIterator、クラス Compare>
  14. インライン void partial_sort(RandomAccessIterator first ,
  15. RandomAccessIterator 中間、
  16. RandomAccessIterator last 、比較 comp) {
  17. __partial_sort( first 、 middle 、 last 、 value_type( first )、 comp );
  18. }

挿入ソートが作用する

__introsort_loop が __stl_threshold しきい値に達すると、データセットはほぼ順序付けられたとみなすことができます。このとき、挿入ソートによってソート速度をさらに向上させることができ、再帰によるシステム消費も回避できます。__final_insertion_sort の具体的な実装を見てみましょう。

  1. テンプレート <class RandomAccessIterator>
  2. void __final_insertion_sort(RandomAccessIterator最初
  3. ランダムアクセスイテレータ最後) {
  4. if (最後-最初> __stl_threshold ) {
  5. __insertion_sort(最初最初+ __stl_threshold);
  6. __unguarded_insertion_sort(最初+ __stl_threshold、最後);
  7. }
  8. それ以外 
  9. __insertion_sort(最初最後);
  10. }

__final_insertion_sort の実装の詳細を分析してみましょう。

  • パラメータランダムアクセスイテレータの導入
  • last-first > __stl_threshold が成立しない場合は、__insertion_sort が呼び出されます。これは、要素数が比較的少ない場合に特別な処理を行わずに直接呼び出すのと同じです。
  • last-first > __stl_threshold の場合、さらに 2 つの部分に分割され、それぞれ __insertion_sort と __unguarded_insertion_sort が呼び出されます。2 つの部分の分割ポイントは __stl_threshold です。これら 2 つの関数の違いは何ですか?

__insertion_sortの実装

  1. //逆順の調整
  2. テンプレート <クラス RandomAccessIterator、クラス T>
  3. void __unguarded_linear_insert(RandomAccessIterator last , T 値) {
  4. RandomAccessIterator=最後;
  5. - 次;  
  6. while (値 < *) {
  7. *最後= *;
  8. 最後=;
  9. - 次;  
  10. }
  11. * last = 値;
  12. }
  13.  
  14. テンプレート <クラス RandomAccessIterator、クラス T>
  15. インラインvoid __linear_insert(RandomAccessIterator first
  16. ランダムアクセスイテレータlast 、 T*) {
  17. T値 = *最後;
  18. if (値 < *最初) {
  19. copy_backward( first , last , last + 1); // 間隔移動
  20. *最初= 値;
  21. }
  22. それ以外 
  23. __unguarded_linear_insert(最後の値 )
  24. }
  25.  
  26. //__挿入ソートエントリ
  27. テンプレート <class RandomAccessIterator>
  28. void __insertion_sort(RandomAccessIterator first 、 RandomAccessIterator last ) {
  29. if ( first == last )戻り値;
  30. (RandomAccessIterator i = first + 1; i != last ; ++i)の場合
  31. __linear_insert(最初、 i、 value_type(最初) );
  32. }

挿入された関数には、__unguarded_xxx 形式の関数も表示されます。unguarded という単語は、保護されていない、保護されていないという意味です。Hou Jie 氏は、この形式の関数は、境界チェック条件なしで特定の条件下で正しく実行できる関数であると述べています。

copy_backward も全体的な移動の最適化であり、1 つずつの調整と移動を回避し、効率的な実装のために基礎となる memmove が呼び出されます。

__unguarded_insertion_sort の実装

  1. テンプレート <クラス RandomAccessIterator、クラス T>
  2. void __unguarded_insertion_sort_aux(RandomAccessIterator最初
  3. ランダムアクセスイテレータlast 、 T*) {
  4. (RandomAccessIterator i =最初; i !=最後; ++i)の場合
  5. __unguarded_linear_insert(i, T(*i));
  6. }
  7.  
  8. テンプレート <class RandomAccessIterator>
  9. インライン void __unguarded_insertion_sort(RandomAccessIterator first
  10. ランダムアクセスイテレータ最後) {
  11. __unguarded_insertion_sort_aux(最初最後、 値型(最初));
  12. }

挿入ソートのこの 2 つの機能の実装と目的は非常に詳細なので、後で挿入ソートを書くときに個別に勉強したいと思います。したがって、この記事では今のところそれについては触れません。興味のある読者はまず関連資料を読んでください。後で一緒に反論します。

要約する

この記事では、主にイントロスペクティブ ソートの考え方と基本的な実装について説明し、これを出発点として SGI STL でのソート アルゴリズムの実装を解釈します。

stl の作者たちは、究極のパフォーマンスを追求するために多くのテクニックを使用しました。この記事では、主に私があまり知識がなく、誤解を恐れているため、これについて詳しく説明しません。賢明な読者は、マスターたちの最高のスキルを分析し、探求してみてください。

<<:  超人工知能は人類を滅ぼすのか?

>>:  2021年世界人工知能会議が開幕。董明珠、馬化騰、李延紅、周紅一などの大物たちは何を語ったのか?

ブログ    
ブログ    
ブログ    

推薦する

モデルデータに偏りがある場合はどうすればいいですか?機械学習における 7 種類のデータバイアスについて 1 つの記事で学ぶ

機械学習におけるデータバイアスとは、データセットの一部の要素が他の要素よりも重み付けされ、または高く...

...

AIは中所得層に影響を与えるでしょうか?周連:移行の痛みに対処するには政策支援が必要

[[403918]]近年、経済の継続的な発展に伴い、わが国では中間所得層の総数が増加しています。現在...

膨大な顔情報が収集されている: 315 Galaが顔認識の混乱を暴露

3月15日、毎年恒例のCCTV Finance 3.15 Galaが開催されています。序文から判断す...

あなたは知っていますか?注文するテイクアウトはすべて、ディープラーニングとの美しい出会いです

[[196940]]多くの学生は、フードデリバリーはオンラインで注文し、オフラインで配達するビジネス...

ワクチン生産を加速するには?答えは医学ではなくテクノロジーにある

世界各国の政府は新型コロナウイルス感染症の流行に対抗するためさまざまな対策を講じているが、世界的な流...

...

AIと天気予報が出会うとどんな火花が散るのでしょうか?

SF作家の劉慈欣はかつて、自身の小説の中でこのような天気予報を描写した。小説の主人公は気象大学を卒...

一枚の紙で AI を騙せる。これが OpenAI の最も先進的な視覚モデルでしょうか?

この記事はLeiphone.comから転載したものです。転載する場合は、Leiphone.com公式...

信頼性の高い人工知能システムのルールをどのように定義し構築するのでしょうか?

この記事は公開アカウント「Reading Core Technique」(ID: AI_Discov...

AIロボットが2025年までにクラウドデータセンターの半分を占める可能性

[[437396]]コネチカット州スタンフォード — 新しいレポートによると、人工知能 (AI) を...

大根畑の問題を解決する C# アルゴリズム

ニンジン畑問題を解決するための C# アルゴリズムは何ですか?まずトピックを見てみましょう:仕事へ向...

すべてを圧縮するだけです! OpenAIの主任科学者イリヤ・スツケバーが教師なし学習に注目

最近、OpenAI の主任科学者 Ilya Sutskever 氏が、計算理論の研究に重点を置く S...

...