【问题标题】:Efficiently remove duplicates, order-agnostic, from list of lists有效地从列表列表中删除重复的、与顺序无关的
【发布时间】:2021-02-16 09:55:11
【问题描述】:

以下列表有一些重复的子列表,其中元素的顺序不同:

l1 = [
    ['The', 'quick', 'brown', 'fox'],
    ['hi', 'there'],
    ['jumps', 'over', 'the', 'lazy', 'dog'],
    ['there', 'hi'],
    ['jumps', 'dog', 'over','lazy', 'the'],
]

如何删除重复项,保留看到的第一个实例,以获取:

l1 = [
    ['The', 'quick', 'brown', 'fox'],
    ['hi', 'there'],
    ['jumps', 'over', 'the', 'lazy', 'dog'],
]

我尝试过:

[list(i) for i in set(map(tuple, l1))]

尽管如此,我不知道这是否是处理大型列表的最快方法,而且我的尝试没有按预期工作。知道如何有效地删除它们吗?

【问题讨论】:

    标签: python python-3.x list list-comprehension unordered


    【解决方案1】:

    这个有点棘手。您想从冻结的计数器中键入一个字典,但计数器在 Python 中是不可散列的。对于渐近复杂度的小幅下降,您可以使用排序元组代替冻结计数器:

    seen = set()
    result = []
    for x in l1:
        key = tuple(sorted(x))
        if key not in seen:
            result.append(x)
            seen.add(key)
    

    单行中的相同想法如下所示:

    [*{tuple(sorted(k)): k for k in reversed(l1)}.values()][::-1]
    

    【讨论】:

    • 感谢您的帮助
    • 如果您不关心保留 l1 中的条目顺序,您可以使用 set(tuple(sorted(x)) for x in l1)
    【解决方案2】:

    我做了一个快速基准测试,比较了各种答案:

    l1 = [['The', 'quick', 'brown', 'fox'], ['hi', 'there'], ['jumps', 'over', 'the', 'lazy', 'dog'], ['there', 'hi'], ['jumps', 'dog', 'over','lazy', 'the']]
    
    from collections import Counter
    
    def method1():
        """manually construct set, keyed on sorted tuple"""
        seen = set()
        result = []
        for x in l1:
            key = tuple(sorted(x))
            if key not in seen:
                result.append(x)
                seen.add(key)
        return result
    
    def method2():
        """frozenset-of-Counter"""
        return list({frozenset(Counter(lst).items()): lst for lst in reversed(l1)}.values())
    
    def method3():
        """wim"""
        return [*{tuple(sorted(k)): k for k in reversed(l1)}.values()][::-1]
    
    from timeit import timeit
    
    print(timeit(lambda: method1(), number=1000))
    print(timeit(lambda: method2(), number=1000))
    print(timeit(lambda: method3(), number=1000))
    

    打印:

    0.0025010189856402576
    0.016385524009820074
    0.0026451340527273715
    

    【讨论】:

    • 这有点问题,因为字符串非常小(每个 3 或 4 个字符)以至于 O(n log(n)) 相对于计数器方法几乎不重要,它具有更好的渐近复杂性但更多分配开销。我认为更公平的方法是对千长度字符串进行基准测试。
    【解决方案3】:

    这个:

    l1 = [['The', 'quick', 'brown', 'fox'], ['hi', 'there'], ['jumps', 'over', 'the', 'lazy', 'dog'], ['there', 'hi'], ['jumps', 'dog', 'over','lazy', 'the']]
    s = {tuple(item) for item in map(sorted, l1)}
    l2 = [list(item) for item in s]
    

    l2 给出删除了反向重复的列表。 比较:Pythonic way of removing reversed duplicates in list

    【讨论】:

    • @wim 你能解释一下输出是如何不正确的吗?我检查过了,似乎我得到了正确的输出(三个嵌套列表)
    • 预期的输出写在问题中,这个不匹配,因为排序丢失了
    • 好吧,顺序改变了,但问题的本质似乎是删除具有相似元素的列表。
    【解决方案4】:

    @wim 的回答效率低下,因为它将列表项排序为唯一标识一组列表项计数的方法,每个子列表的时间复杂度为 O(n log n)

    要在线性时间复杂度上达到相同的效果,您可以使用 collections.Counter 类的冻结项计数。由于dict理解保留了具有重复键的项目的最后一个值,并且由于您希望在问题中保留具有重复键的项目的第一个值,因此您必须以列表的相反顺序构造dict,并在之后再次反转它去重子列表的列表已经构建:

    from collections import Counter
    list({frozenset(Counter(lst).items()): lst for lst in reversed(l1)}.values())[::-1]
    

    这会返回:

    [['The', 'quick', 'brown', 'fox'], ['hi', 'there'], ['jumps', 'over', 'the', 'lazy', 'dog']]
    

    【讨论】:

    • 感谢您的帮助
    • 我考虑过这一点,但除非数据中的 inner 列表非常长,否则构建所有这些frozenset 和 Counter 实例的开销可能比仅排序要差得多首先。即 O(k) 上的大系数在实践中可能会比渐近 O(k * log k) 更差。
    • @wim 如果您考虑过这一点,您就不会说“计数器在 Python 中不可散列”,并且您的解决方案是以“性能略有下降”为代价的。在不了解 OP 的实际用例的情况下,我们应该始终倾向于更好的可扩展性。
    • 在哪个轴上的可扩展性? 我认为这里最明显的大数据案例将是一个非常长的较短内部列表的列表,在这种情况下,您的解决方案将具有更差的可扩展性。渐近复杂度的降低不一定是性能的降低 - 您必须使用真实数据进行测量才能得出这些结论。
    猜你喜欢
    • 2019-12-19
    • 1970-01-01
    • 2020-02-05
    • 2015-01-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-03-29
    • 1970-01-01
    相关资源
    最近更新 更多