如何从列表中删除每次出现的子列表

How to remove every occurrence of sub-list from list

我有两个列表:

big_list = [2, 1, 2, 3, 1, 2, 4]
sub_list = [1, 2]

我想删除 big_list 中出现的所有 sub_list 次。

结果应该是 [2, 3, 4]

对于字符串你可以使用这个:

'2123124'.replace('12', '')

但是据我所知这不适用于列表。

这不是 Removing a sublist from a list 的副本,因为我想从大列表中删除所有子列表。在另一个问题中,结果应该是 [5,6,7,1,2,3,4].

更新:为了简单起见,我在这个例子中使用了整数。但是列表项可以是任意对象。

更新 2:

如果 big_list = [1, 2, 1, 2, 1]sub_list = [1, 2, 1], 我希望结果为 [2, 1](如 '12121'.replace('121', '')

更新3:

我不喜欢将 Whosebug 中的源代码复制粘贴到我的代码中。这就是为什么我在软件推荐中创建了第二个问题:https://softwarerecs.stackexchange.com/questions/51273/library-to-remove-every-occurrence-of-sub-list-from-list-python

Update4:如果你知道一个库可以调用这个方法,请把它写成答案,因为这是我的首选解决方案。

测试应该通过这个测试:

def test_remove_sub_list(self):
    self.assertEqual([1, 2, 3], remove_sub_list([1, 2, 3], []))
    self.assertEqual([1, 2, 3], remove_sub_list([1, 2, 3], [4]))
    self.assertEqual([1, 3], remove_sub_list([1, 2, 3], [2]))
    self.assertEqual([1, 2], remove_sub_list([1, 1, 2, 2], [1, 2]))
    self.assertEquals([2, 1], remove_sub_list([1, 2, 1, 2, 1], [1, 2, 1]))
    self.assertEqual([], remove_sub_list([1, 2, 1, 2, 1, 2], [1, 2]))

您必须自己实施。这是基本思想:

def remove_sublist(lst, sub):
    i = 0
    out = []
    while i < len(lst):
        if lst[i:i+len(sub)] == sub:
            i += len(sub)
        else:
            out.append(lst[i])
            i += 1
    return out

这将遍历原始列表的每个元素,如果它不是子集的成员,则将其添加到输出列表。这个版本不是很有效,但它像您提供的字符串示例一样工作,因为它创建了一个不包含您的子集的新列表。它也适用于任意元素类型,只要它们支持 ==。从 [1,1,1,1] 中删除 [1,1,1] 将正确地导致 [1],对于字符串。

这里 IDEOne link 展示了

的结果
>>> remove_sublist([1, 'a', int, 3, float, 'a', int, 5], ['a', int])
[1, 3, <class 'float'>, 5]

递归方法:

def remove(lst, sub):
    if not lst:
        return []
    if lst[:len(sub)] == sub:
        return remove(lst[len(sub):], sub)
    return lst[:1] + remove(lst[1:], sub)
print(remove(big_list, sub_list))

这输出:

[2, 3, 4]

使用itertools.zip_longest创建n个元素元组(其中n是sub_list的长度),然后在其中一个元素匹配[=19时过滤当前元素和下n-1个元素=]

>>> from itertools import zip_longest, islice
>>> itr = zip_longest(*(big_list[i:] for i in range(len(sub_list))))
>>> [sl[0] for sl in itr if not (sl == tuple(sub_list) and next(islice(itr, len(sub_list)-2, len(sub_list)-1)))]
[2, 3, 4]

为了提高效率,您可以在开始过滤之前计算tuple(sub_list)len(sub_list)

>>> l = len(sub_list)-1
>>> tup = tuple(sub_list)
>>> [sl[0] for sl in itr if not (sl == tup and next(islice(itr, l-1, l)))]
[2, 3, 4]

检查是否lst[i:i+len(sub)] < len(lst)

的改进版本
def remove_sublist(lst, sub):
    i = 0
    out = []
    sub_len = len(sub)
    lst_len = len(lst)
    while i < lst_len:
        if (i+sub_len) < lst_len:
            if lst[i: i+sub_len] == sub:
                i += sub_len
            else:
                out.append(lst[i])
                i += 1
        else:
            out.append(lst[i])
            i += 1

    return out

试试 delslicing。最差的时间复杂度是O(N^2).

sub_list=['a', int]
big_list=[1, 'a', int, 3, float, 'a', int, 5]
i=0
while i < len(big_list):
    if big_list[i:i+len(sub_list)]==sub_list:
        del big_list[i:i+len(sub_list)]
    else:
        i+=1

print(big_list)

结果:

[1, 3, <class 'float'>, 5]

这个怎么样:

def remove_sublist(lst, sub):
    max_ind_sub = len(sub) - 1
    out = []
    i = 0
    tmp = []

    for x in lst:
        if x == sub[i]:
            tmp.append(x)
            if i < max_ind_sub: # partial match 
                i += 1
            else:  # found complete match
                i = 0
                tmp = []
        else:
            if tmp:  # failed partial match 
                i = 0
                out += tmp
            if x == sub[0]:  # partial match
                i += 1
                tmp = [x]
            else:
                out.append(x)

    return out

性能:

lst = [2, 1, 2, 3, 1, 2, 4]
sub = [1, 2]
%timeit remove_sublist(lst, sub)  # solution of Mad Physicist
%timeit remove_sublist_new(lst, sub)
>>> 2.63 µs ± 112 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
>>> 1.77 µs ± 13.7 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

更新

我的第一个解决方案有一个错误。能够修复它(更新了我上面的代码)但该方法现在看起来更复杂了。在性能方面,它仍然比我本地机器上 Mad Physicist 的解决方案要好。

您可以对生成器使用递归:

def remove(d, sub_list):
   if d[:len(sub_list)] == sub_list and len(sub_list) <= len(d[:len(sub_list)]):
      yield from [[], remove(d[len(sub_list):], sub_list)][bool(d[len(sub_list):])]
   else:
      yield d[0]
      yield from [[], remove(d[1:], sub_list)][bool(d[1:])]

tests = [[[2, 1, 2, 3, 1, 2, 4], [1, 2]], [[1, 2, 1, 2], [1, 2]], [[1, 'a', int, 3, float, 'a', int, 5], ['a', int]], [[1, 1, 1, 1, 1], [1,1,1]]]
for a, b in tests:
  print(list(remove(a, b)))

输出:

[2, 3, 4]
[]
[1, 3, <class 'float'>, 5]
[1, 1]

Python 2.x!

有点不同的方法
from more_itertools import locate, windowed
big_list = [1, 2, 1, 2, 1]
sub_list = [1, 2, 1]

"""
Fetching all starting point of indexes (of sub_list in big_list)
to be removed from big_list. 
"""

i = list(locate(windowed(big_list, len(sub_list)), pred=lambda x: x==tuple(sub_list)))

""" 
Here i comes out to be [0, 2] in above case. But index from 2 which 
includes 1, 2, 1 has last 1 from the 1st half of 1, 2, 1 so further code is
to handle this case.
PS: this won't come for-
big_list = [2, 1, 2, 3, 1, 2, 4]
sub_list = [1, 2]
as here i comes out to be [1, 4]
"""

# The further code.
to_pop = []
for ele in i:
    if to_pop:
        if ele == to_pop[-1]:
            continue
    to_pop.extend(range(ele, ele+len(sub_list)))

# Voila! to_pop consists of all the indexes to be removed from big_list.

# Wiping out the elements!
for index in sorted(to_pop, reverse=True):
    del big_list[index]

请注意,您需要按相反的顺序删除它们,以免丢弃后续索引。

在 Python3 中,locate() 的签名会有所不同。

更新more_itertools library has released more_itertool.replace,一个解决这个特殊问题的工具(见选项 3)。

首先,这里有一些适用于通用可迭代对象(列表、字符串、迭代器等)的其他选项:

代码

选项 1 - 无库:

def remove(iterable, subsequence):
    """Yield non-subsequence items; sans libraries."""
    seq = tuple(iterable)
    subsequence = tuple(subsequence)
    n = len(subsequence)
    skip = 0

    for i, x in enumerate(seq):
        slice_ = seq[i:i+n]
        if not skip and (slice_ == subsequence):
            skip = n
        if skip:
            skip -= 1
            continue
        yield x   

选项 2 - more_itertools

import more_itertools as mit


def remove(iterable, subsequence):
    """Yield non-subsequence items."""
    iterable = tuple(iterable)
    subsequence = tuple(subsequence)
    n = len(subsequence)
    indices = set(mit.locate(mit.windowed(iterable, n), pred=lambda x: x == subsequence))

    it_ = enumerate(iterable)
    for i, x in it_:
        if i in indices:
            mit.consume(it_, n-1)
        else:
            yield x

演示

list(remove(big_list, sub_list))
# [2, 3, 4]

list(remove([1, 2, 1, 2], sub_list))
# []

list(remove([1, "a", int, 3, float, "a", int, 5], ["a", int]))
# [1, 3, float, 5]

list(remove("11111", "111"))
# ['1', '1']

list(remove(iter("11111"), iter("111")))
# ['1', '1']

选项 3 - more_itertools.replace:

演示

pred = lambda *args: args == tuple(sub_list)
list(mit.replace(big_list, pred=pred, substitutes=[], window_size=2))
# [2, 3, 4]

pred=lambda *args: args == tuple(sub_list)
list(mit.replace([1, 2, 1, 2], pred=pred, substitutes=[], window_size=2))
# []

pred=lambda *args: args == tuple(["a", int])
list(mit.replace([1, "a", int, 3, float, "a", int, 5], pred=pred, substitutes=[], window_size=2))
# [1, 3, float, 5]

pred=lambda *args: args == tuple("111")
list(mit.replace("11111", pred=pred, substitutes=[], window_size=3))
# ['1', '1']

pred=lambda *args: args == tuple(iter("111"))
list(mit.replace(iter("11111"), pred=pred, substitutes=[], window_size=3))
# ['1', '1']

详情

在所有这些示例中,我们都使用较小的 window 切片扫描主序列。我们放弃在切片中找不到的任何东西并跳过切片中的任何东西。

选项 1 - 无库

迭代枚举序列并计算大小为 n(子序列的长度)的切片。如果即将到来的切片等于子序列,则重置 skip 并产生该项目。否则,迭代过去。 skip 跟踪循环前进了多少次,例如sublist 的大小为 n=2,因此每次匹配都会跳过两次。

请注意,您可以通过删除前两个元组分配并将 iterable 参数替换为 seq,将此选项转换为单独与 sequences 一起使用,例如def remove(seq, subsequence):.

选项 2 - more_itertools

索引位于可迭代对象中的每个匹配子序列。在迭代枚举迭代器时,如果在 indices 中找到索引,则通过消耗迭代器中的下一个 n-1 元素来跳过剩余的子序列。否则,将产生一个项目。

通过 > pip install more_itertools 安装此库。

选项 3 - more_itertools.replace:

此工具用替换值替换谓词中定义的项目子序列。要移除物品,我们用一个空容器代替,例如substitutes=[]。替换项的长度由 window_size 参数指定(该值等于子序列的长度)。

只是为了好玩,这是最接近单行的近似值:

from functools import reduce

big_list = [2, 1, 2, 3, 1, 2, 4]
sub_list = [1, 2]
result = reduce(lambda r, x: r[:1]+([1]+r[2:-r[1]],[min(len(r[0]),r[1]+1)]+r[2:])[r[-r[1]:]!=r[0]]+[x], big_list+[0], [sub_list, 1])[2:-1]

不相信它有效?检查一下 on IDEone!

当然,它远非高效且令人厌恶的神秘,但它应该有助于说服 OP 接受

你想要实现的可以通过将其转换为字符串列表并在替换后再次将其转换为整数类型来完成。

在一行中你可以这样做

map(int,list(("".join(map(str, big_list))).replace("".join(map(str, sub_list)),'').replace(''.join((map(str, sub_list))[::-1]),'')))

输入

big_list = [1, 2, 1, 2, 1]
sub_list = [1, 2, 1]

输出

[2, 1]

输入

big_list = [2, 1, 2, 3, 1, 2, 4]
sub_list = [1, 2]

输出

[2, 3, 4]

比上面任何一个都更具可读性,并且没有额外的内存占用:

def remove_sublist(sublist, mainlist):

    cursor = 0

    for b in mainlist:
        if cursor == len(sublist):
            cursor = 0
        if b == sublist[cursor]:
            cursor += 1
        else:
            cursor = 0
            yield b

    for i in range(0, cursor):
        yield sublist[i]

如果您想要库中的函数,这是给在线用户的,就这样吧

[x for x in remove_sublist([1, 2], [2, 1, 2, 3, 1, 2, 4])]

(最后的方法,见最后的代码片段)

我原以为简单的字符串转换就足够了:

big_list = [2, 1, 2, 3, 1, 2, 4]
sub_list = [1, 2]

new_list = list(map(int, list((''.join(map(str, big_list))).replace((''.join(map(str, sub_list))), ''))))

我实际上是在用列表的字符串等价物做 find/replace。之后我将它们映射到整数,以便保留变量的原始类型。这适用于任何大小的大列表和子列表。

但是,如果您在没有文本表示的任意对象上调用它,则这可能不起作用。此外,此方法只会保留对象的文本版本;如果需要维护原始数据类型,这是一个问题。

为此,我用不同的方法编写了一个解决方案:

new_list = []
i = 0
while new_list != big_list:
    if big_list[i:i+len(sub_list)] == sub_list:
        del big_list[i:i+len(sub_list)]
    else:
        new_list.append(big_list[i])
        i += 1

本质上,当我找到 sub_list 的每个重复项时,我会删除它们,当我找到不属于重复项的元素时,我会附加到 new_list 。当 new_list 和 big_list 相等时,所有重复项都已找到,这就是我停止的时候。我没有使用 try-except,因为我认为不应该有任何索引错误。

这与@MadPhysicist 的回答相似,效率大致相同,但我的消耗更少的内存

第二种方法适用于具有任何列表大小的任何类型的对象,因此比第一种方法灵活得多。但是,如果您的列表只是整数,第一种方法会更快。

然而,我还没有完成!我编造了一个单行列表理解,它与第二种方法具有相同的功能!

import itertools
new_list = [big_list[j] for j in range(len(big_list)) if j not in list(itertools.chain.from_iterable([ list(range(i, i+len(sub_list))) for i in [i for i, x in enumerate(big_list) if x == sub_list[0]] if big_list[i:i+len(sub_list)] == sub_list ]))]

起初,这似乎令人生畏,但我向你保证这很简单!首先,我创建了一个索引列表,其中出现了子列表的第一个元素。接下来,对于这些索引中的每一个,我检查以下元素是否构成子列表。如果他们这样做,则将形成子列表副本的索引范围添加到另一个列表。之后,我使用 itertools 中的一个函数来展平生成的列表列表。这个展平列表中的每个元素都是一个索引,它是子列表的副本。最后,我创建了一个 new_list,它由 big_list 的每个元素组成,其中有一个在展平列表中找不到的索引。

我认为其他任何答案中都没有这种方法。我最喜欢它,因为一旦您了解它的工作原理并且非常高效,它就会非常简洁(由于列表推导的性质)。