Loại bỏ các bản sao khỏi danh sách danh sách


116

Tôi có một danh sách các danh sách bằng Python:

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

Và tôi muốn loại bỏ các yếu tố trùng lặp khỏi nó. Nếu nó là một danh sách bình thường không phải là danh sách tôi có thể sử dụng set. Nhưng thật không may, danh sách đó không thể băm và không thể tạo bộ danh sách. Chỉ trong số các bộ giá trị. Vì vậy, tôi có thể chuyển tất cả danh sách thành bộ giá trị sau đó sử dụng thiết lập và quay lại danh sách. Nhưng điều này không nhanh.

Làm thế nào điều này có thể được thực hiện một cách hiệu quả nhất?

Kết quả của danh sách trên sẽ là:

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

Tôi không quan tâm đến việc giữ gìn trật tự.

Lưu ý: câu hỏi này tương tự nhưng không hoàn toàn là những gì tôi cần. Đã tìm SO nhưng không tìm thấy bản sao chính xác.


Điểm chuẩn:

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" (phương pháp bậc hai) nhanh nhất đối với danh sách ngắn. Đối với danh sách dài, nó nhanh hơn mọi người ngoại trừ phương pháp theo nhóm. Điều này có nghĩa không?

Đối với danh sách ngắn (danh sách trong mã), 100000 lần lặp:

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

Đối với danh sách dài hơn (danh sách trong mã được sao chép 5 lần):

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

1
"Cái này không nhanh", có phải bạn có nghĩa là bạn đã bấm giờ và nó không đủ nhanh cho ứng dụng của bạn, hay bạn nghĩ rằng nó không nhanh?
Torsten Marek

@Torsten, có vẻ như sao chép quá nhiều để trở thành phương pháp thông minh. xin lỗi, cảm giác ruột. sao chép danh sách này để các bộ, sau đó vào bộ, sau đó trở lại danh sách liệt kê (sao chép lại tuples vào danh sách)
zaharpopov

@zaharpopov: đó không phải là cách Python hoạt động, không có gì sẽ được sao chép , chỉ là các vùng chứa mới cho các phần tử hiện có (mặc dù đối với ints, nó khá giống nhau)
Jochen Ritzel

3
1. thời gian cho các phương pháp sử dụng sắp xếp bị lệch, bởi vì "k" được phục hồi về biến thể đã sắp xếp. 2. Phương pháp cuối cùng nhanh hơn vì cách bạn tạo dữ liệu thử nghiệm để lại cho bạn nhiều nhất 4 phần tử riêng biệt. Hãy thử sth. như K = [[int (u) for u in str (random.randrange (1, 1000))] for _ in range (100)]
Torsten Marek

@Torsten: đã sửa, cảm ơn. nhưng vẫn là phương pháp lặp là nhanh ngay cả khi chỉ có một trùng lặp trong danh sách 10
zaharpopov

Câu trả lời:


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

itertoolsthường đưa ra các giải pháp nhanh nhất và mạnh mẽ nhất cho loại vấn đề này và rất đáng để làm quen với! -)

Chỉnh sửa : như tôi đã đề cập trong một nhận xét, các nỗ lực tối ưu hóa thông thường tập trung vào các đầu vào lớn (cách tiếp cận big-O) bởi vì nó dễ dàng hơn nhiều mà nó mang lại lợi nhuận tốt cho các nỗ lực. Nhưng đôi khi (về cơ bản đối với "các nút thắt cổ chai quan trọng" trong các vòng lặp sâu bên trong mã đang đẩy ranh giới của các giới hạn hiệu suất), người ta có thể cần đi vào chi tiết hơn, cung cấp các phân phối xác suất, quyết định các biện pháp hiệu suất để tối ưu hóa (có thể là giới hạn trên hoặc trung tâm thứ 90 quan trọng hơn mức trung bình hoặc trung bình, tùy thuộc vào ứng dụng của một người), thực hiện các kiểm tra có thể theo kinh nghiệm ngay từ đầu để chọn các thuật toán khác nhau tùy thuộc vào đặc điểm dữ liệu đầu vào, v.v.

Các phép đo cẩn thận về hiệu suất "điểm" (mã A so với mã B cho một đầu vào cụ thể) là một phần của quy trình cực kỳ tốn kém này và mô-đun thư viện tiêu chuẩn timeitsẽ giúp ích ở đây. Tuy nhiên, sẽ dễ dàng hơn khi sử dụng nó khi có dấu nhắc của trình bao. Ví dụ: đây là một mô-đun ngắn để giới thiệu cách tiếp cận chung cho vấn đề này, hãy lưu nó dưới dạng 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))

Lưu ý kiểm tra sự tỉnh táo (thực hiện khi bạn vừa làm python nodup.py) và kỹ thuật nâng cơ bản (đặt tên toàn cục liên tục cục bộ cho mỗi chức năng để có tốc độ) để đặt mọi thứ ngang bằng nhau.

Bây giờ chúng ta có thể chạy kiểm tra trên danh sách ví dụ nhỏ:

$ 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

xác nhận rằng cách tiếp cận bậc hai có các hằng số đủ nhỏ để làm cho nó trở nên hấp dẫn đối với các danh sách nhỏ có ít giá trị trùng lặp. Với một danh sách ngắn không trùng lặp:

$ 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

phương pháp tiếp cận bậc hai không tệ, nhưng cách sắp xếp và phân nhóm sẽ tốt hơn. Vân vân.

Nếu (như nỗi ám ảnh về hiệu suất cho thấy) thao tác này nằm ở vòng lặp bên trong cốt lõi của ứng dụng vượt qua ranh giới của bạn, thì bạn nên thử cùng một bộ thử nghiệm trên các mẫu đầu vào đại diện khác, có thể phát hiện ra một số phép đo đơn giản có thể cho phép bạn chọn một hoặc cách tiếp cận khác (tất nhiên, biện pháp phải nhanh).

Nó cũng đáng xem xét giữ một đại diện khác k- tại sao nó phải là một danh sách các danh sách hơn là một tập hợp các bộ giá trị ngay từ đầu? Nếu nhiệm vụ loại bỏ trùng lặp diễn ra thường xuyên và việc lập hồ sơ cho thấy đây là nút thắt cổ chai về hiệu suất của chương trình, thì việc giữ một tập hợp các bộ giá trị luôn luôn và nhận danh sách các danh sách từ đó chỉ khi và ở những nơi cần thiết, về tổng thể có thể nhanh hơn.


@alex cảm ơn bạn đã thay thế. phương pháp này về cùng một tốc độ như danben, một vài% nhanh hơn
zaharpopov

@ Alex: kỳ lạ này là chậm hơn so với một phương pháp bậc hai ngây thơ cho các danh sách ngắn hơn (xem câu hỏi chỉnh sửa)
zaharpopov

@zaharpopov: đó chỉ là cách đó trong trường hợp đặc biệt của bạn, cf. bình luận của tôi cho câu hỏi.
Torsten Marek

@zaharpopov. trung tâm, bất cứ điều gì). Nó hầu như không bao giờ được thực hiện vì ROI rất thấp: thông thường người ta tập trung vào trường hợp đầu vào lớn dễ dàng hơn nhiều (cách tiếp cận big-O), trong đó các thuật toán kém hơn sẽ thực sự làm ảnh hưởng đáng kể đến hiệu suất. Và tôi không thấy bạn chỉ định bất kỳ phân phối xác suất nào trong Q của bạn ;-).
Alex Martelli

@zaharpov, rất vui vì bạn thích nó!
Alex Martelli

21

Thực hiện theo cách thủ công, tạo kdanh sách mới và thêm các mục chưa tìm thấy cho đến nay:

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

Đơn giản để hiểu và bạn bảo toàn thứ tự xuất hiện đầu tiên của mỗi phần tử sẽ hữu ích, nhưng tôi đoán nó phức tạp bậc hai khi bạn đang tìm kiếm toàn bộ new_kcho từng phần tử.


@ Paul: rất lạ - phương pháp này là nhanh hơn so với tất cả những người khác
zaharpopov

Tôi nghi ngờ phương pháp này sẽ không nhanh hơn đối với danh sách rất dài. Nó sẽ phụ thuộc vào ứng dụng của bạn: nếu bạn thực sự chỉ có danh sách sáu phần tử với hai bản sao thì bất kỳ giải pháp nào cũng có thể đủ nhanh và bạn nên sử dụng mã rõ ràng nhất.
Paul Stephenson

@zaharpopov, Nó không phải bậc hai trong điểm chuẩn của bạn vì bạn lặp đi lặp lại cùng một danh sách. Bạn đang đo điểm chuẩn với trường hợp góc tuyến tính.
Mike Graham

k = ([[1, 2], [4], [5, 6, 2], [1, 2], [3], [5, 2], [6], [8], [9]] +[[x] for x in range(1000)]) *5sẽ hiển thị hành vi bậc hai một cách độc đáo
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]]

Tôi không biết liệu nó có nhất thiết phải nhanh hơn hay không, nhưng bạn không cần phải sử dụng đến các bộ và bộ.


Cảm ơn bạn danben. điều này nhanh hơn chuyển sang bộ giá trị rồi 'đặt' rồi quay lại danh sách?
zaharpopov

Bạn có thể dễ dàng kiểm tra điều đó - viết cả hai phương pháp loại trừ, tạo một số danh sách ngẫu nhiên bằng cách sử dụng randomvà thời gian cho nó time.
danben

4

Cho đến nay, tất cả các setgiải pháp liên quan đến vấn đề này đều yêu cầu tạo một tổng thể settrước khi lặp lại.

Có thể làm cho điều này trở nên lười biếng, đồng thời bảo toàn trật tự, bằng cách lặp lại danh sách các danh sách và thêm vào một "đã xem" set. Sau đó, chỉ mang lại một danh sách nếu nó không được tìm thấy trong trình theo dõi này set.

Công unique_everseenthức này có sẵn trong itertools tài liệu . Nó cũng có sẵn trong toolzthư viện của bên thứ 3 :

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

Lưu ý rằng tuplechuyển đổi là cần thiết vì không thể băm danh sách.


3

Ngay cả danh sách "dài" của bạn cũng khá ngắn. Ngoài ra, bạn có chọn chúng để khớp với dữ liệu thực tế không? Hiệu suất sẽ thay đổi tùy theo những dữ liệu này thực sự trông như thế nào. Ví dụ: bạn có một danh sách ngắn được lặp đi lặp lại để tạo ra một danh sách dài hơn. Điều này có nghĩa là giải pháp bậc hai là tuyến tính trong điểm chuẩn của bạn, nhưng không phải trong thực tế.

Đối với các danh sách thực sự lớn, mã tập hợp là cách tốt nhất của bạn — nó tuyến tính (mặc dù rất tốn không gian). Các phương thức sắp xếp và chia nhóm là O (n log n) và vòng lặp trong phương thức rõ ràng là bậc hai, vì vậy bạn biết chúng sẽ mở rộng như thế nào khi n thực sự lớn. Nếu đây là kích thước thực của dữ liệu bạn đang phân tích, thì ai quan tâm? Nó nhỏ.

Ngẫu nhiên, tôi thấy một tốc độ tăng đáng kể nếu tôi không tạo danh sách trung gian để tạo bộ, nghĩa là nếu tôi thay thế

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

với

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

Giải pháp thực sự có thể phụ thuộc vào nhiều thông tin hơn: Bạn có chắc chắn rằng một danh sách các danh sách thực sự là đại diện bạn cần?


3

Danh sách bộ tuple và {} có thể được sử dụng để xóa các bản sao

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

1

Tạo một từ điển với tuple làm khóa và in các khóa.

  • tạo từ điển với tuple làm khóa và chỉ mục dưới dạng giá trị
  • in danh sách các khóa từ điển

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

Điều này sẽ hoạt động.

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

Thật kỳ lạ, các câu trả lời ở trên loại bỏ 'bản sao' nhưng nếu tôi cũng muốn loại bỏ giá trị trùng lặp thì sao ?? Phần sau sẽ hữu ích và không tạo ra một đối tượng mới trong bộ nhớ!

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)

và o / p là:

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

Một giải pháp khác có lẽ chung chung và đơn giản hơn là tạo từ điển được khóa bởi phiên bản chuỗi của các đối tượng và nhận các giá trị () ở cuối:

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

Điểm lưu ý là điều này chỉ hoạt động đối với các đối tượng có biểu diễn chuỗi là một khóa duy nhất đủ tốt (điều này đúng với hầu hết các đối tượng gốc).

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.