10億の数値の配列から最大100の数値を見つけるプログラムを作成する


300

私は最近、「10億の数の配列から100の最大数を見つけるプログラムを作成する」ように求められたインタビューに最近参加しました。

私は、配列をO(nlogn)時間の複雑さでソートし、最後の100個の数値を取るという力ずくの解決策を与えることができました。

Arrays.sort(array);

インタビュアーは、より良い時間の複雑さを探していました。他の解決策をいくつか試しましたが、彼に答えることができませんでした。時間の複雑さの解決策はありますか?


70
たぶん問題は、それが並べ替えの質問ではなく、求めるものだったということです。
geomagas 2013年

11
技術的なメモとして、並べ替えは問題を解決する最良の方法ではないかもしれませんが、私はそれが力ずくであるとは思いません-それを行うにはもっと悪い方法を考えることができます。
Bernhard Barker

88
さらに愚かな総当たりの方法を考えただけです... 10億要素の配列から100要素のすべての可能な組み合わせを見つけ、これらの組み合わせのどれが最大の合計を持っているかを確認してください。
Shashank 2013年

10
次元の増加がないため、すべての確定的(かつ正しい)アルゴリズムがO(1)この場合にあることに注意してください。インタビュアーは、「n >> mのnの配列からm個の最大の要素を見つける方法」を尋ねているはずです。
Bakuriu 2013年

回答:


328

キュー内の最小数(キューの先頭)より大きい数に遭遇した場合は常に、100の最大数の優先キューを保持し、10億の数まで反復し、キューの先頭を削除して新しい数を追加できます。キューに。

編集: Devが指摘したように、ヒープで実装された優先キューでは、キューへの挿入の複雑さはO(logN)

最悪の場合、どちらよりも優れていますbillionlog2(100)billionlog2(billion)

一般に、N個の数値のセットから最大のK個の数値が必要な場合、複雑さはO(NlogK)ではなくO(NlogN)、KがNに比べて非常に小さい場合に非常に重要になります。

EDIT2:

このアルゴリズムの予想時間は非常に興味深いものです。反復ごとに挿入が行われる場合と行われない場合があるためです。キューに挿入されるi番目の数の確率は、確率変数が少なくともi-K同じ分布からの確率変数よりも大きい確率です(最初のk個の数が自動的にキューに追加されます)。この確率を計算するには、注文統計(リンクを参照)を使用できます。たとえば、数値がから一様にランダムに選択され{0, 1}、(iK)番目の数値(i数値のうち)の期待値が(i-k)/iであり、確率変数がこの値より大きい可能性がであると仮定します1-[(i-k)/i] = k/i

したがって、予想される挿入数は次のとおりです。

ここに画像の説明を入力してください

また、予想実行時間は次のように表すことができます。

ここに画像の説明を入力してください

(上記のように、k最初のk要素、次にn-k比較、および予想される挿入数を含むキューを生成する時間。それぞれに平均log(k)/2時間がかかります)

Nと比較して非常に大きい場合K、この式はにn比べてかなり近いことに注意してくださいNlogK。これは、質問の場合と同様に、直感的です。10000回の反復後(10億に比べて非常に小さい)でも、数値がキューに挿入される可能性は非常に小さくなります。


6
実際には、挿入ごとにO(100)のみです。
MrSmith42 2013年

8
@RonTellerリンクリストを効率的にバイナリ検索することはできません。そのため、通常、優先キューはヒープで実装されます。説明されている挿入時間はO(logn)ではなくO(n)です。Skizzが2番目に推測するまで、最初は正しく(順序付きキューまたは優先キュー)ありました。
Dev

17
@ThomasJungblut億それは場合だそうであれば、それはO(1)だも一定である:P
ロン・テラー

9
@RonTeller:通常、この種の質問は、何十億ものGoogle検索結果からトップ10ページ、ワードクラウドで最も頻繁に使用される50ワード、またはMTVで最も人気のある10曲などを見つけることと考えられます。したがって、通常の状況ではと比較してk 一定小さいと考えても安全nです。ただし、この「通常の状況」を常に心に留めておく必要があります。
ffriend 2013年

5
1Gアイテムがあるので、1000要素をランダムにサンプリングし、最大の100を選びます。これにより、縮退したケース(ソート、逆ソート、ほとんどソート)を回避し、挿入数を大幅に削減できます。
ChuckCottrill 2013年

136

これがインタビューで尋ねられた場合、インタビュアーはおそらくアルゴリズムの知識だけでなく、問題解決プロセスを見たいと思うでしょう。

説明は非常に一般的であるため、問題を明確にするために、これらの数値の範囲または意味を彼に尋ねることができます。これを行うと、面接担当者が感動する場合があります。たとえば、これらの数値が国内(中国など)の人々の年齢を表す場合は、はるかに簡単な問題です。生きている人が200歳を超えていないという妥当な仮定のもとで、サイズ200(多分201)のint配列を使用して、1回の反復で同じ年齢の人の数をカウントできます。ここで、インデックスは年齢を意味します。この後、100個の最大数を見つけるのは簡単です。ちなみに、このアルゴはカウンティングソートと呼ばれています

とにかく、質問をより具体的かつ明確にすることは、インタビューであなたにとって良いことです。


26
非常に良い点。他の誰もこれらの数の分布について何も質問したり示したりしていません-それは問題への取り組み方にすべての違いを生む可能性があります。
NealB 2013年

13
この答えを拡張するのに十分だと思います。数値を1回読んで最小値/最大値を取得し、分布を推測できるようにします。次に、2つのオプションのいずれかを実行します。範囲が十分に小さい場合は、発生した数値を簡単にチェックできる配列を構築します。範囲が広すぎる場合は、上記で説明したソート済みヒープアルゴリズムを使用してください。
Richard_G 2013年

2
私は同意します。インタビュアーに質問を返すことは確かに多くの違いを生みます。実際、計算能力によって制限されているかどうかなどの質問も、複数の計算ノードを使用してソリューションを並列化するのに役立ちます。
Sumit Nigam 2013年

1
@R_Gリスト全体を調べる必要はありません。有用な統計を取得するために、リストのランダムなメンバーのごく一部(たとえば、100万)をサンプリングするのに十分です。
Itamar 2013年

その解決策について考えていなかった人のために、カウントソートen.wikipedia.org/wiki/Counting_sortについて読むことをお勧めします。それは実際にはかなり一般的なインタビューの質問です。配列をO(nlogn)よりも上でソートできますか。この質問は単なる延長です。
MaximeChéramy2013年

69

O(n)をとる数値を反復できます

現在の最小値より大きい値を見つけたら、サイズ100の循環キューに新しい値を追加します。

その循環キューの最小値は、新しい比較値です。そのキューに追加し続けます。いっぱいの場合は、キューから最小値を抽出します。


3
これは機能しません。たとえば、{1、100、2、99}の2トップ2と{100,1}を与えるトップ見つける
Skizz

7
並べ替えられたキューを保持するために移動することはできません。(次の最小要素を毎回ホールキューで検索したくない場合)
MrSmith42 2013年

3
@ MrSmith42ヒープのように、部分的なソートで十分です。Ron Tellerの回答を参照してください。
2013年

1
はい、extract-min-queueはヒープとして実装されていると暗黙のうちに想定していました。
Regenschein 2013年

循環キューでは、サイズ100の最小ヒープを使用しますが、これには最上位の数が100以上になります。これは、キューの場合のo(n)と比較して、挿入にO(log n)のみを使用します
techExplorer

33

これは「アルゴリズム」でタグ付けされていることに気づきましたが、おそらく「インタビュー」もタグ付けされているはずなので、他のいくつかのオプションは破棄されます。

10億の数値の原因は何ですか?それがデータベースの場合、「値からのテーブルの順序から値を選択desc limit 100」は非常にうまく機能します-方言の違いがあるかもしれません。

これは1回限りのものですか、それとも繰り返されるものですか。繰り返される場合、どのくらいの頻度ですか?1回限りのもので、データがファイル内にある場合は、 'cat srcfile | ソート(必要に応じてオプション)| head -100 'は、コンピューターがこの些細な雑用を処理している間に、給与を得ている生産的な作業をすばやく実行します。

それが繰り返される場合は、適切なアプローチを選択して最初の回答を取得し、結果を保存/キャッシュして、上位100を継続的に報告できるようにすることをお勧めします。

最後に、この考慮事項があります。あなたはエントリーレベルの仕事を探して、マニアックなマネージャーや将来の同僚にインタビューしていますか?もしそうなら、あなたは相対的な技術的な長所と短所を説明するあらゆる方法のアプローチを放棄することができます。さらに管理職を探している場合は、マネージャーのようにアプローチし、ソリューションの開発と保守のコストを考慮して、「ありがとうございました」と言って、面接担当者がCSの雑学に集中したい場合はそのままにします。 。彼とあなたはそこに多くの進歩の可能性を持っている可能性は低いでしょう。

次の面接で頑張ってください。


2
卓越した答え。他の誰もが質問の技術的な側面に集中しましたが、この回答はビジネスの社会的な部分に取り組みます。
vbocan 2013年

2
感謝の意を表してインタビューを終了し、インタビューが終わるのを待たないとは想像もしていませんでした。心を開いてくれてありがとう。
UrsulRosu 2013年

1
10億の要素のヒープを作成し、100の最大の要素を抽出できないのはなぜですか。この方法のコスト= O(10億)+ 100 * O(log(10億))??
Mohit Shah 2016

17

これに対する私の直接の反応はヒープを使用することですが、一度にすべての入力値を保持せずにQuickSelectを使用する方法があります。

サイズ200の配列を作成し、最初の200個の入力値でそれを埋めます。QuickSelectを実行して低い100を破棄し、100の空き場所を残します。次の100個の入力値を読み取り、QuickSelectを再度実行します。100のバッチで入力全体を実行するまで続けます。

最後に、上位100個の値があります。N値の場合、QuickSelectをおよそN / 100回実行しました。各Quickselectのコストは定数の約200倍なので、合計コストは定数の2N倍になります。これは、この説明でハードワイヤリングしているパラメーターのサイズが100であっても、私への入力のサイズは直線的に見えます。


10
小さいが重要な最適化を追加できます。QuickSelectを実行してサイズ200の配列をパーティション分割すると、上位100要素の最小値がわかります。次に、データセット全体を反復処理するとき、現在の値が現在の最小値より大きい場合にのみ、下位100の値を入力します。C ++でのこのアルゴリズムの簡単な実装はpartial_sort、2億個の32ビットint(MT19937を介して作成され、均一に分散された)のデータセットでlibstdc ++ を直接実行するのと同等です。
dyp 2013年

1
良い考え-最悪のケースの分析には影響しませんが、実行する価値は十分にあります。
mcdowella 2013年

@mcdowella試してみる価値はありますが、私はやります、ありがとう!
userx 2013年

8
これがまさにグアバの仕事 Ordering.greatestOf(Iterable, int)です。それは完全に線形時間でシングルパスであり、とてもかわいいアルゴリズムです。FWIW、私たちは実際のベンチマークもいくつか持っています:その一定の要因は平均的なケースでは従来の優先キューよりも遅いですが、この実装は「最悪の場合」の入力(たとえば、厳密に昇順の入力)に対してはるかに耐性があります。
Louis Wasserman 2013年

15

クイック選択アルゴリズムを使用して、(順序で)インデックス[billion-101]で数値を検索し、その数値を反復処理して、その数値よりも大きい数値を検索できます。

array={...the billion numbers...} 
result[100];

pivot=QuickSelect(array,billion-101);//O(N)

for(i=0;i<billion;i++)//O(N)
   if(array[i]>=pivot)
      result.add(array[i]);

このアルゴリズムの時間は次のとおりです。2 XO(N)= O(N)(平均ケースパフォーマンス)

Thomas Jungblutのような2番目のオプションは次のとおりです。

ヒープを使用してMAXヒープを作成すると、O(N)がかかります。その後、上位100の最大数がヒープの上部に配置されます。必要なのは、ヒープ(100 XO(Log(N))から取得することだけです。

このアルゴリズムの時間は:O(N)+ 100 XO(Log(N))= O(N)


8
リスト全体を3回処理しています。1バイオ。整数は約4GBですが、メモリに収まらない場合はどうしますか?この場合、quickselectは最悪の選択肢です。1回の繰り返しで上位100項目のヒープを保持することは、O(n)で最高のパフォーマンスを発揮するソリューションです(ヒープのnは100 =定数=非常に小さいため、ヒープ挿入のO(log n)を切り捨てることができます) )。
Thomas Jungblut 2013年

3
それでもO(N)、2つのQuickSelectsと別の線形スキャンを実行すると、必要以上にオーバーヘッドが大きくなります。
ケビン

これはPSEUDOコードであり、ここでのすべてのソリューションには時間がかかります(O(NLOG(N)または100 * O(N))
One Man Crew

1
100*O(N)(それが有効な構文である場合)= O(100*N)= O(N)(確かに100は可変である可能性があります。そうである場合、これは厳密には当てはまりません)。ああ、そしてQuickselectはO(N ^ 2)痛い)の最悪の場合のパフォーマンスを持っています。また、メモリに収まらない場合は、ディスクからデータを2回リロードします。これは、1回よりもはるかに悪いです(これがボトルネックです)。
Bernhard Barker

これは予想される実行時間であり、最悪のケースではないという問題がありますが、適切なピボット選択戦略を使用することで(たとえば、21の要素をランダムに選択し、それらの21の中央値をピボットとして選択する)、比較の数を任意の小さい定数cに対して最大(2 + c)nになる確率が高いことが保証されています。
One Man Crew、

10

他のクイックセレクトソリューションは反対投票されましたが、クイックセレクトはサイズ100のキューを使用するよりも速くソリューションを見つけるという事実が残っています。クイックセレクトの実行時間は、比較の観点から2n + o(n)です。非常に単純な実装は

array = input array of length n
r = Quickselect(array,n-100)
result = array of length 100
for(i = 1 to n)
  if(array[i]>r)
     add array[i] to result

これは、平均で3n + o(n)の比較になります。さらに、quickselectが配列の最大100項目を右端の100の場所に残すという事実を使用して、より効率的にすることができます。したがって、実際には、実行時間は2n + o(n)に改善できます。

これは予想される実行時間であり、最悪のケースではないという問題がありますが、適切なピボット選択戦略を使用することで(たとえば、21の要素をランダムに選択し、それらの21の中央値をピボットとして選択する)、比較の数を任意の小さい定数cに対して最大(2 + c)nであることが高い確率で保証されます。

実際、最適化されたサンプリング戦略を使用することにより(例:sqrt(n)要素をランダムにサンプリングし、99パーセンタイルを選択)、任意に小さいcの実行時間を(1 + c)n + o(n)に減らすことができます。 (Kと仮定すると、選択される要素の数はo(n)です)。

一方、サイズ100のキューを使用するには、O(log(100)n)の比較が必要であり、100のログベース2は約6.6です。

サイズNの配列から最大のK要素を選択するというより抽象的な意味でこの問題を考えると、K = o(N)ですが、KとNの両方が無限大になるため、クイック選択バージョンの実行時間は次のようになります。 O(N)とキューのバージョンはO(N log K)になるため、この意味では、クイック選択も漸近的に優れています。

コメントでは、キューソリューションはランダムな入力で予想時間N + K log Nで実行されると述べられました。もちろん、ランダム入力の仮定は、質問で明示的に指定されていない限り有効ではありません。キューソリューションは、ランダムな順序で配列をトラバースするように作成できますが、これにより、乱数ジェネレーターへのN呼び出しの追加コストが発生するだけでなく、入力配列全体を並べ替えるか、または長さNの新しい配列を割り当てて、ランダムなインデックス。

問題が原因で元の配列内の要素を移動できず、メモリを割り当てるコストが高いため、配列の複製がオプションではない場合、それは別の問題です。しかし、厳密には実行時間に関しては、これが最良のソリューションです。


4
最後の段落が重要なポイントです。10億の数値では、すべてのデータをメモリに保持したり、要素を交換したりすることはできません。(少なくとも、インタビューの質問だったとしても、それが問題を解釈する方法です。)
Ted Hopp 2013年

14
アルゴリズムの質問では、データの読み取りが問題である場合は、それを質問に含める必要があります。質問には、「メモリに適合せず、アルゴリズムの分析の標準であるフォンノイマンモデルに従って操作できないディスク上のアレイを与える」ではなく、「アレイを与える」と述べています。最近では、8ギガのRAMを搭載したラップトップを入手できます。10億の数値をメモリに保持するという考えが実現不可能であるという考えがどこから生まれたかはわかりません。現在、ワークステーションのメモリには数十億の数値があります。
mrip 2013年

クイック選択のFYI最悪のランタイムはO(n ^ 2)(en.wikipedia.org/wiki/Quickselectを参照)であり、入力配列内の要素の順序も変更します。非常に大きな定数(en.wikipedia.org/wiki/Median_of_medians)を使用して、最悪の場合のO(n)ソリューションが存在する可能性があります。
2013年

クイック選択の最悪のケースは指数関数的に発生する可能性が低く、これは実際の目的ではこれは無関係です。クイックセレクトを変更するのは簡単で、高い確率で任意の小さいcに対して比較の数が(2 + c)n + o(n)になります。
mrip 2013年

「Quickselectがサイズ100のキューを使用するよりも速くソリューションを見つけるという事実は変わりません」—いいえ。ヒープソリューションは、約N + Klog(N)の比較に対して、クイックセレクトの平均は2Nで、中央値の中央値は2.95です。これは、与えられたK.のために明確に高速です
ニール・G

5

10億の最初の100の数を取り、それらを並べ替えます。10億を反復処理するだけで、ソース番号が最小の100より大きい場合は、ソート順に挿入します。最終的には、セットのサイズよりもO(n)にかなり近いものになります。


3
おっと、私よりも詳しい答えは見当たりませんでした。
Samuel Thurston

最初の500程度の数値を取得し、リストがいっぱいになったときにのみ、ソート(および下位400を破棄)します。(そして、リストに追加するのは、新しい数が選択した100の中で最も小さい場合のみであることは言うまでもありません。)
Hot Licks

4

2つのオプション:

(1)ヒープ(priorityQueue)

サイズ100の最小ヒープを維持します。アレイをトラバースします。要素がヒープ内の最初の要素よりも小さくなったら、それを交換します。

InSERT ELEMENT INTO HEAP: O(log100)
compare the first element: O(1)
There are n elements in the array, so the total would be O(nlog100), which is O(n)

(2)Map-reduceモデル。

これは、hadoopのワードカウントの例とよく似ています。マップジョブ:すべての要素の頻度または出現回数をカウントします。削減:上位K要素を取得します。

通常、私は採用担当者に2つの答えを与えます。彼らが好きなものを彼らに与えなさい。もちろん、すべての正確なパラメータを知る必要があるため、map reduceコーディングは面倒です。練習しても害はありません。幸運を。


MapReduceの+1、10億の数字でHadoopについて言及したのはあなただけではなかったと思います。インタビュアーが10億の数値を要求した場合はどうなりますか?私の意見では、より多くの賛成票を投じる価値があります。
Silviu Burcea 2013年

@Silviu Burceaありがとうございます。MapReduceも重要です。:)
Chris Su

この例では100のサイズは一定ですが、実際にはこれを個別の変数に一般化する必要があります。k。100は10億と一定なので、小さい数値のセットではなく、大きい数値のセットのサイズにnのサイズ変数を与えるのはなぜですか?本当にあなたの複雑さはO(n)ではないO(nlogk)であるべきです。
トム・ハード

1
しかし、私の質問は、あなたが質問に答えているだけの場合、10億も質問に固定されているので、なぜ10億をnに一般化し、100をkに一般化しないのかです。あなたの論理に従って、この質問では10億と100の両方が修正されるため、複雑さは実際にはO(1)になるはずです。
Tom Heard、2014年

1
@TomHeardわかりました。O(nlogk)結果に影響を与える要因は1つだけです。つまり、nが次第に大きくなると、「結果レベル」は直線的に増加します。または、1兆の数値を与えられたとしても、最大100の数値を取得することができます。ただし、言うことはできません。nを増やすと、kは増加するため、kは結果に影響します。そのため、私はO(nlogk)を使用し、O(nlogn)は使用しません
Chris Su

4

非常に簡単な解決策は、アレイを100回繰り返すことです。それはO(n)です。

最大数を引き出すたびに(そしてその値を最小値に変更して、次の反復で表示されないようにするか、以前の回答のインデックスを追跡します(インデックスを追跡することにより、元の配列が持つことができます)同じ数の倍数))。100回の反復の後、100の最大数になります。


1
2つの欠点-(1)プロセスで入力を破棄する-これは回避することが望ましい。(2)アレイを複数回実行している-アレイがディスクに格納されていてメモリに収まらない場合、受け入れられた回答よりもほぼ100倍遅くなる可能性があります。(はい、どちらもO(n)ですが、まだです)
Bernhard Barker

@Dukelingさん、よろしくお願いします。以前の回答インデックスを追跡して元の入力を変更しないようにする方法について、追加の表現を追加しました。それでもコーディングはかなり簡単です。
James Oravec 2013年

O(n log n)よりもはるかに遅いO(n)ソリューションの素晴らしい例。log2(10億)はわずか30です...
gnasher729

@ gnasher729 O(n log n)に隠されている定数の大きさは?
miracle173

1

@ron tellerの回答に触発されて、ここにあなたがやりたいことをするための最低限のCプログラムがあります。

#include <stdlib.h>
#include <stdio.h>

#define TOTAL_NUMBERS 1000000000
#define N_TOP_NUMBERS 100

int 
compare_function(const void *first, const void *second)
{
    int a = *((int *) first);
    int b = *((int *) second);
    if (a > b){
        return 1;
    }
    if (a < b){
        return -1;
    }
    return 0;
}

int 
main(int argc, char ** argv)
{
    if(argc != 2){
        printf("please supply a path to a binary file containing 1000000000"
               "integers of this machine's wordlength and endianness\n");
        exit(1);
    }
    FILE * f = fopen(argv[1], "r");
    if(!f){
        exit(1);
    }
    int top100[N_TOP_NUMBERS] = {0};
    int sorts = 0;
    for (int i = 0; i < TOTAL_NUMBERS; i++){
        int number;
        int ok;
        ok = fread(&number, sizeof(int), 1, f);
        if(!ok){
            printf("not enough numbers!\n");
            break;
        }
        if(number > top100[0]){
            sorts++;
            top100[0] = number;
            qsort(top100, N_TOP_NUMBERS, sizeof(int), compare_function);
        }

    }
    printf("%d sorts made\n"
    "the top 100 integers in %s are:\n",
    sorts, argv[1] );
    for (int i = 0; i < N_TOP_NUMBERS; i++){
        printf("%d\n", top100[i]);
    }
    fclose(f);
    exit(0);
}

私のマシン(高速SSDを搭載したコアi3)では25秒かかり、1724ソートされます。dd if=/dev/urandom/ count=1000000000 bs=1この実行用にバイナリファイルを生成しました。

明らかに、ディスクから一度に4バイトのみを読み取るとパフォーマンスの問題がありますが、これは例のためです。プラス面では、必要なメモリはごくわずかです。


1

最も単純な解決策は、数十億の大きな配列をスキャンし、これまでに見つかった100個の最大値を並べ替えなしで小さな配列バッファーに保持し、このバッファーの最小値を記憶することです。最初、この方法はfordprefectによって提案されたと思いましたが、コメントで、彼は100数値データ構造がヒープとして実装されていると想定していると述べました。より大きい新しい数値が見つかるたびに、バッファーの最小値が新しい値で上書きされ、バッファーで現在の最小値が再度検索されます。10億配列の数値がランダムに分布している場合は、ほとんどの場合、大きい配列の値が小さい配列の最小値と比較され、破棄されます。数値の非常に小さい部分についてのみ、値を小さな配列に挿入する必要があります。したがって、小さい数を保持するデータ構造を操作する違いは無視できます。少数の要素の場合、優先キューの使用が実際に私の素朴なアプローチを使用するよりも速いかどうかを判断することは困難です。

10 ^ 9要素の配列がスキャンされたときに、小さな100要素の配列バッファーの挿入数を推定したいと思います。プログラムはこの大きな配列の最初の1000要素をスキャンし、最大で1000要素をバッファーに挿入する必要があります。バッファには、スキャンされた1000エレメントのうち100エレメント、つまりスキャンされたエレメントの0.1が含まれます。したがって、大きな配列の値が現在のバッファの最小値よりも大きい確率は約0.1であると想定します。このような要素はバッファに挿入する必要があります。プログラムは、大きな配列から次の10 ^ 4要素をスキャンします。新しい要素が挿入されるたびに、バッファの最小値が増えるからです。現在の最小値より大きい要素の比率は約0.1であるため、挿入する要素は0.1 * 10 ^ 4 = 1000であると推定しました。実際には、バッファーに挿入される要素の予想数は少なくなります。この10 ^ 4要素のスキャン後、バッファ内の数の割合は、これまでにスキャンされた要素の約0.01になります。したがって、次の10 ^ 5の数値をスキャンする場合、0.01 * 10 ^ 5 = 1000以下がバッファーに挿入されると想定します。この議論を続けて、大きな配列の1000 + 10 ^ 4 + 10 ^ 5 + ... + 10 ^ 9〜10 ^ 9要素をスキャンした後、約7000の値を挿入しました。したがって、ランダムなサイズの10 ^ 9要素を持つ配列をスキャンする場合、バッファへの挿入は10 ^ 4(= 7000に切り上げ)以下であることが期待されます。バッファーに挿入するたびに、新しい最小値を見つける必要があります。バッファが単純な配列の場合、新しい最小値を見つけるには100の比較が必要です。バッファーが別のデータ構造(ヒープなど)である場合、最小値を見つけるには少なくとも1つの比較が必要です。大きな配列の要素を比較するには、10 ^ 9の比較が必要です。したがって、全体として、配列をバッファとして使用する場合は約10 ^ 9 + 100 * 10 ^ 4 = 1.001 * 10 ^ 9の比較、別のタイプのデータ構造(ヒープなど)を使用する場合は少なくとも1.000 * 10 ^ 9の比較が必要です。 。したがって、パフォーマンスが比較の数によって決定される場合、ヒープを使用しても0.1%のゲインしか得られません。しかし、100要素のヒープに要素を挿入し、100要素の配列の要素を置き換えて、その新しい最小値を見つけることの実行時間の違いは何ですか?000 *別のタイプのデータ構造(ヒープなど)を使用する場合の10 ^ 9の比較。したがって、パフォーマンスが比較の数によって決定される場合、ヒープを使用しても0.1%のゲインしか得られません。しかし、100要素のヒープに要素を挿入し、100要素の配列の要素を置き換えて、その新しい最小値を見つけることの実行時間の違いは何ですか?000 *別のタイプのデータ構造(ヒープなど)を使用する場合の10 ^ 9の比較。したがって、パフォーマンスが比較の数によって決定される場合、ヒープを使用しても0.1%のゲインしか得られません。しかし、100要素のヒープに要素を挿入し、100要素の配列の要素を置き換えて、その新しい最小値を見つけることの実行時間の違いは何ですか?

  • 理論レベル:ヒープに挿入するために必要な比較の数。O(log(n))であることはわかっていますが、定数係数はどのくらいの大きさですか?私

  • マシンレベル:ヒープ挿入と配列内の線形検索の実行時間に対するキャッシュと分岐予測の影響は何ですか。

  • 実装レベル:ライブラリまたはコンパイラによって提供されるヒープデータ構造には、どのような追加コストが隠されていますか?

これらは、100要素のヒープまたは100要素の配列のパフォーマンスの実際の違いを推定する前に回答する必要があるいくつかの質問だと思います。したがって、実験を行い、実際のパフォーマンスを測定することは理にかなっています。


1
これがヒープの機能です。
Neil G

@ニール・G:「あれ」って何?
miracle173 2013年

1
ヒープの上部はヒープ内の最小要素であり、新しい要素は1回の比較で拒否されます。
Neil G

1
私はあなたの言っていることを理解していますが、漸近的な比較の数ではなく絶対数の比較を行ったとしても、「新しい要素を挿入し、古い最小値を破棄して新しい最小値を見つける」ための時間は約7ではなく100
Neil G

1
さて、あなたの見積もりは非常に回り道です。予想される挿入数を直接計算して、k(digamma(n)-digamma(k))にすることができます。これは、klog(n)よりも小さくなります。いずれの場合でも、ヒープと配列の両方のソリューションは、1回の比較で要素を破棄します。唯一の違いは、挿入された要素の比較の数がヒープ14まで対ソリューションのために100である(平均場合は、おそらくはるかに小さいが)
ニールG

1
 Although in this question we should search for top 100 numbers, I will 
 generalize things and write x. Still, I will treat x as constant value.

nからのアルゴリズム最大のx要素:

戻り値をLISTと呼びます。これはx要素のセットです(私の意見では、リンクリストにする必要があります)

  • 最初のx要素は、「そのまま」プールから取得され、LISTでソートされます(xは定数として処理されるため、これは一定時間で行われます-O(x log(x))時間)
  • 次に来るすべての要素について、それがLISTの最小要素よりも大きいかどうかを確認し、最小の場合はポップして現在の要素をLISTに挿入します。これは順序付きリストであるため、すべての要素は対数時間でその場所を見つける必要があります(バイナリ検索)。また、順序付きリストであるため、挿入は問題ではありません。すべてのステップも一定の時間(O(log(x))time)で行われます。

それで、最悪のシナリオは何ですか?

x log(x)+(nx)(log(x)+1)= nlog(x)+ n-x

これが最悪の場合のO(n)時間です。+1は、番号がLISTの最小値より大きいかどうかをチェックします。平均ケースの予想時間は、これらのn要素の数学的分布に依存します。

可能な改善

このアルゴリズムは、最悪のシナリオでもわずかに改善できますが、平均的な動作を低下させるIMHO(この主張は証明できません)。漸近的な振る舞いは同じです。

このアルゴリズムの改善は、要素が最小より大きいかどうかをチェックしないことです。要素ごとに挿入を試み、最小よりも小さい場合は無視します。最悪の場合のシナリオのみを考えると、それは途方もなく聞こえますが

x log(x)+(nx)log(x)= nlog(x)

操作。

この使用例では、これ以上の改善は見られません。しかし、あなたは自分自身に問いかける必要があります。log(n)回以上、異なるx-esに対してこれを実行する必要がある場合はどうでしょうか。明らかに、その配列をO(n log(n))でソートし、必要なときはいつでもx要素を取得します。


1

この質問は、1行のC ++コードで(N log Nではなく)N log(100)の複雑さで答えられます。

 std::vector<int> myvector = ...; // Define your 1 billion numbers. 
                                 // Assumed integer just for concreteness 
 std::partial_sort (myvector.begin(), myvector.begin()+100, myvector.end());

最後の答えは、最初の100要素が配列の最大100の数であることが保証され、残りの要素が順序付けされていないベクトルです。

C ++ STL(標準ライブラリ)は、この種の問題に非常に便利です。

注:これが最適なソリューションであると言っているわけではありませんが、面接を節約できたでしょう。


1

単純な解決策は、優先度キューを使用して、キューに最初の100個の数を追加し、キュー内の最小数を追跡し、次に他の10億個の数を繰り返し、最大数よりも大きい数を見つけるたびに優先キューでは、最小の番号を削除し、新しい番号を追加して、キュー内の最小の番号を追跡します。

数がランダムな順序である場合、10億個の乱数を反復処理するときに、次の数がこれまでで最大の100の中にあることは非常にまれであるため、これは美しく機能します。しかし、数字はランダムではないかもしれません。配列がすでに昇順で並べ替えられている場合は、常に優先キューに要素を挿入します。

したがって、最初に配列から100,000個の乱数を選びます。遅くなる可能性のあるランダムアクセスを回避するために、250の連続する数値からなる400のランダムグループを追加します。このランダムな選択により、残りの数の数が上位100に入るのはごくわずかであるため、実行時間は、10億の数をいくつかの最大値と比較する単純なループの実行時間に非常に近くなります。


1

10億の数値から上位100を見つけるには、100要素の最小ヒープを使用するのが最適です。

最初に、遭遇した最初の100個の数値で最小ヒープを準備します。min-heapは、ルート(上部)で最初の100の数値の最小値を格納します。

ここで、残りの数値に沿って、ルートと比較します(100のうち最小のもの)。

検出された新しい番号がmin-heapのルートより大きい場合は、ルートをその番号に置き換え、そうでない場合は無視します。

min-heapへの新しい数値の挿入の一部として、ヒープ内の最小の数値が先頭(ルート)になります。

すべての数値を調べたら、最小ヒープに最大100の数値があります。


0

誰かが興味を持っている場合に備えて、Pythonで簡単なソリューションを作成しました。これは、bisectモジュールと、ソートされ続ける一時的な戻りリストを使用します。これは、優先キューの実装に似ています。

import bisect

def kLargest(A, k):
    '''returns list of k largest integers in A'''
    ret = []
    for i, a in enumerate(A):
        # For first k elements, simply construct sorted temp list
        # It is treated similarly to a priority queue
        if i < k:
            bisect.insort(ret, a) # properly inserts a into sorted list ret
        # Iterate over rest of array
        # Replace and update return array when more optimal element is found
        else:
            if a > ret[0]:
                del ret[0] # pop min element off queue
                bisect.insort(ret, a) # properly inserts a into sorted list ret
    return ret

100,000,000の要素と、ソートされたリストである最悪の場合の入力での使用:

>>> from so import kLargest
>>> kLargest(range(100000000), 100)
[99999900, 99999901, 99999902, 99999903, 99999904, 99999905, 99999906, 99999907,
 99999908, 99999909, 99999910, 99999911, 99999912, 99999913, 99999914, 99999915,
 99999916, 99999917, 99999918, 99999919, 99999920, 99999921, 99999922, 99999923,
 99999924, 99999925, 99999926, 99999927, 99999928, 99999929, 99999930, 99999931,
 99999932, 99999933, 99999934, 99999935, 99999936, 99999937, 99999938, 99999939,
 99999940, 99999941, 99999942, 99999943, 99999944, 99999945, 99999946, 99999947,
 99999948, 99999949, 99999950, 99999951, 99999952, 99999953, 99999954, 99999955,
 99999956, 99999957, 99999958, 99999959, 99999960, 99999961, 99999962, 99999963,
 99999964, 99999965, 99999966, 99999967, 99999968, 99999969, 99999970, 99999971,
 99999972, 99999973, 99999974, 99999975, 99999976, 99999977, 99999978, 99999979,
 99999980, 99999981, 99999982, 99999983, 99999984, 99999985, 99999986, 99999987,
 99999988, 99999989, 99999990, 99999991, 99999992, 99999993, 99999994, 99999995,
 99999996, 99999997, 99999998, 99999999]

1億個の要素に対してこれを計算するのに約40秒かかったので、10億個計算するのは怖いです。ただし、公平を期すために、最悪の場合の入力(皮肉にも、既に並べ替えられている配列)に入力していました。


0

O(N)の議論がたくさんあるので、思考の練習のためだけに別のことを提案します。

これらの数値の性質に関する既知の情報はありますか?それが本質的にランダムである場合、それ以上行くことはなく、他の答えを見てください。彼らよりも良い結果は得られません。

しかしながら!リストを生成するメカニズムが、そのリストを特定の順序で生成したかどうかを確認します。それらは、リストの特定の領域または特定の間隔で最大数の数値が見つかることが確実にわかる、明確に定義されたパターンにありますか?それにパターンがあるかもしれません。そうである場合、たとえば、特性の隆起が真ん中にある種の正規分布であることが保証されている場合、定義されたサブセット間で常に上昇傾向が繰り返され、データの真ん中のある時点Tでスパイクが長くなるおそらく、インサイダー取引または機器の障害の発生のように設定するか、または大災害後の力の分析のように、Nごとに「急上昇」するだけで、チェックする必要のあるレコードの数を大幅に減らすことができます。

とにかく考えるための食べ物がいくつかあります。多分これはあなたが将来の面接官に思慮深い答えを与えるのを助けるでしょう。このような問題に対して誰かが私にそのような質問をすると、私は感心するでしょう-それは彼らが最適化を考えていることを教えてくれるでしょう。常に最適化する可能性があるとは限らないことを認識してください。


0
Time ~ O(100 * N)
Space ~ O(100 + N)
  1. 100個の空のスロットの空のリストを作成します

  2. input-listのすべての数値について:

    • 数が最初の数より小さい場合は、スキップします

    • それ以外の場合は、この番号に置き換えます

    • 次に、隣接するスワップを介して番号をプッシュします。次のものより小さくなるまで

  3. リストを返す


注:の場合、log(input-list.size) + c < 100最適な方法は入力リストを並べ替え、最初の100アイテムを分割することです。


0

複雑さはO(N)です

最初に100整数の配列を作成し、この配列の最初の要素をN値の最初の要素として初期化し、別の変数を使用して現在の要素のインデックスを追跡し、CurrentBigと呼びます

N個の値を反復する

if N[i] > M[CurrentBig] {

M[CurrentBig]=N[i]; ( overwrite the current value with the newly found larger number)

CurrentBig++;      ( go to the next position in the M array)

CurrentBig %= 100; ( modulo arithmetic saves you from using lists/hashes etc.)

M[CurrentBig]=N[i];    ( pick up the current value again to use it for the next Iteration of the N array)

} 

完了したら、CurrentBigのM配列を100を法として100倍に出力します。


0

別のO(n)アルゴリズム-

アルゴリズムは除去により最大100を見つけます

バイナリ表現で100万の数値をすべて考慮します。最上位ビットから始めます。MSBが1であるかどうかを確認するには、適切な数値を使用したブール演算の乗算を実行します。100万以上の1がこれらの100万個ある場合は、他の数値をゼロで削除します。残りの数字のうち、次に重要なビットに進みます。除去後も残りの数を数えておき、この数が100を超えている限り続行します。

主要なブール演算は、GPUで並行して実行できます。


0

私は10億の数字を配列に入れて発砲する時間のある人を見つけました。政府のために働く必要があります。少なくとも、リンクされたリストがある場合は、スペースを確保するために5億を移動することなく、数字を中央に挿入できます。さらに優れたBtreeでは、バイナリ検索が可能です。比較するたびに、合計の半分が削除されます。ハッシュアルゴリズムを使用すると、チェッカーボードのようにデータ構造を入力できますが、スパースデータにはあまり適していません。最善の策は、100の整数のソリューション配列を持ち、ソリューション配列の最小数を追跡して、元の配列でより大きい数に遭遇したときに置換できるようにすることです。元々配列がソートされていない場合は、元の配列のすべての要素を調べる必要があります。


0

あなたはそれをO(n)間に合わせることができます。リストを反復処理して、任意の時点で表示された100の最大数とそのグループの最小値を追跡します。10の最小値よりも大きい新しい数値を見つけたら、それを置き換えて、100の新しい最小値を更新します(これを行うたびにこれを決定するために100の一定の時間がかかる場合がありますが、これは全体的な分析には影響しません) )。


1
このアプローチは、この質問に対する最も支持されている回答と2番目に支持されている回答の両方とほぼ同じです。
Bernhard Barker

0

別のリストの管理は余分な作業であり、別のリストを見つけるたびにリスト全体を移動する必要があります。ちょうどそれをqsortし、トップ100を選びます。


-1クイックソートはO(n log n)で、これはまさにOPが行い、改善を求めているものです。別のリストを管理する必要はなく、100個の数字のリストのみを管理します。提案には、元のリストを変更したり、コピーしたりするという望ましくない副作用もあります。それは4GiB程度のメモリです。

0
  1. n番目の要素を使用して100番目の要素O(n)を取得する
  2. 2回目は繰り返しますが、1回だけ繰り返し、この特定の要素より大きいすべての要素を出力します。

特に注意してください。2番目のステップは並列計算が簡単かもしれません!また、100万の最大要素が必要な場合にも効率的になります。


0

これは、Googleや他の業界大手からの質問です。おそらく、次のコードは、インタビュアーが期待する正しい答えです。時間コストとスペースコストは、入力配列の最大数に依存します。32ビットint配列入力の場合、最大スペースコストは4 * 125Mバイト、時間コストは5 * 10億です。

public class TopNumber {
    public static void main(String[] args) {
        final int input[] = {2389,8922,3382,6982,5231,8934
                            ,4322,7922,6892,5224,4829,3829
                            ,6892,6872,4682,6723,8923,3492};
        //One int(4 bytes) hold 32 = 2^5 value,
        //About 4 * 125M Bytes
        //int sort[] = new int[1 << (32 - 5)];
        //Allocate small array for local test
        int sort[] = new int[1000];
        //Set all bit to 0
        for(int index = 0; index < sort.length; index++){
            sort[index] = 0;
        }
        for(int number : input){
            sort[number >>> 5] |= (1 << (number % 32));
        }
        int topNum = 0;
        outer:
        for(int index = sort.length - 1; index >= 0; index--){
            if(0 != sort[index]){
                for(int bit = 31; bit >= 0; bit--){
                    if(0 != (sort[index] & (1 << bit))){
                        System.out.println((index << 5) + bit);
                        topNum++;
                        if(topNum >= 3){
                            break outer;
                        }
                    }
                }
            }
        }
    }
}

0

私は自分のコードを実行しましたが、それが探している「インタビュアー」かどうかわかりません

private static final int MAX=100;
 PriorityQueue<Integer> queue = new PriorityQueue<>(MAX);
        queue.add(array[0]);
        for (int i=1;i<array.length;i++)
        {

            if(queue.peek()<array[i])
            {
                if(queue.size() >=MAX)
                {
                    queue.poll();
                }
                queue.add(array[i]);

            }

        }

0

可能な改善。

ファイルに10億個の数値が含まれている場合、その読み取りは非常に長くなる可能性があります ...

この作業を改善するために、次のことができます。

  • ファイルをn個の部分に分割し、n個のスレッドを作成し、n個のスレッドがファイルのその部分で100個の最大数を探し(優先度キューを使用)、最後にすべてのスレッド出力の100個の最大数を取得します。
  • クラスタを使用して、hadoopなどのソリューションでこのようなタスクを実行します。ここでは、ファイルをさらに分割して、10億(または10 ^ 12)の数値ファイルの出力を高速化できます。

0

まず1000個の要素を取り、最大ヒープに追加します。最初の最大100要素を取り出して、どこかに保存します。次に、ファイルから次の900要素を選択し、最後の100個の最も高い要素とともにヒープに追加します。

ヒープから100要素を取得し、ファイルから900要素を追加するこのプロセスを繰り返し続けます。

100個の要素の最後の選択により、10億の数から最大100個の要素が得られます。


-1

問題:n >>> mであるn項目のm最大要素を見つける

誰にでも明らかな最も簡単な解決策は、バブルソートアルゴリズムのmパスを実行することです。

次に、配列の最後のn個の要素を出力します。

これは外部データ構造を必要とせず、誰もが知っているアルゴリズムを使用します。

実行時間の見積もりはO(m * n)です。これまでの最良の答えはO(n log(m))なので、このソリューションはmが小さい場合はそれほど高価ではありません。

これを改善できないと言っているわけではありませんが、これははるかに簡単な解決策です。


1
外部データ構造はありませんか?並べ替える10億の配列についてはどうですか?このサイズの配列は、格納する時間と格納するスペースの両方で大きなオーバーヘッドです。すべての「大きな」数値が配列の間違った端にある場合はどうなりますか?それらを適切な位置に「バブル」するには、約1,000億のスワップが必要になります。別の大きなオーバーヘッド...最後に、M N = 1000億vs M Log2(N)= 66.4十億で、ほぼ2桁違います。多分これを考え直してください。最大数のデータ構造を維持しながらワンパススキャンを実行すると、このアプローチが大幅に実行されます。
NealB 2013年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.