Duplikate aus einer Liste von Listen entfernen

116

Ich habe eine Liste von Listen in Python:

k = [[1, 2], [4], [5, 6, 2], [1, 2], [3], [4]]

Und ich möchte doppelte Elemente daraus entfernen. War es eine normale Liste, nicht von Listen, die ich verwenden könnte set. Leider ist diese Liste nicht hashbar und kann keine Listen erstellen. Nur von Tupeln. So kann ich alle Listen in Tupel umwandeln und dann set und zurück zu Listen verwenden. Das geht aber nicht schnell.

Wie kann dies am effizientesten erfolgen?

Das Ergebnis der obigen Liste sollte sein:

k = [[5, 6, 2], [1, 2], [3], [4]]

Es ist mir egal, Ordnung zu bewahren.

Hinweis: Diese Frage ist ähnlich, aber nicht ganz das, was ich brauche. SO gesucht, aber kein genaues Duplikat gefunden.


Benchmarking:

import itertools, time


class Timer(object):
    def __init__(self, name=None):
        self.name = name

    def __enter__(self):
        self.tstart = time.time()

    def __exit__(self, type, value, traceback):
        if self.name:
            print '[%s]' % self.name,
        print 'Elapsed: %s' % (time.time() - self.tstart)


k = [[1, 2], [4], [5, 6, 2], [1, 2], [3], [5, 2], [6], [8], [9]] * 5
N = 100000

print len(k)

with Timer('set'):
    for i in xrange(N):
        kt = [tuple(i) for i in k]
        skt = set(kt)
        kk = [list(i) for i in skt]


with Timer('sort'):
    for i in xrange(N):
        ks = sorted(k)
        dedup = [ks[i] for i in xrange(len(ks)) if i == 0 or ks[i] != ks[i-1]]


with Timer('groupby'):
    for i in xrange(N):
        k = sorted(k)
        dedup = list(k for k, _ in itertools.groupby(k))

with Timer('loop in'):
    for i in xrange(N):
        new_k = []
        for elem in k:
            if elem not in new_k:
                new_k.append(elem)

"loop in" (quadratische Methode) am schnellsten für kurze Listen. Bei langen Listen ist es schneller als bei allen anderen, außer bei der Groupby-Methode. Macht das Sinn?

Für eine kurze Liste (die im Code) 100000 Iterationen:

[set] Elapsed: 1.3900001049
[sort] Elapsed: 0.891000032425
[groupby] Elapsed: 0.780999898911
[loop in] Elapsed: 0.578000068665

Für eine längere Liste (die im Code wurde 5 Mal dupliziert):

[set] Elapsed: 3.68700003624
[sort] Elapsed: 3.43799996376
[groupby] Elapsed: 1.03099989891
[loop in] Elapsed: 1.85900020599
zaharpopov
quelle
1
Meinen Sie mit "das ist nicht schnell", dass Sie es zeitlich festgelegt haben und es für Ihre Anwendung nicht schnell genug ist, oder dass Sie denken, dass es nicht schnell ist?
Torsten Marek
@ Torsten, es scheint einfach zu viel zu kopieren, um eine kluge Methode zu sein. Entschuldigung, Bauchgefühl. Kopieren Sie Listen in Tupel, dann in Set, dann zurück in die Liste der Listen (kopieren Sie erneut Tupel in Listen)
Zaharpopov
@zaharpopov: So funktioniert Python nicht, nichts wird kopiert , nur neue Container für die vorhandenen Elemente (obwohl es für Ints ziemlich gleich ist)
Jochen Ritzel
3
1. Die Timings für die Methoden, die die Sortierung verwenden, werden entleert, da "k" auf die sortierte Variante zurückgebunden wird. 2. Die letzte Methode ist schneller, da Sie durch die Art und Weise, wie Sie die Testdaten generieren, höchstens 4 verschiedene Elemente erhalten. Versuchen Sie es mit etw. wie K = [[int (u) für u in str (random.randrange (1, 1000))] für _ in range (100)]
Torsten Marek
@ Torsten: behoben danke. aber immer noch ist die Schleifenmethode schnell, selbst wenn es nur ein Duplikat in der Liste von 10 gibt
zaharpopov

Antworten:

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

itertoolsoft bietet die schnellsten und leistungsstärksten Lösungen für diese Art von Problemen, und ist gut lohnt sich , mit bestens vertraut! -)

Bearbeiten : Wie ich in einem Kommentar erwähne, konzentrieren sich normale Optimierungsbemühungen auf große Eingaben (der Big-O-Ansatz), da dies so viel einfacher ist, dass es eine gute Rendite bietet. Aber manchmal (im Wesentlichen für "tragisch entscheidende Engpässe" in tiefen inneren Codeschleifen, die die Grenzen von Leistungsgrenzen überschreiten) muss man möglicherweise viel detaillierter vorgehen, Wahrscheinlichkeitsverteilungen bereitstellen und entscheiden, welche Leistungsmaße optimiert werden sollen (möglicherweise die Obergrenze oder Das 90. Zentil ist wichtiger als ein Durchschnitt oder Median, abhängig von den eigenen Apps. Es führt zu Beginn möglicherweise heuristische Überprüfungen durch, um je nach den Eigenschaften der Eingabedaten unterschiedliche Algorithmen auszuwählen.

Sorgfältige Messungen der "Punkt" -Leistung (Code A gegenüber Code B für eine bestimmte Eingabe) sind Teil dieses äußerst kostspieligen Prozesses, und das Standardbibliotheksmodul timeithilft hier. Es ist jedoch einfacher, es an einer Shell-Eingabeaufforderung zu verwenden. Im Folgenden finden Sie ein kurzes Modul, in dem der allgemeine Ansatz für dieses Problem vorgestellt wird. Speichern Sie es wie folgt 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))

Beachten Sie die Überprüfung der geistigen Gesundheit (wird durchgeführt, wenn Sie dies gerade tun python nodup.py) und die grundlegende Hebetechnik (machen Sie konstante globale Namen für jede Funktion lokal, um die Geschwindigkeit zu erhöhen), um die Dinge gleichberechtigt zu machen.

Jetzt können wir die winzige Beispielliste überprüfen:

$ 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

Bestätigung, dass der quadratische Ansatz klein genug ist, um für kleine Listen mit wenigen doppelten Werten attraktiv zu sein. Mit einer kurzen Liste ohne Duplikate:

$ 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

Der quadratische Ansatz ist nicht schlecht, aber die Sortierung und die Gruppierung sind besser. Usw. usw.

Wenn sich diese Operation (wie die Besessenheit von der Leistung nahelegt) in einer inneren Kernschleife Ihrer Anwendung zum Überschreiten der Grenzen befindet, lohnt es sich, die gleichen Tests an anderen repräsentativen Eingabebeispielen durchzuführen, um möglicherweise eine einfache Maßnahme zu ermitteln, die Sie heuristisch zulassen könnte Wählen Sie den einen oder anderen Ansatz (aber die Maßnahme muss natürlich schnell sein).

Es lohnt sich auch, eine andere Darstellung beizubehalten k- warum muss es sich überhaupt um eine Liste von Listen und nicht um eine Reihe von Tupeln handeln? Wenn die Aufgabe zum Entfernen von Duplikaten häufig ist und die Profilerstellung zeigt, dass es sich um den Leistungsengpass des Programms handelt, kann es beispielsweise insgesamt schneller sein, ständig eine Reihe von Tupeln beizubehalten und nur dann eine Liste von Listen abzurufen, wenn und wo dies erforderlich ist.

Alex Martelli
quelle
@alex danke für die Alternative. Diese Methode ist ungefähr so ​​schnell wie die von Danben, ein paar% schneller
Zaharpopov
@alex: Seltsamerweise ist dies langsamer als eine naive quadratische Methode für kürzere Listen (siehe Frage bearbeiten)
Zaharpopov
@zaharpopov: so ist es nur in deinem speziellen Fall, vgl. Mein Kommentar zur Frage.
Torsten Marek
@zaharpopov, wenn Sie eine Wahrscheinlichkeitsverteilung der Listen- und Unterlistenlängen sowie die Wahrscheinlichkeit von Duplikaten angeben, ist es (mit großem Aufwand) möglich, die Wahrscheinlichkeitsverteilung für die Laufzeit für einen bestimmten Code zu berechnen / zu messen und das von Ihnen benötigte Maß zu optimieren (Median, Mittelwert, 90.) Zentil, was auch immer). Aufgrund des sehr niedrigen ROI wird dies kaum jemals durchgeführt: Normalerweise konzentriert man sich auf den viel einfacheren Fall großer Eingaben (Big-O-Ansatz), bei dem minderwertige Algorithmen die Leistung erheblich beeinträchtigen würden. Und ich sehe sowieso keine Wahrscheinlichkeitsverteilungen in deinem Q angegeben ;-).
Alex Martelli
@zaharpov, ich bin froh, dass es dir gefallen hat!
Alex Martelli
21

Manuell ausführen, eine neue kListe erstellen und Einträge hinzufügen, die bisher nicht gefunden wurden:

k = [[1, 2], [4], [5, 6, 2], [1, 2], [3], [4]]
new_k = []
for elem in k:
    if elem not in new_k:
        new_k.append(elem)
k = new_k
print k
# prints [[1, 2], [4], [5, 6, 2], [3]]

Einfach zu verstehen, und Sie behalten die Reihenfolge des ersten Auftretens jedes Elements bei, sollte dies nützlich sein, aber ich denke, die Komplexität ist quadratisch, wenn Sie das gesamte new_kElement nach jedem Element durchsuchen .

Paul Stephenson
quelle
@ Paul: sehr seltsam - diese Methode ist schneller als alle anderen
Zaharpopov
Ich vermute, dass diese Methode bei sehr langen Listen nicht schneller ist. Dies hängt von Ihrer Anwendung ab: Wenn Sie wirklich nur Listen mit sechs Elementen und zwei Duplikaten haben, ist jede Lösung wahrscheinlich schnell genug und Sie sollten den klarsten Code verwenden.
Paul Stephenson
@zaharpopov, es ist nicht quadratisch in Ihrem Benchmark, weil Sie die gleiche Liste immer und immer wieder duplizieren. Sie messen mit einem linearen Eckfall.
Mike Graham
k = ([[1, 2], [4], [5, 6, 2], [1, 2], [3], [5, 2], [6], [8], [9]] +[[x] for x in range(1000)]) *5wird das quadratische Verhalten schön zeigen
John La Rooy
17
>>> k = [[1, 2], [4], [5, 6, 2], [1, 2], [3], [4]]
>>> k = sorted(k)
>>> k
[[1, 2], [1, 2], [3], [4], [4], [5, 6, 2]]
>>> dedup = [k[i] for i in range(len(k)) if i == 0 or k[i] != k[i-1]]
>>> dedup
[[1, 2], [3], [4], [5, 6, 2]]

Ich weiß nicht, ob es unbedingt schneller ist, aber Sie müssen Tupel und Sets nicht verwenden.

danben
quelle
Danke danben. das schneller als sich Tupeln zuzuwenden, dann 'setzen' und dann zurück zu Listen?
Zaharpopov
Sie können dies leicht testen - schreiben Sie beide Dedupierungsmethoden, generieren Sie einige zufällige Listen mit randomund messen Sie sie mit time.
Danben
4

Für alle setbisherigen Lösungen für dieses Problem muss setvor der Iteration ein Ganzes erstellt werden.

Es ist möglich, dies faul zu machen und gleichzeitig die Reihenfolge beizubehalten, indem die Liste der Listen iteriert und zu einem "Gesehenen" hinzugefügt wird set. Geben Sie dann nur dann eine Liste aus, wenn diese in diesem Tracker nicht gefunden wird set.

Dieses unique_everseenRezept ist in den itertools Dokumenten verfügbar . Es ist auch in der toolzBibliothek von Drittanbietern verfügbar :

from toolz import unique

k = [[1, 2], [4], [5, 6, 2], [1, 2], [3], [4]]

# lazy iterator
res = map(list, unique(map(tuple, k)))

print(list(res))

[[1, 2], [4], [5, 6, 2], [3]]

Beachten Sie, dass eine tupleKonvertierung erforderlich ist, da Listen nicht hashbar sind.

jpp
quelle
3

Sogar Ihre "lange" Liste ist ziemlich kurz. Haben Sie sie auch so ausgewählt, dass sie mit den tatsächlichen Daten übereinstimmen? Die Leistung hängt davon ab, wie diese Daten tatsächlich aussehen. Beispielsweise wird eine kurze Liste immer wieder wiederholt, um eine längere Liste zu erstellen. Dies bedeutet, dass die quadratische Lösung in Ihren Benchmarks linear ist, in der Realität jedoch nicht.

Für tatsächlich große Listen ist der festgelegte Code die beste Wahl - er ist linear (obwohl platzhungrig). Die Sortier- und Groupby-Methoden sind O (n log n) und die Schleife in der Methode ist offensichtlich quadratisch, sodass Sie wissen, wie diese skaliert werden, wenn n wirklich groß wird. Wenn dies die tatsächliche Größe der Daten ist, die Sie analysieren, wen interessiert das dann? Es ist winzig.

Übrigens sehe ich eine spürbare Beschleunigung, wenn ich keine Zwischenliste für die Erstellung des Sets bilde, dh wenn ich ersetze

kt = [tuple(i) for i in k]
skt = set(kt)

mit

skt = set(tuple(i) for i in k)

Die wirkliche Lösung kann von weiteren Informationen abhängen: Sind Sie sicher, dass eine Liste von Listen wirklich die Darstellung ist, die Sie benötigen?

Mike Graham
quelle
3

Die Liste der Tupel und {} kann verwendet werden, um Duplikate zu entfernen

>>> [list(tupl) for tupl in {tuple(item) for item in k }]
[[1, 2], [5, 6, 2], [3], [4]]
>>> 
SuperNova
quelle
1

Erstellen Sie ein Wörterbuch mit Tupel als Schlüssel und drucken Sie die Schlüssel.

  • Erstellen Sie ein Wörterbuch mit Tupel als Schlüssel und Index als Wert
  • Liste der Schlüssel des Wörterbuchs drucken

k = [[1, 2], [4], [5, 6, 2], [1, 2], [3], [4]]

dict_tuple = {tuple(item): index for index, item in enumerate(k)}

print [list(itm) for itm in dict_tuple.keys()]

# prints [[1, 2], [5, 6, 2], [3], [4]]
SuperNova
quelle
1

Das sollte funktionieren.

k = [[1, 2], [4], [5, 6, 2], [1, 2], [3], [4]]

k_cleaned = []
for ele in k:
    if set(ele) not in [set(x) for x in k_cleaned]:
        k_cleaned.append(ele)
print(k_cleaned)

# output: [[1, 2], [4], [5, 6, 2], [3]]
Zoe L.
quelle
0

Seltsamerweise entfernen die obigen Antworten die 'Duplikate', aber was ist, wenn ich den duplizierten Wert auch entfernen möchte? Das Folgende sollte nützlich sein und erstellt kein neues Objekt im Speicher!

def dictRemoveDuplicates(self):
    a=[[1,'somevalue1'],[1,'somevalue2'],[2,'somevalue1'],[3,'somevalue4'],[5,'somevalue5'],[5,'somevalue1'],[5,'somevalue1'],[5,'somevalue8'],[6,'somevalue9'],[6,'somevalue0'],[6,'somevalue1'],[7,'somevalue7']]


print(a)
temp = 0
position = -1
for pageNo, item in a:
    position+=1
    if pageNo != temp:
        temp = pageNo
        continue
    else:
        a[position] = 0
        a[position - 1] = 0
a = [x for x in a if x != 0]         
print(a)

und das o / p ist:

[[1, 'somevalue1'], [1, 'somevalue2'], [2, 'somevalue1'], [3, 'somevalue4'], [5, 'somevalue5'], [5, 'somevalue1'], [5, 'somevalue1'], [5, 'somevalue8'], [6, 'somevalue9'], [6, 'somevalue0'], [6, 'somevalue1'], [7, 'somevalue7']]
[[2, 'somevalue1'], [3, 'somevalue4'], [7, 'somevalue7']]
zorze
quelle
-1

Eine andere wahrscheinlich allgemeinere und einfachere Lösung besteht darin, ein Wörterbuch zu erstellen, das von der Zeichenfolgenversion der Objekte verschlüsselt wird, und am Ende die Werte () abzurufen:

>>> dict([(unicode(a),a) for a in [["A", "A"], ["A", "A"], ["A", "B"]]]).values()
[['A', 'B'], ['A', 'A']]

Der Haken ist, dass dies nur für Objekte funktioniert, deren Zeichenfolgendarstellung ein ausreichend eindeutiger Schlüssel ist (was für die meisten nativen Objekte gilt).

jacmkno
quelle