[python] 목록 목록에서 중복 제거

Python에 목록 목록이 있습니다.

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

그리고 중복 요소를 제거하고 싶습니다. 내가 사용할 수있는 목록이 아닌 일반 목록이었다 set. 그러나 불행히도 그 목록은 해시 할 수 없으며 목록 집합을 만들 수 없습니다. 튜플 만. 따라서 모든 목록을 튜플으로 전환 한 다음 집합을 사용하고 다시 목록으로 돌아갈 수 있습니다. 그러나 이것은 빠르지 않습니다.

가장 효율적인 방법으로이를 수행 할 수있는 방법은 무엇입니까?

위 목록의 결과는 다음과 같아야합니다.

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

나는 질서를 보존하는 것에 관심이 없습니다.

참고 : 이 질문 은 비슷하지만 내가 필요한 것은 아닙니다. 검색했지만 정확한 중복을 찾지 못했습니다.


벤치마킹 :

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)

짧은 목록의 경우 가장 빠른 “루프 인”(2 차 방법)입니다. 긴 목록의 경우 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



답변

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

2 차 접근법이 중복 된 값이 거의없는 작은 목록에 매력적으로 보이도록 상수가 충분히 작음을 확인합니다. 중복이없는 짧은 목록 :

$ 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

2 차 접근 방식은 나쁘지 않지만 sort 및 groupby 접근 방식이 더 좋습니다. 기타 등등

(성능에 대한 집착에서 알 수 있듯이)이 작업이 경계를 넘나 드는 애플리케이션의 핵심 내부 루프에있는 경우 다른 대표적인 입력 샘플에 대해 동일한 테스트 세트를 시도해 볼 가치가 있습니다. 하나 또는 다른 접근 방식을 선택합니다 (물론 측정은 빠릅니다).

또한 다른 표현을 유지하는 것도 고려해 볼 가치가 있습니다. k왜 처음에 튜플 세트가 아닌 목록 목록이어야합니까? 중복 제거 작업이 빈번하고 프로파일 링을 통해 프로그램의 성능 병목 현상이 나타나는 경우, 항상 튜플 집합을 유지하고 필요한 경우에만 목록을 가져 오는 것이 전체적으로 더 빠를 수 있습니다.


답변

수동으로 수행하고 새 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 차적이라고 생각합니다 .


답변

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

필연적으로 더 빠른지 모르겠지만 튜플과 세트를 사용할 필요는 없습니다.


답변

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목록이 해쉬하지 않기 때문에 변환이 필요하다.


답변

“긴”목록조차도 꽤 짧습니다. 또한 실제 데이터와 일치하도록 선택 했습니까? 성능은 이러한 데이터가 실제로 어떻게 보이는지에 따라 달라집니다. 예를 들어, 긴 목록을 만들기 위해 짧은 목록이 반복해서 반복됩니다. 즉, 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)

실제 솔루션은 더 많은 정보에 따라 달라질 수 있습니다. 목록 목록이 실제로 필요한 표현이라고 확신하십니까?


답변

튜플 및 {} 목록을 사용하여 중복을 제거 할 수 있습니다.

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


답변

튜플을 키로 사용하여 사전을 만들고 키를 인쇄합니다.

  • 튜플을 키로, 인덱스를 값으로 사용하여 사전 생성
  • 사전의 키 목록 인쇄

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