Recherche de doublons dans le temps O (n) et dans l'espace O (1)

121

Entrée: étant donné un tableau de n éléments qui contient des éléments de 0 à n-1, avec l'un de ces nombres apparaissant un nombre illimité de fois.

Objectif: trouver ces nombres répétés en O (n) et en n'utilisant que l'espace mémoire constant.

Par exemple, soit n 7 et array {1, 2, 3, 1, 3, 0, 6}, la réponse doit être 1 & 3. J'ai vérifié des questions similaires ici mais les réponses utilisaient des structures de données comme HashSetetc.

Un algorithme efficace pour le même?

Zaki
la source

Réponses:

164

Voici ce que j'ai proposé, qui ne nécessite pas le bit de signe supplémentaire:

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

La première boucle permute le tableau de sorte que si l'élément xest présent au moins une fois, alors l'une de ces entrées sera à la position A[x].

Notez qu'il peut ne pas sembler O (n) à première vue, mais c'est le cas - bien qu'il ait une boucle imbriquée, il fonctionne toujours dans le O(N)temps. Un échange se produit uniquement s'il existe un itel que A[i] != i, et chaque échange définit au moins un élément tel que A[i] == i, là où ce n'était pas vrai auparavant. Cela signifie que le nombre total de swaps (et donc le nombre total d'exécutions du whilecorps de la boucle) est au maximum N-1.

La deuxième boucle imprime les valeurs de xpour qui A[x]n'est pas égal x- puisque la première boucle garantit que si elle xexiste au moins une fois dans le tableau, l'une de ces instances sera à A[x], cela signifie qu'elle imprime les valeurs xdont ne sont pas présentes dans le tableau.

(Lien Ideone pour que vous puissiez jouer avec)

caf
la source
10
@arasmussen: Ouais. Cependant, j'ai d'abord proposé une version cassée. Les contraintes du problème donnent un petit indice sur la solution - le fait que chaque valeur de tableau valide est également un indice d'index de tableau valide a[a[i]], et la contrainte d'espace O (1) indique que l' swap()opération est la clé.
caf
2
@caf: Veuillez exécuter votre code avec le tableau comme {3,4,5,3,4} il échoue.
NirmalGeo
6
@NirmalGeo: Ce n'est pas une entrée valide, car elle 5n'est pas dans la plage 0..N-1( Ndans ce cas étant 5).
caf
2
@caf la sortie pour {1,2,3,1,3,0,0,0,0,6} est 3 1 0 0 0 ou dans tous les cas où la répétition est supérieure à 2. Est-ce correct o / p?
Terminal
3
Ceci est incroyable! J'ai vu un certain nombre de variantes sur cette question, généralement plus contraignantes, et c'est la façon la plus générale de la résoudre que j'ai vue. Je mentionnerai simplement que changer la printdéclaration en print itransforme cela en une solution à stackoverflow.com/questions/5249985/… et (en supposant que le "sac" est un tableau modifiable) Qk de stackoverflow.com/questions/3492302/… .
j_random_hacker
35

La réponse brillante de caf imprime chaque nombre qui apparaît k fois dans le tableau k-1 fois. C'est un comportement utile, mais la question demande sans doute que chaque duplicata soit imprimé une seule fois, et il fait allusion à la possibilité de le faire sans faire sauter les limites linéaires temps / espace constant. Cela peut être fait en remplaçant sa deuxième boucle par le pseudocode suivant:

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

Cela exploite la propriété qu'après l'exécution de la première boucle, si une valeur mapparaît plus d'une fois, alors l'une de ces apparences est garantie d'être dans la bonne position, à savoir A[m]. Si nous faisons attention, nous pouvons utiliser cet emplacement «d'origine» pour stocker des informations indiquant si des doublons ont déjà été imprimés ou non.

Dans la version de caf, lorsque nous avons parcouru le tableau, A[i] != icela impliquait qu'il s'agissait d' A[i]un doublon. Dans ma version, je m'appuie sur un invariant légèrement différent: cela A[i] != i && A[A[i]] == A[i]implique qu'il A[i]s'agit d'un doublon que nous n'avons jamais vu auparavant . (Si vous supprimez la partie "que nous n'avons jamais vu auparavant", le reste peut être vu comme étant impliqué par la vérité de l'invariant de caf et la garantie que tous les doublons ont une copie dans un emplacement d'origine.) Cette propriété tient à le début (après la fin de la première boucle de caf) et je montre ci-dessous qu'elle est maintenue après chaque étape.

Au fur et à mesure que nous parcourons le tableau, le succès A[i] != idu test implique qu'il A[i] pourrait s'agir d' un doublon qui n'a jamais été vu auparavant. Si nous ne l 'avons pas vu auparavant, nous nous attendons à ce que l A[i]' emplacement d 'origine de la personne se dirige vers lui - c'est ce qui est testé par la seconde moitié de la ifcondition. Si tel est le cas, nous l'imprimons et modifions l'emplacement du domicile pour qu'il pointe vers ce premier doublon trouvé, créant un «cycle» en 2 étapes.

Pour voir que cette opération ne modifie pas notre invariant, supposons m = A[i]qu'une position particulière soit isatisfaisante A[i] != i && A[A[i]] == A[i]. Il est évident que le changement que nous faisons ( A[A[i]] = i) travaillera pour éviter que d' autres événements non domestiques de md'être sortie comme des doublons en provoquant la 2ème moitié de leurs ifconditions à l' échec, mais il lorsqu'il iarrive à l'endroit de la maison, m? Oui, car maintenant, même si à ce nouveau inous constatons que la première moitié de la ifcondition A[i] != iest vraie, la deuxième moitié teste si l'emplacement vers lequel elle pointe est un emplacement d'origine et constate que ce n'est pas le cas. Dans cette situation, nous ne savons plus si mou A[m]était la valeur en double, mais nous savons que de toute façon,cela a déjà été signalé , car ces 2 cycles sont garantis de ne pas apparaître dans le résultat de la 1ère boucle de caf. (Notez que si m != A[m]alors exactement l'un des met A[m]se produit plus d'une fois, et l'autre ne se produit pas du tout.)

j_random_hacker
la source
1
Oui, c'est très similaire à ce que j'ai proposé. Il est intéressant de voir comment une première boucle identique est utile pour plusieurs problèmes différents, juste avec une boucle d'impression différente.
caf
22

Voici le 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

Exemple de code en C ++

Prasoon Saurav
la source
3
Très intelligent - encoder la réponse dans le bit de signe de l'entrée indexée!
holtavolt
3
@sashang: Ça ne peut pas être. Consultez la spécification du problème. "Étant donné un tableau de n éléments qui contient des éléments de 0 à n-1 "
Prasoon Saurav
5
Cela ne détectera pas les 0 en double et repérera le même nombre comme étant un doublon plusieurs fois.
Null Set le
1
@Null Set: Vous pouvez simplement remplacer -par ~pour le problème zéro.
user541686
26
C'est peut-être la réponse à laquelle le problème se dirige, mais techniquement, il utilise O(n)un espace caché - les nbits de signe. Si le tableau est défini de telle sorte que chaque élément ne peut contenir que des valeurs comprises entre 0et n-1, alors cela ne fonctionne évidemment pas.
caf
2

Pour un N relativement petit, nous pouvons utiliser des opérations 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

Pas C / C ++ mais de toute façon

http://ideone.com/GRZPI

hoha
la source
+1 Belle solution. L'arrêt de l'ajout de n à une entrée après deux fois peut accueillir un n plus grand .
Apshir
1

Pas vraiment joli mais au moins il est facile de voir les propriétés O (N) et O (1). Fondamentalement, nous parcourons le tableau et, pour chaque nombre, nous voyons si la position correspondante a été marquée déjà vue une fois (N) ou déjà vue plusieurs fois (N + 1). S'il est signalé déjà vu une fois, nous l'imprimons et le marquons déjà vu plusieurs fois. S'il n'est pas marqué, nous le marquons déjà vu une fois et nous déplaçons la valeur d'origine de l'index correspondant vers la position actuelle (le marquage est une opération destructive).

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

ou, mieux encore (plus rapide, malgré la double boucle):

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;
    }
  }
}
CAFxX
la source
+1, cela fonctionne bien, mais il a fallu un peu de réflexion pour comprendre exactement pourquoi cela if (value > i) a[i--] = a[value];fonctionne: si value <= ialors nous avons déjà traité la valeur à a[value]et pouvons l'écraser en toute sécurité. Je ne dirais pas non plus que la nature O (N) est évidente! Épelez-le: la boucle principale s'exécute N, plus le nombre de fois que la a[i--] = a[value];ligne s'exécute. Cette ligne ne peut s'exécuter que si a[value] < N, et à chaque fois qu'elle s'exécute, immédiatement après, une valeur de tableau qui n'était pas déjà Ndéfinie est définie sur N, de sorte qu'elle puisse s'exécuter la plupart du Ntemps, pour un total d'au plus des 2Nitérations de boucle.
j_random_hacker
1

Une solution en C est:

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

C'est la complexité du temps O (n) et de l'espace O (1).

Garg Anshul
la source
1
La complexité spatiale de ceci est O (N), car elle utilise N bits de signe supplémentaires. L'algorithme devrait fonctionner sous l'hypothèse que le type d'élément de tableau ne peut contenir que des nombres de 0 à N-1.
caf
oui c'est vrai mais pour l'algo demandé, c'est parfait car ils voulaient l'algo pour les nombres 0 à n-1 uniquement et j'ai également vérifié votre solution, elle va au-dessus de O (n) alors j'ai pensé à ceci
Anshul garg
1

Supposons que nous présentions ce tableau comme une structure de données de graphe unidirectionnelle - chaque nombre est un sommet et son index dans le tableau pointe vers un autre sommet formant une arête du graphe.

Pour encore plus de simplicité, nous avons des indices de 0 à n-1 et une plage de nombres de 0..n-1. par exemple

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

0 (3) -> 3 (3) est un cycle.

Réponse: Traversez simplement le tableau en vous basant sur les indices. si a [x] = a [y] alors c'est un cycle et donc dupliquer. Passer à l'index suivant et continuer encore et ainsi de suite jusqu'à la fin d'un tableau. Complexité: temps O (n) et espace O (1).

Ivan Voroshilin
la source
0

Un petit code python pour démontrer la méthode de caf ci-dessus:

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] )
vigne
la source
Notez que l'échange peut devoir se produire plus d'une fois pour une seule ivaleur - notez le whiledans ma réponse.
caf
0

L'algorithme peut être facilement vu dans la fonction C suivante. La récupération du tableau d'origine, bien que non obligatoire, sera possible en prenant chaque entrée 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 pour les tests.

Apshir
la source
J'ai bien peur que ce soit techniquement de la «triche», car travailler avec des nombres jusqu'à 2 * n nécessite 1 bit supplémentaire d'espace de stockage par entrée de tableau par rapport à ce qui est nécessaire pour stocker les numéros d'origine. En fait, vous avez besoin de plus près de log2 (3) = 1,58 bits supplémentaires par entrée, car vous stockez des nombres jusqu'à 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();
}
Eli
la source
0

J'ai créé un exemple d'application de jeu dans Swift pour trouver des doublons dans une complexité de temps 0 (n) et un espace supplémentaire constant. Veuillez vérifier l'URL Recherche de doublons

La solution IMP ci- dessus fonctionnait lorsqu'un tableau contient des éléments de 0 à n-1, l'un de ces nombres apparaissant un nombre illimité de fois.

CrazyPro007
la source
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;
            }
        }

    }
user12704811
la source
La solution ci-dessus permettra d'obtenir la même complexité temporelle de O (n) et d'espace constant.
user12704811
3
Merci pour cet extrait de code, qui pourrait fournir une aide limitée à court terme. Une explication appropriée améliorerait considérablement sa valeur à long terme en montrant pourquoi c'est une bonne solution au problème, et la rendrait plus utile aux futurs lecteurs avec d'autres questions similaires. Veuillez modifier votre réponse pour ajouter des explications, y compris les hypothèses que vous avez formulées.
Toby Speight le
3
BTW, la complexité temporelle semble être O (n²) ici - cacher la boucle interne ne change pas cela.
Toby Speight le
-2

Si le tableau n'est pas trop grand, cette solution est plus simple, elle crée un autre tableau de même taille pour le cocher.

1 Créez un bitmap / tableau de même taille que votre tableau d'entrée

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

2 scannez votre tableau d'entrée et incrémentez son nombre dans le tableau ci-dessus

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

3 Maintenant, scannez le tableau check_list et imprimez le duplicata une ou autant de fois qu'ils ont été dupliqués

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

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

Bien sûr, cela prend deux fois l'espace consommé par la solution donnée ci-dessus, mais l'efficacité en temps est O (2n) qui est fondamentalement O (n).

Pensée profonde
la source
Ce n'est pas de l' O(1)espace.
Daniel Kamil Kozar
Oups ...! je n'ai pas remarqué que ... mon mal.
Deepthought
@nikhil comment est-il O (1) ?. Mon tableau check_list croît linéairement à mesure que la taille de l'entrée augmente, alors comment est-ce O (1) si oui, quelles sont les heuristiques que vous utilisez pour l'appeler O (1).
Deepthought
Pour une entrée donnée, vous avez besoin d'un espace constant, n'est-ce pas O (1)? Je pourrais bien me tromper :)
nikhil
Ma solution a besoin de plus d'espace à mesure que l'entrée augmente. L'efficacité (espace / temps) d'un algorithme n'est pas mesurée pour une entrée particulière (dans ce cas, l'efficacité temporelle de chaque algorithme de recherche serait constante, c'est-à-dire l'élément trouvé dans le 1er index où nous avons recherché). la raison pour laquelle nous avons le meilleur cas, le pire des cas et la moyenne.
Deepthought