Finden von Duplikaten in O (n) Zeit und O (1) Raum


121

Eingabe: Bei einem Array von n Elementen, das Elemente von 0 bis n-1 enthält, wobei eine dieser Zahlen beliebig oft vorkommt.

Ziel: Diese sich wiederholenden Zahlen in O (n) finden und nur konstanten Speicherplatz verwenden.

Zum Beispiel sei n 7 und das Array {1, 2, 3, 1, 3, 0, 6}, die Antwort sollte 1 & 3 sein. Ich habe hier ähnliche Fragen geprüft, aber die Antworten verwendeten einige Datenstrukturen wie HashSetusw.

Irgendein effizienter Algorithmus dafür?

Antworten:


164

Das habe ich mir ausgedacht, für das kein zusätzliches Vorzeichenbit erforderlich ist:

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

Die erste Schleife permutiert das Array, sodass sich xeiner dieser Einträge an der Position befindet , wenn das Element mindestens einmal vorhanden ist A[x].

Beachten Sie, dass es beim ersten Erröten möglicherweise nicht wie O (n) aussieht, aber es ist - obwohl es eine verschachtelte Schleife hat, läuft es immer noch in der O(N)Zeit. Ein Swap findet nur statt, wenn es einen isolchen gibt A[i] != i, und jeder Swap setzt mindestens ein Element so, dass A[i] == i, wo das vorher nicht wahr war. Dies bedeutet, dass die Gesamtzahl der Swaps (und damit die Gesamtzahl der Ausführungen des whileSchleifenkörpers) höchstens beträgt N-1.

Die zweite Schleife gibt die Werte aus, xfür die A[x]nicht gleich ist. xDa die erste Schleife garantiert, dass xeine dieser Instanzen vorhanden ist , wenn sie mindestens einmal im Array vorhanden ist, A[x]bedeutet dies, dass die Werte gedruckt werden, in xdenen sie nicht vorhanden sind das Array.

(Ideone-Link, damit Sie damit spielen können)


10
@arasmussen: Ja. Ich habe mir aber zuerst eine kaputte Version ausgedacht. Die Einschränkungen des Problems geben einen kleinen Hinweis auf die Lösung - die Tatsache, dass jeder gültige Array-Wert auch ein gültiger Array-Index ist a[a[i]], und die O (1) -Raumbeschränkung deuten darauf hin, dass die swap()Operation der Schlüssel ist.
Café

2
@caf: Bitte führen Sie Ihren Code mit dem Array aus, da {3,4,5,3,4} fehlschlägt.
NirmalGeo

6
@NirmalGeo: Dies ist keine gültige Eingabe, da sie 5nicht im Bereich liegt 0..N-1( Nin diesem Fall 5).
Café

2
@caf die Ausgabe für {1,2,3,1,3,0,0,0,0,6} ist 3 1 0 0 0 oder in jedem Fall, wenn die Wiederholung mehr als 2 ist. Ist es richtig o / p?
Terminal

3
Das ist großartig! Ich habe eine Reihe von Varianten dieser Frage gesehen, die normalerweise eingeschränkter sind, und dies ist der allgemeinste Weg, sie zu lösen, den ich gesehen habe. Ich werde nur erwähnen, dass das Ändern der printAnweisung print iin eine Lösung für stackoverflow.com/questions/5249985/… und (vorausgesetzt, die "Tasche" ist ein modifizierbares Array) Qk von stackoverflow.com/questions/3492302/… ist .
j_random_hacker

35

Die brillante Antwort von caf druckt jede Zahl, die k-mal im Array k-1-mal erscheint. Das ist ein nützliches Verhalten, aber die Frage verlangt wohl, dass jedes Duplikat nur einmal gedruckt wird, und er spielt auf die Möglichkeit an, dies zu tun, ohne die linearen Grenzen von Zeit und konstantem Raum zu überschreiten. Dies kann erreicht werden, indem seine zweite Schleife durch den folgenden Pseudocode ersetzt wird:

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

Dies nutzt die Eigenschaft aus, dass nach dem Ausführen der ersten Schleife, wenn ein Wert mmehr als einmal erscheint, garantiert ist, dass sich eine dieser Erscheinungen an der richtigen Position befindet, nämlich A[m]. Wenn wir vorsichtig sind, können wir diesen "Heimatort" verwenden, um Informationen darüber zu speichern, ob noch Duplikate gedruckt wurden oder nicht.

In der Version von caf wurde beim Durchlaufen des Arrays A[i] != iimpliziert, dass A[i]es sich um ein Duplikat handelt. In meiner Version verlasse ich mich auf eine etwas andere Invariante: Dies A[i] != i && A[A[i]] == A[i]impliziert, dass A[i]es sich um ein Duplikat handelt , das wir zuvor noch nicht gesehen haben . (Wenn Sie den Teil "den wir noch nicht gesehen haben" löschen, kann der Rest durch die Wahrheit der Invariante des Cafés und die Garantie, dass alle Duplikate eine Kopie an einem Heimatort haben, impliziert werden.) Diese Eigenschaft gilt für Der Anfang (nachdem die erste Runde des Cafés beendet ist) und ich zeigen unten, dass er nach jedem Schritt beibehalten wird.

Während wir das Array durchgehen, A[i] != iimpliziert der Erfolg des Tests, dass es A[i] sich um ein Duplikat handeln kann, das zuvor noch nicht gesehen wurde. Wenn wir es noch nicht gesehen haben, erwarten wir, dass A[i]der Heimatort auf sich selbst zeigt - darauf wird in der zweiten Hälfte der ifErkrankung getestet . Wenn dies der Fall ist, drucken wir es aus und ändern den Heimatort, um auf dieses zuerst gefundene Duplikat zu verweisen. Dadurch wird ein zweistufiger "Zyklus" erstellt.

Um zu sehen, dass diese Operation unsere Invariante nicht verändert, nehmen wir an, dass m = A[i]eine bestimmte Position izufriedenstellend ist A[i] != i && A[A[i]] == A[i]. Es ist offensichtlich, dass die von uns vorgenommene Änderung ( A[A[i]] = i) dazu beiträgt, zu verhindern, dass andere Ereignisse außerhalb des Hauses mals Duplikate ausgegeben werden, indem die zweite Hälfte ihrer ifBedingungen fehlschlägt. Funktioniert sie jedoch, wenn sie iam Heimatort eintrifft m? Ja, das wird es, denn jetzt, obwohl iwir bei dieser neuen Version feststellen, dass die erste Hälfte der ifBedingung A[i] != iwahr ist, prüft die zweite Hälfte, ob der Ort, auf den sie zeigt, ein Heimatort ist, und stellt fest, dass dies nicht der Fall ist. In dieser Situation wissen wir nicht mehr, ob der doppelte Wert war moder A[m]war, aber wir wissen das so oder so,Es wurde bereits berichtet , da diese 2 Zyklen garantiert nicht im Ergebnis der ersten Schleife des Cafés erscheinen. (Beachten Sie, dass wenn m != A[m]dann genau eines von mund A[m]mehrmals vorkommt und das andere überhaupt nicht vorkommt.)


1
Ja, das ist dem sehr ähnlich, was ich mir ausgedacht habe. Es ist interessant, wie nützlich eine identische erste Schleife für verschiedene Probleme ist, nur mit einer anderen Druckschleife.
Café

22

Hier ist der Pseudocode

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

Beispielcode in C ++


3
Sehr clever - Kodierung der Antwort im Vorzeichenbit des indizierten Eintrags!
Holtavolt

3
@sashang: Das kann nicht sein. Überprüfen Sie die Problemspezifikation. "Gegeben ein Array von n Elementen, das Elemente von 0 bis n-1 enthält "
Prasoon Saurav

5
Dies erkennt keine doppelten Nullen und erkennt dieselbe Zahl wie ein doppeltes.
Null Set

1
@Null Set: Sie können einfach ersetzen -mit ~dem Null Problem.
user541686

26
Dies mag die Antwort sein, auf die das Problem abzielt, aber technisch verwendet es O(n)verborgenen Raum - die nVorzeichenbits. Wenn das Array so definiert ist, dass jedes Element nur Werte zwischen 0und enthalten kann n-1, funktioniert es offensichtlich nicht.
Café

2

Für relativ kleine N können wir div / mod-Operationen verwenden

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

Nicht C / C ++ aber trotzdem

http://ideone.com/GRZPI


+1 Schöne Lösung. Wenn Sie das Hinzufügen von n zu einem Eintrag nach zwei Mal beenden, wird ein größeres n berücksichtigt .
Apshir

1

Nicht wirklich hübsch, aber zumindest sind die Eigenschaften O (N) und O (1) leicht zu erkennen. Grundsätzlich scannen wir das Array und sehen für jede Zahl, ob die entsprechende Position bereits einmal gesehen (N) oder mehrfach gesehen (N + 1) markiert wurde. Wenn es bereits einmal gesehen markiert ist, drucken wir es aus und markieren es bereits mehrfach gesehen. Wenn es nicht markiert ist, markieren wir es bereits einmal und verschieben den ursprünglichen Wert des entsprechenden Index an die aktuelle Position (das Markieren ist eine destruktive Operation).

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;
  }
}

oder noch besser (schneller, trotz der Doppelschleife):

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, es funktioniert gut, aber es hat ein wenig nachgedacht, um genau herauszufinden, warum es if (value > i) a[i--] = a[value];funktioniert: Wenn value <= iwir den Wert dann bereits bei verarbeitet haben a[value]und ihn sicher überschreiben können. Ich würde auch nicht sagen, dass die O (N) -Natur offensichtlich ist! Rechtschreibung: Die Hauptschleife läuft Nmal und wie oft die a[i--] = a[value];Linie läuft. Diese Zeile kann nur ausgeführt werden a[value] < N, wenn und jedes Mal unmittelbar danach ein Array-Wert festgelegt wird, der noch nicht Nfestgelegt wurde N, sodass sie höchstens Nfür insgesamt höchstens 2NSchleifeniterationen ausgeführt werden kann.
j_random_hacker

1

Eine Lösung in C ist:

#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;
}

Es ist O (n) Zeit und O (1) Raumkomplexität.


1
Die räumliche Komplexität ist O (N), da N zusätzliche Vorzeichenbits verwendet werden. Der Algorithmus sollte unter der Annahme arbeiten, dass der Array-Elementtyp nur Zahlen von 0 bis N-1 enthalten kann.
Café

Ja, das stimmt, aber für gefragtes Algo ist es perfekt, da sie das Algo nur für die Zahlen 0 bis n-1 wollten. Außerdem habe ich überprüft,
ob

1

Nehmen wir an, wir präsentieren dieses Array als unidirektionale Diagrammdatenstruktur - jede Zahl ist ein Scheitelpunkt und sein Index im Array zeigt auf einen anderen Scheitelpunkt, der eine Kante des Diagramms bildet.

Für noch mehr Einfachheit haben wir Indizes 0 bis n-1 und einen Zahlenbereich von 0..n-1. z.B

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

0 (3) -> 3 (3) ist ein Zyklus.

Antwort: Durchlaufen Sie einfach das Array anhand von Indizes. Wenn a [x] = a [y] ist, dann ist es ein Zyklus und somit ein Duplikat. Fahren Sie mit dem nächsten Index fort und fahren Sie bis zum Ende eines Arrays fort. Komplexität: O (n) Zeit und O (1) Raum.


0

Ein winziger Python-Code zur Demonstration der obigen Methode von caf:

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] )

Beachten Sie, dass der Austausch möglicherweise mehr als einmal für einen einzelnen iWert erfolgen muss - beachten Sie dies whilein meiner Antwort.
Café

0

Der Algorithmus ist in der folgenden C-Funktion leicht zu erkennen. Abrufen ursprüngliche Arrays, obwohl dies nicht erforderlich ist , möglich sein wird , jede Eingabe modulo unter 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 zum Testen.


Ich befürchte, dies ist technisch gesehen "Betrug", da für die Arbeit mit Zahlen bis zu 2 * n zusätzlich 1 Bit Speicherplatz pro Array-Eintrag erforderlich ist, als zum Speichern der ursprünglichen Zahlen erforderlich ist. Tatsächlich benötigen Sie näher an log2 (3) = 1,58 zusätzliche Bits pro Eintrag, da Sie Zahlen bis zu 3 * n-1 speichern.
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

Ich habe schnell eine Beispielspielplatz-App erstellt, um Duplikate in 0 (n) Zeitkomplexität und konstantem zusätzlichen Platz zu finden. Bitte überprüfen Sie die URL Finding Duplicates

Die oben genannte IMP- Lösung funktionierte, wenn ein Array Elemente von 0 bis n-1 enthält, wobei eine dieser Zahlen beliebig oft vorkommt.


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;
            }
        }

    }

Die obige Lösung erreicht die gleiche zeitliche Komplexität von O (n) und konstantem Raum.
user12704811

3
Vielen Dank für dieses Code-Snippet, das möglicherweise nur begrenzte kurzfristige Hilfe bietet. Eine richtige Erklärung würde ihren langfristigen Wert erheblich verbessern, indem sie zeigt, warum dies eine gute Lösung für das Problem ist, und es für zukünftige Leser mit anderen, ähnlichen Fragen nützlicher machen. Bitte bearbeiten Sie Ihre Antwort, um eine Erklärung hinzuzufügen, einschließlich der von Ihnen getroffenen Annahmen.
Toby Speight

3
Übrigens scheint die zeitliche Komplexität hier O (n²) zu sein - das Ausblenden der inneren Schleife ändert daran nichts.
Toby Speight

-2

Wenn das Array nicht zu groß ist, ist diese Lösung einfacher. Es wird ein weiteres Array mit derselben Größe zum Aktivieren erstellt.

1 Erstellen Sie eine Bitmap / ein Array mit der gleichen Größe wie Ihr Eingabearray

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

2 Scannen Sie Ihr Eingabearray und erhöhen Sie die Anzahl im obigen Array

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

3 Scannen Sie nun das Array check_list und drucken Sie das Duplikat entweder einmal oder so oft, wie es dupliziert wurde

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

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

Natürlich nimmt es doppelt so viel Platz ein wie die oben angegebene Lösung, aber die Zeiteffizienz beträgt O (2n), was im Grunde genommen O (n) ist.


Das ist kein O(1)Platz.
Daniel Kamil Kozar

Hoppla ...! habe das nicht bemerkt ... mein schlechtes.
Deepthought

@nikhil wie ist es O (1)?. Meine Array-Checkliste wächst linear mit zunehmender Größe der Eingabe. Wie ist es also mit O (1), wenn ja, mit welchen Heuristiken bezeichnen Sie sie als O (1)?
Deepthought

Für eine bestimmte Eingabe benötigen Sie konstanten Platz, ist das nicht O (1)? Ich könnte mich irren :)
Nikil

Meine Lösung benötigt mehr Platz, wenn die Eingabe wächst. Die Effizienz (Raum / Zeit) eines Algorithmus wird für eine bestimmte Eingabe nicht gemessen. (In diesem Fall wäre die Zeiteffizienz jedes Suchalgorithmus konstant, dh das Element, das im ersten Index gefunden wurde, in dem wir gesucht haben.) Es wird für jede Eingabe gemessen der Grund, warum wir den besten Fall, den schlechtesten Fall und den durchschnittlichen Fall haben.
Deepthought
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.