要使用蹦床,您的递归函数必须是 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