【问题标题】:Non-recursive traversal of nested list - building a similar nested list in Python嵌套列表的非递归遍历——在Python中构建类似的嵌套列表
【发布时间】:2019-08-06 03:55:52
【问题描述】:

我需要遍历一个嵌套列表,使用str() 处理每个非列表项,并返回保持结构的类似列表。使用递归会很容易,但我需要以迭代的方式进行。下面是我对while 循环的尝试:

def myiter(e):
    a = [e] # initial list
    c = [[]] # final result
    get_last = lambda x: x[len(x)-1] # get ref to the final sublist
    l = get_last(c)
    while a:
        b = a.pop(0)
        if isinstance(b, list):
            # if there are more items to process in the original list
            if a:
                a = b + a
            # else extend original list to process sublists
            else:
                a.extend(b)
            # make a new sublist ref
            l = get_last(c)
            c.append([])
        else:
             # walk and process every item in the nested list
             l.append(str(b))
    return c

这有几个问题,如输出所示:

myiter([1, [2, [3, 4], 5]]) # [['1'], ['2'], ['3', '4', '5'], []]

想要的结果是:

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

有没有简单的迭代方式在 Python 中完成任务?

【问题讨论】:

  • 期望的结果是什么?
  • 另外,a = [e] 似乎被滥用了。您正在制作一个包含 e 的列表,因为它是唯一的元素。 get_last 写得更好:lambda x: x[-1]
  • 最初创建一个输入列表确保我们在第一手处理一个列表。在函数的开头可以通过其他方式处理。我会将 -1 更改为 get last lambda 函数,这是一种更好的方式。但我会将代码留在原帖中,供答案和cmets参考。
  • 您可以像使用递归一样模拟堆栈运算符。 l = get_last(c) 喜欢推栈,但弹出栈没有出现在你的代码中,你可以尝试从这里做一些工作。
  • 请问为什么不能使用递归?

标签: python recursion iteration


【解决方案1】:

这似乎有效:

def stringify(a):
    a = a[:]                           # Make copy of what was passed in.
    res = []                           # Initialize result list.
    my_stack = []                      # Initialize our own LIFO stack.
    while (a or my_stack):                            # While a or my_stack is non-empty
        if (a):
            elem = a.pop(0)
            if (not isinstance(elem, list)):          # If popped elem is not a list
                res.append(str(elem))                 # Append stringified elem to res
            else:
                my_stack.append((a, res))           # Push some stuff, to resume working upon later.
                a = elem                            # Let's start iterating on this inner list
                res = []                            # This inner list needs a clean res, to start with.
        else:                                       # my_stack is non-empty
            a, res_prev = my_stack.pop()   # Pop some stuff, to resume, work on outer list
            res_prev.append(res)           # First, append our just-completed inner list.
            res = res_prev
    return res

输出:

a = [1, [2, [3, 4], 5]]
stringify(a)
['1', ['2', ['3', '4'], '5']]

通过了以下测试用例:

a = [1, [[[2]]]]
a = [[[1]], 2]
a = [1, [[2]]]
a = [1, [2, [3, 4], 5], [6, [7, [8]]], 9]
a = [1, [2, [3, 4], 5]]
a = [1, 2, 3, 4, 5]

有关其工作原理的一些说明:

  1. 如果列表a 中的pop 产生一个整数,我们只需将字符串化整数附加到res
  2. 如果我们的列表a 上的pop 生成一个内部列表,我们需要在处理该内部列表之后出现的元素之前开始处理该内部列表。处理完内部列表后,我们将不得不回到a 的剩余未弹出元素。
  3. 每当我们检测到我们的当前列表a 已变为空时,我们的res 将指向等效的字符串化列表,是时候将我们的res 附加到它可能的(字符串化)列表中外部列表
  4. 为了处理每个遇到的内部列表,我们采用该内部列表作为我们的新a(赋值a = elem),并将一个空列表作为我们的新resres = [])。在我们这样做之前,我们需要将我们当前的a 和我们当前的res 压入堆栈
  5. 为什么选择后进先出法?好吧,这样看:last 推送到 my_stack 的任何内容都代表我们当前正在处理的任何列表 (a) 的直接外部列表。

【讨论】:

  • 值得进一步调整。我没有像您那样广泛地尝试修改“非列表案例”上的列表。为了进一步改进,此测试用例目前失败:[1, [2, [3, 4], 5], [6, [7, [8]]], 9]
  • 已修复,并进一步测试了其他测试用例。请检查。
  • 现在很酷!我什至在某些时候检查两个列表时也有相同的部分。你能解释一下 lifo stack 的用途吗,我会接受这个答案,因为它对我的目的有足够的帮助。
  • @MarkokraM:请检查添加到答案中的注释。
【解决方案2】:

为什么不递归?使用递归过程处理递归数据结构是自然而直接的。将递归过程转换为迭代过程不必涉及克隆输入、创建stack 或其他中间值。您的大脑可以摆脱如此严重的复杂性 -

def first (a = []):
  return a[0]

def rest (a = []):
  return a[1:]

def myiter (a = []):
  # base: empty a
  if not a:
    return []
  # inductive: non-empty a, first elem is list
  elif isinstance(first(a), list):
    return [ myiter(first(a)) ] + myiter(rest(a))
  # inductive: non-empty a, first elem is non-list
  else:
    return [ str(first(a)) ] + myiter(rest(a))     

print(myiter([1, [2, [3, 4], 5]]))

当然,将str作为函数的参数是有意义的,f -

def myiter (f, a = []):
  # base: empty a
  if not a:
    return []
  # inductive: non-empty a, first elem is list
  elif isinstance(first(a), list):
    return [ myiter(f, first(a)) ] + myiter(f, rest(a))
  # inductive: non-empty a, first elem is non-list
  else:
  return [ f(first(a)) ] + myiter(f, rest(a))

现在你可以随心所欲地深度映射str -

print(myiter(str, [1, [2, [3, 4], 5]]))
# ['1', ['2', ['3', '4'], '5']]

或使用您选择的任何功能 -

def square (x):
  return x * x

print(myiter(square, [1, [2, [3, 4], 5]]))
# [1, [4, [9, 16], 25]]

您是否因为堆栈限制而试图避免递归?如果你让它尾递归 -

def identity (x):
  return x

def myiter (f, init = []):
  def run (a = init, then = identity):
    if not a:
      return \
        then([])
    # inductive: non-empty a, first elem is list
    elif isinstance(first(a), list):
      return \
        recur(first(a), lambda l: \
          recur(rest(a), lambda r: \
            then([ l ] + r)))
    # inductive: non-empty a, first elem is non-list
    else:
      return \
        recur(rest(a), lambda r: \
          then([ f(first(a)) ] + r))
  # loop inner function
  return loop (run)

然后实现一个通用的loop,它将递归调用堆栈转换为一个迭代序列-

def recur (*values):
  return (recur, values)

def loop (f):
  acc = f ()
  while type(acc) is tuple and acc[0] is recur:
    acc = f(*acc[1])
  return acc

输出是一样的,但现在myiter 可以接受一个 any 嵌套限制的数组。无限制的递归;多么美好的事情-

print(myiter(str, [1, [2, [3, 4], 5]]))
# ['1', ['2', ['3', '4'], '5']]

print(myiter(square, [1, [2, [3, 4], 5]]))
# [1, [4, [9, 16], 25]]

使用repl.it在您自己的浏览器中查看此程序并验证结果。


为什么不能使用递归? - 戳

@Jab,三个原因:第一个最大递归限制经常在我的一个应用程序上达到,第二个性能问题虽然 map 可能与迭代方法正面交锋,第三个只是用于训练和研究这两种不同风格的编码。 – MarkokraM

所以你还没有达到递归限制,但你担心你的程序会?最好了解实际限制,而不是围绕幽灵编写程序。在使用生成器的简化实现中,请注意递归仅在遇到嵌套级别时发生。即使您按原样保留此实现,它也可以支持任何长度的列表和嵌套级别,直至堆栈限制,默认值可能约为 1,000。这意味着唯一会破坏您的堆栈的数据输入是嵌套 1,000 次或更多的数据输入。在达到实际限制之前离开这个程序可能是安全的。

def square (x):
  return x * x

def myiter (f, init = []):
  def gen (a):
    for x in a:
      if isinstance(x, list):
        yield list(gen(x)) # recursion
      else:
        yield f(x)
  return list(gen(init))

print(myiter(str, [1, [2, [3, 4], 5]]))
# ['1', ['2', ['3', '4'], '5']]

print(myiter(square, [1, [2, [3, 4], 5]]))
# [1, [4, [9, 16], 25]]

【讨论】:

  • 由于领域语言特定的问题,我经常反对最大递归限制,这里解释得太复杂了。使用映射递归遍历所有列表项并保持列表结构的简化版本是:def recmap(a): return list(map(recmap, a)) if isinstance(a, list) else str(a)。与迭代方法相比,它非常简短和优雅。纯匿名函数版本也可以:print((lambda f, a: f(f, a)) ((lambda f, a: list(map(lambda b: f(f, b), a)) if isinstance(a, list) else str(a)), [1, [2, [3, 4]], 5]))
  • 确实是一个更好的答案;认为递归但解决迭代。像scheme 这样的函数式语言的行为是这样的吗?
  • @Nishant 你明白了。方案为王^_^
  • @user633183,但是这种方法在 Python 中是否有效,因为它不支持尾递归?例如,请参阅此代码 - myiter(str, range(0, 957))。你能分享你的想法吗?
  • @Nishant 当然。你试过运行它吗?它将返回[ '0', '1', '2', ..., '954', '955', '956' ]。此示例甚至适用于使用生成器(而不是 loop)实现的最后一个程序。
猜你喜欢
  • 2021-09-19
  • 2012-11-11
  • 2019-08-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多