将多个递归调用转换为尾递归

时间:2018-06-09 16:10:18

标签: javascript recursion tail-recursion

只是想知道这样的函数是否可以通过尾递归方式完成。我发现它很难,因为它自称两次。

这是我在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);
}

2 个答案:

答案 0 :(得分:2)

一种方法是使用延续传递方式。在此技术中,会在函数中添加一个附加参数,以指定继续计算的方法

下面我们用/**/

强调每个尾调用

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

下面我们将其转换为继续传递样式

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)之类的数组变异操作。当然,您可以使用这些操作来达到预期的结果,但在功能程序中,我们会以不同的方式思考问题。我们只是读取值并构造 new ,而不是通过覆盖它来销毁旧值。

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

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

  1. 如果size为零,则返回空结果[[]]
  2. 如果输入数组为空,则返回空集[]
  3. 否则size至少为一个且我们至少有一个元素x - 获取一个小于r1的子列表,获取相同大小的子列表{{1在r2
  4. 中,将r1r2前置x的合并结果返回给每个结果

    我们可以直接对此进行编码。注意结构与原始程序的相似性。

    r1

    我们用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 ] // ] identityNoneconcat - 以下prepend是一个提供功能接口的示例对象的方法。这是用于增加程序中函数重用并同时提高可读性的众多技术之一

    concat

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

    const identity = x =>
      x 
    
    const None =
      {}
    
    const concat = (xs, ys) =>
      xs .concat (ys)
    
    const prepend = (value, arr) =>
      concat ([ value ], arr)
    

答案 1 :(得分:1)

这是我在累加器的帮助下的解决方案。它远非完美,但却有效。

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;
}