Encontrar duplicados en O (n) tiempo y O (1) espacio


121

Entrada: Dada una matriz de n elementos que contiene elementos de 0 a n-1, y cualquiera de estos números aparece cualquier cantidad de veces.

Objetivo: encontrar estos números repetitivos en O (n) y usar solo espacio de memoria constante.

Por ejemplo, que n sea 7 y la matriz sea {1, 2, 3, 1, 3, 0, 6}, la respuesta debería ser 1 y 3. Verifiqué preguntas similares aquí, pero las respuestas usaron algunas estructuras de datos como HashSetetc.

¿Algún algoritmo eficiente para lo mismo?

Respuestas:


164

Esto es lo que se me ocurrió, que no requiere el bit de signo adicional:

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

El primer bucle permuta la matriz de modo que si el elemento xestá presente al menos una vez, una de esas entradas estará en la posición A[x].

Tenga en cuenta que puede no parecer O (n) a primera vista, pero lo es, aunque tiene un bucle anidado, todavía se ejecuta a O(N)tiempo. Un intercambio solo ocurre si hay un ital que A[i] != i, y cada intercambio establece al menos un elemento tal que A[i] == i, donde eso no era cierto antes. Esto significa que el número total de intercambios (y, por lo tanto, el número total de ejecuciones del whilecuerpo del bucle) es como máximo N-1.

El segundo ciclo imprime los valores xpara los cuales A[x]no es igual x, ya que el primer ciclo garantiza que si xexiste al menos una vez en la matriz, una de esas instancias estará en A[x], esto significa que imprime aquellos valores de los xcuales no están presentes en la matriz

(Ideone enlace para que puedas jugar con él)


10
@arasmussen: Sí. Sin embargo, primero se me ocurrió una versión rota. Las limitaciones del problema dan una pequeña pista sobre la solución: el hecho de que cada valor de matriz válido también es un índice de matriz válido sugiere a[a[i]], y la restricción de espacio O (1) sugiere que la swap()operación es clave.
caf

2
@caf: ejecute su código con la matriz ya que {3,4,5,3,4} falla.
NirmalGeo

66
@NirmalGeo: Esa no es una entrada válida, porque 5no está en el rango 0..N-1( Nen este caso, estar 5).
caf

2
@caf la salida para {1,2,3,1,3,0,0,0,0,6} es 3 1 0 0 0 o en cualquier caso donde la repetición es mayor que 2. ¿Es correcto o / p?
Terminal

3
¡Esto es increíble! He visto varias variantes en esta pregunta, generalmente más limitadas, y esta es la forma más general de resolverla que he visto. Simplemente mencionaré que cambiar la printdeclaración para print iconvertir esto en una solución para stackoverflow.com/questions/5249985/… y (suponiendo que la "bolsa" es una matriz modificable) Qk de stackoverflow.com/questions/3492302/… .
j_random_hacker

35

La brillante respuesta de caf imprime cada número que aparece k veces en la matriz k-1 veces. Ese es un comportamiento útil, pero la pregunta podría decirse que cada duplicado se imprima una sola vez, y alude a la posibilidad de hacerlo sin soplar los límites de tiempo lineal / espacio constante. Esto se puede hacer reemplazando su segundo bucle con el siguiente pseudocódigo:

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

Esto explota la propiedad de que después de que se ejecuta el primer ciclo, si algún valor maparece más de una vez, se garantiza que uno de esos aspectos esté en la posición correcta, es decir A[m]. Si tenemos cuidado, podemos usar esa ubicación de "hogar" para almacenar información sobre si todavía se han impreso o no duplicados.

En la versión de caf, a medida que avanzábamos por la matriz, A[i] != iimplicaba que A[i]era un duplicado. En mi versión, confío en un invariante ligeramente diferente: eso A[i] != i && A[A[i]] == A[i]implica que A[i]es un duplicado que no hemos visto antes . (Si deja caer la parte "que no hemos visto antes", el resto puede verse implicado por la verdad de invariante de caf, y la garantía de que todos los duplicados tienen alguna copia en la ubicación de una casa). Esta propiedad se mantiene en al principio (después de que finaliza el 1er bucle de caf) y muestro a continuación que se mantiene después de cada paso.

A medida que avanzamos por la matriz, el éxito por A[i] != iparte de la prueba implica que A[i] podría ser un duplicado que no se haya visto antes. Si no lo hemos visto antes, entonces esperamos que A[i]la ubicación de la casa se señale a sí misma, eso es lo que se prueba en la segunda mitad de la ifcondición. Si ese es el caso, lo imprimimos y modificamos la ubicación de inicio para volver a este primer duplicado encontrado, creando un "ciclo" de 2 pasos.

Para ver que esta operación no altera nuestra invariante, supongamos m = A[i]que una posición particular es isatisfactoria A[i] != i && A[A[i]] == A[i]. Es obvio que el cambio que hacemos ( A[A[i]] = i) funcionará para evitar que se produzcan mduplicados otros sucesos ajenos al hogar ifal hacer que falle la segunda mitad de sus condiciones, pero ¿funcionará cuando illegue a la ubicación del hogar m? Sí, porque ahora, a pesar de que en este nuevo inos encontramos con que la primera mitad de la ifcondición A[i] != ies verdadera, la segunda mitad prueba si la ubicación a la que apunta es una ubicación de origen y descubre que no lo es. En esta situación, ya no sabemos si el valor duplicado fue mo no A[m], pero sabemos que de cualquier manera,ya se ha informado , porque se garantiza que estos 2 ciclos no aparecerán en el resultado del primer bucle de caf. (Tenga en cuenta que si m != A[m]entonces exactamente uno de my A[m]ocurre más de una vez, y el otro no ocurre en absoluto).


1
Sí, eso es muy similar a lo que se me ocurrió. Es interesante cómo un primer bucle idéntico es útil para varios problemas diferentes, solo con un bucle de impresión diferente.
caf

22

Aquí está el pseudocódigo

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

Código de muestra en C ++


3
Muy inteligente: ¡codifica la respuesta en el bit de signo de la entrada indexada!
holtavolt

3
@sashang: No puede ser. Echa un vistazo a la especificación del problema. "Dada una matriz de n elementos que contiene elementos de 0 a n-1 "
Prasoon Saurav

55
Esto no detectará 0 duplicados y detectará el mismo número que un duplicado varias veces.
Null Set

1
@ Conjunto nulo: simplemente puede reemplazar -con ~el problema cero.
user541686

26
Esta puede ser la respuesta a la que se dirige el problema, pero técnicamente utiliza O(n)un espacio oculto: los nbits de los signos. Si la matriz se define de manera que cada elemento solo pueda contener valores entre 0y n-1, entonces obviamente no funciona.
caf

2

Para N relativamente pequeño podemos usar operaciones 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

No C / C ++ pero de todos modos

http://ideone.com/GRZPI


+1 Buena solución. Dejar de agregar n a una entrada después de dos veces acomodará n más grande .
Apshir

1

No es realmente bonito, pero al menos es fácil ver las propiedades O (N) y O (1). Básicamente, escaneamos la matriz y, para cada número, vemos si la posición correspondiente se ha marcado como ya visto una vez (N) o ya visto varias veces (N + 1). Si está marcado ya visto una vez, lo imprimimos y lo marcamos ya visto varias veces. Si no está marcado, lo marcamos ya visto una vez y movemos el valor original del índice correspondiente a la posición actual (marcar es una operación destructiva).

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, mejor aún (más rápido, a pesar del doble bucle):

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, funciona bien, pero me tomó un poco de tiempo entender exactamente por qué if (value > i) a[i--] = a[value];funciona: si value <= iya hemos procesado el valor en a[value]y podemos sobrescribirlo de manera segura. ¡Tampoco diría que la naturaleza O (N) es obvia! Explicándolo: el bucle principal ejecuta Ntiempos, más las veces a[i--] = a[value];que se ejecuta la línea. Esa línea solo puede ejecutarse si a[value] < N, y cada vez que se ejecuta, inmediatamente después se establece un valor de matriz que aún no estaba Nconfigurado N, por lo que puede ejecutarse en la mayoría de las Nveces, para un total de 2Niteraciones de bucle como máximo .
j_random_hacker

1

Una solución en C es:

#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 O (n) tiempo y O (1) complejidad espacial.


1
La complejidad espacial de esto es O (N), porque usa N bits de signo adicionales. El algoritmo debería funcionar bajo el supuesto de que el tipo de elemento de matriz solo puede contener números del 0 al N-1.
caf

sí, eso es cierto, pero para preguntar algo es perfecto, ya que querían el algo para los números 0 a n-1 y también verifiqué que su solución está por encima de O (n), así que pensé en esto
Anshul garg el

1

Supongamos que presentamos esta matriz como una estructura de datos de gráfico unidireccional: cada número es un vértice y su índice en la matriz apunta a otro vértice que forma un borde del gráfico.

Para mayor simplicidad, tenemos índices de 0 a n-1 y rango de números de 0..n-1. p.ej

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

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

Respuesta: solo recorra la matriz basándose en índices. si a [x] = a [y] entonces es un ciclo y, por lo tanto, se duplica. Salte al siguiente índice y continúe de nuevo y así sucesivamente hasta el final de una matriz. Complejidad: O (n) tiempo y O (1) espacio.


0

Un pequeño código de Python para demostrar el método de caf anterior:

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

Tenga en cuenta que es posible que el intercambio tenga que suceder más de una vez por un solo ivalor; tenga whileen cuenta lo que aparece en mi respuesta.
caf

0

El algoritmo se puede ver fácilmente en la siguiente función C. La recuperación de la matriz original, aunque no es necesaria, será posible tomando cada módulo de entrada 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 para pruebas.


Me temo que esto es técnicamente "trampa", ya que trabajar con números de hasta 2 * n requiere 1 bit adicional de espacio de almacenamiento por entrada de matriz sobre lo que se requiere para almacenar los números originales. De hecho, necesita más cerca de log2 (3) = 1.58 bits adicionales por entrada, porque está almacenando números de hasta 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

He creado una aplicación de juegos de muestra de forma rápida para encontrar duplicados en 0 (n) complejidad de tiempo y espacio extra constante. Por favor revise la url Encontrar duplicados

La solución IMP anterior funcionó cuando una matriz contiene elementos de 0 a n-1, y cualquiera de estos números aparece varias veces.


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 solución anterior logrará lo mismo en la complejidad temporal de O (n) y el espacio constante.
user12704811

3
Gracias por este fragmento de código, que podría proporcionar una ayuda limitada a corto plazo. Una explicación adecuada mejoraría en gran medida su valor a largo plazo al mostrar por qué esta es una buena solución al problema y lo haría más útil para futuros lectores con otras preguntas similares. Por favor, editar su respuesta a añadir un poco de explicación, incluyendo los supuestos realizados.
Toby Speight

3
Por cierto, la complejidad del tiempo parece ser O (n²) aquí: ocultar el bucle interno no cambia eso.
Toby Speight

-2

Si la matriz no es demasiado grande, esta solución es más simple, crea otra matriz del mismo tamaño para marcar.

1 Cree un mapa de bits / matriz del mismo tamaño que su matriz de entrada

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

2 escanee su matriz de entrada e incremente su recuento en la matriz anterior

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

3 Ahora escanee la matriz check_list e imprima el duplicado una o tantas veces como se hayan duplicado

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

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

Por supuesto, toma el doble del espacio consumido por la solución dada anteriormente, pero la eficiencia de tiempo es O (2n), que es básicamente O (n).


Esto no es O(1)espacio.
Daniel Kamil Kozar

¡Uy ...! no me di cuenta de que ... mi mal.
Pensamiento profundo

@nikhil ¿cómo es O (1) ?. Mi matriz check_list crece linealmente a medida que aumenta el tamaño de la entrada, entonces, ¿cómo es O (1)? Si es así, ¿cuáles son las heurísticas que está utilizando para llamarlo O (1)?
Pensamiento profundo

Para una entrada dada, necesita espacio constante, ¿no es eso O (1)? Bien podría estar equivocado :)
nikhil

Mi solución necesita más espacio a medida que crece la entrada. La eficiencia (espacio / tiempo) de un algoritmo no se mide para una entrada en particular. (En tal caso, la eficiencia en el tiempo de cada algoritmo de búsqueda sería constante, es decir, el elemento encontrado en el primer índice donde buscamos). Eso se mide para cualquier entrada, eso es La razón por la cual tenemos el mejor caso, el peor caso y el caso promedio.
Pensamiento profundo
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.