【问题标题】:How to refactor a binary recursion to make it compatible with trampoline function?如何重构二进制递归以使其与蹦床功能兼容?
【发布时间】:2018-11-27 04:36:02
【问题描述】:

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

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

我想将此函数传递给蹦床函数以提高效率。但是,要使递归函数与蹦床兼容,我必须返回一个调用外部函数的函数。如下图:

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 函数?

【问题讨论】:

  • 我喜欢这个 q,因为它是一个非常好的教学示例,带有漂亮的现代 js 和函数式编程(赞成)
  • 蹦床不会提高效率——事实上,几乎在所有情况下,它的性能都可能较低。使用蹦床的唯一原因是为了防止递归程序破坏堆栈。

标签: javascript recursion functional-programming


【解决方案1】:

我看不出蹦床如何让您的quickSort 更有效率。您将添加的最少的事情是基于硬字符串的检查,如果您处于返回状态,具有值,或者处于进一步划分结果的状态,具有函数。这会增加相当多的计算时间。

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

此外,要利用蹦床,您必须创建一个返回函数或值的函数。但你需要…… 可以返回一个除法quickSort两个递归子调用)。这就是你需要重用蹦床的方式(递归中的一种递归,你可能想要调用它的二级递归)。

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,这段代码实际上确实有效,甚至可以排序。

我是否已经提到:这不能比原来的直接递归调用更快。这会堆积很多函数对象。

【讨论】:

  • 谢谢!我检查了 trampoline(qs) 和原始 quickSort 的性能,结果发现后者稍微快一些。我还发现快速排序的命令式版本比我的递归版本性能要好得多。所以如果性能是一个问题,我们应该避免使用递归吗?
  • 据我记忆,所谓的“结束递归”总是可以转换为迭代。是的,如果可以的话,更喜欢迭代。迭代和递归在性能上可能存在巨大差异。最著名的例子是 fib(x)。进一步检查 O 符号!
  • 这不是蹦床的有效使用。在你的程序中,trampoline 调用 qs 调用 trampoline 调用 qs... 这是 mutual recursion 如果输入数据足够大,你仍然会遇到堆栈溢出
  • @user633183 回答已经提到递归问题和堆栈问题(但感谢精确的英文术语,我的学习时间很久以前)
  • @user633183 经过更多阅读:我明白你的意思。确实,我的回答是纯粹从句法的角度来解决它。它没有将其优化为尾递归(我记得德语中的术语“Endrekursion”),然后将其转换为迭代,消除堆栈问题。
【解决方案2】:

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

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

也许在你的程序中很难看到,但是你程序中的尾调用是一个数组 concat 操作。如果你在不使用 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 函数,它只是将其输入传递给输出 - bold

的变化
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

【讨论】:

    猜你喜欢
    • 2016-03-24
    • 2010-09-16
    • 2018-11-02
    • 1970-01-01
    • 1970-01-01
    • 2012-03-09
    • 1970-01-01
    • 1970-01-01
    • 2021-06-01
    相关资源
    最近更新 更多