การลบรายการที่ซ้ำกันออกจากรายการ


116

ฉันมีรายชื่อใน Python:

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

และฉันต้องการลบองค์ประกอบที่ซ้ำกันออกไป ถ้าเป็นรายการปกติที่ฉันไม่สามารถsetใช้ได้ แต่โชคร้ายที่รายการนี้ไม่สามารถแฮชได้และไม่สามารถสร้างชุดรายการได้ เฉพาะของ tuples ดังนั้นฉันสามารถเปลี่ยนรายการทั้งหมดเป็นสิ่งที่เพิ่มขึ้นจากนั้นใช้การตั้งค่าและกลับไปที่รายการ แต่นี่ไม่เร็ว

วิธีนี้สามารถทำได้อย่างมีประสิทธิภาพมากที่สุด?

ผลลัพธ์ของรายการด้านบนควรเป็น:

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

ฉันไม่สนใจเรื่องการรักษาระเบียบ

หมายเหตุ: คำถามนี้คล้ายกัน แต่ไม่ใช่สิ่งที่ฉันต้องการ ค้นหา SO แต่ไม่พบรายการที่ซ้ำกัน


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" (วิธีกำลังสอง) เร็วที่สุดสำหรับรายการสั้น ๆ สำหรับรายการแบบยาวจะเร็วกว่าทุกคนยกเว้นเมธอด 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 ดูเหมือนว่าการคัดลอกมากเกินไปจะเป็นวิธีที่ชาญฉลาด ขอโทษด้วยความรู้สึก คัดลอกรายการไปยัง tuples จากนั้นเข้าสู่การตั้งค่าจากนั้นกลับไปที่รายการ (คัดลอกอีกครั้ง tuples ไปยังรายการ)
zaharpopov

@zaharpopov: นั่นไม่ใช่วิธีการทำงานของ Python ไม่มีอะไรจะถูกคัดลอกเพียงแค่คอนเทนเนอร์ใหม่สำหรับองค์ประกอบที่มีอยู่ (แม้ว่าสำหรับ ints ก็ค่อนข้างเหมือนกัน)
Jochen Ritzel

3
1. การกำหนดเวลาสำหรับวิธีการที่ใช้การเรียงลำดับจะยวบเนื่องจาก "k" ถูกตอบกลับไปยังตัวแปรที่เรียง 2. วิธีสุดท้ายเร็วกว่าเนื่องจากวิธีที่คุณสร้างข้อมูลทดสอบทำให้คุณมีองค์ประกอบที่แตกต่างกันไม่เกิน 4 องค์ประกอบ ลอง sth เช่น K = [[int (u) สำหรับ u ใน str (random.randrange (1, 1000))] สำหรับ _ ในระยะ (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

การยืนยันว่าวิธีกำลังสองมีค่าคงที่น้อยพอที่จะทำให้น่าสนใจสำหรับรายการเล็ก ๆ ที่มีค่าซ้ำกันน้อย ด้วยรายการสั้น ๆ ที่ไม่มีรายการซ้ำ:

$ 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

วิธีกำลังสองไม่ได้แย่ แต่วิธีการเรียงลำดับและกลุ่มจะดีกว่า ฯลฯ ฯลฯ

หาก (ตามที่ความหลงใหลในประสิทธิภาพแนะนำ) การดำเนินการนี้อยู่ในวงในหลักของแอปพลิเคชัน push-the-bound ของคุณก็คุ้มค่าที่จะลองชุดการทดสอบเดียวกันกับตัวอย่างอินพุตที่เป็นตัวแทนอื่น ๆ ซึ่งอาจตรวจพบการวัดง่ายๆบางอย่างที่อาจทำให้คุณเข้าใจได้ เลือกวิธีใดวิธีหนึ่ง (แต่การวัดผลต้องรวดเร็วแน่นอน)

นอกจากนี้ยังควรค่าแก่การพิจารณารักษาการเป็นตัวแทนที่แตกต่างกันk- ทำไมจึงต้องเป็นรายการของรายการแทนที่จะเป็นชุดสิ่งที่เพิ่มขึ้นในตอนแรก? หากงานการลบที่ซ้ำกันเกิดขึ้นบ่อยครั้งและการทำโปรไฟล์แสดงให้เห็นว่าเป็นปัญหาคอขวดด้านประสิทธิภาพของโปรแกรมการเก็บชุดสิ่งที่รวมไว้ตลอดเวลาและการรับรายการจากงานนั้นเฉพาะในกรณีที่จำเป็นเท่านั้นตัวอย่างเช่นอาจเร็วกว่าโดยรวม


@alex ขอบคุณสำหรับทางเลือก วิธีนี้เกี่ยวกับความเร็วเดียวกันกับของ danben เร็วขึ้นสองสาม%
zaharpopov

@alex: แปลกนี่ช้ากว่าวิธีกำลังสองไร้เดียงสาสำหรับรายการที่สั้นกว่า (ดูการแก้ไขคำถาม)
zaharpopov

@zaharpopov: เป็นแบบนั้นเฉพาะในกรณีพิเศษของคุณ cf. ความคิดเห็นของฉันต่อคำถาม
Torsten Marek

@zaharpopov หากคุณให้การแจกแจงความน่าจะเป็นของรายการและความยาวของรายการย่อยและโอกาสที่จะซ้ำกันเป็นไปได้ (ด้วยความพยายามอย่างมาก) ในการคำนวณ / วัดการแจกแจงความน่าจะเป็นรันไทม์สำหรับโค้ดใด ๆ และเพิ่มประสิทธิภาพการวัดที่คุณต้องการ (ค่ามัธยฐานค่าเฉลี่ย 90th centile อะไรก็ได้). แทบจะไม่เคยทำมาก่อนเนื่องจาก 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แต่ละองค์ประกอบ


@paul: แปลกมาก - วิธีนี้เร็วกว่าวิธีอื่นทั้งหมด
zaharpopov

ฉันสงสัยว่าวิธีนี้จะไม่เร็วกว่านี้สำหรับรายการที่ยาวมาก ขึ้นอยู่กับแอปพลิเคชันของคุณ: หากคุณมีเพียงแค่รายการหกองค์ประกอบที่มีรายการที่ซ้ำกันสองรายการโซลูชันใด ๆ ก็น่าจะเร็วพอและคุณควรใช้รหัสที่ชัดเจนที่สุด
Paul Stephenson

@zaharpopov ไม่ใช่กำลังสองในเกณฑ์มาตรฐานของคุณเพราะคุณทำซ้ำรายการเดียวกันซ้ำแล้วซ้ำอีก คุณกำลังเปรียบเทียบกับกรณีมุมเชิงเส้น
Mike Graham

k = ([[1, 2], [4], [5, 6, 2], [1, 2], [3], [5, 2], [6], [8], [9]] +[[x] for x in range(1000)]) *5จะแสดงพฤติกรรมกำลังสองอย่างสวยงาม
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. เร็วกว่านี้เปลี่ยนเป็น tuples แล้ว 'set' แล้วกลับไปที่รายการ?
zaharpopov

คุณสามารถได้อย่างง่ายดายทดสอบที่ - เขียนวิธีการทั้งสอง deduping, สร้างรายการแบบสุ่มโดยใช้และเวลาที่มันด้วยrandom time
danben

4

การsetแก้ปัญหาทั้งหมดที่เกี่ยวข้องกับปัญหานี้จนถึงขณะนี้จำเป็นต้องสร้างทั้งหมดsetก่อนที่จะทำซ้ำ

มันเป็นไปได้ที่จะทำให้เรื่องนี้ขี้เกียจและในเวลาเดียวกันเพื่อรักษาโดยการทำซ้ำรายการของรายการและเพิ่มไปยัง 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

แม้แต่รายการ "ยาว" ของคุณก็ค่อนข้างสั้น นอกจากนี้คุณได้เลือกให้ตรงกับข้อมูลจริงหรือไม่ ประสิทธิภาพจะแตกต่างกันไปตามความเป็นจริงของข้อมูลเหล่านี้ ตัวอย่างเช่นคุณมีรายการสั้น ๆ ซ้ำแล้วซ้ำอีกเพื่อสร้างรายการที่ยาวขึ้น ซึ่งหมายความว่าการแก้ปัญหากำลังสองเป็นเส้นตรงในเกณฑ์มาตรฐานของคุณ แต่ไม่ใช่ในความเป็นจริง

สำหรับรายการที่มีขนาดใหญ่จริงรหัสชุดเป็นทางออกที่ดีที่สุดของคุณ - เป็นแบบเชิงเส้น (แม้ว่าจะหิวโหย) วิธีการเรียงลำดับและ groupby คือ O (n log n) และวิธีการวนซ้ำเป็นกำลังสองอย่างเห็นได้ชัดดังนั้นคุณจะรู้ว่าสิ่งเหล่านี้จะปรับขนาดอย่างไรเมื่อ n มีขนาดใหญ่มาก ถ้านี่คือขนาดจริงของข้อมูลที่คุณกำลังวิเคราะห์ใครจะสนล่ะ? มันเล็กมาก

อนึ่งฉันเห็นการเร่งความเร็วที่เห็นได้ชัดหากฉันไม่สร้างรายการกลางเพื่อสร้างชุดกล่าวคือถ้าฉันแทนที่

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

กับ

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

วิธีแก้ปัญหาที่แท้จริงอาจขึ้นอยู่กับข้อมูลเพิ่มเติม: คุณแน่ใจหรือว่ารายชื่อเป็นตัวแทนที่คุณต้องการจริงๆ?



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

วิธีแก้ปัญหาทั่วไปและง่ายกว่านั้นอีกวิธีหนึ่งคือการสร้างพจนานุกรมที่คีย์โดยเวอร์ชันสตริงของอ็อบเจ็กต์และรับค่า () ในตอนท้าย:

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