O(n)時間とO(1)空間での重複の検索


121

入力:0からn-1までの要素を含むn個の要素の配列が与えられ、これらの数値のいずれかが何度も現れる。

目標:O(n)でこれらの繰り返し数を検索し、一定のメモリ空間のみを使用する。

たとえば、nを7、配列を{1、2、3、1、3、0、6}とすると、答えは1と3になります。同様の質問をここで確認しましたが、回答にはいくつかのデータ構造HashSetなどが使用されています。

同じための効率的なアルゴリズムはありますか?

回答:


164

これは私が思いついたものであり、追加の符号ビットを必要としません:

for i := 0 to n - 1
    while A[A[i]] != A[i] 
        swap(A[i], A[A[i]])
    end while
end for

for i := 0 to n - 1
    if A[i] != i then 
        print A[i]
    end if
end for

最初のループは配列を並べ替え、要素xが少なくとも1回存在する場合、それらのエントリの1つが位置にあるようにしますA[x]

最初は赤く見えてもO(n)に見えないかもしれませんが、そうです-ネストされたループがありますが、それでもO(N)時間内に実行されます。スワップが発生するのは、iそのようなが存在する場合のみA[i] != iであり、各スワップは、そのようなのような要素を少なくとも1つ設定しA[i] == iます。これは、スワップの合計数(つまり、whileループ本体の実行の合計数)が最大でであることを意味しN-1ます。

2番目のループは、等しくないxfor の値を出力します。最初のループは、配列内に少なくとも1つ存在する場合、それらのインスタンスの1つがにあることを保証するため、これには、存在しない値が出力されます。配列。A[x]xxA[x]x

(あなたがそれで遊ぶことができるようにIdeonリンク)


10
@arasmussen:ええ。しかし、私は最初に壊れたバージョンを思いつきました。問題のa[a[i]]制約は、解決策に少しの手掛かりを与えます-すべての有効な配列値は、有効な配列インデックスのヒントでもあり、O(1)スペース制約は、swap()操作がキーであることを示唆しています。
カフェ

2
@caf:失敗した{3,4,5,3,4}の配列でコードを実行してください。
NirmalGeo

6
@NirmalGeo:5は範囲内にないため0..N-1Nこの場合は5)、これは有効な入力ではありません。
カフェ

2
@caf {1,2,3,1,3,0,0,0,0,6}の出力は3 1 0 0 0であるか、繰り返しが2を超える場合はどのような場合でも正しいですか。
ターミナル

3
これは素晴らしいです!この質問には多くのバリエーションがあり、通常はより制約があります。これが、これを解決するための最も一般的な方法です。printステートメントを変更してprint iこれをstackoverflow.com/questions/5249985/…の解決策に変えることと、(「bag」が変更可能な配列であると仮定して)stackoverflow.com/questions/3492302/…の Qk について簡単に触れます。
j_random_hacker

35

cafのすばらしい答えは、配列にk回出現する各数値をk-1回出力します。これは便利な動作ですが、問題は間違いなく各複製を1回だけ印刷することを求めており、彼は線形の時間/一定の空間境界を壊すことなくこれを実行する可能性について言及しています。これは、2番目のループを次の疑似コードに置き換えることで実行できます。

for (i = 0; i < N; ++i) {
    if (A[i] != i && A[A[i]] == A[i]) {
        print A[i];
        A[A[i]] = i;
    }
}

これは、最初のループの実行後に、値mが2回以上出現する場合、それらの出現の1つが正しい位置にあることが保証されるという特性を利用しますA[m]。注意が必要な場合は、その「ホーム」ロケーションを使用して、重複がまだ印刷されているかどうかに関する情報を保存できます。

cafのバージョンでは、配列をA[i] != i調べたときに、それA[i]が重複していることを暗示していました。私のバージョンでは、わずかに異なる不変式に依存していますこれは、これまでに見たことのない重複であることをA[i] != i && A[A[i]] == A[i]意味しています。(「これまでに見たことがない」部分をドロップすると、残りはcafの不変条件の真実と、すべての重複がホームロケーションにコピーを持っていることが保証されることによって暗示されることがわかります。)このプロパティは、最初に(cafの最初のループが終了した後)と、各ステップの後で維持されることを以下に示します。A[i]

配列をA[i] != i調べていくと、テストの部分での成功は、これまで見られなかった重複であるA[i] 可能性があることを意味します。これまでに見たことがない場合は、A[i]のホームロケーションがそれ自体を指していると想定しifます。これが、条件の後半でテストされます。その場合は、それを印刷し、ホームの場所を変更して、最初に見つかった重複を指すようにして、2ステップの「サイクル」を作成します。

この操作が不変条件を変更しないことを確認するm = A[i]ために、特定の位置がiを満たすと仮定しA[i] != i && A[A[i]] == A[i]ます。私たちが行った変更(A[A[i]] = i)は、他のの非ホームオカレンスが条件としてm後半ifに失敗することによって重複して出力されるのを防ぐために機能しiますが、ホームロケーションに到着したときに機能しmますか?はい、そうです。今、この新しい条件のi前半がtrueであることがわかったとしても、後半は、それが指している場所がホームの場所であるかどうかをテストし、そうでないことがわかります。この状況では、重複した値であるかどうかはわかりませんが、どちらの方法でもそれがわかります。ifA[i] != imA[m]これらの2サイクルはcafの最初のループの結果に表示されないことが保証されているため、すでに報告されています。(注場合にm != A[m]、その後、正確の1 mとは、A[m]複数回発生、およびその他は全く発生しません。)


1
はい、それは私が思いついたものと非常に似ています。同一の最初のループが、印刷ループが異なるだけで、いくつかの異なる問題にどのように役立つかは興味深いです。
カフェ

22

これが疑似コードです

for i <- 0 to n-1:
   if (A[abs(A[i])]) >= 0 :
       (A[abs(A[i])]) = -(A[abs(A[i])])
   else
      print i
end for

C ++のサンプルコード


3
非常に賢い-インデックス付きエントリの符号ビットで回答をエンコードする!
holtavolt

3
@sashang:できません。問題の仕様を確認してください。" 0からn-1までの要素を含む n要素の配列を与える"
Prasoon Saurav

5
これは重複する0を検出せず、同じ番号を重複として検出します。
ヌルセット

1
@Nullセット:あなたは置き換えることができ-~、ゼロの問題のために。
user541686 2011

26
これは問題が動いているという答えかもしれませんが、技術的にはO(n)隠れたスペース、つまりn符号ビットを使用しています。アレイは、各要素の間の値をのみ保持することができるように定義されている場合0n-1、それは明らかに仕事をしません。
カフェ

2

比較的小さいNの場合、div / mod操作を使用できます

n.times do |i|
  e = a[i]%n
  a[e] += n
end

n.times do |i| 
  count = a[i]/n
  puts i if count > 1
end

C / C ++ではなく、とにかく

http://ideone.com/GRZPI


+1素晴らしい解決策。2回後にエントリへのnの追加を停止すると、より大きいnに対応できます。
Apshir

1

それほどきれいではありませんが、少なくともO(N)およびO(1)プロパティを確認するのは簡単です。基本的に、配列をスキャンし、各番号について、対応する位置にすでに一度だけ(N)またはすでに複数回(N + 1)のフラグが付けられているかどうかを確認します。既に一度だけフラグが立てられている場合は、それを印刷して、すでに複数回フラグを立てます。フラグが立てられていない場合は、一度だけフラグを立てて、対応するインデックスの元の値を現在の位置に移動します(フラグは破壊的な操作です)。

for (i=0; i<a.length; i++) {
  value = a[i];
  if (value >= N)
    continue;
  if (a[value] == N)  {
    a[value] = N+1; 
    print value;
  } else if (a[value] < N) {
    if (value > i)
      a[i--] = a[value];
    a[value] = N;
  }
}

または、より良い(二重ループにもかかわらず、より高速):

for (i=0; i<a.length; i++) {
  value = a[i];
  while (value < N) {
    if (a[value] == N)  {
      a[value] = N+1; 
      print value;
      value = N;
    } else if (a[value] < N) {
      newvalue = value > i ? a[value] : N;
      a[value] = N;
      value = newvalue;
    }
  }
}

1、それはうまく動作しますが、それは正確な理由を把握する思考のビットを取ったif (value > i) a[i--] = a[value];作品を次の場合value <= i、我々はすでにで値を処理しているa[value]し、安全にそれを上書きすることができます。また、O(N)の性質が明白であるとは言えません!綴り:メインループの実行N回数、およびa[i--] = a[value];行の実行回数。その行が実行できるのはa[value] < N、であり、実行するたびに、直後にまだN設定されていない配列の値がに設定されNているNため、合計で最大で2Nループを繰り返し実行できるためです。
j_random_hacker

1

Cの1つのソリューションは次のとおりです。

#include <stdio.h>

int finddup(int *arr,int len)
{
    int i;
    printf("Duplicate Elements ::");
    for(i = 0; i < len; i++)
    {
        if(arr[abs(arr[i])] > 0)
          arr[abs(arr[i])] = -arr[abs(arr[i])];
        else if(arr[abs(arr[i])] == 0)
        {
             arr[abs(arr[i])] = - len ;
        }
        else
          printf("%d ", abs(arr[i]));
    }

}
int main()
{   
    int arr1[]={0,1,1,2,2,0,2,0,0,5};
    finddup(arr1,sizeof(arr1)/sizeof(arr1[0]));
    return 0;
}

それはO(n)時間とO(1)空間の複雑さです。


1
Nの追加の符号ビットを使用するため、これのスペースの複雑さはO(N)です。アルゴリズムは、配列要素タイプが0からN-1までの数しか保持できないという前提の下で機能するはずです。
ca

はい、そうですが、0からn-1までのアルゴが欲しいので、アルゴを完璧に求めました。また、O(n)を超えるソリューションを確認したので、これを考えました
Anshul garg

1

この配列を一方向のグラフデータ構造として提示するとします。各数値は頂点であり、配列内のインデックスは、グラフのエッジを形成する別の頂点を指します。

さらに簡単にするために、0からn-1までのインデックスと0..n-1からの数値の範囲があります。例えば

   0  1  2  3  4 
 a[3, 2, 4, 3, 1]

0(3)-> 3(3)はサイクルです。

回答:インデックスに依存して配列をトラバースするだけです。a [x] = a [y]の場合、それはサイクルであるため、複製されます。次のインデックスにスキップして、配列の最後まで繰り返します。複雑さ:O(n)時間とO(1)空間。


0

上記のcafのメソッドを示す小さなpythonコード:

a = [3, 1, 1, 0, 4, 4, 6] 
n = len(a)
for i in range(0,n):
    if a[ a[i] ] != a[i]: a[a[i]], a[i] = a[i], a[a[i]]
for i in range(0,n):
    if a[i] != i: print( a[i] )

単一のi値に対してスワップが複数回発生する可能性があることに注意してください- while私の答えに注意してください。
ca

0

アルゴリズムは、次のC関数で簡単に見ることができます。必須ではありませんが、元の配列を取得するには、各エントリをnを法として取得します。

void print_repeats(unsigned a[], unsigned n)
{
    unsigned i, _2n = 2*n;
    for(i = 0; i < n; ++i) if(a[a[i] % n] < _2n) a[a[i] % n] += n;
    for(i = 0; i < n; ++i) if(a[i] >= _2n) printf("%u ", i);
    putchar('\n');
}

テスト用のIdeone Link。


2 * nまでの数値を扱うには、元の数値を格納するために必要なものよりも、配列エントリごとに1ビットの追加のストレージスペースが必要になるため、これは技術的には「不正行為」だと思います。実際、最大3 * n-1までの数値を格納しているため、エントリごとにlog2(3)= 1.58追加ビットに近づく必要があります。
j_random_hacker

0
static void findrepeat()
{
    int[] arr = new int[7] {0,2,1,0,0,4,4};

    for (int i = 0; i < arr.Length; i++)
    {
        if (i != arr[i])
        {
            if (arr[i] == arr[arr[i]])
            {
                Console.WriteLine(arr[i] + "!!!");
            }

            int t = arr[i];
            arr[i] = arr[arr[i]];
            arr[t] = t;
        }
    }

    for (int j = 0; j < arr.Length; j++)
    {
        Console.Write(arr[j] + " ");
    }
    Console.WriteLine();

    for (int j = 0; j < arr.Length; j++)
    {
        if (j == arr[j])
        {
            arr[j] = 1;
        }
        else
        {
            arr[arr[j]]++;
            arr[j] = 0;
        }
    }

    for (int j = 0; j < arr.Length; j++)
    {
        Console.Write(arr[j] + " ");
    }
    Console.WriteLine();
}

0

0(n)時間の複雑さと一定の余分なスペースで重複を見つけるために、迅速に1つのサンプルプレイグラウンドアプリを作成しました。URL Finding Duplicatesを確認してください

IMP上記のソリューションは、配列に0からn-1までの要素が含まれ、これらの数値のいずれかが何度も現れる場合に機能しました。


0
private static void printRepeating(int arr[], int size) {
        int i = 0;
        int j = 1;
        while (i < (size - 1)) {
            if (arr[i] == arr[j]) {
                System.out.println(arr[i] + " repeated at index " + j);
                j = size;
            }
            j++;
            if (j >= (size - 1)) {
                i++;
                j = i + 1;
            }
        }

    }

上記のソリューションは、O(n)と一定の空間の同じ時間の複雑さを実現します。
user12704811

3
このコードスニペットをありがとうございます。このコードスニペットは、短期間の限定的なヘルプを提供する場合があります。適切な説明は、なぜこれが問題の優れた解決策であるを示すことにより、長期的な価値を大幅に改善し、他の同様の質問を持つ将来の読者にとってより有用になります。答えを編集して、仮定を含めて説明を追加してください。
Toby Speight

3
ところで、ここでは時間の複雑さはO(n²)のようです-内部ループを非表示にしてもそれは変わりません。
Toby Speight

-2

配列が大きすぎない場合、このソリューションはより簡単です。ティックするために同じサイズの別の配列を作成します。

1入力配列と同じサイズのビットマップ/配列を作成します

 int check_list[SIZE_OF_INPUT];
 for(n elements in checklist)
     check_list[i]=0;    //initialize to zero

2入力配列をスキャンし、上記の配列のカウントをインクリメントします

for(i=0;i<n;i++) // every element in input array
{
  check_list[a[i]]++; //increment its count  
}  

3ここで、check_list配列をスキャンして、複製を1回または複数回印刷します

for(i=0;i<n;i++)
{

    if(check_list[i]>1) // appeared as duplicate
    {
        printf(" ",i);  
    }
}

もちろん、上記のソリューションで消費されるスペースの2倍かかりますが、時間効率は基本的にO(n)であるO(2n)です。


これはO(1)スペースではありません。
Daniel Kamil Kozar

おっとっと ...!気づかなかった...私の悪い。
Deepthought 2012

@nikhil O(1)はいかがですか?私の配列check_listは、入力のサイズが大きくなるにつれて直線的に大きくなります。O(1)と呼ぶのに使用しているヒューリスティックはどのようにO(1)になるのでしょうか。
Deepthought 2012

与えられた入力に対して一定のスペースが必要です、それはO(1)ではありませんか?私は間違っている可能性があります:)
nikhil

私のソリューションでは、入力が増えるにつれてより多くのスペースが必要になります。アルゴリズムの効率(空間/時間)は、特定の入力に対して測定されません(このような場合、すべての検索アルゴリズムの時間効率は一定になります(つまり、検索した最初のインデックスで見つかった要素)。任意の入力に対して測定されます。最高のケース、最悪のケース、平均的なケースがある理由。
Deepthought 2012
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.