Trovare duplicati nel tempo O (n) e nello spazio O (1)


121

Input: dato un array di n elementi che contiene elementi da 0 a n-1, con uno qualsiasi di questi numeri che appare un numero qualsiasi di volte.

Obiettivo: trovare questi numeri ripetuti in O (n) e utilizzare solo lo spazio di memoria costante.

Ad esempio, sia n 7 e array {1, 2, 3, 1, 3, 0, 6}, la risposta dovrebbe essere 1 e 3. Ho controllato domande simili qui ma le risposte hanno utilizzato alcune strutture di dati come HashSetecc.

Qualche algoritmo efficiente per lo stesso?

Risposte:


164

Questo è ciò che mi è venuto in mente, che non richiede il bit di segno aggiuntivo:

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

Il primo ciclo permuta l'array in modo che se l'elemento xè presente almeno una volta, una di quelle voci sarà in posizione A[x].

Nota che potrebbe non sembrare O (n) a prima vista, ma lo è - sebbene abbia un ciclo annidato, funziona comunque nel O(N)tempo. Uno scambio si verifica solo se esiste un itale A[i] != i, e ogni scambio imposta almeno un elemento tale A[i] == i, dove prima non era vero. Ciò significa che il numero totale di swap (e quindi il numero totale di esecuzioni del whilecorpo del ciclo) è al massimo N-1.

Il secondo ciclo stampa i valori di xper i quali A[x]non è uguale x- poiché il primo ciclo garantisce che se xesiste almeno una volta nell'array, una di quelle istanze sarà a A[x], questo significa che stampa quei valori di xcui non sono presenti in l'array.

(Link ideone così puoi giocarci)


10
@arasmussen: Sì. Tuttavia, ho pensato prima a una versione non funzionante. I vincoli del problema danno un piccolo indizio sulla soluzione: il fatto che ogni valore di array valido è anche un indice di array valido suggerisce a[a[i]], e il vincolo di spazio O (1) suggerisce che l' swap()operazione sia la chiave.
caf

2
@caf: esegui il codice con l'array poiché {3,4,5,3,4} non riesce.
NirmalGeo

6
@NirmalGeo: questo non è un input valido, perché 5non è compreso nell'intervallo 0..N-1( Nin questo caso 5).
CAF

2
@caf l'output per {1,2,3,1,3,0,0,0,0,6} è 3 1 0 0 0 o comunque dove la ripetizione è maggiore di 2. È corretto o / p?
Terminal

3
È fantastico! Ho visto un numero di varianti su questa domanda, solitamente più vincolata, e questo è il modo più generale per risolverlo che ho visto. Menzionerò semplicemente che la modifica printdell'istruzione in la print itrasforma in una soluzione per stackoverflow.com/questions/5249985/… e (supponendo che "bag" sia un array modificabile) Qk di stackoverflow.com/questions/3492302/… .
j_random_hacker

35

La brillante risposta di caf stampa ogni numero che appare k volte nell'array k-1 volte. È un comportamento utile, ma la domanda richiede probabilmente che ogni duplicato venga stampato una sola volta, e allude alla possibilità di farlo senza superare i limiti di tempo lineare / spazio costante. Questo può essere fatto sostituendo il suo secondo ciclo con il seguente pseudocodice:

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

Questo sfrutta la proprietà che, dopo l'esecuzione del primo ciclo, se un valore mappare più di una volta, è garantito che uno di questi aspetti sia nella posizione corretta, ovvero A[m]. Se stiamo attenti, possiamo utilizzare quella posizione "home" per memorizzare le informazioni sul fatto che eventuali duplicati siano stati ancora stampati o meno.

Nella versione caf, mentre passavamo attraverso l'array, A[i] != iimplicava che A[i]fosse un duplicato. Nella mia versione, mi affido a un invariante leggermente diverso: ciò A[i] != i && A[A[i]] == A[i]implica che A[i]è un duplicato che non abbiamo mai visto prima . (Se si elimina la parte "che non abbiamo visto prima", il resto può essere visto come implicito dalla verità dell'invariante caf e dalla garanzia che tutti i duplicati abbiano una copia in una posizione domestica.) Questa proprietà è valida per all'inizio (al termine del primo ciclo di caf) e di seguito mostro che viene mantenuto dopo ogni passaggio.

Mentre passiamo attraverso l'array, il successo da A[i] != iparte del test implica che A[i] potrebbe essere un duplicato che non è stato visto prima. Se non l'abbiamo mai visto prima, allora ci aspettiamo che A[i]la posizione della casa punti a se stessa - questo è ciò che viene testato dalla seconda metà della ifcondizione. In tal caso, lo stampiamo e modifichiamo la posizione della casa in modo che faccia riferimento al primo duplicato trovato, creando un "ciclo" in due fasi.

Per vedere che questa operazione non altera il nostro invariante, supponiamo m = A[i]per una particolare posizione isoddisfacente A[i] != i && A[A[i]] == A[i]. E 'ovvio che il cambiamento che facciamo ( A[A[i]] = i) lavorerà per evitare che altri eventi non-casa di mdi essere uscita come duplicati provocando la seconda metà delle loro ifcondizioni di fallire, ma funzionerà quando iarriva alla posizione della propria abitazione, m? Sì, lo sarà, perché ora, anche se in questa nuova situazione itroviamo che la prima metà della ifcondizione A[i] != i,, è vera, la seconda metà verifica se la posizione a cui punta è una posizione domestica e scopre che non lo è. In questa situazione non sappiamo più se mo A[m]era il valore duplicato, ma sappiamo che in entrambi i casi,è già stato segnalato , perché è garantito che questi 2 cicli non compaiono nel risultato del primo ciclo di caf. (Nota che se m != A[m]poi esattamente uno di me si A[m]verifica più di una volta e l'altro non si verifica affatto.)


1
Sì, è molto simile a quello che mi è venuto in mente. È interessante come un primo ciclo identico sia utile per diversi problemi, solo con un ciclo di stampa diverso.
caf

22

Ecco lo pseudocodice

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

Codice di esempio in C ++


3
Molto intelligente: codificare la risposta nel bit di segno della voce indicizzata!
holtavolt

3
@sashang: non può essere. Controlla le specifiche del problema. "Dato un array di n elementi che contiene elementi da 0 a n-1 "
Prasoon Saurav

5
Questo non rileverà gli 0 duplicati e individuerà lo stesso numero come duplicato più volte.
Null Set

1
@Null Set: puoi semplicemente sostituire -con ~per il problema zero.
user541686

26
Questa potrebbe essere la risposta a cui sta guidando il problema, ma tecnicamente utilizza lo O(n)spazio nascosto: i nbit di segno. Se l'array è definito in modo tale che ogni elemento possa contenere solo valori compresi tra 0e n-1, ovviamente non funziona.
caf

2

Per N relativamente piccoli possiamo usare operazioni 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

Non C / C ++ ma comunque

http://ideone.com/GRZPI


+1 Bella soluzione. Fermare l'aggiunta di n ad una voce dopo due volte più grande può ospitare n .
Apshir

1

Non molto carino, ma almeno è facile vedere le proprietà O (N) e O (1). Fondamentalmente scansioniamo l'array e, per ogni numero, vediamo se la posizione corrispondente è stata contrassegnata come già vista una volta (N) o già vista più volte (N + 1). Se è contrassegnato come già visto una volta, lo stampiamo e lo contrassegniamo come già visto più volte. Se non è contrassegnato, lo contrassegniamo già visto una volta e spostiamo il valore originale dell'indice corrispondente nella posizione corrente (il contrassegno è un'operazione distruttiva).

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

o, meglio ancora (più veloce, nonostante il doppio loop):

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, funziona bene, ma ci è voluto un po 'di riflessione per capire esattamente perché if (value > i) a[i--] = a[value];funziona: se value <= ipoi abbiamo già elaborato il valore a a[value]e possiamo sovrascriverlo in sicurezza. Inoltre non direi che la natura O (N) è ovvia! Spiegazione: il ciclo principale viene eseguito Nvolte, più quante volte a[i--] = a[value];viene eseguita la linea. Quella riga può essere eseguita solo se a[value] < N, e ogni volta che viene eseguita, immediatamente dopo Nviene impostato un valore di matrice che non era già impostato N, quindi può essere eseguito la maggior parte delle Nvolte, per un totale al massimo di 2Niterazioni di ciclo.
j_random_hacker

1

Una soluzione in C è:

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

È la complessità del tempo O (n) e dello spazio O (1).


1
La complessità spaziale di questo è O (N), perché utilizza N bit di segno aggiuntivi. L'algoritmo dovrebbe funzionare partendo dal presupposto che il tipo di elemento dell'array possa contenere solo numeri da 0 a N-1.
caf

sì, è vero ma per l'algo chiesto è perfetto perché volevano solo l'algo per i numeri da 0 a n-1 e ho anche controllato la tua soluzione che andava sopra O (n), quindi ho pensato a questo
Anshul garg

1

Supponiamo di presentare questo array come una struttura dati del grafo unidirezionale: ogni numero è un vertice e il suo indice nell'array punta a un altro vertice che forma un bordo del grafico.

Per ancora più semplicità abbiamo indici da 0 a n-1 e un intervallo di numeri da 0..n-1. per esempio

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

0 (3) -> 3 (3) è un ciclo.

Risposta: basta attraversare l'array affidandosi agli indici. se a [x] = a [y] allora è un ciclo e quindi duplicato. Passa all'indice successivo e continua di nuovo e così via fino alla fine di un array. Complessità: tempo O (n) e spazio O (1).


0

Un piccolo codice Python per dimostrare il metodo caf sopra:

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

Nota che lo scambio potrebbe dover accadere più di una volta per un singolo ivalore: nota whilenella mia risposta.
caf

0

L'algoritmo può essere facilmente visto nella seguente funzione C. Il recupero dell'array originale, sebbene non richiesto, sarà possibile prendendo ogni voce 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');
}

Link Ideone per i test.


Temo che questo sia tecnicamente un "imbroglio", poiché lavorare con numeri fino a 2 * n richiede 1 bit extra di spazio di archiviazione per voce di array rispetto a quanto richiesto per memorizzare i numeri originali. In effetti è necessario un valore più vicino a log2 (3) = 1,58 bit extra per voce, perché stai memorizzando numeri fino a 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

Ho creato un'app di playground di esempio in Swift per trovare duplicati in 0 (n) complessità temporale e spazio extra costante. Si prega di controllare l'URL Finding Duplicates

IMP La soluzione sopra ha funzionato quando un array contiene elementi da 0 a n-1, con uno qualsiasi di questi numeri che appare un numero qualsiasi di volte.


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

    }

La soluzione di cui sopra otterrà la stessa complessità temporale di O (n) e spazio costante.
user12704811

3
Grazie per questo snippet di codice, che potrebbe fornire un aiuto limitato a breve termine. Una spiegazione adeguata migliorerebbe notevolmente il suo valore a lungo termine mostrando perché questa è una buona soluzione al problema e la renderebbe più utile per i futuri lettori con altre domande simili. Si prega di modificare la risposta di aggiungere qualche spiegazione, tra le ipotesi che hai fatto.
Toby Speight

3
A proposito, la complessità temporale sembra essere O (n²) qui - nascondere il ciclo interno non cambia questo.
Toby Speight

-2

Se l'array non è troppo grande questa soluzione è più semplice, crea un altro array della stessa dimensione per il ticking.

1 Creare una bitmap / matrice della stessa dimensione della matrice di input

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

2 scansiona il tuo array di input e aumenta il suo conteggio nell'array sopra

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

3 Ora scansiona l'array check_list e stampa il duplicato una o più volte sono stati duplicati

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

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

Ovviamente ci vuole il doppio dello spazio consumato dalla soluzione data sopra, ma l'efficienza temporale è O (2n) che è fondamentalmente O (n).


Questo non è O(1)spazio.
Daniel Kamil Kozar

oops ...! non ho notato che ... colpa mia.
Pensiero profondo

@nikhil com'è O (1) ?. La mia check_list dell'array cresce linearmente con l'aumentare della dimensione dell'input, quindi com'è O (1) se sì quali sono le euristiche che stai usando per chiamarlo O (1).
Deep Thought

Per un dato input è necessario uno spazio costante, non è O (1)? Potrei sbagliarmi :)
nikhil

La mia soluzione ha bisogno di più spazio man mano che l'input cresce. L'efficienza (spazio / tempo) di un algoritmo non viene misurata per un particolare input (in tal caso l'efficienza temporale di ogni algoritmo di ricerca sarebbe costante, ovvero l'elemento trovato nel 1 ° indice in cui abbiamo cercato). il motivo per cui abbiamo caso migliore, caso peggiore e caso medio.
Deep Thought
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.