数字のリストで「穴」を見つける


14

並べ替えられていない整数のリストに存在しない(リストの最小値より大きい)最初の(最小の)整数を見つける最も速い方法は何ですか?

私の基本的なアプローチは、それらをソートしてリストをステップすることです、より良い方法はありますか?


6
@Jodrell無限の進行をソートするのは難しいと思う;-)
maple_shaft

3
@maple_shaftは同意しましたが、しばらく時間がかかります。
ジョドレル

4
ソートされていないリストに対して最初にどのように定義しますか?
ジョドレル

1
これはおそらく概念的な問題ではないため、おそらくStackOverflowに属していることに気付きました。
ジェイソンTrue

2
@JasonTrue FAQから、If you have a question about… •algorithm and data structure conceptsそれはトピックIMHOにあります。
maple_shaft

回答:


29

「数値」と言うときに「整数」を意味すると仮定すると、サイズ2 ^ nのビットベクトルを使用できます。nは要素の数です(範囲に1〜256の整数が含まれている場合、256-ビット、または32バイト、ビットベクトル)。範囲の位置nの整数に出会ったとき、n番目のビットを設定します。

整数のコレクションの列挙が完了したら、ビットセット内のビットを反復処理して、ビットセット0の位置を探します。これらは、欠落している整数の位置nと一致します。

これはO(2 * N)であるため、O(N)であり、おそらくリスト全体をソートするよりもメモリ効率が高くなります。


6
さて、直接比較として、すべての正の符号なし32ビット整数が1である場合、約0.5ギガバイトのメモリで整数不足の問題を解決できます。代わりにソートした場合、8ギガバイトを超えるメモリを使用する必要があります。そして、このような特別な場合(ビットベクトルを取得するとリストが並べ替えられる)を除き、並べ替えはほぼ常にn log n以上です。したがって、定数がコストの複雑さを上回る場合を除き、線形アプローチが優先されます。
JasonTrue

1
先験的に範囲がわからない場合はどうなりますか?
Blrfl

2
整数データ型Blrflを使用している場合、さらに絞り込むための十分な情報がなくても、範囲の最大範囲を確実に把握できます。それが小さなリストであることを知っていても、正確なサイズがわからない場合は、ソートがより簡単な解決策になる可能性があります。
ジェイソントゥルー

1
または、最初にリストをループして、最小要素と最大要素を見つけます。次に、最小値を基本オフセットとして正確なサイズの配列を割り当てることができます。続いている)。
セキュア

1
@JPatrick:宿題やビジネスではなく、私は何年も前にCSを卒業しました:)。
ファビアンZeindl

4

リスト全体を最初にソートすると、最悪の場合のランタイムが保証されます。また、ソートアルゴリズムの選択も重要です。

この問題にどのようにアプローチするかを以下に示します。

  1. リスト内の最小の要素に焦点を合わせて、ヒープソートを使用します
  2. 各スワップの後に、ギャップがあるかどうかを確認します。
  3. ギャップreturnが見つかった場合:答えが見つかりました。
  4. ギャップが見つからない場合は、交換を続けます。

ここだヒープソートの可視化


質問の1つは、リストの「最小」要素をどのように識別するかです。
ジョドレル

4

難解で「賢い」ために、「ホール」が1つしかないアレイの特殊なケースでは、XORベースのソリューションを試すことができます。

  • 配列の範囲を決定します。これは、配列の最初の要素に「max」および「min」変数を設定し、その後の各要素について、その要素が最小よりも小さいか最大よりも大きい場合、最小または最大を新しい価値。
  • 範囲がセットのカーディナリティより1つ小さい場合、XORを使用できるように「ホール」は1つしかありません。
  • 整数変数Xをゼロに初期化します。
  • minからmaxまでの各整数について、その値をXとXORし、結果をXに格納します。
  • 次に、配列内の各整数をXとXORし、連続する各結果を以前のようにXに格納します。
  • 完了したら、Xが「ホール」の値になります。

これは、ビットベクトルソリューションと同様に約2Nの時間で実行されますが、N> sizeof(int)に対して必要なメモリスペースは少なくなります。ただし、配列に複数の「穴」がある場合、Xはすべての穴のXOR「合計」になり、実際の穴の値に分けることは困難または不可能になります。その場合、他の答えから「ピボット」または「ビットベクトル」アプローチなどの他の方法にフォールバックします。

ピボット方式に似たものを使用して、これを再帰して、複雑さをさらに軽減することもできます。ピボットポイントに基づいて配列を再配置します(これは左側の最大値と右側の最小値になります。ピボット中に完全な配列の最大値と最小値を見つけるのは簡単です)。ピボットの左側に1つ以上の穴がある場合は、その側のみに再帰します。そうでなければ、反対側に再帰します。穴が1つしかないと判断できる任意の時点で、XORメソッドを使用して見つけます(これは、既知の穴を持つ2つの要素のコレクションにピボットし続けるよりも全体的に安価である必要があります。純粋なピボットアルゴリズム)。


それは途方もなく賢くて素晴らしいです!可変数の穴を使用してこれを行う方法を考え出すことができますか?:-D

2

あなたが遭遇する数字の範囲は何ですか?その範囲があまり大きくない場合は、数と同じ数の要素を持つ配列を使用して、2回のスキャン(線形時間O(n))でこれを解決できます。もう1回スキャンすると、範囲を動的に見つけることができます。スペースを削減するために、各数値に1ビットを割り当てて、1バイトあたり8数値のストレージを割り当てることができます。

メモリをコピーする代わりに初期のシナリオに適している可能性があり、他のオプションは、スキャンパスで見つかった最小値が最後に見つかった最小値よりも1つ多くない場合、選択ソートを変更して早期に終了することです。


1

いいえ、そうでもありません。まだスキャンされていない番号は、常に特定の「穴」を満たす番号になる可能性があるため、各番号を少なくとも1回スキャンしてから、可能な番号と比較することは避けられません。おそらくバイナリツリーなどを構築し、左から右に穴が見つかるまでそれを走査することで速度を上げることができますが、それは並べ替えと本質的に同じ時間の複雑さです。そして、おそらくあなたはティムソートよりも速いものを思い付かないでしょう。


1
リストの走査はソートと同じ時間の複雑さであると言っていますか?
maple_shaft

@maple_shaft:いいえ、ランダムデータからバイナリツリーを構築し、それを左から右にトラバースすることは、ソートしてから小から大にトラバースすることと同じです。
pillmuncher

1

ここでのほとんどのアイデアは、単なる並べ替えに過ぎません。ビットベクトルバージョンは、プレーンバケットソートです。ヒープの並べ替えについても言及しました。基本的には、時間/空間の要件に加えて、要素の範囲と数にも依存する適切な並べ替えアルゴリズムを選択することに要約されます。

私の見解では、ヒープ構造を使用することがおそらく最も一般的なソリューションです(基本的に、ヒープは完全なソートなしで最小の要素を効率的に提供します)。

また、最初に最小数を見つけてからそれより大きい整数をスキャンするアプローチを分析することもできます。または、ギャップが生じることを期待して、5つの最小数を見つけます。

これらのアルゴリズムはすべて、入力特性とプログラム要件に応じて強度があります。


0

追加のストレージを使用しない、または整数の幅(32ビット)を想定しないソリューション。

  1. 1つの線形パスで最小数を見つけます。これを「最小」と呼びましょう。O(n)時間の複雑さ。

  2. ランダムなピボット要素を選択し、クイックソートスタイルのパーティションを作成します。

  3. ピボットが=( "pivot"-"min")の位置になった場合、パーティションの右側で再帰し、そうでない場合はパーティションの左側で再帰します。ここでの考え方は、最初から穴がない場合、ピボットは(「ピボット」-「最小」)番目の位置にあるため、最初の穴はパーティションの右側に、その逆も同様であるということです。

  4. 基本ケースは1つの要素の配列であり、この要素と次の要素の間に穴があります。

予想される総実行時間の複雑さはO(n)(定数を使用した8 * n)であり、最悪の場合はO(n ^ 2)です。同様の問題の時間複雑性分析はここで見つけることができます


0

重複がないことが保証されている場合、一般的かつ効率的に動作するものを考え出したと思います*(ただし、任意の数のホールと任意の範囲の整数に拡張可能である必要があります)。

この方法の背後にある考え方は、クイックソートに似ています。つまり、ピボットとその周りのパーティションを見つけ、穴のある側で再帰します。どの側面に穴があるかを調べるために、最小値と最大値を見つけ、それらをその側面のピボットと値の数と比較します。ピボットが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の数値を2つの側(18、20)と比較して、19が欠落しているか、もう一度再帰していることを確認できます。

[18] |20| [21]

左側のサイズはゼロで、ピボット(20)と前の境界(18)の間にギャップがあるため、19が穴です。

*:重複がある場合、おそらくハッシュセットを使用してO(N)時間でそれらを削除し、メソッド全体をO(N)のままにしておくことができますが、他のメソッドを使用するよりも時間かかる場合があります。


1
私は、OPが1つだけの穴があることについて何かを言ったとは思わない。入力は、番号の並べ替えられていないリストです。何でもかまいません。説明から、そこにあるべき数字の数をどのように決定するかは明確ではありません。
カレブ

@calebホールがいくつあっても問題ありません。重複はありません(実際には他の方法よりもオーバーヘッドが大きくなる可能性がありますが、ハッシュセットを使用してO(N)で削除できます)。説明を改善しようとしましたが、それが良いかどうかを確認してください。
ケビン

これは線形ではありません、IMO。(logN)^ 2のようなものです。各ステップで、関心のあるコレクションのサブセット(最初の「穴」があると特定した前のサブアレイの半分)をピボットし、「穴」がある場合は左側に再帰します。左側がそうでない場合は右側。(logN)^ 2は線形よりも優れています。Nが10倍に増加した場合、2(log(N)-1)+さらに1ステップのオーダーになります。
キース

@Keith-残念ながら、各レベルのすべての数値を見てピボットする必要があるため、約n + n / 2 + n / 4 + ... = 2n(技術的には2(nm))の比較が必要になります。
ケビン
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.