【问题标题】:Convert multiple recursive calls into tail-recursion将多个递归调用转换为尾递归
【发布时间】:2018-06-12 23:40:00
【问题描述】:

只是想知道这样的函数是否可以尾递归完成。我觉得这很困难,因为它会调用自己两次。

这是我在 javascript 中的非尾递归实现。 (是的,我知道大多数 javascript 引擎不支持 TCO,但这只是理论上的。)目标是找到给定数组(arr)的特定长度(大小)的所有子列表。例如:getSublistsWithFixedSize([1,2,3] ,2) 返回 [[1,2], [1,3], [2,3]]

function getSublistsWithFixedSize(arr, size) {
    if(size === 0) {
        return [[]];
    }
    if(arr.length === 0 ) {
        return [];
    }
    let [head, ...tail] = arr;
    let sublists0 = getSublistsWithFixedSize(tail, size - 1);
    let sublists1 = getSublistsWithFixedSize(tail, size);
    let sublists2 = sublists0.map(x => {
        let y = x.slice();
        y.unshift(head);
        return y;
    });

    return  sublists1.concat(sublists2);
}

【问题讨论】:

  • 任何函数最多只能有一个尾调用。所以不,除非你重构它以使用显式堆栈,否则你不能只使用尾递归。
  • ^^^^^ IOW 你可以做到这一点,如果你重构它以使用显式堆栈(即跟踪自己下一步要做什么,依赖递归为你做)。
  • 如果调用不止一次,性质将是递归的,而不是迭代的。您可以使用显式堆栈或嵌套延续,虽然您有 100% 的尾递归,但其成本可能比一开始就增加堆栈大小要高。

标签: javascript recursion tail-recursion


【解决方案1】:

其中一种方法是使用延续传递样式。在这种技术中,一个额外的参数被添加到你的函数中来指定如何继续计算

下面我们用/**/强调每个尾调用

function identity(x) {
/**/return x;
}

function getSublistsWithFixedSize(arr, size, cont = identity) {
    if(size === 0) {
/**/   return cont([[]]);
    }
    if(arr.length === 0 ) {
/**/    return cont([]);
    }
    let [head, ...tail] = arr;
/**/return getSublistsWithFixedSize(tail, size - 1, function (sublists0) {
/**/    return getSublistsWithFixedSize(tail, size, function (sublists1) {
            let sublists2 = sublists0.map(x => {
                let y = x.slice();
                y.unshift(head);
                return y;
            });
/**/        return cont(sublists1.concat(sublists2));
        });
    });
}

console.log(getSublistsWithFixedSize([1,2,3,4], 2))
// [ [ 3, 4 ], [ 2, 4 ], [ 2, 3 ], [ 1, 4 ], [ 1, 3 ], [ 1, 2 ] ]

你可以把延续想象成我们发明了自己的return机制;这里只是一个函数,不是特殊语法。

如果我们在调用站点指定我们自己的延续,这可能会更明显

getSublistsWithFixedSize([1,2,3,4], 2, console.log)
// [ [ 3, 4 ], [ 2, 4 ], [ 2, 3 ], [ 1, 4 ], [ 1, 3 ], [ 1, 2 ] ]

甚至

getSublistsWithFixedSize([1,2,3,4], 2, sublists => sublists.length)
// 6

使用更简单的函数可能更容易看到该模式。考虑著名的fib

const fib = n =>
  n < 2
    ? n
    : fib (n - 1) + fib (n - 2)

console.log (fib (10))
// 55

下面我们将其转换为 continuation-passing 样式

const identity = x =>
  x

const fib = (n, _return = identity) =>
  n < 2
    ? _return (n)
    : fib (n - 1, x =>
        fib (n - 2, y =>
          _return (x + y)))

fib (10, console.log)
// 55

console.log (fib (10))
// 55

我想指出,对于这个特定问题,使用 .slice.unshift 是不必要的。在分享替代方案之前,我会让您有机会提出一些其他解决方案。


编辑

您在重写程序方面做得很好,但正如您所指出的,仍有一些地方可以改进。我认为您最苦苦挣扎的一个领域是使用数组变异操作,例如arr[0] = xarr.push(x)arr.pop()arr.unshift(x)。当然,您可以使用这些操作来达到预期的结果,但是在函数式程序中,我们以不同的方式思考事物。我们不是通过覆盖来破坏旧值,而是仅读取值并构造值。

我们还将避免像 Array.filluniq 这样的高级操作(不确定您选择了哪个实现),因为我们可以使用递归自然地构建结果。

你的递归函数的归纳推理是完美的,所以我们不需要调整它

  1. 如果size为零,则返回空结果[[]]
  2. 如果输入数组为空,则返回一个空集,[]
  3. 否则size至少为1且我们至少有一个元素x - 获取小一号r1的子列表,获取相同大小r2的子列表,返回组合结果r1r2r1 中的每个结果前面加上 x

我们可以用一种简单的方式对其进行编码。请注意与原始程序相比的结构相似。

const sublists = (size, [ x = None, ...rest ], _return = identity) =>
  size === 0
    ? _return ([[]])

  : x === None
    ? _return ([])

  : sublists              // get sublists of 1 size smaller, r1
      ( size - 1
      , rest
      , r1 =>
          sublists        // get sublists of same size, r2
            ( size
            , rest
            , r2 =>
                _return   // return the combined result
                  ( concat
                      ( r1 .map (r => prepend (x, r)) // prepend x to each r1
                      , r2
                      )
                  )
            )
      )

我们用size 和一个输入数组来调用它

console.log (sublists (2, [1,2,3,4,5]))
// [ [ 1, 2 ]
// , [ 1, 3 ]
// , [ 1, 4 ]
// , [ 1, 5 ]
// , [ 2, 3 ]
// , [ 2, 4 ]
// , [ 2, 5 ]
// , [ 3, 4 ]
// , [ 3, 5 ]
// , [ 4, 5 ]
// ]

最后,我们提供了依赖关系 identityNoneconcatprepend - 下面的 concat 是一个为对象方法提供功能接口的示例。这是用于增加程序中函数重用并同时提高可读性的众多技术之一

const identity = x =>
  x 

const None =
  {}

const concat = (xs, ys) =>
  xs .concat (ys)

const prepend = (value, arr) =>
  concat ([ value ], arr)

您可以在下面的浏览器中运行完整的程序

const identity = x =>
  x 
  
const None =
  {}

const concat = (xs, ys) =>
  xs .concat (ys)

const prepend = (value, arr) =>
  concat ([ value ], arr)

const sublists = (size, [ x = None, ...rest ], _return = identity) =>
  size === 0
    ? _return ([[]])
  
  : x === None
    ? _return ([])

  : sublists             // get sublists of 1 size smaller, r1
      ( size - 1
      , rest
      , r1 =>
          sublists       // get sublists of same size, r2
            ( size
            , rest
            , r2 =>
                _return   // return the combined result
                  ( concat
                      ( r1 .map (r => prepend (x, r)) // prepend x to each r1
                      , r2
                      )
                  )
            )
      )

console.log (sublists (3, [1,2,3,4,5,6,7]))
// [ [ 1, 2, 3 ]
// , [ 1, 2, 4 ]
// , [ 1, 2, 5 ]
// , [ 1, 2, 6 ]
// , [ 1, 2, 7 ]
// , [ 1, 3, 4 ]
// , [ 1, 3, 5 ]
// , [ 1, 3, 6 ]
// , [ 1, 3, 7 ]
// , [ 1, 4, 5 ]
// , [ 1, 4, 6 ]
// , [ 1, 4, 7 ]
// , [ 1, 5, 6 ]
// , [ 1, 5, 7 ]
// , [ 1, 6, 7 ]
// , [ 2, 3, 4 ]
// , [ 2, 3, 5 ]
// , [ 2, 3, 6 ]
// , [ 2, 3, 7 ]
// , [ 2, 4, 5 ]
// , [ 2, 4, 6 ]
// , [ 2, 4, 7 ]
// , [ 2, 5, 6 ]
// , [ 2, 5, 7 ]
// , [ 2, 6, 7 ]
// , [ 3, 4, 5 ]
// , [ 3, 4, 6 ]
// , [ 3, 4, 7 ]
// , [ 3, 5, 6 ]
// , [ 3, 5, 7 ]
// , [ 3, 6, 7 ]
// , [ 4, 5, 6 ]
// , [ 4, 5, 7 ]
// , [ 4, 6, 7 ]
// , [ 5, 6, 7 ]
// ]

【讨论】:

  • 哇,这种风格确实很“实用”。就靠它了,谢谢。
  • @user1285245 非常好地修改了你的程序。我提供了更新并添加了一些评论。如果你觉得这有帮助,我鼓励你阅读我的其他一些javascript answers;它们是使用相似的风格和技术编写的:D
【解决方案2】:

这是我借助蓄电池的解决方案。它远非完美,但它确实有效。

function getSublistsWithFixedSizeTailRecRun(arr, size) {
    let acc= new Array(size + 1).fill([]);
    acc[0] = [[]];
    return getSublistsWithFixedSizeTailRec(arr, acc);
}


function getSublistsWithFixedSizeTailRec(arr, acc) {
    if(arr.length === 0 ) {
        return acc[acc.length -1];
    }
    let [head, ...tail] = arr;
    //add head to acc
    let accWithHead = acc.map(
        x => x.map(
            y => {
                let z = y.slice()
                z.push(head);
                return z;
            }
        )
    );
    accWithHead.pop();
    accWithHead.unshift([[]]);

    //zip accWithHead and acc
    acc = zipMerge(acc, accWithHead);

    return getSublistsWithFixedSizeTailRec(tail, acc);
}

function zipMerge(arr1, arr2) {
    let result = arr1.map(function(e, i) {
        return uniq(e.concat(arr2[i]));
    });
    return result;
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-09-27
    • 1970-01-01
    • 1970-01-01
    • 2023-01-13
    • 2019-09-14
    • 2016-03-21
    相关资源
    最近更新 更多