クイックソートが実際に他のソートアルゴリズムよりも優れているのはなぜですか?


308

標準アルゴリズムコースでは、クイックソートは平均でであり、最悪の場合はであると教えられてい。同時に、最悪の場合(mergesortheapsortのようなであり、最高の場合(bubblesortのような)線形時間であるが、メモリの追加の必要性がある他のソートアルゴリズムが研究されています。O n 2O n log n O(nlogn)O(n2)O(nlogn)

いくつかの実行時間をひと目見た後、クイックソート他の効率ほど効率的であってはならないと言うのが自然です。

また、学生は基本的なプログラミングコースで、再帰があまりにも多くのメモリを使用するなどの理由であまり良くないことを学ぶと考えてください。したがって(これは本当の議論ではありませんが)、これはクイックソートがそうではないという考えを与えますそれは再帰アルゴリズムであるため、本当に良いです。

それでは、実際には、クイックソートが実際に他のソートアルゴリズムよりも優れているのはなぜですか?実世界のデータの構造に関係していますか?コンピューターのメモリの動作方法に関係していますか?一部のメモリは他のメモリよりもはるかに高速であることは知っていますが、それがこの直感に反するパフォーマンスの本当の理由であるかどうかはわかりません(理論的な推定と比較した場合)。


1更新:正規答えはに関与定数と言っている平均的な場合の、他のに関与する定数よりも小さいアルゴリズム。ただし、直感的なアイデアだけではなく、正確な計算を使用して、これを適切に正当化する方法はまだありません。O n log n O(nlogn)O(nlogn)

いずれにせよ、いくつかの答えが示唆するように、メモリレベルでは実装がコンピューターの内部構造を利用し、たとえばキャッシュメモリがRAMよりも速いことを使用して、本当の違いが生じるようです。議論はすでに興味深いものですが答えはそれに関係しているように思われるので、メモリ管理に関してさらに詳細を見たいと思います。


更新2:ソートアルゴリズムの比較を提供するいくつかのWebページがあり、一部は他よりも洗練されています(最も顕著なのはsort-algorithms.com)。素晴らしい視覚補助を提示する以外に、このアプローチは私の質問に答えません。


2
最悪の場合、マージソートはであり、整数のサイズに既知の限界がある整数配列のソートは、カウントソートを使用して時間で実行できます。O n O(nlogn)O(n)
カールマンマート

13
sort-algorithms.comには、ソートアルゴリズムのかなり徹底した比較があります。
ジョー

2
広告の更新1:厳密な分析または現実的な仮定のいずれかを使用できると思います。私は両方を見たことがありません。たとえば、ほとんどの正式な分析では比較のみがカウントされます。
ラファエル

9
この質問は、最近のprogrammers.SEに関するコンテストで優勝しました!
ラファエル

3
興味深い質問。ランダムデータとクイックソートとマージソートの単純な実装を使用して、しばらく前にいくつかのテストを実行しました。両方のアルゴリズムは、小さなデータセット(最大100000アイテム)で非常によく機能しましたが、その後、マージソートの方がはるかに優れていることがわかりました。これは、クイックソートが非常に優れているという一般的な仮定と矛盾しているようで、まだ説明がありません。私が思いついた唯一のアイデアは、通常、クイックソートという用語はイントロソートのようなより複雑なアルゴリズムに使用され、ランダムピボットを使用したクイックソートの素朴な実装はそれほど良くないということです。
ジョルジオ

回答:


215

簡潔な答え

キャッシュ効率の引数についてはすでに詳細に説明しています。さらに、Quicksortが高速である理由、本質的な議論があります。2つの「交差ポインタ」のように実装された場合、たとえばここで、内側のループは非常に小さな本体を持ちます。これが最も頻繁に実行されるコードであるため、これは報われます。

ロングアンサー

まず第一に、

平均的なケースは存在しません!

最良の場合と最悪の場合は、実際にはめったに発生しない極端な場合が多いため、平均的なケース分析が行われます。ただし、平均的なケース分析では、入力の分布を想定しています!ソートの場合、一般的な選択肢はランダム置換モデルです(Wikipediaで暗黙的に想定されています)。

なぜ記法なのか?O

アルゴリズムの分析における定数の破棄は、1つの主な理由で行われます:正確な実行時間に興味がある場合、関連するすべての基本操作の相対的な)コストが必要です(キャッシュの問題を無視し、最新のプロセッサでパイプライン処理を行うことでも...)。数学的分析では、各命令の実行頻度をカウントできますが、単一命令の実行時間は、32ビット整数の乗算に加算と同じくらいの時間がかかるかどうかなど、プロセッサの詳細に依存します。

次の2つの方法があります。

  1. 一部のマシンモデルを修正します。

    これは、著者によって発明された人工の「典型的な」コンピューター向けのドンクヌースの著書シリーズ「コンピュータープログラミングのアート」で行われています。ボリューム3 では、多くのソートアルゴリズムの正確な平均ケース結果を見つけます。たとえば、

    • クイックソート:11.667(n+1)ln(n)1.74n18.74
    • マージソート:12.5nln(n)
    • ヒープソート: 16nln(n)+0.01n
    • Insertionsort: [ ソース ]2.25n2+7.75n3ln(n) いくつかのソートアルゴリズムのランタイム

    これらの結果は、クイックソートが最速であることを示しています。しかし、それはKnuthの人工機械でのみ証明されており、x86 PCに関して必ずしも何も意味するものではありません。また、小さな入力ではアルゴリズムの関係が異なることに注意してください:
    小さな入力用のいくつかのソートアルゴリズムのランタイム
    [ ソース ]

  2. 抽象的な基本操作を分析ます。

    比較ベースのソートの場合、これは通常スワップキー比較です。ロバート・セッジウィックの本、例えば「アルゴリズム」では、このアプローチが追求されています。そこにある

    • クイックソート:平均で比較とスワップ12nln(n)13nln(n)
    • Mergesort:比較、ただし最大配列アクセス(mergesortはスワップベースではないため、カウントできません)。8.66 n ln n 1.44nln(n)8.66nln(n)
    • Insertionsort:平均で比較とスワップ。114n214n2

    ご覧のとおり、これでは正確なランタイム分析としてのアルゴリズムの比較は容易にできませんが、結果はマシンの詳細には依存しません。

その他の入力分布

上記のように、平均的なケースは常に何らかの入力分布に関するものであるため、ランダムな順列以外のケースを考慮する場合があります。例えば、同等の要素持つQuicksortの研究が行われ、Javaの標準的なソート機能に関する素晴らしい記事があります。


8
タイプ2の結果は、マシン依存の定数を挿入することにより、タイプ1の結果に変換できます。したがって、2は優れたアプローチであると主張します。
ラファエル

2
@ラファエル+1。マシン依存も実装依存であると仮定していると思いますか?つまり、高速マシン+実装の低さは、おそらくあまり効率的ではありません。
ジャノマ

2
@Janoma私は、分析されたアルゴリズムは非常に詳細な形式(分析が詳細になっているため)で提供され、実装は可能な限り文字通りであると想定しました。しかし、はい、実装も同様に考慮します。
ラファエル

3
実際、タイプ2の分析は実際には劣っています。実世界のマシンは非常に複雑であるため、タイプ2の結果をタイプ1に適切に変換することはできません。
ジュール

4
@Jules:「実験実行時間のプロット」はタイプ1ではありません。それは一種の正式な分析ではなく、他のマシンに転送できません。結局、正式な分析を行うのはそのためです。
ラファエル

78

この質問に関しては、複数のポイントを作成できます。

通常、クイックソートは高速です

O(n2)

n1O(nlogn)

クイックソートは通常、ほとんどのソートよりも高速です

O(nlogn)O(n2)n

O(nlogn)O(nBlog(nB))B

このキャッシュ効率の理由は、入力を線形にスキャンし、入力を線形に分割するためです。これは、キャッシュを他のキャッシュと交換する前に、キャッシュにロードするすべての数値を読み取るときに、すべてのキャッシュを最大限に活用できることを意味します。特に、アルゴリズムはキャッシュを無視するため、すべてのキャッシュレベルで優れたキャッシュパフォーマンスが得られます。

O(nBlogMB(nB))Mk

通常、クイックソートはマージソートよりも高速です

この比較は、完全に一定の要因に関するものです(典型的なケースを検討する場合)。特に、選択は、Quicksortのピボットの準最適な選択とMergesortの入力全体のコピー(またはこのコピーを回避するために必要なアルゴリズムの複雑さ)の間です。前者の方が効率的であることがわかりました。この背後に理論はありません。たまたま高速になっています。

nO(logn)O(n)

最後に、Quicksortは正しい順序の入力にわずかに敏感であることに注意してください。この場合、一部のスワップをスキップできます。Mergesortにはそのような最適化機能がないため、Mergesortに比べてQuicksortが少し速くなります。

ニーズに合った並べ替えを使用する

結論:常に最適なソートアルゴリズムはありません。ニーズに合ったものを選択してください。ほとんどの場合に最も速いアルゴリズムが必要で、まれに少し遅くなることを気にせず、安定した並べ替えが必要ない場合は、Quicksortを使用します。それ以外の場合は、ニーズに合ったアルゴリズムを使用してください。


3
最後の発言は特に価値があります。私の同僚は現在、さまざまな入力分布の下でQuicksortの実装を分析しています。それらのいくつかは、例えば、多くの重複のために故障します。
ラファエル

4
O(n2)

8
「[T]この背後にある理論はありません。たまたま高速です。」その声明は科学的な観点からは非常に不満足です。ニュートンが「蝶が飛ぶ、りんごが落ちる:りんごがたまたま落ちるという理論はありません」と言っていると想像してください。
デビッドリチャービー14

2
@Alex ten Brink、「特に、アルゴリズムはキャッシュを無視する」とはどういう意味ですか?
Hibou57 14

4
@David Richerby、「その文は科学的な観点から非常に不満足です」:彼は私たちがそれに満足しているふりをせずに事実を目撃しているだけかもしれません。一部のアルゴリズムファミリは、完全な形式化の欠如に苦しんでいます。ハッシュ関数は例です。
Hibou57 14

45

私の大学でのプログラミングチュートリアルの1つで、クイックソート、マージソート、挿入ソートとPythonの組み込みlist.sort(Timsortと呼ばれる)のパフォーマンスを比較するように生徒に求めました。組み込みのlist.sortは、クイックソートやマージソートのクラッシュを簡単に発生させるインスタンスであっても、他のソートアルゴリズムよりもはるかに優れたパフォーマンスを発揮したため、実験結果は深く驚きました。そのため、通常のクイックソートの実装が実際には最良であると結論付けるのは時期尚早です。しかし、クイックソートの実装がはるかに優れていると確信しています。

これは、David R. MacIverによる素敵なブログ記事で、Timsortを適応マージソートの形式として説明しています。


17
@Raphael簡潔に言うと、Timsortは、漸近線のマージソート、短い入力の挿入ソート、および時折既にソートされたバースト(実際に頻繁に発生する)を効率的に処理するためのヒューリスティックです。Dai:アルゴリズムに加えて、list.sort専門家によって最適化された組み込み関数であることの利点があります。より公平な比較では、すべての関数が同じレベルの労力で同じ言語で記述されます。
ジル

1
@Dai:少なくとも、どのような状況(それらの分布)でどのような状況(低RAM、1つの実装の並列化、...)で結果が得られたかを説明できます。
ラファエル

7
乱数のリストでテストし、部分的にソート、完全にソート、および逆にソートしました。それは入門的な1年目のコースでしたので、それは深い経験的研究ではありませんでした。しかし、Java SE 7およびAndroidプラットフォームで配列をソートするために現在公式に使用されているという事実は、何かを意味します。

3
これもここで議論されました:cstheory.stackexchange.com/a/927/74
ユッカスオメラ

34

QuickSortが他のソートアルゴリズムと比較して非常に高速である主な理由の1つは、それがキャッシュフレンドリーだからだと思います。QSは配列のセグメントを処理するとき、セグメントの最初と最後の要素にアクセスし、セグメントの中心に向かって移動します。

したがって、開始すると、配列の最初の要素にアクセスし、メモリ(「場所」)がキャッシュにロードされます。そして、2番目の要素にアクセスしようとすると、それは(ほとんどの場合)すでにキャッシュにあるので、非常に高速です。

heapsortのような他のアルゴリズムはこのようには機能せず、配列に頻繁にジャンプするため、速度が低下します。


5
これは議論の余地のある説明です。mergesortもキャッシュフレンドリーです。
ドミトロKorduban

2
この答えは基本的に正しいと思いますが、詳細はyoutube.com/watch?v=aMnn0Jq0J-E
rgrig

3
おそらく、クイックソートの平均ケース時間の複雑さの乗法的定数も優れています(言及したキャッシュファクターとは無関係です)。
カヴェー

1
クイックソートの他の優れたプロパティと比較して、あなたが言及したポイントはそれほど重要ではありません。
MMS

1
@Kaveh:「クイックソートの平均ケース時間の複雑さの乗法定数も優れています」これに関するデータはありますか?
ジョルジオ

29

他の人は、Quicksortの漸近平均実行時間が他のソートアルゴリズム(特定の設定)よりも(一定で)優れていると既に述べています。

O(nlogn)

Quicksortには多くのバリエーションがあることに注意してください(Sedgewickの論文などを参照)。異なる入力分布(均一、ほぼソート済み、ほぼ逆ソート済み、多数の複製など)で異なるパフォーマンスを発揮し、他のアルゴリズムの方が優れている場合があります。

k10


20

O(nlgn)

ps:正確には、他のアルゴリズムよりも優れていることはタスクに依存します。一部のタスクでは、他のソートアルゴリズムを使用した方がよい場合があります。

こちらもご覧ください:


3
@Janomaこれは、使用する言語とコンパイラの問題です。ほとんどすべての関数型言語(ML、Lisp、Haskell)は、スタックの成長を防ぐ最適化を行うことができ、命令型言語のよりスマートなコンパイラーも同じことを行うことができます(GCC、G ++、およびMSVCはすべてこれを行うと信じています)。注目すべき例外はJavaであり、この最適化は行われないため、Javaで再帰を反復として書き換えることは理にかなっています。
レイフケトラー

4
@JD、それは自分自身を2回呼び出すため、テールコール最適化をクイックソートで使用できません(少なくとも完全ではありません)。2番目の呼び出しは最適化できますが、最初の呼び出しは最適化できません。
-svick

1
@Janoma、あなたは本当に再帰的な実装を必要としません。たとえば、Cでのqsort関数の実装を見ると、再帰呼び出しは使用されないため、実装ははるかに高速になります。
カベ

1
ヒープソートも適切に配置されていますが、なぜQSが頻繁に高速になるのですか?
ケビン

6
23240

16

Θ(n2)Θ(nlogn)

2番目の理由は、in-placeソートを実行し、仮想メモリ環境で非常にうまく機能することです。

更新::(ジャノマとスヴィックのコメントの後)

これをわかりやすく説明するために、Merge Sortを使用した例を示します(Merge sortはクイックソートの次に広く採用されているソートアルゴリズムであるため、私はそう思います)。クイックソートの方が優れています):

次のシーケンスを検討してください。

12,30,21,8,6,9,1,7. The merge sort algorithm works as follows:

(a) 12,30,21,8    6,9,1,7  //divide stage
(b) 12,30   21,8   6,9   1,7   //divide stage
(c) 12   30   21   8   6   9   1   7   //Final divide stage
(d) 12,30   8,21   6,9   1,7   //Merge Stage
(e) 8,12,21,30   .....     // Analyze this stage

最後の段階がどのように行われているのかをよく見ると、最初の12は8と比較され、8は小さいので最初に進みます。現在、12は21と比較してAGAINであり、12は次へと続きます。最終的なマージ、つまり4つの要素と4つの他の要素をマージすると、クイックソートでは発生しない定数として多くのEXTRA比較が発生します。これが、クイックソートが優先される理由です。


1
しかし、定数が非常に小さいのはなぜですか?
svick

1
@svickソートされているため、in-placeつまり、余分なメモリは必要ありません。
0x0の

Θ(nlgn)

15

実世界のデータを扱う私の経験では、クイックソートは適切な選択ではありません。クイックソートはランダムデータでうまく機能しますが、実際のデータはほとんどの場合ランダムではありません。

2008年に、私はクイックソートの使用に至るまで、ソフトウェアのハングアップのバグを追跡しました。しばらくしてから、挿入ソート、クイックソート、ヒープソート、マージソートの簡単な実装を作成し、これらをテストしました。私のマージソートは、大規模なデータセットで作業している間、他のすべてよりも優れていました。

それ以来、マージソートは私の選択したソートアルゴリズムです。エレガントです。実装は簡単です。安定したソートです。クイックソートのように二次的な動作に退化することはありません。挿入ソートに切り替えて、小さな配列をソートします。

多くの場合、特定の実装はクイックソートでは驚くほどうまく機能すると考えているが、実際にはクイックソートではないことがわかりました。実装によってクイックソートと別のアルゴリズムが切り替えられる場合と、クイックソートがまったく使用されない場合があります。例として、GLibcのqsort()関数は実際にマージソートを使用します。作業スペースの割り当てに失敗した場合にのみ、コードコメントが「遅いアルゴリズム」と呼ぶインプレースクイックソートにフォールバックします。

編集:Java、Python、Perlなどのプログラミング言語でも、マージソート、またはより正確には派生物(Timsortや大きなセットのマージソート、小さなセットの挿入ソートなど)を使用します。(Javaは、通常のクイックソートよりも高速なデュアルピボットクイックソートも使用します。)


既にソートされたデータのバッチに挿入するために、常に追加/再ソートを行っていたため、これに似たものを見ました。ランダム化されたクイックソートを使用して平均的にこれを回避できます(そして、まれでランダムなひどく遅いソートに驚かされます)。ソートの安定性も必要になる場合があります。Javaは、マージソートの使用からクイックソートバリアントに移行しました。
ロブ

@Robこれは正確ではありません。Javaは、今日でもmergesortのバリアント(Timsort)を使用しています。クイックソートのバリアントも使用します(デュアルピボットクイックソート)。
エルワンルグラン

14

1-クイックソートはインプレースです(一定量以外の追加のメモリは必要ありません)。

2-クイックソートは、他の効率的なソートアルゴリズムよりも簡単に実装できます。

3-クイックソートの実行時間は、他の効率的なソートアルゴリズムよりも小さい一定の要因があります。

更新:マージソートの場合、「マージ」を行う必要があります。これには、マージする前にデータを保存するための追加の配列が必要です。しかし、簡単に言えばそうではありません。そのため、クイックソートが適切に行われます。また、マージのために行われたいくつかの特別な比較があり、マージソートの定数係数が増加します。


3
あなたはしている見て、反復クイックソートの実装をインプレース高度な?それらは多くのものですが、「簡単」ではありません。
ラファエル

2
私の意見では、2番は私の質問にはまったく答え、1番と3番は適切な正当化が必要です。
ジャノマ

@Raphael:彼らは簡単です。ポインタの代わりに配列を使用して、インプレースでクイックソートを実装する方がはるかに簡単です。そして、インプレースにするために反復する必要はありません。
MMS

マージ用の配列はそれほど悪くはありません。1つのアイテムをソースパイルからデスティネーションパイルに移動すると、そこにある必要はなくなります。動的配列を使用している場合、マージ時に一定のメモリオーバーヘッドがあります。
オスカースコグ

@ 1 Mergesortも同様に配置できます。@ 2効率的な定義は何ですか?私の意見では、マージソートは非常にシンプルでありながら効率的であるため、マージソートが好きです。@ 3大量のデータを並べ替えるときは関係なく、アルゴリズムを効率的に実装する必要があります。
オスカースコグ

11

特定のソートアルゴリズムは、どのような条件下で実際に最速のものですか?

Θ(log(n)2)Θ(nlog(n)2)

Θ(nk)Θ(nm)k=2#number_of_Possible_valuesm=#maximum_length_of_keys

3)基礎となるデータ構造はリンクされた要素で構成されていますか?はい->常にインプレースマージソートを使用します。固定サイズを実装するのが簡単であるか、またはリンクされたデータ構造のさまざまなアリティのマージソートをインプレースボトムアップインプレースで実装します。また、各ステップでデータ全体をコピーする必要がなく、再帰も必要ないため、他の一般的な比較ベースのソートよりも高速で、クイックソートよりも高速です。

Θ(n)

5)基礎となるデータのサイズを中小規模にバインドできますか?例:n <10,000 ... 100,000,000(基になるアーキテクチャとデータ構造に依存)はい-> bitonic sortまたはBatcher odd-even mergesortを使用します。後藤1)

Θ(n)Θ(n2)Θ(nlog(n)2)最悪の場合の実行時間はわかっているか、コームソートを試してください。シェルソートまたはコームソートのどちらが実際に適切に機能するかはわかりません。

Θ(log(n))Θ(n)Θ(n)Θ(log(n))Θ(n2)Θ(n)Θ(n)Θ(log(n))Θ(nlog(n))

Θ(nlog(n))

クイックソートの実装のヒント:

Θ(n)Θ(log(n))Θ(nlogk(k1))

2)クイックソートにはボトムアップの反復バリアントがありますが、知る限りでは、トップダウンと同じ漸近的な空間と時間の境界があり、実装が難しいという追加の側面があります(明示的にキューを管理するなど)。私の経験では、実用的な目的のために、それらを検討する価値はありません。

mergesortの実装のヒント:

1)bottum-up mergesortは、再帰呼び出しを必要としないため、常にトップダウンmergesortよりも高速です。

2)ダブルバッファを使用して非常に単純なマージソートを高速化し、各ステップの後にテンポラル配列からデータをコピーする代わりにバッファを切り替えることができます。

3)多くの実世界のデータでは、適応マージソートは固定サイズのマージソートよりもはるかに高速です。

Θ(k)Θ(log(k))Θ(1)Θ(n)

私が書いたことから、次の条件がすべて当てはまる場合を除いて、クイックソートはしばしば最速のアルゴリズムではないことは明らかです。

1)「少数」以上の可能な値がある

2)基礎となるデータ構造がリンクされていない

3)安定した注文は必要ありません

4)データが十分に大きいため、バイトニックソーターまたはバッチャーの奇数/偶数マージソートのわずかに準最適な漸近ランタイムが作動する

5)データはほとんどソートされておらず、ソート済みの大きなパーツで構成されていない

6)複数の場所から同時にデータシーケンスにアクセスできます

Θ(log(n))Θ(n)

ps:テキストの書式設定を手伝ってくれる人がいます。


(5):Appleのソート実装は、最初に配列の最初と最後の両方で1つの実行を昇順または降順でチェックします。そのような要素があまりない場合、これは非常に迅速であり、n / ln nを超える要素がある場合、これらの要素を非常に効果的に処理できます。2つの並べ替えられた配列を連結して結果を並べ替えると、マージが得られます
-gnasher729

8

ソート方法のほとんどは、短い手順でデータを移動する必要があります(たとえば、マージソートはローカルで変更を行い、この小さなデータをマージしてから、大きなデータをマージします。..)。その結果、データが宛先から遠く離れている場合、多くのデータの移動が必要になります。

ab


5
クイックソートとマージソートについてのあなたの議論には水がかかりません。クイックソートは、大きな動きから始まり、次第に小さくなります(各ステップで約半分の大きさ)。マージソートは、小さな動きから始まり、次第に大きく動きます(各ステップで約2倍)。これは、一方が他方よりも効率的であることを示すものではありません。
ジル
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.