如何重构二进制递归以使其与trampoline函数兼容?

时间:2018-06-18 10:39:08

标签: javascript recursion functional-programming

我写了一个这样的快速排序函数:

const quickSort = list => {
  if (list.length === 0) return list
  const [pivot, ...rest] = list
  const smaller = []
  const bigger = []
  for (x of rest) {
    x < pivot ? smaller.push(x) : bigger.push(x)
  }

  return [...quickSort(smaller), pivot, ...quickSort(bigger)]
}

我想将此函数传递给trampoline函数以使其更有效。但是,为了使递归函数与trampoline兼容,我必须返回一个调用外部函数的函数。如下所示:

const trampoline = fn => (...args) => {
  let result = fn(...args)
  while (typeof result === "function") {
    result = result()
  }
  return result
}

const sumBelowRec = (number, sum = 0) => (
  number === 0
    ? sum
    : () => sumBelowRec(number - 1, sum + number)
)

const sumBelow = trampoline(sumBelowRec)
sumBelow(100000)
// returns 5000050000

如何转换quickSort函数以使其利用trampoline函数?

2 个答案:

答案 0 :(得分:0)

要使用蹦床,您的递归功能必须为tail recursive。您的quickSort函数尾递归,因为quickSort的递归调用不会出现在尾部位置,即

return [...quickSort(smaller), pivot, ...quickSort(bigger)]

也许在您的程序中很难看到,但程序中的尾部调用是一个数组连接操作。如果您在不使用ES6语法的情况下编写它,我们可以更容易地看到它

const a = quickSort(smaller)
const b = quickSort(bigger)
const res1 = a.concat(pivot)
const res2 = res1.concat(b) // <-- last operation is a concat
return res2

为了使quickSort尾递归,我们可以使用continuation-passing style来表达我们的程序。转换我们的程序很简单:我们通过向函数添加一个参数并使用它来指定计算应该如何继续来实现。默认延续是identity函数,它只是将其输入传递给输出 - 更改为粗体

const identity = x =>
  x

const quickSort = (list, cont = identity) => {
  if (list.length === 0)
    return cont(list)

  const [pivot, ...rest] = list
  const smaller = []
  const bigger = []
  for (const x of rest) { // don't forget const keyword for x here
    x < pivot ? smaller.push(x) : bigger.push(x)
  }

  return quickSort (smaller, a =>
           quickSort (bigger, b =>
             cont ([...a, pivot, ...b])))
}

现在我们可以看到quickSort总是出现在尾部位置。但是,如果我们使用大输入调用我们的函数,直接递归将导致许多调用帧累积并最终溢出堆栈。为了防止这种情况发生,我们bounce每个尾部调用蹦床

const quickSort = (list, cont) => {
  if (list.length === 0)
    return bounce (cont, list);

  const [pivot, ...rest] = list
  const smaller = []
  const bigger = []
  for (const x of rest) {
    x < pivot ? smaller.push(x) : bigger.push(x)
  }

  return bounce (quickSort, smaller, a =>
           bounce (quickSort, larger, b =>
             bounce (cont, [...a, pivot, ...b])))
}

现在我们需要一个trampoline

const bounce = (f, ...args) =>
  ({ tag: bounce, f, args })

const trampoline = t =>
{ while (t && t.tag === bounce)
    t = t.f (...t.args)
  return t
}

果然有效

console.log (trampoline (quickSort ([ 6, 3, 4, 8, 1, 6, 2, 9, 5, 0, 7 ])))
// [ 0, 1, 2, 3, 4, 5, 6, 6, 7, 8, 9 ]

我们验证它适用于大数据。百万数字在零到一百万之间......

const rand = () =>
  Math.random () * 1e6 >> 0

const big = 
  Array.from (Array (1e6), rand)

console.time ('1 million numbers')
console.log (trampoline (quickSort (big)))
console.timeEnd ('1 million numbers')
// [ 1, 1, 2, 4, 5, 5, 6, 6, 6, 7, ... 999990 more items ]
// 1 million numbers: 2213 ms

在另一个问题I answered recently中,我展示了将其他两个常见函数转换为延续传递函数。

堆栈安全递归是我已经广泛讨论的内容,主题为almost 30 answers

答案 1 :(得分:-1)

我无法看到蹦床如何让你的quickSort更有效率。你要添加的最少的东西是基于硬字符串的检查,如果你处于返回状态,有一个值,或者处于进一步划分结果的状态,有一个函数。这将增加计算时间。

你可以做的是让它更通用。一般来说,我会说蹦床是解释递归的好方法,但与直接函数调用相比,在效率方面从来都不好。

此外,要利用蹦床,您必须创建一个返回函数或值的函数。但你需要s.th. 可以返回鸿沟quickSort处有两个递归子呼叫)。这是你需要重用trampoline的方式(递归中的一种递归,你可能想要调用它的二级递归)。

const qs = (list) => {
  if (list.length === 0)
    return list;
  const [pivot, ...rest] = list;
  const smaller = [];
  const bigger = [];
  for (let x of rest) {
    x < pivot ? smaller.push(x) : bigger.push(x);
  }
  return [...trampoline(qs)(smaller), pivot, ...trampoline(qs)(bigger)];
};
const trampoline = fn => (...args) => {
  let result = fn(...args);
  while (typeof result === "function") {
    result = result();
  }
  return result;
};
console.log(trampoline(qs)([1, 6, 2, 4]));
console.log(trampoline(qs)([4, 5, 6, 1, 3, 2]));

我使用Chromium进行了检查,这段代码实际上正在运行甚至是排序。

我是否已经提到:这不能比原始的直接递归调用更快。这将叠加许多函数对象。