只是想知道这样的函数是否可以通过尾递归方式完成。我发现它很难,因为它自称两次。
这是我在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);
}
答案 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] = x
或arr.push(x)
,arr.pop()
和arr.unshift(x)
之类的数组变异操作。当然,您可以使用这些操作来达到预期的结果,但在功能程序中,我们会以不同的方式思考问题。我们只是读取值并构造 new ,而不是通过覆盖它来销毁旧值。
我们还会避免像Array.fill
或uniq
这样的高级操作(不确定您选择了哪种实现),因为我们可以使用递归自然地构建结果。
递归函数的归纳推理是完美的,所以我们不需要调整
size
为零,则返回空结果[[]]
[]
size
至少为一个且我们至少有一个元素x
- 获取一个小于r1
的子列表,获取相同大小的子列表{{1在r2
r1
和r2
前置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 ]
// ]
,identity
,None
和concat
- 以下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;
}