リストのリストから重複を削除する


116

Pythonのリストのリストがあります。

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

そして、そこから重複する要素を削除したいと思います。それが私が使用できるリストではなく、通常のリストであったかどうかsetです。しかし残念なことに、そのリストはハッシュ可能ではなく、リストのセットを作成できません。タプルのみ。したがって、すべてのリストをタプルに変換してから、セットを使用してリストに戻すことができます。しかし、これは速くはありません。

これを最も効率的な方法で行うにはどうすればよいですか?

上記のリストの結果は次のようになります。

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

順序の保存は気にしません。

注:この質問は似ていますが、私が必要としているものとはまったく異なります。SOを検索しましたが、完全な重複は見つかりませんでした。


ベンチマーク:

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)

短いリストの場合、「ループイン」(二次法)の中で最も高速です。長いリストの場合、groupbyメソッドを除く全員よりも高速です。これは理にかなっていますか?

短いリスト(コード内のリスト)の場合、100000回の繰り返し:

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

より長いリストの場合(コード内の5回複製されたもの):

[set] Elapsed: 3.68700003624
[sort] Elapsed: 3.43799996376
[groupby] Elapsed: 1.03099989891
[loop in] Elapsed: 1.85900020599

1
「これは速くない」とは、時間を計ってアプリケーションにとって十分に速くないという意味ですか、それとも速くないと思いますか?
Torsten Marek

@Torsten、それはスマートな方法にはコピーが多すぎるようです。すみません、直感。リストをタプルにコピーしてからセットに入れ、リストのリストに戻す(タプルをリストにもう一度コピーする)
zaharpopov

@zaharpopov:これはPythonの動作方法ではなく、何もコピーされません。既存の要素の新しいコンテナーだけです(ただし、intの場合はほとんど同じです)
Jochen Ritzel

3
1. "k"はソートされたバリアントにリバウンドされるため、ソートを使用するメソッドのタイミングは低下します。2.テストデータの生成方法には最大4つの異なる要素が残るため、最後の方法の方が高速です。sthを試してください。like K = [[int(u)for u for str(random.randrange(1、1000))] for _ in range(100)]
Torsten Marek

@Torsten:おかげで修正。ただし、10のリストに重複が1つしかない場合でも、ループ方式は高速です
zaharpopov

回答:


167
>>> 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多くの場合、この種の問題への最速かつ最も強力なソリューションを提供しています、とあるだけでなく) -に精通し得る価値!

編集:コメントで述べたように、通常の最適化の取り組みは大規模な入力(big-Oアプローチ)に重点を置いています。しかしときどき(本質的に、パフォーマンスの限界を押し広げているコードの深い内部ループの「悲劇的に重大なボトルネック」の場合)確率分布を提供し、最適化するパフォーマンス測定を決定する(おそらく上限または90パーセンタイルは、アプリに応じて平均または中央値よりも重要です。最初にヒューリスティックチェックを実行して、入力データの特性に応じて異なるアルゴリズムを選択します。

「ポイント」パフォーマンス(特定の入力に対するコードAとコードB)を注意深く測定することは、この非常にコストのかかるプロセスの一部であり、標準ライブラリモジュールがtimeit役立ちます。ただし、シェルプロンプトで使用する方が簡単です。たとえば、この問題の一般的なアプローチを紹介する短いモジュールを以下に示します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))

健全性チェック(を実行したときに実行されるpython nodup.py)と基本的な巻き上げ手法(速度を上げるために定数のグローバル名を各関数に対してローカルにする)に注意してください。

これで、小さなサンプルリストのチェックを実行できます。

$ 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

二次アプローチには十分に小さい定数があり、重複する値がほとんどない小さなリストにとって魅力的であることを確認します。重複のない短いリスト:

$ 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

二次アプローチは悪くありませんが、並べ替えやグループ化のほうが優れています。等

(パフォーマンスへの執着が示唆するように)この操作が境界を押すアプリケーションのコア内部ループにある場合、他の代表的な入力サンプルで同じテストのセットを試してみる価値があります。どちらか一方のアプローチを選択します(ただし、メジャーは高速でなければなりません)。

また、別の表現を維持することも検討する価値があります。kなぜ、そもそもタプルのセットではなくリストのリストである必要があるのでしょうか。重複する削除タスクが頻繁であり、プロファイリングでプログラムのパフォーマンスのボトルネックであることがわかった場合、タプルのセットを常に維持し、必要な場合にのみタプルのセットをそこからリストのリストを取得すると、全体的に高速になる可能性があります。


@alex代替をありがとう。この方法は、ダンベンとほぼ同じ速度で、数%高速です
ザハーポポフ

@alex:奇妙なことに、これは短いリストの単純な2次法よりも遅い(質問の編集を参照)
zaharpopov

@zaharpopov:それはあなたの特別な場合にのみそうです、cf。質問に対する私のコメント。
Torsten Marek

@zaharpopov、リストとサブリストの長さの確率分布と重複の可能性を与えると、特定のコードの実行時確率分布を計算/測定し、必要な測定値(中央値、平均値、90番目)を最適化することができます百分位数など)。ROIが非常に低いため、これを行うことはほとんどありません。通常、大規模な入力(big-Oアプローチ)のはるかに簡単なケースに焦点を当てます。この場合、劣ったアルゴリズムはパフォーマンスを大幅に低下させます。とにかく、Qに確率分布を指定しているようには見えません;-)。
Alex Martelli、2010

@zaharpov、あなたがそれを気に入って良かった!
Alex Martelli、2010

21

手動で行い、新しいkリストを作成して、これまでにないエントリを追加します。

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

理解するのは簡単で、各要素の最初の出現の順序を維持しておくと便利ですが、new_k各要素の全体を検索するので、複雑さが2次式になると思います。


@paul:非常に奇妙です-この方法は他のどの方法よりも高速です
zaharpopov

この方法は、非常に長いリストに対しては高速にならないと思います。それはあなたのアプリケーションに依存します:あなたが本当に2つの重複を持つ6要素のリストを持っているなら、どんな解決策も十分に速くなる可能性が高く、最も明確なコードで行くべきです。
ポールスティーブンソン

@zaharpopov、同じリストを何度も複製するため、ベンチマークでは2次ではありません。あなたは線形コーナーケースでベンチマークを行っています。
マイクグラハム

k = ([[1, 2], [4], [5, 6, 2], [1, 2], [3], [5, 2], [6], [8], [9]] +[[x] for x in range(1000)]) *52次の振る舞いがうまく表示されます
John La Rooy、2010

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

必ずしも高速かどうかはわかりませんが、タプルやセットを使用する必要はありません。


ダンベンありがとうございます。これはタプルに切り替えてから「設定」してからリストに戻るよりも速くですか?
zaharpopov

あなたはそれを簡単にテストすることができます-両方の重複排除方法を書き、を使用してランダムなリストを生成しrandom、で時間を計りますtime
danben

4

setこれまでのところ、この問題に対するすべての関連ソリューションでは、set反復前に全体を作成する必要があります。

リストのリストを反復して「seen」に追加することで、これを遅延させ、同時に順序を維持することができますset。次に、このトラッカーでリストが見つからない場合にのみリストを生成しますset

このunique_everseenレシピはitertools ドキュメントで入手できます。サードパーティtoolzライブラリでも利用できます。

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

tupleリストはハッシュ化できないため、変換が必要であることに注意してください。


3

「長い」リストでもかなり短いです。また、実際のデータと一致するようにそれらを選択しましたか?パフォーマンスは、これらのデータが実際にどのように見えるかによって異なります。たとえば、長いリストを作成するために短いリストを何度も繰り返します。これは、2次解がベンチマークで線形であることを意味しますが、実際にはそうではありません。

実際に大きいリストの場合、セットコードが最善の策です—線形です(ただし、スペースを大量に消費します)。sortメソッドとgroupbyメソッドはO(n log n)であり、メソッドのループは明らかに2次なので、nが本当に大きくなると、これらがどのようにスケールするかがわかります。これが分析しているデータの実際のサイズである場合、誰が気にしますか?小さいです。

ちなみに、セットを作成するための中間リストを作成しなかった場合、つまり、置き換えた場合、大幅なスピードアップが見られます

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

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

実際の解決策は、より多くの情報に依存する可能性があります。リストのリストが本当に必要な表現であることを確信していますか?


3

タプルと{}のリストを使用して重複を削除できます

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

1

タプルをキーとして辞書を作成し、キーを出力します。

  • タプルをキー、インデックスを値とする辞書を作成する
  • 辞書のキーのリストを印刷する

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

1

これはうまくいくはずです。

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

0

奇妙なことに、上記の答えは「重複」を削除しますが、重複した値も削除したい場合はどうなりますか?以下は役に立ち、メモリに新しいオブジェクトを作成しません!

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)

そしてo / pは:

[[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']]

-1

別のおそらくより一般的で簡単な解決策は、オブジェクトの文字列バージョンをキーとして辞書を作成し、最後にvalues()を取得することです。

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

問題は、文字列表現が十分にユニークなキーであるオブジェクトに対してのみ機能することです(ほとんどのネイティブオブジェクトに当てはまります)。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.