Convert multiple recursive calls into tail-recursion

限于喜欢 提交于 2019-12-12 20:34:16

问题


Just wondering if a function like this can be done tail-recursively. I find it quite difficult because it calls itself twice.

Here is my non-tail-recursive implementation in javascript. (Yes I know most javascript engine doesn't support TCO, but this is just for theory.) The goal is to find all sublists of a certain length(size) of a given array(arr). Ex: getSublistsWithFixedSize([1,2,3] ,2) returns [[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);
}

回答1:


One such way is to use continuation-passing style. In this technique, an additional parameter is added to your function to specify how to continue the computation

Below we emphasize each tail call with /**/

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 ] ]

You can think of the continuation almost like we invent our own return mechanism; only it's a function here, not a special syntax.

This is perhaps more apparent if we specify our own continuation at the call site

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

Or even

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

The pattern might be easier to see with a simpler function. Consider the famous fib

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

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

Below we convert it to continuation-passing style

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

I want to remark that the use of .slice and .unshift is unnecessary for this particular problem. I'll give you an opportunity to come up with some other solutions before sharing an alternative.


Edit

You did a good job rewriting your program, but as you identified, there are still areas which it can be improved. One area I think you're struggling the most is by use of array mutation operations like arr[0] = x or arr.push(x), arr.pop(), and arr.unshift(x). Of course you can use these operations to arrive at the intended result, but in a functional program, we think about things in a different way. Instead of destroying an old value by overwriting it, we only read values and construct new ones.

We'll also avoid high level operations like Array.fill or uniq (unsure which implementation you chose) as we can build the result naturally using recursion.

The inductive reasoning for your recursive function is perfect, so we don't need to adjust that

  1. if the size is zero, return the empty result [[]]
  2. if the input array is empty, return an empty set, []
  3. otherwise the size is at least one and we have at least one element x - get the sublists of one size smaller r1, get the sublists of the same size r2, return the combined result of r1 and r2 prepending x to each result in r1

We can encode this in a straightforward way. Notice the similarity in structure compared to your original program.

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
                      )
                  )
            )
      )

We call it with a size and an input array

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 ]
// ]

Lastly, we provide the dependencies identity, None, concat, and prepend - Below concat is an example of providing a functional interface to an object's method. This is one of the many techniques used to increase reuse of functions in your programs and help readability at the same time

const identity = x =>
  x 

const None =
  {}

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

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

You can run the full program in your browser below

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 ]
// ]



回答2:


Here is my solution with the help of an accumulator. It's far from perfect but it works.

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


来源:https://stackoverflow.com/questions/50776158/convert-multiple-recursive-calls-into-tail-recursion

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!