【问题标题】:How to implement combinations tail-recursively?如何实现尾递归组合?
【发布时间】:2019-11-05 14:56:44
【问题描述】:

我正在通过阅读 Ierusalimschy 的 Programming in Lua(第 4 版)自学 Lua,并进行练习。习题 6.5 是

编写一个函数,该函数接受一个数组并打印数组中元素的所有组合。

在这个简洁的陈述之后,本书给出了一个提示,清楚地表明人们期望做的是编写一个函数来打印所有 C(n, m) m 个元素的组合,来自 n 个元素的数组。

我实现了如下所示的combinations函数:

function combinations (array, m)

  local append = function (array, item)
    local copy = {table.unpack(array)}
    copy[#copy + 1] = item
    return copy
  end

  local _combinations
  _combinations = function (array, m, prefix)
    local n = #array
    if n < m then
      return
    elseif m == 0 then
      print(table.unpack(prefix))
      return
    else
      local deleted = {table.unpack(array, 2, #array)}
      _combinations(deleted, m - 1, append(prefix, array[1]))
      _combinations(deleted, m, prefix)
    end
  end

  _combinations(array, m, {})

end

它工作正常,但它不是尾递归的。

谁能告诉我一个尾递归函数,它与上面的combinations 做同样的事情吗?

(无论如何,我使用的是 Lua 5.3。)

注意:我意识到这个练习不需要函数是尾递归的。出于好奇,这是我自己添加的要求。

编辑:我稍微简化了函数,但删除了几个没有增加多少的嵌套函数。

【问题讨论】:

    标签: lua tail-recursion


    【解决方案1】:

    还有第三种选择,一种没有蛇吃尾巴的选择。尽管尾调用递归不会导致堆栈溢出,但出于个人喜好,我会避免这样做。我使用一个 while 循环和一个堆栈来保存每次迭代的信息。在循环中,您从堆栈中弹出下一个任务,完成工作,然后将下一个任务推入堆栈。我觉得它看起来更干净,嵌套更容易可视化。

    以下是我如何将您的代码转换为我编写它的方式:

    function combinations(sequence, item)
        local function append(array, item)
            local copy = {table.unpack(array)}
            copy[#copy + 1] = item
            return copy
        end
    
        local stack = {}
        local node = { sequence, item, {} }
    
        while true do
            local seq = node[ 1 ]
            local itm = node[ 2 ]
            local pre = node[ 3 ]
    
            local n = #seq
            if itm == 0 then
                print(table.unpack(pre))
            elseif n < itm then
                -- do nothing
            else
                local reserve = {table.unpack(seq, 2, #seq)}
                table.insert(stack, { reserve, itm, pre })
                table.insert(stack, { reserve, itm-1, append(pre, seq[ 1 ]) })
            end
    
            if #stack > 0 then
                node = stack[ #stack ] -- LIFO
                stack[ #stack ] = nil
            else
                break
            end
        end
    end
    

    您可以将这种 while-loop 堆栈/节点技术用于几乎任何递归方法。这是一个应用于打印深度嵌套表的示例:https://stackoverflow.com/a/42062321/5113346

    我的版本,使用您的输入示例给出相同的输出: 1 2 3 1 2 4 1 2 5 1 3 4 1 3 5 1 4 5 2 3 4 2 3 5 2 4 5 3 4 5

    如果它不适用于其他传递的参数,请原谅我,因为我没有尝试解决练习的答案,而只是重写了原始帖子中的代码。

    【讨论】:

      【解决方案2】:

      好的,我想我找到了一种方法:

      function combinations (array, m)
      
        local dropfirst = function (array)
          return {table.unpack(array, 2, #array)}
        end
      
        local append = function (array, item)
          local copy = {table.unpack(array)}
          copy[#copy + 1] = item
          return copy
        end
      
        local _combinations
        _combinations = function (sequence, m, prefix, queue)
          local n = #sequence
          local newqueue
          if n >= m then
            if m == 0 then
              print(table.unpack(prefix))
            else
              local deleted = dropfirst(sequence)
              if n > m then
                newqueue = append(queue, {deleted, m, prefix})
              else
                newqueue = queue
              end
              return _combinations(deleted, m - 1,
                                   append(prefix, sequence[1]),
                                   newqueue)
            end
          end
          if #queue > 0 then
             newqueue = dropfirst(queue)
             local newargs = append(queue[1], newqueue)
             return _combinations(table.unpack(newargs))
          end
      
        end
      
        _combinations(sequence, m, {}, {})
      
      end
      

      我认为这个版本是尾递归的。不幸的是,它并没有像我原来的非尾递归版本那样以良好的顺序打印出结果(更不用说增加的代码复杂性),但不能拥有一切!


      编辑:好吧,不,一个可以拥有一切!下面的版本是尾递归的,并且以与原始非尾递归版本相同的顺序打印其结果:

      function combinations (sequence, m, prefix, stack)
        prefix, stack = prefix or {}, stack or {}
      
        local n = #sequence
        if n < m then return end
      
        local newargs, newstack
        if m == 0 then
          print(table.unpack(prefix))
          if #stack == 0 then return end
          newstack = droplast(stack)
          newargs = append(stack[#stack], newstack)
        else
          local deleted = dropfirst(sequence)
          if n > m then
             newstack = append(stack, {deleted, m, prefix})
          else
             newstack = stack
          end
          local newprefix = append(prefix, sequence[1])
          newargs = {deleted, m - 1, newprefix, newstack}
        end
      
        return combinations(table.unpack(newargs)) -- tail call
      
      end
      

      它使用以下辅助功能:

      function append (sequence, item)
        local copy = {table.unpack(sequence)}
        copy[#copy + 1] = item
        return copy
      end
      
      function dropfirst (sequence)
        return {table.unpack(sequence, 2, #sequence)}
      end
      
      function droplast (sequence)
        return {table.unpack(sequence, 1, #sequence - 1)}
      end
      

      例子:

      > combinations({1, 2, 3, 4, 5}, 3)
      1   2   3
      1   2   4
      1   2   5
      1   3   4
      1   3   5
      1   4   5
      2   3   4
      2   3   5
      2   4   5
      3   4   5
      

      具有讽刺意味的是,这个版本通过实现自己的堆栈来实现尾递归,所以我不确定它最终是否比非尾递归版本更好......再说一次,我猜函数的 stack 确实存在在堆中(对吗?),因为 Lua 的表是通过引用传递的(对吗?),所以也许这毕竟是一种改进。 (如果我错了,请纠正我!)

      【讨论】:

        猜你喜欢
        • 2019-04-07
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2014-03-23
        • 1970-01-01
        • 2010-12-04
        相关资源
        最近更新 更多