【问题标题】:Recursively emptying a nested list while preserving its structure递归清空嵌套列表,同时保留其结构
【发布时间】:2025-12-05 13:35:01
【问题描述】:

我正在尝试编写一个函数来读取包含列表的列表并返回相同的列表结构但没有元素。

def remove_from_list(l):
    for e in l:
        if isinstance(e, list):
            remove_from_list(e)
        else:
            l.remove(e)
    return l

所以对于这样的输入:

[1, 2, [], [3,[4]], 5]

我应该得到这样的东西:

[[], [[]]]

我尝试了这些输入:

[1, 2, [], [3,[4]], 5]
[1, 2, [], [2,[3]], 2]

得到了这些输出:

[2, [], [[4]]]
[[], [[3]], 2]

这很令人困惑,因为两个列表的结构相同;只有元素不同。所以我不仅犯了一个错误,而且我得到了一个不同的错误。非常感谢您对我的错误的功能和解释提供帮助。

【问题讨论】:

  • 确实,这是一个初学者的错误。您不应该在迭代期间删除列表元素。
  • 你一定要修改你传入的列表吗?
  • @cᴏʟᴅsᴘᴇᴇᴅ 不正确。可以使用反向迭代和del,不需要副本。
  • @wim 您提到的关于递归列表的有趣挑战。您如何看待我在回答中试图解决的问题?

标签: python algorithm list recursion


【解决方案1】:

问题是您在迭代同一列表时要从列表中删除元素。一种解决方案是制作副本并对其进行迭代,如下所示:

def remove_from_list(l):
    for e in l[:]: # make a copy by slicing
        if isinstance(e, list):
            remove_from_list(e)
        else:
            l.remove(e)
    return l

结果:

>>> remove_from_list([1, 2, [], [3,[4]], 5])
[[], [[]]]

Python documentation 对此行为的解释:

当序列被循环修改时有一个微妙之处(这只会发生在可变序列,即列表中)。内部计数器用于跟踪接下来使用哪个项目,并在每次迭代时递增。当此计数器达到序列的长度时,循环终止。这意味着如果套件从序列中删除当前(或前一个)项目,则将跳过下一个项目(因为它获取已处理的当前项目的索引)。同样,如果套件在当前项之前插入序列中的一项,则当前项将在下一次循环中再次处理。

【讨论】:

    【解决方案2】:

    这里的关键问题是for 循环具有一定的迭代次数,但是当您删除循环内的列表时,您会缩小它们。因此,循环指针保持固定,而列表变小。在某一时刻,循环没有机会完成迭代。

    选项 1
    作为一个简单的修复,您可以在函数内创建一个新列表。这应该更简单,并且不会改变您的原始列表。

    def remove_from_list(l):
        r = []
        for e in l:
            if isinstance(e, list):
                r.append(remove_from_list(e))
        return r
    

    >>> remove_from_list([1, 2, [], [3,[4]], 5])
    [[], [[]]]
    

    此外,由于您只是附加空结构,这应该比创建数据副本和随后的删除调用更快。


    选项 2
    wim's idea 为基础,反向迭代并使用del,如果您想就地改变列表

    def remove_from_list(l):
        r = []
        for i in range(len(l) - 1, -1, -1):
            if isinstance(l[i], list):
                remove_from_list(l[i])
            else:
                del l[i]
    

    >>> l = [1, 2, [], [3,[4]], 5]
    >>> remove_from_list(l)
    >>> l
    [[], [[]]]
    

    从良好实践的角度来看,我建议要么返回副本,要么在原地修改而不返回,但不能同时返回。


    您可以执行 timeit 比较以确定哪种方法对您的数据运行更快。

    首先,设置-

    def remove_from_list(l):
        r = []
        for e in l:
            if isinstance(e, list):
                r.append(remove_from_list(e))
        return r
    
    def remove_from_list_reverse_del(l):
        r = []
        for i in range(len(l) - 1, -1, -1):
            if isinstance(l[i], list):
                remove_from_list(l[i])
            else:
                del l[i]
    
    
    def remove_from_list_copy(l):
        for e in l[:]: # make a copy by slicing
            if isinstance(e, list):
                remove_from_list_copy(e)
            else:
                l.remove(e)
        return l
    
    y = [1, 2, [], [3,[4]], 5]
    z = copy.deepcopy(y  * 10000)
    

    接下来,时间安排——

    %timeit remove_from_list(z)
    19.3 ms ± 334 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
    

    %%timeit
    z2 = copy.deepcopy(z)    # copying because this function mutates the original
    remove_from_list_reverse_del(z2)
    
    78.6 ms ± 157 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
    

    尽管在创建z2 时浪费了大量时间。

    【讨论】:

    • 嗨,当列表包含对自身的引用时,这会崩溃。
    • 嗯,他们都会的。
    • 是的,我从来没有说过这里有更好的答案。你问过它在技术上是否在某些方面不合理。
    • @wim 好吧,添加了反向删除,但处理递归列表超出了我愿意对这样的问题投入多少注意力的范围。
    【解决方案3】:

    这是另一种使用递归的方法

    def remove_from_list (l):
      if not l:
        return []
      elif isinstance (l[0], list):
        return [remove_from_list (l[0])] + remove_from_list (l[1:])
      else:
        return remove_from_list (l[1:])
    
    print (remove_from_list ([1, 2, [], [3, [4]], 5]))
    # [[], [[]]]
    

    如果你觉得以这种方式思考问题很好,你会发现一些通用函数让事情变得更好

    def is_empty (l):
      return not l
    
    def is_list (l):
      return isinstance (l, list)
    
    def head (l):
      return l[0]
    
    def tail (l):
      return l[1:]
    
    def remove_from_list (l):
      if is_empty (l):
        return []
      elif is_list (head (l)):
        return [remove_from_list (head (l))] + remove_from_list (tail (l))
      else:
        return remove_from_list (tail (l))
    
    print (remove_from_list ([1, 2, [], [3, [4]], 5]))
    # [[], [[]]]
    

    它不会改变输入

    data = [1, 2, [], [3, [4]], 5]
    
    print (remove_from_list (data))
    # [[], [[]]]
    
    print (data)
    # [1, 2, [], [3, [4]], 5]
    

    还有一个尾递归版本,可以做成stack-safe(更改为粗体

    def identity (x):
      return x
    
    def remove_from_list (l, k = identity):
      if is_empty (l):
        return k ([])
      elif is_list (head (l)):
        return remove_from_list (head (l), lambda x:
          remove_from_list (tail (l), lambda y:
            k ([x] + y)))
      else:
        return remove_from_list (tail (l), k)
    
    print (remove_from_list (data))
    # [[], [[]]]

    【讨论】:

      【解决方案4】:

      良好的老式递归发生了什么? (顺便说一句,这个答案还处理包含对自己的引用的列表。)

      def f(l, i, ids):
        if i >= len(l):
          return l
        if isinstance(l[i], list):
          if not id(l[i]) in ids:
            ids.add(id(l[i]))
            f(l[i], 0, ids)
          return f(l, i + 1, ids)
        else:
          del l[i]
          return f(l, i, ids)
      
      a = [1, 2, [], [3,[4]], 5]
      a.append(a)
      a[3].append(a[3])
      
      print a # [1, 2, [], [3, [4], [...]], 5, [...]]
      print f(a, 0, set([id(a)])) # [[], [[], [...]], [...]]
      

      (至于您的误解 - 正如 cᴏʟᴅsᴘᴇᴇᴅ 提到的,在 for 循环期间删除列表的一部分可能会导致意外结果,因为迭代范围是在开始之前设置的,但列表在中途被修改。)

      【讨论】:

      • 什么也没发生。这里所有的答案都是递归的?
      • @cᴏʟᴅsᴘᴇᴇᴅ 递归,是的;老式的,没有。
      【解决方案5】:

      简单递归

      def remove_from_list(l):
        if l == []:
          return []
        elif not isinstance(l[0], list):
          return remove_from_list(l[1:])
        else:
          return [remove_from_list(l[0])] + remove_from_list(l[1:])
      

      有点复杂

      def remove_from_list(l):
        def remove_inner(l_in,l_out):
          if l_in == []:
            return l_out
          elif not isinstance(l_in[0], list) or l[0] == []:
            return remove_inner(l_in[1:],l_out)
          else:
            return remove_inner(l_in[1:], l_out + [remove_from_list(l_in[0])])
        return remove_inner(l,[])
      
      print(remove_from_list([1, 2, [], [3,[4]], 5]))
      

      【讨论】: