Znajdowanie duplikatów w czasie O (n) i przestrzeni O (1)


121

Dane wejściowe: biorąc pod uwagę tablicę n elementów, która zawiera elementy od 0 do n-1, przy czym każda z tych liczb pojawia się dowolną liczbę razy.

Cel: znaleźć te powtarzające się liczby w O (n) i używając tylko stałej przestrzeni pamięci.

Na przykład niech n wynosi 7, a tablica {1, 2, 3, 1, 3, 0, 6}, odpowiedź powinna wynosić 1 i 3. Sprawdziłem tutaj podobne pytania, ale w odpowiedziach wykorzystano pewne struktury danych, takie jak HashSetitp.

Jakiś skuteczny algorytm do tego samego?

Odpowiedzi:


164

Oto, co wymyśliłem, co nie wymaga dodatkowego bitu znaku:

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

Pierwsza pętla permutuje tablicę, więc jeśli element xwystępuje przynajmniej raz, to jeden z tych wpisów będzie na pozycji A[x].

Zauważ, że może nie wyglądać na O (n) na pierwszy rzut oka, ale tak jest - chociaż ma zagnieżdżoną pętlę, nadal działa w O(N)czasie. Zamiana występuje tylko wtedy, gdy istnieje itaka A[i] != i, a każda zamiana ustawia co najmniej jeden element taki A[i] == i, że wcześniej nie było to prawdą. Oznacza to, że całkowita liczba swapów (a tym samym całkowita liczba wykonań whileciała pętli) wynosi co najwyżej N-1.

Druga pętla wypisuje wartości xfor, które A[x]nie są równe x- ponieważ pierwsza pętla gwarantuje, że jeśli xistnieje co najmniej raz w tablicy, jedno z tych wystąpień będzie w miejscu A[x], oznacza to, że drukuje te wartości, xktórych nie ma w tablica.

(Link Ideone, abyś mógł się nim bawić)


10
@arasmussen: Tak. Najpierw jednak wymyśliłem zepsutą wersję. Ograniczenia problemu dają wskazówkę co do rozwiązania - fakt, że każda poprawna wartość tablicy jest również poprawną wskazówką indeksu tablicy a[a[i]], a ograniczenie spacji O (1) wskazuje na swap()kluczową operację.
kawiarnia

2
@caf: Uruchom swój kod z tablicą jako {3,4,5,3,4} niepowodzenie.
NirmalGeo,

6
@NirmalGeo: To nie jest poprawne dane wejściowe, ponieważ 5nie należy do zakresu 0..N-1( Nw tym przypadku jest 5).
kawiarnia

2
@caf wynik dla {1,2,3,1,3,0,0,0,0,6} to 3 1 0 0 0 lub w każdym przypadku, gdy liczba powtórzeń jest większa niż 2. Czy jest to poprawne o / p?
Terminal

3
To jest niesamowite! Widziałem wiele wariantów tego pytania, zwykle bardziej ograniczonych, i jest to najbardziej ogólny sposób rozwiązania tego problemu, jaki widziałem. Po prostu wspomnę, że zmiana printinstrukcji na print izmienia ją w rozwiązanie na stackoverflow.com/questions/5249985/ ... i (zakładając, że "torba" jest modyfikowalną tablicą) Qk of stackoverflow.com/questions/3492302/ ...
j_random_hacker

35

Znakomita odpowiedź caf wypisuje każdą liczbę, która pojawia się k razy w tablicy k-1 razy. To przydatne zachowanie, ale pytanie prawdopodobnie wymaga, aby każdy duplikat był drukowany tylko raz, a on nawiązuje do możliwości zrobienia tego bez przekraczania granic czasu liniowego / stałej przestrzeni. Można to zrobić, zastępując jego drugą pętlę następującym pseudokodem:

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

Wykorzystuje to właściwość, która mpolega na tym, że po uruchomieniu pierwszej pętli, jeśli jakakolwiek wartość pojawi się więcej niż raz, to jeden z tych wyglądów będzie miał właściwą pozycję, a mianowicie A[m]. Jeśli będziemy ostrożni, możemy użyć tej „domowej” lokalizacji do przechowywania informacji o tym, czy jakiekolwiek duplikaty zostały jeszcze wydrukowane, czy nie.

W wersji caf, gdy przeszliśmy przez tablicę, A[i] != isugerowaliśmy, że A[i]jest to duplikat. W mojej wersji opieram się na nieco innym niezmienniku: A[i] != i && A[A[i]] == A[i]oznacza A[i]to, że jest to duplikat , którego wcześniej nie widzieliśmy . (Jeśli pominiesz część „której nie widzieliśmy wcześniej”, resztę można uznać za wynikającą z prawdy niezmiennika kawiarni i gwarancji, że wszystkie duplikaty mają jakąś kopię w lokalizacji domowej). początek (po zakończeniu pierwszej pętli kawiarni) i pokazuję poniżej, że jest utrzymywany po każdym kroku.

Gdy przechodzimy przez tablicę, sukces A[i] != iczęści testu sugeruje, że A[i] może to być duplikat, którego wcześniej nie widziano. Jeśli wcześniej tego nie widzieliśmy, spodziewamy się A[i], że lokalizacja domu wskazuje na siebie - to jest sprawdzane pod kątem drugiej połowy ifwarunku. W takim przypadku drukujemy go i zmieniamy lokalizację początkową, aby wskazywała z powrotem na ten pierwszy znaleziony duplikat, tworząc dwuetapowy „cykl”.

Aby zobaczyć, że ta operacja nie zmienia naszego niezmiennika, załóżmy, że m = A[i]określona pozycja jest isatysfakcjonująca A[i] != i && A[A[i]] == A[i]. To oczywiste, że zmiana robimy ( A[A[i]] = i) będzie działać, aby zapobiec inne wystąpienia non-domu mod bycia wyjście jako duplikaty powodując 2. połowę swoich ifwarunkach zawodzą, ale to będzie działać, gdy iprzybywa do lokalizacji domowej m? Tak, będzie, ponieważ teraz, chociaż iteraz okazuje się, że pierwsza połowa ifwarunku A[i] != ijest prawdziwa, druga połowa sprawdza, czy lokalizacja, na którą wskazuje, jest lokalizacją domową i stwierdza, że ​​tak nie jest. W tej sytuacji już nie wiem czy mczy A[m]był duplikatem wartość, ale wiemy, że tak czy inaczej,zostało to już zgłoszone , ponieważ gwarantuje się, że te 2 cykle nie pojawią się w wyniku pierwszej pętli kawiarni. (Zauważ, że jeśli m != A[m]wtedy dokładnie jeden z mi A[m]występuje więcej niż raz, a drugi nie występuje w ogóle).


1
Tak, to jest bardzo podobne do tego, które wymyśliłem. Ciekawe, jak identyczna pierwsza pętla jest przydatna w przypadku kilku różnych problemów, tylko z inną pętlą drukowania.
kawiarnia

22

Oto pseudokod

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

Przykładowy kod w C ++


3
Bardzo sprytne - zakodowanie odpowiedzi w bicie znaku zindeksowanego wpisu!
holtavolt

3
@sashang: To niemożliwe. Sprawdź specyfikację problemu. „Biorąc pod uwagę tablicę n elementów, która zawiera elementy od 0 do n-1
Prasoon Saurav

5
To nie wykryje podwójnych zer i wykryje tę samą liczbę jako duplikat wiele razy.
Null Set

1
@Null Set: Można po prostu zastąpić -z ~o zerowej emisji.
user541686

26
Może to być odpowiedź, do której zmierza problem, ale technicznie wykorzystuje O(n)ukrytą przestrzeń - nbity znaku. Jeśli tablica jest zdefiniowana w taki sposób, że każdy element może przechowywać tylko wartości między 0a n-1, to oczywiście nie działa.
kawiarnia

2

Dla stosunkowo małego N możemy użyć operacji 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

Nie C / C ++, ale w każdym razie

http://ideone.com/GRZPI


+1 Niezłe rozwiązanie. Zatrzymanie dodawania n do wpisu po dwukrotnym uwzględnieniu większego n .
Apshir

1

Niezbyt ładne, ale przynajmniej łatwo jest zobaczyć właściwości O (N) i O (1). Zasadniczo skanujemy tablicę i dla każdej liczby sprawdzamy, czy odpowiadająca jej pozycja została oznaczona jako już widziana-raz (N), czy już-widziana-wiele razy (N + 1). Jeśli jest oznaczony jako już widziany raz, drukujemy go i wielokrotnie oznaczamy jako już widziany. Jeśli nie jest oflagowany, oznaczamy go już raz widzianym i przenosimy oryginalną wartość odpowiedniego indeksu do bieżącej pozycji (oznaczanie jest operacją destrukcyjną).

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

lub jeszcze lepiej (szybciej, pomimo podwójnej pętli):

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, działa dobrze, ale wymagało trochę namysłu, aby dowiedzieć się dokładnie, dlaczego if (value > i) a[i--] = a[value];działa: jeśli value <= iwięc przetworzyliśmy już wartość w a[value]i możemy ją bezpiecznie nadpisać. Nie powiedziałbym też, że natura O (N) jest oczywista! Literowanie: czas wykonania głównej pętli Nplus ile razy a[i--] = a[value];przebiega linia. Ta linia może być uruchamiana tylko wtedy a[value] < N, gdy i za każdym razem, gdy jest uruchamiana, zaraz potem wartość tablicy, która nie została jeszcze Nustawiona N, jest więc uruchamiana w większości Nprzypadków, w sumie co najwyżej 2Niteracji pętli.
j_random_hacker

1

Jedno rozwiązanie w C to:

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

Jest to złożoność przestrzeni O (n) i O (1).


1
Złożoność przestrzeni to O (N), ponieważ używa N dodatkowych bitów znaku. Algorytm powinien działać przy założeniu, że typ elementu tablicy może zawierać tylko liczby od 0 do N-1.
kawiarnia

tak, to prawda, ale dla zapytanego algo jest idealne, ponieważ chcieli algo tylko dla liczb od 0 do n-1, a także sprawdziłem twoje rozwiązanie, które przekracza O (n), więc pomyślałem o tym
Anshul garg

1

Załóżmy, że prezentujemy tę tablicę jako jednokierunkową strukturę danych grafu - każda liczba jest wierzchołkiem, a jej indeks w tablicy wskazuje na inny wierzchołek tworzący krawędź wykresu.

Dla jeszcze większej prostoty mamy indeksy od 0 do n-1 i zakres liczb od 0..n-1. na przykład

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

0 (3) -> 3 (3) to cykl.

Odpowiedź: Wystarczy przejść przez tablicę opartą na indeksach. jeśli a [x] = a [y], to jest to cykl, a zatem duplikat. Przejdź do następnego indeksu i kontynuuj jeszcze raz i tak dalej, aż do końca tablicy. Złożoność: O (n) czas i O (1) przestrzeń.


0

Mały kod w Pythonie pokazujący metodę caf powyżej:

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

Zwróć uwagę, że wymiana może nastąpić więcej niż raz dla jednej iwartości - zwróć uwagę na whilemoją odpowiedź.
kawiarnia

0

Algorytm można łatwo zobaczyć w poniższej funkcji C. Odzyskanie oryginalnej tablicy, chociaż nie jest wymagane, będzie możliwe z każdym wpisem modulo 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 do testowania.


Obawiam się, że jest to technicznie „oszustwo”, ponieważ praca z liczbami do 2 * n wymaga dodatkowego 1 bitu miejsca na wpis w tablicy ponad to, co jest wymagane do przechowywania oryginalnych liczb. W rzeczywistości potrzebujesz bliżej log2 (3) = 1,58 dodatkowych bitów na wpis, ponieważ przechowujesz liczby do 3 * n-1.
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

Stworzyłem jedną przykładową aplikację na plac zabaw w bardzo krótkim czasie, aby znaleźć duplikaty w czasie 0 (n) złożoności i stałej dodatkowej przestrzeni. Sprawdź adres URL Znajdowanie duplikatów

Rozwiązanie IMP Above działało, gdy tablica zawiera elementy od 0 do n-1, a każda z tych liczb pojawia się dowolną liczbę razy.


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

    }

Powyższe rozwiązanie pozwoli osiągnąć tę samą złożoność czasową O (n) i przestrzeni stałej.
user12704811

3
Dziękujemy za ten fragment kodu, który może zapewnić ograniczoną krótkoterminową pomoc. Właściwe wyjaśnienie znacznie poprawiłoby jego długoterminową wartość, pokazując, dlaczego jest to dobre rozwiązanie problemu i uczyniłoby go bardziej użytecznym dla przyszłych czytelników z innymi, podobnymi pytaniami. Proszę edytować swoją odpowiedź dodać kilka wyjaśnień, w tym założeń już wykonanych.
Toby Speight

3
Przy okazji, złożoność czasowa wydaje się być tutaj O (n²) - ukrycie wewnętrznej pętli tego nie zmienia.
Toby Speight

-2

Jeśli tablica nie jest zbyt duża, to rozwiązanie jest prostsze, tworzy kolejną tablicę o tym samym rozmiarze do zaznaczania.

1 Utwórz bitmapę / tablicę o takim samym rozmiarze jak tablica wejściowa

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

2 zeskanuj swoją tablicę wejściową i zwiększ jej liczbę w powyższej tablicy

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

3 Teraz zeskanuj tablicę check_list i wydrukuj duplikat raz lub tyle razy, ile zostały powielone

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

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

Oczywiście zajmuje to dwa razy więcej miejsca niż rozwiązanie podane powyżej, ale efektywność czasowa wynosi O (2n), czyli w zasadzie O (n).


To nie jest O(1)przestrzeń.
Daniel Kamil Kozar

ups ...! nie zauważyłem, że ... moja wina.
Głębokie przemyślenie

@nikhil jak to jest O (1) ?. Moja tablica check_list rośnie liniowo wraz ze wzrostem wielkości danych wejściowych, więc jak to jest O (1), a jeśli tak, jakie heurystyki używasz, aby nazwać to O (1).
Głębokie przemyślenie

Dla danego wejścia potrzebujesz stałej przestrzeni, czy to nie O (1)? Mogę się mylić :)
nikhil

Moje rozwiązanie wymaga więcej miejsca w miarę wzrostu nakładów. Wydajność (przestrzeń / czas) algorytmu nie jest mierzona dla danego wejścia (w takim przypadku efektywność czasowa każdego algorytmu wyszukiwania byłaby stała, tj. Element znajdujący się w pierwszym indeksie, w którym szukaliśmy). powód, dla którego mamy przypadek najlepszy, najgorszy i średni.
Deepthought
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.