목록 목록에서 중복 제거


116

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

1
"이건 빠르지 않다"라는 말은 시간을 정했고 응용 프로그램에 충분히 빠르지 않다는 뜻입니까, 아니면 빠르지 않다고 생각하십니까?
Torsten Marek

@Torsten, 똑똑한 방법으로 복사하는 것이 너무 많은 것 같습니다. 미안해, 직감. 목록의 목록에 다시 다음 세트로, 튜플에 목록을 복사 (복사 다시 목록에 튜플)
zaharpopov

@zaharpopov : 파이썬이 작동하는 방식이 아닙니다. 아무것도 복사 되지 않고 기존 요소에 대한 새 컨테이너 만 있습니다 (int의 경우 거의 동일 함)
Jochen Ritzel 2010

3
1. "k"가 정렬 된 변형에 리 바인드되기 때문에 정렬을 사용하는 방법의 타이밍이 축소됩니다. 2. 테스트 데이터를 생성하는 방식에 따라 최대 4 개의 개별 요소가 남으므로 마지막 방법이 더 빠릅니다. sth를 시도하십시오. like K = [[int (u) for u in str (random.randrange (1, 1000))] for _ in range (100)]
Torsten Marek

@Torsten : 고정 감사합니다. 그러나 여전히 루프 방법은 10 개 목록에 중복이 하나만있는 경우에도 빠릅니다.
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

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


@alex 대안에 감사드립니다. 이 방법은 danben과 거의 같은 속도, 몇 % 더 빠름
zaharpopov 2010

@ 알렉스 : 이상하게이 (질문 편집 참조) 짧은 목록에 대한 순진 차 방법보다 느리지
zaharpopov

@zaharpopov : 당신의 특별한 경우에만 그렇습니다. 질문에 대한 내 의견.
Torsten Marek

@zaharpopov, 목록 및 하위 목록 길이의 확률 분포와 중복 가능성을 제공하면 주어진 코드에 대한 런타임 확률 분포를 계산 / 측정하고 필요한 모든 측정 값 (중앙값, 평균, 90 번째)을 최적화 할 수 있습니다 (큰 노력으로). 백분위 수). ROI가 매우 낮기 때문에 거의 수행되지 않습니다. 일반적으로 열등한 알고리즘이 성능을 크게 저하시키는 대규모 입력 (big-O 접근 방식)의 훨씬 더 쉬운 경우에 초점을 맞 춥니 다. 그리고 나는 당신이 어쨌든 당신의 Q에 확률 분포를 지정하지 않는 것을 봅니다 ;-).
Alex Martelli

@zaharpov, 당신이 그것을 좋아해서 다행입니다!
Alex Martelli

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 2010

이 방법은 매우 긴 목록의 경우 더 빠르지 않을 것이라고 생각합니다. 응용 프로그램에 따라 다릅니다. 두 개의 중복 항목이있는 6 개 요소 목록 만있는 경우 모든 솔루션이 충분히 빠르며 가장 명확한 코드를 사용해야합니다.
Paul Stephenson

@zaharpopov, 동일한 목록을 반복해서 복제하기 때문에 벤치 마크에서 2 차가 아닙니다. 선형 코너 케이스로 벤치마킹하고 있습니다.
Mike Graham

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

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

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


Danben 감사합니다. 튜플로 전환 한 다음 '설정'한 다음 목록으로 돌아가는 것보다 더 빠릅니다.
zaharpopov 2010

두 가지 중복 제거 방법을 모두 작성하고를 사용하여 임의의 목록을 생성 random한 다음 time.
danben 2010

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.