web-dev-qa-db-ja.com

メモリ効率の良いアプローチで配列内の重複を見つける

Aは整数の配列です。

すべての値は0からA.Length-1の間です

0 <= A[i] <= A.Length-1を意味します

繰り返し要素を見つけることになっています。繰り返し要素が複数ある場合は、繰り返し項目のインデックスが低い方を選択します。

例えば:

a = [3, 4, 2, 5, 2, 3]

それから

result = 2

これはインタビューの質問でした。別の配列を使用してアイテムを保存し、繰り返しているかどうかを確認しました。その後、いくつかのテストケースでタイムアウトが発生しました。インタビュアーは、配列を1回だけループし、追加のデータ構造を作成しないことを推奨しました。

22
Kiana Montazeri

別のデータ構造は必要ありません。入力自体をハッシュセットとして使用できます。

値が表示されるたびに、そのインデックスに対応するアイテムにA.Lengthを追加します。値は既にインクリメントされている可能性があるため、値はA[i] mod A.length

すでにA.length ..以上のアイテムを見つけた場合、繰り返しがあります。 (問題では、すべてのアイテムが間隔[0, A.Length-1]

繰り返し検出された最低のインデックスを追跡します。

これにより、O(N)複雑度(シングルパス)となり、追加のデータ構造、つまりサイズO(1)を使用しません。

このアプローチの背後にある重要な概念は、ハッシュセットがこのように機能することです。概念的には、これは鳩の巣の原理に間接的に関連しています。 https://en.wikipedia.org/wiki/Pigeonhole_principle

注:インタビュー中に、実装固有の質問をし、制限、仮定などについて議論することが重要です。-リスト内のアイテムのデータ型は何ですか? -値が[0..A.length-1]の範囲にある場合、すべてのアイテムは符号なしですか、または必要に応じて負の数を使用できますか? -など.

インタビュー中、私はこれが完璧な答えであると主張するのではなく、その仮定をインタビュアーと話し合い、それに応じて調整します。たとえば、別の答えは負の数を使用することを提案しましたが、アイテムのデータ型が符号なしの型などである可能性があります。

インタビューは、あなたの知識と創造性の両方を探求するための技術的な議論を引き起こすことになっています。

22
Juan Leni

注意:値がゼロの要素がある場合、ソリューションは失敗します。Olivierのソリューションはそのようなケースを処理できます。

A [i]のインデックスが負の要素を作成します。ループを1回だけ通過します。

for(int i=0; i<A.Length; i++)
    {
        if (A[Math.Abs(A[i])] < 0){ return Math.Abs(A[i]);}
        A[Math.Abs(A[i])] = -A[Math.Abs(A[i])];
    }
6
Aryan Firouzian

_yield return_を使用して、@ AryanFirouzianのソリューションを改良し、すべての重複を返したいと思います。また、一時変数を使用すると、コードが簡素化されます。

_public static IEnumerable<int> FindDuplicates(int[] A)
{
    for (int i = 0; i < A.Length; i++) {
        int absAi = Math.Abs(A[i]);
        if (A[absAi] < 0) {
            yield return absAi;
        } else {
            A[absAi] *= -1;
        }
    }
}
_

ただし、このソリューションでは、インデックスの低い要素は返されず、同一のコピーが2つ以上ある場合は、同じ値が複数回返されます。もう1つの問題は、0を負にできないことです。

より良いソリューションは結果の繰り返しを排除しますが、2番目のインデックスを返しますが、値が0の問題があります。また、インデックス自体を返し、誤ったインデックスの問題を示します

_public static IEnumerable<(int index, int value)> FindDuplicates(int[] A)
{
    for (int i = 0; i < A.Length; i++) {
        int x = A[i] % A.Length;
        if (A[x] / A.Length == 1) {
            yield return (i, x);
        }
        A[x] += A.Length;
    }
}
_

でテスト済み

_var A = new int[] { 3, 4, 2, 5, 2, 3, 3 };
foreach (var item in FindDuplicates(A)) {
    Console.WriteLine($"[{item.index}] = {item.value}");
}
_

返す

_[4] = 2
[5] = 3
_

これらのすべての問題を排除する私の最終的なソリューション(少なくともそうすることを望みます):値の最初の出現に_(i + 1) * A.Length_を追加することにより、最初のインデックス自体をエンコードします。 _(i + 1)_ iは_0_になる可能性があるため。その後、インデックスは逆の操作_(A[x] / A.Length) - 1_でデコードできます。

次に、最初の繰り返し値でのみ結果を返したいので、値を負の値に設定して、以降の処理から除外します。その後、Math.Abs(A[i]) % A.Lengthを使用して元の値を取得できます。

_public static IEnumerable<(int index, int value)> FindDuplicates(int[] A)
{
    for (int i = 0; i < A.Length; i++) {
        int x = Math.Abs(A[i]) % A.Length;
        if (A[x] >= 0) {
            if (A[x] < A.Length) { // First occurrence.
                A[x] += (i + 1) * A.Length; // Encode the first index.
            } else { // Second occurrence.
                int firstIndex = (A[x] / A.Length) - 1; // Decode the first index.
                yield return (firstIndex, x);

                // Mark the value as handeled by making it negative;
                A[x] *= -1; // A[x] is always >= A.Length, so no zero problem.
            }
        }
    }
}
_

期待される結果を返します

_[2] = 2
[0] = 3
_

私たちの要素は、アイデンティティを持たない整数です。つまり2つの等しいintを区別できないため、任意のインデックスで重複の1つを返すことができます。要素に同一性がある場合(値は等しいが参照が異なる参照型であるか、または等価性テストに関係しない追加フィールドがある場合)、最初の出現を返す必要があります

_yield return (firstIndex, Math.Abs(A[firstIndex]) % A.Length);
_

すべての要件を満たします。

問題の実装が必要な人には、受け入れられた回答を使用するものと、要素の反対を使用する別の回答のアプローチを使用する2つのバリアント(タグのようにc#で)をお勧めします。ただし、最後の解決策には値ゼロの問題があり、何らかのトリックが必要です。

最初の解決策

using System;
public class Program
{
    public static void Main()
    {
        int[] a = {3, 4, 0, 5, 2, 3};
        int N = 6;
        int min_index = 0; 
        bool found = false;
        int index = -1;
        int i = 0;
        while(i < N && !found)
        {

            if(a[i] >= N) 
                index = a[i] % N;
            else
                index = a[i];

            if(a[index] >= N) //its a duplicated elements 
            {
                min_index = i;
                found = true;
            }else
            {
                a[index] += N;
            }
            i++;

        }

        Console.WriteLine("Result = " + a[min_index] % N);
    }
}

第二の解決策

    using System;
public class Program
{
    public static void Main()
    {
        int[] a = {3, 4, 2, 5, 2, 3};
        int N = 6;
        int min_index = N-1; 
        bool found = false;
        int index = -1;
        int i = 0;
        while(i < N && !found)
        {
            if(a[i] == -N+1) //it was 0
                index = 0;
            else
                index = Math.Abs(a[i]);

            if(a[index] < 0 || a[index] == -N+1) //its a duplicated elements 
            {
                min_index = i;
                found = true;
            }else
            {
                if(a[index] > 0)
                {
                    a[index] = -a[index];
                }else
                {
                    a[index] += -N+1;
                }
            }
            i++;
        }

        if(a[min_index] == -N+1)
            a[min_index] = 0;

        Console.WriteLine("Result = " + Math.Abs(a[min_index]));
    }
}
2
Andrea Bellizzi