web-dev-qa-db-ja.com

2つの配列間の一意の要素を見つけるためのより高速なアルゴリズム?

[〜#〜] edit [〜#〜]:この質問の初心者のために、何が起こっているのかを明確にする回答を投稿しました。受け入れられた回答は、最初に投稿されたとおりに私の質問に最もよく回答すると思われる回答ですが、詳細については私の回答を参照してください。

[〜#〜] note [〜#〜]:この問題は、最初は疑似コードであり、リストを使用していました。私はそれをJavaと配列に適合させました。そのため、Java固有のトリック(またはそのことについては任意の言語のトリック!)を使用するソリューションを見たいのですが、元の問題は言語に依存しません。

問題

2つの並べ替えられていない整数配列abがあり、要素の繰り返しが許可されているとします。それらは同一です(含まれている要素に関して)except配列の1つに追加の要素があります。例として:

int[] a = {6, 5, 6, 3, 4, 2};
int[] b = {5, 7, 6, 6, 2, 3, 4};

これら2つの配列を入力として取り、単一の一意の整数(上記の場合は7)を出力するアルゴリズムを設計します。

ソリューション(これまでのところ)

私はこれを思いつきました:

public static int getUniqueElement(int[] a, int[] b) {
    int ret = 0;
    for (int i = 0; i < a.length; i++) {
        ret ^= a[i];
    }
    for (int i = 0; i < b.length; i++) {
        ret ^= b[i];
    }
    return ret;
}

クラスで提示された「公式」ソリューション:

public static int getUniqueElement(int[] a, int[] b) {
    int ret = 0;
    for (int i = 0; i < a.length; i++) {
        ret += a[i];
    }
    for (int i = 0; i < b.length; i++) {
        ret -= b[i];
    }
    return Math.abs(ret);
}

したがって、両方が概念的に同じことを行っています。そして、aが長さmであり、bが長さnであるとすると、両方のソリューションの実行時間はO(m + n)になります。

質問

後で私は先生と話をするようになり、彼はそれを行うためにより速い方法があることをほのめかしました。正直なところ、その方法はわかりません。要素が一意であるかどうかを確認するには、少なくともすべての要素を調べる必要があるようです。それは少なくともO(m + n)です...そうですか?

より速い方法はありますか?もしそうなら、それは何ですか?

59
William Gaul

これはおそらく、コメントでHotLickの提案を使用してJavaで実行できる最速です。これはb.length == a.length + 1 so bは、「一意の」要素を追加した大きな配列です。

public static int getUniqueElement(int[] a, int[] b) {
    int ret = 0;
    int i;
    for (i = 0; i < a.length; i++) {
        ret = ret ^ a[i] ^ b[i];
    }
    return ret ^ b[i];
}

仮説が立てられない場合でも、それを簡単に拡張して、aまたはbが一意の要素を持つより大きな配列になる場合を含めることができます。それでもO(m + n)であり、ループ/割り当てのオーバーヘッドのみが削減されます。

編集:

言語実装の詳細により、これは(驚くべきことに)CPythonでそれを行う最も速い方法です。

def getUniqueElement1(A, B):
    ret = 0
    for a in A: ret = ret ^ a
    for b in B: ret = ret ^ b
    return ret

私はtimeitモジュールでこれをテストし、いくつかの興味深い結果を見つけました。表記がret = ret ^ aは、Pythonの方が速記よりも確かに高速ですret ^= a。また、ループの要素を繰り返し処理する方が、インデックスを繰り返し処理してからPythonで添字操作を行うよりもはるかに高速です。そのため、このコードは、Javaをコピーしようとした以前の方法よりもはるかに高速です。

いずれにせよ、その質問は偽であるので、正解はないというのがこの話の教訓だと思います。 OPが以下の別の回答で述べているように、これについてはO(m + n)よりも速く進むことはできず、先生は足を引っ張っているだけでした。したがって、問題は、2つの配列のすべての要素を反復処理し、それらすべてのXORを累積するための最速の方法を見つけることで減少します。これは、言語の実装に完全に依存していることを意味します。アルゴリズム全体が変更されないため、使用している実装で真の「最速」のソリューションを取得するために、いくつかのテストと再生を行います。

28
Shashank

さてさて、私たちは...もっと速い解決を期待している人に謝罪します。私の先生は私と一緒に少し楽しんでいたことがわかり、私は彼が言っていることの意味を完全に逃しました。

最初に、私が何を意味するのかを明確にする必要があります。

彼はそれを行うためのより速い方法があることをほのめかしました

私たちの会話の要点は次のとおりです:彼は私のXORアプローチは興味深いものであり、私が私の解決策にたどり着く方法についてしばらく話しました。私の解決策が最適であるかどうか私に尋ねました。私はそうしたと述べました(私の質問で述べた理由により)。その後、彼は「あなたはよろしいですか?」と彼の顔を見ながら尋ねました。 「独り善がり」。私は躊躇しましたが、ええと言いました。彼は私にそれを行うより良い方法を考えることができるかどうか尋ねました。私は「もっと速い方法があるということですか?」それについて考えるように言われました。

それで私はそれについて考えました、私の先生が私が知らない何かを知っていることを確認してください。そして、一日何も考えなかった後、私はここに来ました。

私の先生が実際に私にしてほしいと思ったことはdefend私の解決策が最適であること、ではないより良いソリューション。彼が言ったように、Niceアルゴリズムの作成は簡単な部分ですが、難しい部分はそれが機能することを証明しています(そしてそれが最高です)。彼は、Find-A-Better-Way LandでO(n)の簡単な証明を実行するよりもはるかに短い時間で済んだのではなく、最終的にそうしました。興味がある場合は、以下を参照してください)。

ここで学んだ大きな教訓だと思います。私はShashank Guptaの回答を受け入れます。なぜなら、質問に欠陥があったとしても、元の質問にはなんとか回答できると思うからです。

証明を入力しているときに見つけた小さなPythonワンライナーを手元に置いておきます。これは効率的ではありませんが、気に入っています:

def getUniqueElement(a, b):
    return reduce(lambda x, y: x^y, a + b)

非常に非公式な「証明」

質問の元の2つの配列abから始めましょう。

int[] a = {6, 5, 6, 3, 4, 2};
int[] b = {5, 7, 6, 6, 2, 3, 4};

ここでは、短い配列の長さはnであり、長い配列の長さはn + 1である必要があります。線形の複雑さを証明する最初のステップは、配列を3番目の配列に追加することです(これをcと呼びます)。

int[] c = {6, 5, 6, 3, 4, 2, 5, 7, 6, 6, 2, 3, 4};

長さは2n + 1です。なぜこれを行うのですか?さて、今、もう1つの問題があります。cで奇数回発生する要素を見つけることです(これ以降、「奇数回」と「一意」は同じことを意味すると見なされます)。これは実際には かなり人気のあるインタビューの質問 であり、明らかに私の先生が問題について考えを得た場所なので、私の質問にはいくつかの実用的な意味があります。やったー!

O(log n)のように、O(n)よりも高速なアルゴリズムisがあるとします。これは、cの要素のsomeのみにアクセスすることを意味します。たとえば、O(log n)アルゴリズムは、一意の要素を決定するために、サンプル配列の要素のlog(13)〜4をチェックするだけでよい場合があります。私たちの質問は、これは可能ですか?

最初に、要素のanyの削除(「削除」とは、アクセスする必要がないことを意味します)で済むかどうかを確認します。 2つの要素を削除して、アルゴリズムが長さ2n - 1cのサブ配列のみをチェックするようにしたらどうでしょうか?これは依然として線形の複雑さですが、それができれば、さらに改善できるかもしれません。

では、完全にランダムにcの2つの要素を選択して削除してみましょう。ここで実際に発生する可能性のあることがいくつかあります。これらをケースにまとめます。

// Case 1: Remove two identical elements
{6, 5, 6, 3, 4, 2, 5, 7, 2, 3, 4};

// Case 2: Remove the unique element and one other element
{6, 6, 3, 4, 2, 5, 6, 6, 2, 3, 4};

// Case 3: Remove two different elements, neither of which are unique
{6, 5, 6, 4, 2, 5, 7, 6, 6, 3, 4};

配列はどのようになっていますか?最初のケースでは、7はまだユニークな要素です。 2番目のケースでは、new一意の要素、5があります。3番目のケースでは、3つの一意の要素があります...ええ、それは完全に混乱しています。

さて、質問は次のようになります。このサブ配列を見ただけでcの一意の要素を特定できますか?最初のケースでは、7がサブ配列の一意の要素であることがわかりますが、cの一意の要素でもあるかどうかはわかりません。削除された2つの要素は、7と1である可能性もあります。2番目のケースにも同様の議論が当てはまります。ケース3では、3つの一意の要素があるため、cで2つが一意でない要素を特定する方法がありません。

2n - 1アクセスを使用しても、問題を解決するのに十分な情報がないことが明らかになります。したがって、最適なソリューションは線形ソリューションです。

もちろん、実際の証明は帰納法を使用し、例による証明は使用しませんが、私はそれを他の誰かに任せます:)

14
William Gaul

各値の数は、配列やハッシュマップなどのコレクションに格納できます。 O(n)すると、他のコレクションの値を確認して、一致しないとわかったらすぐに停止できます。これは、平均して2番目の配列の半分しか検索できないことを意味します。

7
Peter Lawrey

これは少し少し高速です:

public static int getUniqueElement(int[] a, int[] b) {
    int ret = 0;
    int i;
    for (i = 0; i < a.length; i++) {
        ret += (a[i] - b[i]);
    }
    return Math.abs(ret - b[i]);
}

それはO(m)ですが、順序は全体の話をしません。 「公式」ソリューションのループ部分には約3 * m + 3 * nの演算があり、少し高速なソリューションには4 * mがあります。

(ループ「i ++」と「i <a.length」をそれぞれ1つの操作として数えます)。

-アル。

3
A. I. Breveleri

要素の繰り返しが許可されている、ソートされていない2つの整数配列aとbがあるとします。 それらは同じです (含まれる要素に関して) を除いて アレイの1つは 追加の要素 ..

元の質問で2つの点を強調し、値がnon-zeroであるという追加の仮定を追加していることに注意してください。

C#では、これを行うことができます。

int[, , , , ,] a=new int[6, 5, 6, 3, 4, 2];
int[, , , , , ,] b=new int[5, 7, 6, 6, 2, 3, 4];
Console.WriteLine(b.Length/a.Length);

見る? 追加要素が何であれ、長さを除算するだけで常にそれを知ることができます。

これらのステートメントでは、与えられた一連の整数を配列の値として格納するのではなく、それらのdimensionsとして格納します。

整数の短い系列が与えられたとしても、長い整数は1つの余分な整数のみを持つべきです。したがって、整数の順序に関係なく、追加の整数がなければ、これらの2つの多次元配列の合計サイズは同じです。余分な次元に長い方のサイズを掛け、短い方のサイズで割ると、余分な整数が何であるかがわかります。

私があなたの質問から引用したように、この解決策はこの特定のケースでのみ機能します。それをJavaに移植したい場合があります。

質問自体はトリックだと思ったので、これは単なるトリックです。私たちは間違いなくそれを生産のためのソリューションとは見なしません。

1
Ken Kin

要素が1つだけ追加され、配列が最初と同じであるとすると、O(log(base 2)n)を押すことができます。

理論的根拠は、どの配列もバイナリでO(log n)を検索する必要があるということです。この場合を除いて、順序付けられた配列で値を検索するのではなく、最初の一致しない要素を検索します。このような状況では、a [n] == b [n]は低すぎることを意味し、a [n]!= b [n]はa [n-1] == bでない限り高すぎる可能性があることを意味します[n-1]。

残りは基本的なバイナリ検索です。真ん中の要素を確認し、回答が必要な部門を決定し、その部門でサブサーチを実行します。

1
Edwin Buck

注意、O(n + m)表記を使用するのは間違っています。 nであるサイズパラメータは1つだけです(漸近的な意味では、nとn + 1は等しい)。 O(n)とだけ言ってください。 [m> n + 1の場合、問題は異なり、より困難になります。]

他の人が指摘したように、すべての値を読み取る必要があるため、これは最適です。

できることは、漸近定数を減らすことだけです。明らかな解決策はすでに非常に効率的であるため、改善の余地はほとんどありません。 (10)の単一ループはおそらく打ちにくいです。少し展開すると、ブランチを回避することで(わずかに)改善するはずです。

目標が純粋なパフォーマンスである場合は、ベクトル化(AXV命令を使用して、一度に8整数)やマルチコアまたはGPGPUでの並列化など、移植性のないソリューションに頼る必要があります。古き良きダーティCと64ビットプロセッサでは、データを64ビット整数の配列にマップし、一度に2つのペアの要素をxorすることができます;)

1
Yves Daoust

より高速なアルゴリズムはありません。質問で提示されたものはO(n)にあります。これを解決するための算術的な「トリック」では、少なくとも両方の配列の各要素を1回読み取る必要があるため、O(n)(またはそれより悪い)のままにします。

O(n)(O(log n)など)の実際のサブセット内にある検索戦略には、ソートされた配列または他のビルド済みのソートされた構造(バイナリツリー、ハッシュ)が必要です。すべてのソート人類に知られているアルゴリズムは、平均して少なくともO(n * log n)(Quicksort、Hashsort)であり、O(n)よりも劣ります。

したがって、数学的な観点から見ると、高速アルゴリズムはありません。いくつかのコードの最適化があるかもしれませんが、ランタイムは配列の長さに比例して大きくなるので、大規模には関係ありません。

0
Hans Hohenfeld

これは 一致するナットとボルトの問題 に似ていると思います。

おそらくO(nlogn)でこれを実現できます。この場合、それがO(n + m)よりも小さいかどうかはわかりません。

0
Neeraj