web-dev-qa-db-fra.com

Un algorithme plus rapide pour trouver un élément unique entre deux tableaux?

MODIFIER : Pour toute personne nouvelle à cette question, j'ai publié une réponse clarifiant ce qui se passait. La réponse acceptée est celle qui, selon moi, répond le mieux à ma question telle qu'elle a été publiée à l'origine, mais pour plus de détails, veuillez consulter ma réponse.

NOTE : Ce problème était à l'origine un pseudocode et des listes utilisées. Je l'ai adapté à Java et tableaux. Donc, même si j'aimerais voir des solutions qui utilisent des astuces spécifiques à Java (ou des astuces dans n'importe quel langage d'ailleurs!), Rappelez-vous simplement que le le problème d'origine est indépendant de la langue.

Le problème

Disons qu'il existe deux tableaux entiers non triés a et b, avec répétition d'élément autorisée. Ils sont identiques (par rapport aux éléments contenus) sauf l'un des tableaux a un élément supplémentaire. Par exemple:

int[] a = {6, 5, 6, 3, 4, 2};
int[] b = {5, 7, 6, 6, 2, 3, 4};

Concevez un algorithme qui prend en entrée ces deux tableaux et génère le seul entier unique (dans le cas ci-dessus, 7).

La solution (jusqu'à présent)

Je suis venu avec ceci:

public static int getUniqueElement(int[] a, int[] b) {
    int ret = 0;
    for (int i = 0; i < a.length; i++) {
        ret ^= a[i];
    }
    for (int i = 0; i < b.length; i++) {
        ret ^= b[i];
    }
    return ret;
}

La solution "officielle" présentée en classe:

public static int getUniqueElement(int[] a, int[] b) {
    int ret = 0;
    for (int i = 0; i < a.length; i++) {
        ret += a[i];
    }
    for (int i = 0; i < b.length; i++) {
        ret -= b[i];
    }
    return Math.abs(ret);
}

Donc, les deux font conceptuellement la même chose. Et étant donné que a est de longueur m et b est de longueur n, alors les deux solutions ont un temps d'exécution de O (m + n).

La question

Plus tard, j'ai pu parler avec mon professeur et il a laissé entendre qu'il y avait une manière encore plus rapide de le faire. Honnêtement, je ne vois pas comment; pour savoir si un élément est unique, il semble qu'il faille au moins regarder chaque élément. Au moins c'est O (m + n) ... non?

Existe-t-il un moyen plus rapide? Et si oui, c'est quoi?

59
William Gaul

C'est probablement le plus rapide que vous pouvez faire en Java en utilisant la suggestion de HotLick dans les commentaires. Il fait l'hypothèse que b.length == a.length + 1 donc b est le plus grand tableau avec l'élément supplémentaire "unique".

public static int getUniqueElement(int[] a, int[] b) {
    int ret = 0;
    int i;
    for (i = 0; i < a.length; i++) {
        ret = ret ^ a[i] ^ b[i];
    }
    return ret ^ b[i];
}

Même si l'hypothèse ne peut pas être faite, vous pouvez facilement l'étendre pour inclure le cas où a ou b peut être le plus grand tableau avec l'élément unique. C'est toujours O (m + n) et seule la surcharge de boucle/affectation est réduite.

Éditer:

En raison des détails de l'implémentation du langage, c'est toujours (étonnamment) le moyen le plus rapide de le faire dans CPython.

def getUniqueElement1(A, B):
    ret = 0
    for a in A: ret = ret ^ a
    for b in B: ret = ret ^ b
    return ret

J'ai testé cela avec le module timeit et j'ai trouvé des résultats intéressants. Il s'avère que la main ret = ret ^ a est en effet plus rapide en Python qu'en raccourci ret ^= a. Il est également beaucoup plus rapide d'itérer sur les éléments d'une boucle que d'itérer sur les index et d'effectuer des opérations en indice en Python. C'est pourquoi ce code est beaucoup plus rapide que ma méthode précédente où j'avais essayé de copier Java.

Je suppose que la morale de l'histoire est qu'il n'y a pas de bonne réponse parce que la question est de toute façon bidon. Comme l'OP l'a noté dans une autre réponse ci-dessous, il s'avère que vous ne pouvez pas vraiment aller plus vite que O (m + n) sur ce point et son professeur lui tirait juste la jambe. Ainsi, le problème se résume à trouver le moyen le plus rapide d'itérer sur tous les éléments dans les deux tableaux et à accumuler le XOR de chacun d'eux. Et cela signifie qu'il dépend entièrement de l'implémentation du langage, et vous devez faites des tests et jouez pour obtenir la vraie solution "la plus rapide" dans l'implémentation que vous utilisez, car l'algorithme global ne changera pas.

28
Shashank

D'accord, nous allons ... excuses à tous ceux qui s'attendent à une solution plus rapide. Il s'avère que mon professeur s'amusait un peu avec moi et j'ai complètement raté le point de ce qu'il disait.

Je devrais commencer par clarifier ce que je voulais dire par:

il a laissé entendre qu'il y avait une manière plus rapide de le faire

L'essentiel de notre conversation était le suivant: il a dit que mon XOR approche était intéressante, et nous avons discuté pendant un moment de la façon dont j'étais parvenu à ma solution. Il m'a demandé si je pensais que ma solution était optimale . - J'ai dit que oui (pour les raisons que j'ai mentionnées dans ma question). Puis il m'a demandé: "Êtes-vous bien sûr?" avec un regard sur son visage, je ne peux que décrire comme "suffisant". J'étais hésitant mais j'ai dit oui. Il m'a demandé si je pouvais penser à une meilleure façon de le faire. Je me disais "Tu veux dire qu'il y a un moyen plus rapide?" mais au lieu de me donner une réponse directe, il m'a dit d'y penser. J'ai dit que je le ferais.

Alors j'y ai pensé, sûr que mon professeur savait quelque chose que je ne savais pas. Et après n'avoir rien trouvé pendant une journée, je suis venu ici.

Ce que mon professeur voulait vraiment que je fasse, c'est défendre ma solution comme étant optimale, pas essayer de trouver une meilleure solution . Comme il le dit: créer un algorithme de Nice est la partie facile, la partie difficile prouve que cela fonctionne (et que c'est le meilleur). Il pensait que c'était assez drôle que j'ai passé autant de temps dans Find-A-Better-Way Land au lieu d'élaborer une simple preuve de O(n) qui aurait pris beaucoup moins de temps ( nous avons fini par le faire, voir ci-dessous si vous êtes intéressé).

Donc je suppose que la grande leçon apprise ici. J'accepterai la réponse de Shashank Gupta parce que je pense qu'elle le fait parvient à répondre à la question d'origine, même si la question était défectueuse.

Je vous laisse les gars avec un petit Python one-liner que j'ai trouvé en tapant la preuve. Ce n'est pas plus efficace mais j'aime ça:

def getUniqueElement(a, b):
    return reduce(lambda x, y: x^y, a + b)

Une "preuve" très informelle

Commençons par les deux tableaux d'origine de la question, a et b:

int[] a = {6, 5, 6, 3, 4, 2};
int[] b = {5, 7, 6, 6, 2, 3, 4};

Nous dirons ici que le tableau le plus court a une longueur n, alors le tableau le plus long doit avoir une longueur n + 1. La première étape pour prouver la complexité linéaire consiste à ajouter les tableaux ensemble dans un troisième tableau (nous l'appellerons c):

int[] c = {6, 5, 6, 3, 4, 2, 5, 7, 6, 6, 2, 3, 4};

qui a une longueur 2n + 1. Pourquoi faire ceci? Eh bien, nous avons maintenant un tout autre problème: trouver l'élément qui se produit un nombre impair de fois dans c (à partir de maintenant, "nombre impair de fois" et "unique" signifient la même chose). C'est en fait une question d'entrevue assez populaire et c'est apparemment où mon professeur a eu l'idée de son problème, alors maintenant ma question a une signification pratique. Hourra!

Supposons qu'il y ait est un algorithme plus rapide que O (n), tel que O (log n). Cela signifie qu'il n'accédera qu'à certains des éléments de c. Par exemple, un algorithme O (log n) peut ne devoir vérifier que log (13) ~ 4 des éléments de notre exemple de tableau pour déterminer l'élément unique. Notre question est, est-ce possible?

Voyons d'abord si nous pouvons nous en sortir en supprimant any des éléments (en "supprimant" je veux dire ne pas avoir à y accéder). Que diriez-vous si nous supprimons 2 éléments, de sorte que notre algorithme vérifie uniquement un sous-tableau de c de longueur 2n - 1? Il s'agit toujours d'une complexité linéaire, mais si nous pouvons le faire, nous pourrons peut-être l'améliorer encore plus.

Donc, choisissons deux éléments de c complètement au hasard pour les supprimer. Il y a en fait plusieurs choses qui pourraient arriver ici, que je résumerai en cas:

// Case 1: Remove two identical elements
{6, 5, 6, 3, 4, 2, 5, 7, 2, 3, 4};

// Case 2: Remove the unique element and one other element
{6, 6, 3, 4, 2, 5, 6, 6, 2, 3, 4};

// Case 3: Remove two different elements, neither of which are unique
{6, 5, 6, 4, 2, 5, 7, 6, 6, 3, 4};

À quoi ressemble maintenant notre réseau? Dans le premier cas, 7 est toujours l'élément unique. Dans le deuxième cas, il y a un nouveau élément unique, 5. Et dans le troisième cas, il y a maintenant 3 éléments uniques ... oui, c'est un gâchis total là-bas.

Maintenant, notre question devient: pouvons-nous déterminer l'élément unique de c simplement en regardant ce sous-tableau? Dans le premier cas, nous voyons que 7 est l'élément unique du sous-tableau, mais nous ne pouvons pas être sûrs que c'est également l'élément unique de c; les deux éléments supprimés auraient tout aussi bien pu être 7 et 1. Un argument similaire s'applique pour le deuxième cas. Dans le cas 3, avec 3 éléments uniques, nous n'avons aucun moyen de savoir lesquels ne sont pas uniques dans c.

Il devient clair que même avec 2n - 1 accède, il n'y a tout simplement pas assez d'informations pour résoudre le problème. Et donc la solution optimale est linéaire.

Bien sûr, une vraie preuve utiliserait l'induction et non pas la preuve par l'exemple, mais je laisse cela à quelqu'un d'autre :)

14
William Gaul

Vous pouvez stocker le nombre de chaque valeur dans une collection telle qu'un tableau ou une carte de hachage. O(n) alors vous pouvez vérifier les valeurs de l'autre collection et arrêter dès que vous savez que vous avez une correspondance manquée. Cela pourrait signifier que vous ne recherchez en moyenne que la moitié du deuxième tableau.

7
Peter Lawrey

C'est un pe peu plus rapide:

public static int getUniqueElement(int[] a, int[] b) {
    int ret = 0;
    int i;
    for (i = 0; i < a.length; i++) {
        ret += (a[i] - b[i]);
    }
    return Math.abs(ret - b[i]);
}

C'est O (m), mais l'ordre ne raconte pas toute l'histoire. La partie en boucle de la solution "officielle" a environ 3 * m + 3 * n opérations, et la solution légèrement plus rapide a 4 * m.

(Compter la boucle "i ++" et "i <a.length" comme une opération chacun).

-Al.

3
A. I. Breveleri

Disons qu'il y a deux tableaux entiers non triés a et b, avec répétition d'élément autorisée. Ils sont identiques (en ce qui concerne les éléments contenus) sauf l'un des tableaux a un élément supplémentaire ..

Vous pouvez noter que j'ai souligné deux points dans votre question initiale, et j'ajoute une hypothèse supplémentaire selon laquelle les valeurs sont non nulles.

En C #, vous pouvez faire ceci:

int[, , , , ,] a=new int[6, 5, 6, 3, 4, 2];
int[, , , , , ,] b=new int[5, 7, 6, 6, 2, 3, 4];
Console.WriteLine(b.Length/a.Length);

Voir? Quel que soit le élément supplémentaire, vous le saurez toujours en divisant simplement leur longueur.

Avec ces instructions, nous ne stockons pas la série donnée d'entiers en tant que valeurs dans des tableaux, mais en tant que dimensions.

Comme quelle que soit la série d'entiers la plus courte donnée, la plus longue ne devrait avoir qu'un seul entier supplémentaire. Donc, peu importe l'ordre des entiers, sans celui supplémentaire, la taille totale de ces deux tableaux multidimensionnels est identique. La dimension supplémentaire multiplie par la taille du plus long, et pour diviser par la taille du plus court, nous savons ce qu'est l'entier supplémentaire.

Cette solution ne fonctionnerait que pour ce cas particulier, comme je l'ai cité dans votre question. Vous voudrez peut-être le porter sur Java.

Ce n'est qu'un truc, car je pensais que la question elle-même était un truc. Nous ne le considérerons certainement pas comme une solution de production.

1
Ken Kin

En supposant qu'un seul élément a été ajouté et que les tableaux étaient identiques pour commencer, vous pouvez appuyer sur O (log (base 2) n).

La raison en est que tout tableau est soumis à une recherche binaire O (log n). Sauf que dans ce cas, vous ne recherchez pas une valeur dans un tableau ordonné, vous recherchez le premier élément non correspondant. Dans une telle circonstance, a [n] == b [n] signifie que vous êtes trop bas et a [n]! = B [n] signifie que vous pourriez être trop haut, sauf si a [n-1] == b [n-1].

Le reste est une recherche binaire de base. Vérifiez l'élément du milieu, décidez quelle division doit avoir la réponse et effectuez une sous-recherche sur cette division.

1
Edwin Buck

Attention, il est faux d'utiliser la notation O (n + m). Il n'y a qu'un seul paramètre de taille qui est n (au sens asymptotique, n et n + 1 sont égaux). Vous devez simplement dire O (n). [Pour m> n + 1, le problème est différent et plus difficile.]

Comme indiqué par d'autres, c'est optimal car vous devez lire toutes les valeurs.

Tout ce que vous pouvez faire est de réduire la constante asymptotique. Il y a peu de place à l'amélioration, car les solutions évidentes sont déjà très efficaces. La boucle unique en (10) est probablement difficile à battre. Le dérouler un peu devrait s'améliorer (légèrement) en évitant une branche.

Si votre objectif est la pure performance, vous devriez vous tourner vers des solutions non portables telles que la vectorisation (en utilisant les instructions AXV, 8 pouces à la fois) et la parallélisation sur multicœurs ou GPGPU. Dans un bon vieux C sale et un processeur 64 bits, vous pouvez mapper les données sur un tableau d'entrées 64 bits et xor les éléments deux paires à la fois;)

1
Yves Daoust

Il n'y a tout simplement pas d'algorithme plus rapide. Celles présentées dans la question sont en O (n). Toute "astuce" arithmétique pour résoudre ce problème nécessitera au moins la lecture de chaque élément des deux tableaux, donc nous restons dans O(n) (ou pire).

Toute stratégie de recherche qui se trouve dans un sous-ensemble réel de O(n) (comme O (log n)) nécessitera des tableaux triés ou une autre structure triée pré-construite (arbre binaire, hachage). les algorithmes connus de l'humanité sont au moins O (n * log n) (Quicksort, Hashsort) en moyenne, ce qui est pire que O (n).

Par conséquent, d'un point de vue mathématique, il n'y a pas d'algorithme plus rapide. Il peut y avoir des optimisations de code, mais elles n'auront pas d'importance à grande échelle, car le temps d'exécution augmentera de façon linéaire avec la longueur du ou des tableaux.

0
Hans Hohenfeld

Je pense que cela est similaire à Problème de boulons et d'écrous correspondant .

Vous pourriez peut-être y parvenir en O (nlogn). Je ne sais pas si c'est plus petit que O (n + m) dans ce cas.

0
Neeraj