nsorted integers(およびリストの最小値より大きい)の指定されたリストに存在しない最初の(最小の)整数を見つける最も速い方法は何ですか?
私の基本的なアプローチは、それらを並べ替えてリストをステップスルーすることですが、もっと良い方法はありますか?
「数値」と言うときに「整数」を意味すると仮定すると、サイズ2 ^ nのビットベクトルを使用できます。ここで、nは要素の数です(たとえば、範囲に1から256までの整数が含まれているとすると、256-ビット、または32バイト、ビットベクトル)。範囲の位置nで整数を見つけたら、n番目のビットを設定します。
整数のコレクションの列挙が完了したら、ビットベクトルのビットを反復処理して、ビットセット0の位置を探します。これらのビットは、不足している整数の位置nと一致します。
これはO(2 * N)なので、O(N)であり、リスト全体をソートするよりもメモリ効率が良いでしょう。
リスト全体を最初にソートすると、最悪の場合の実行時間が保証されます。また、ソートアルゴリズムの選択も重要です。
これが私がこの問題に取り組む方法です:
return
:答えが見つかりました。これが ヒープソートの視覚化 です。
難解で「賢い」ために、「穴」が1つしかない配列の特殊なケースでは、XORベースのソリューションを試すことができます。
これは、ビットベクトルソリューションとほぼ同じ2N時間で実行されますが、N> sizeof(int)に対して必要なメモリ領域は少なくなります。ただし、配列に複数の「穴」がある場合、Xはすべての穴のXOR "合計"であり、実際の穴の値に分離するのは困難または不可能です。その場合「pivot」や「bitvector」などの他の方法にフォールバックします。
さらに複雑さを軽減するために、ピボットメソッドに似たものを使用してこれを再帰することもできます。ピボットポイントに基づいて配列を再配置します(これは左側の最大値と右側の最小値になります。ピボット中に完全な配列の最大値と最小値を見つけるのは簡単です)。ピボットの左側に1つ以上の穴がある場合は、その側にのみ再帰します。それ以外の場合は、反対側に再帰します。穴が1つだけであると判断できる任意の時点で、XORメソッドを使用してそれを見つけます(これは、既知の穴。これは純粋なピボットアルゴリズムの基本ケースです)。
あなたが遭遇する数字の範囲は何ですか?その範囲があまり大きくない場合は、2つのスキャンでこれを解決できます(線形時間O(n))数と同じ数の要素を含む配列を使用し、時間とスペースを交換します。スペースを減らすために、各数値に1ビットを割り当て、バイトごとに8つの数値に相当するストレージを与えることができます。
メモリをコピーする代わりに初期のシナリオに適している可能性があり、その場である他のオプションは、選択パスを変更して、スキャンパスで見つかった分が最後に見つかった分よりも1少ない場合に早く終了することです。
ここでのほとんどのアイデアは、単に並べ替えにすぎません。 bitvectorバージョンはプレーンなBucketsortです。ヒープのソートについても言及されました。基本的には、時間/スペースの要件、および要素の範囲と数に応じて適切なソートアルゴリズムを選択することになります。
私の見解では、ヒープ構造を使用することがおそらく最も一般的な解決策です(ヒープは基本的に、完全なソートなしで最小の要素を効率的に提供します)。
最初に最小数を見つけ、次にそれより大きい各整数をスキャンするアプローチを分析することもできます。または、ギャップがあることを期待して5つの最小数を見つけます。
これらのアルゴリズムはすべて、入力特性とプログラム要件に応じて長所があります。
いいえ、そうではありません。まだスキャンされていない番号は常に特定の「穴」を埋める番号である可能性があるため、少なくとも1回は各番号をスキャンしてから、それを可能な近隣と比較することを避けることはできません。おそらく、バイナリツリーなどを構築し、穴が見つかるまで左から右にトラバースすることで、速度を上げることができますが、これは、ソートであるため、ソートと基本的に同じ時間の複雑さです。そして、おそらく Timsort よりも高速なものを思い付くことはないでしょう。
重複がないことが保証されている場合は、一般的かつ効率的に機能するものを思いついたと思います*(ただし、任意の数のホールおよび任意の範囲の整数に拡張可能である必要があります)。
このメソッドの背後にあるアイデアは、ピボットとその周りのパーティションを見つけて、穴のある側で再帰するという点で、クイックソートに似ています。どちらの側に穴があるかを確認するために、最小値と最大値を見つけ、ピボットとその側の値の数と比較します。ピボットが17で、最小数が11であるとします。穴がない場合は、6つの数(11、12、13、14、15、16、17)があるはずです。 5がある場合、その側に穴があることを知っているので、その側だけを再帰して見つけることができます。それ以上にわかりやすく説明できないので、例を挙げてみましょう。
15 21 10 13 18 16 22 23 24 20 17 11 25 12 14
ピボット:
10 13 11 12 14 |15| 21 18 16 22 23 24 20 17 25
15はピボットであり、パイプ(||
)。ピボットの左側には5つの数字があり(15-10)、右側には9があり、10(25-15)です。したがって、右側を再帰します。穴が隣接している場合、前の境界は15であったことに注意してください(16)。
[15] 18 16 17 20 |21| 22 23 24 25
これで、左側に4つの数値がありますが、5(21-16)になるはずです。そこで、ここを再帰し、前の範囲(角かっこ内)に再度注目します。
[15] 16 17 |18| 20 [21]
左側には正しい2つの数字(18-16)がありますが、右側には2ではなく1(20-18)があります。終了条件に応じて、1の数値を両側(18、20)と比較すると、19が欠落しているか、またはもう一度再帰していることがわかります。
[18] |20| [21]
左側のサイズはゼロで、ピボット(20)と前の境界(18)の間にギャップがあるため、19が穴になります。
*:重複がある場合は、おそらくハッシュセットを使用して、O(N)時間でそれらを削除し、全体的なメソッドO(N)を維持しますが、可能性があります他の方法を使用するよりも時間がかかります。
追加のストレージを使用しない、または整数の幅(32ビット)を想定しないソリューション。
1つの線形パスで最小数を見つけます。これを「分」と呼びましょう。 O(n)時間の複雑さ。
ランダムなピボット要素を選択し、クイックソートスタイルのパーティションを作成します。
ピボットが位置=( "pivot"-"min")で終了した場合、パーティションの右側で再帰し、それ以外の場合はパーティションの左側で再帰します。ここでの考え方は、最初から穴がない場合、ピボットは( "pivot"-"min")番目の位置にあるため、最初の穴はパーティションの右側にあり、逆も同様です。
基本ケースは1つの要素の配列であり、穴はこの要素と次の要素の間にあります。
予想される総実行時間の複雑さはO(n)(定数を含む8 * n)であり、最悪の場合はO(n ^ 2)です。同様の問題の時間複雑度分析を見つけることができます ここ 。