>>> k = [[1, 2], [4], [5, 6, 2], [1, 2], [3], [4]]
>>> import itertools
>>> k.sort()
>>> list(k for k,_ in itertools.groupby(k))
[[1, 2], [3], [4], [5, 6, 2]]
itertools
offre souvent les solutions les plus rapides et les plus puissantes à ce type de problèmes, et est bien Cela vaut la peine de se familiariser intimement avec elle !-)
Modifier Comme je l'ai mentionné dans un commentaire, les efforts d'optimisation normaux se concentrent sur les grandes entrées (l'approche big-O) parce que c'est tellement plus facile que cela offre un bon retour sur les efforts. Mais parfois (essentiellement pour les "goulots d'étranglement tragiquement cruciaux" dans les boucles internes profondes d'un code qui repousse les limites de performance), il peut être nécessaire d'aller beaucoup plus loin dans le détail, en fournissant des distributions de probabilité, en décidant des mesures de performance à optimiser (la limite supérieure ou le 90e centile est peut-être plus important qu'une moyenne ou une médiane, selon les applications), en effectuant des vérifications éventuellement heuristiques au début pour choisir différents algorithmes en fonction des caractéristiques des données d'entrée, et ainsi de suite.
Des mesures minutieuses de la performance "ponctuelle" (code A par rapport au code B pour une entrée spécifique) font partie de ce processus extrêmement coûteux, et le module standard de bibliothèque timeit
est utile ici. Cependant, il est plus facile de l'utiliser à l'invite d'un shell. Par exemple, voici un court module pour présenter l'approche générale de ce problème, enregistrez-le sous le nom de nodup.py
:
import itertools
k = [[1, 2], [4], [5, 6, 2], [1, 2], [3], [4]]
def doset(k, map=map, list=list, set=set, tuple=tuple):
return map(list, set(map(tuple, k)))
def dosort(k, sorted=sorted, xrange=xrange, len=len):
ks = sorted(k)
return [ks[i] for i in xrange(len(ks)) if i == 0 or ks[i] != ks[i-1]]
def dogroupby(k, sorted=sorted, groupby=itertools.groupby, list=list):
ks = sorted(k)
return [i for i, _ in itertools.groupby(ks)]
def donewk(k):
newk = []
for i in k:
if i not in newk:
newk.append(i)
return newk
# sanity check that all functions compute the same result and don't alter k
if __name__ == '__main__':
savek = list(k)
for f in doset, dosort, dogroupby, donewk:
resk = f(k)
assert k == savek
print '%10s %s' % (f.__name__, sorted(resk))
Notez le contrôle de sanité (effectué lorsque vous faites simplement python nodup.py
) et la technique de levage de base (rendre les noms globaux constants locaux à chaque fonction pour plus de rapidité) pour mettre les choses sur un pied d'égalité.
Nous pouvons maintenant effectuer des vérifications sur la petite liste d'exemples :
$ python -mtimeit -s'import nodup' 'nodup.doset(nodup.k)'
100000 loops, best of 3: 11.7 usec per loop
$ python -mtimeit -s'import nodup' 'nodup.dosort(nodup.k)'
100000 loops, best of 3: 9.68 usec per loop
$ python -mtimeit -s'import nodup' 'nodup.dogroupby(nodup.k)'
100000 loops, best of 3: 8.74 usec per loop
$ python -mtimeit -s'import nodup' 'nodup.donewk(nodup.k)'
100000 loops, best of 3: 4.44 usec per loop
confirmant que l'approche quadratique a des constantes suffisamment petites pour la rendre intéressante pour les petites listes avec peu de valeurs dupliquées. Avec une courte liste sans doublons :
$ python -mtimeit -s'import nodup' 'nodup.donewk([[i] for i in range(12)])'
10000 loops, best of 3: 25.4 usec per loop
$ python -mtimeit -s'import nodup' 'nodup.dogroupby([[i] for i in range(12)])'
10000 loops, best of 3: 23.7 usec per loop
$ python -mtimeit -s'import nodup' 'nodup.doset([[i] for i in range(12)])'
10000 loops, best of 3: 31.3 usec per loop
$ python -mtimeit -s'import nodup' 'nodup.dosort([[i] for i in range(12)])'
10000 loops, best of 3: 25 usec per loop
l'approche quadratique n'est pas mauvaise, mais celles de sort et groupby sont meilleures. Etc, etc.
Si (comme l'obsession de la performance le suggère) cette opération est au cœur de la boucle interne de votre application qui repousse les limites, il vaut la peine d'essayer le même ensemble de tests sur d'autres échantillons d'entrée représentatifs, en détectant éventuellement une mesure simple qui pourrait heuristiquement vous permettre de choisir l'une ou l'autre approche (mais la mesure doit être rapide, bien sûr).
Il est également intéressant d'envisager de conserver une représentation différente pour les éléments suivants k
-- pourquoi faut-il que ce soit une liste de listes plutôt qu'un ensemble de tuples en premier lieu ? Si la tâche de suppression des doublons est fréquente, et que le profilage montre que c'est le goulot d'étranglement des performances du programme, conserver un ensemble de tuples en permanence et n'en tirer une liste de listes que si et quand cela est nécessaire, pourrait être plus rapide dans l'ensemble, par exemple.
1 votes
Par "ce n'est pas rapide", voulez-vous dire que vous l'avez chronométré et que ce n'est pas assez rapide pour votre application, ou que vous pensez que ce n'est pas rapide ?
0 votes
@Torsten, cela semble juste être trop de copie pour être une méthode intelligente. désolé, c'est mon intuition. copier les listes en tuples, puis en ensemble, puis revenir à la liste des listes (copier à nouveau les tuples en listes).
0 votes
@zaharpopov : ce n'est pas comme ça que Python fonctionne, rien ne le sera. copié Il s'agit simplement de nouveaux conteneurs pour les éléments existants (pour les ints, c'est à peu près la même chose).
3 votes
1. les temps pour les méthodes utilisant le tri sont déflatés, car "k" est rebondi à la variante triée. 2. La dernière méthode est plus rapide parce que la façon dont vous générez les données de test vous laisse avec au plus 4 éléments distincts. Essayez quelque chose comme K = [[int(u) for u in str(random.randrange(1, 1000))] for _ in range(100)]
0 votes
@Torsten : corrigé merci. mais la méthode de la boucle est toujours rapide même quand il n'y a qu'un seul doublon dans la liste de 10.
0 votes
@zaharpopov : Oui, pour les petites listes d'entrée, "loop" sera plus rapide. Sa complexité est O(mn), où m est le nombre d'éléments uniques (de n). Donc, moins il y a d'éléments uniques, plus il est rapide (c'est-à-dire linéaire pour les listes avec un seul élément unique). Pour les listes courtes, les facteurs constants de "sort" et "groupby" sont trop élevés pour qu'ils soient plus rapides que "loop".
0 votes
@zaharpopov, pour mesurer les performances dans un cas spécifique, utilisez le module de la bibliothèque standard.
timeit
à l'invite du shell (le plus simple et le plus sain, bien mieux que de l'intégrer dans un code). Je vais modifier mon A pour montrer cela.0 votes
@zaharpopov : comme le dit Torsten, la boucle est rapide avec de petits échantillons. C'est tout à fait normal dans les algorithmes qui encourent de petits coûts constants (créer une liste vide vd. créer une liste à partir d'un tuple). Mais la croissance est toujours quadratique, ce qui signifie que les méthodes linéaires seront plus rapides que la boucle à un moment donné.
0 votes
Je voudrais juste souligner que vous ne testez que sur ce que j'appellerais des listes "courtes". Pour moi, une liste "longue" comporterait peut-être 10 000 ou 100 000 éléments, voire quelques millions.