我正在阅读一些声称有关两个递归Quicksort调用的顺序的文本:
...首先调用较小的子问题很重要,这与尾递归一起确保堆栈深度为log n。
我完全不确定这意味着什么,我为什么要首先在较小的子阵列上调用Quicksort?
答案 0 :(得分:8)
将quicksort视为隐式二叉树。数据透视表是根,左右子树是您创建的分区。
现在考虑对此树进行深度优先搜索。递归调用实际上对应于在上述隐式树上进行深度优先搜索。还假设树总是有一个较小的子树作为左子,所以建议实际上是在这棵树上做预订。
现在假设您使用堆栈实现预订单,其中您只推送左子项(但将父项保留在堆栈中)以及何时推送正确的子项(假设您保持一个状态,您知道是否如果节点有左子节点探索,则替换堆栈顶部,而不是推动右子节点(这对应于尾递归部分)。
最大堆栈深度是最大“左深度”:即如果将每个边缘标记为左子项为1,并将右侧子项标记为0,则表示您正在查看具有最大边缘总和的路径(基本上你没算右边缘。)
现在,由于左子树的元素数不超过一半,因此每次向左移动(即遍历和边标记为1)时,您将剩余的节点数量减少至少一半。
因此,您看到的标记为1的最大边数不超过log n。
因此,如果你总是选择较小的分区,并使用尾递归,那么堆栈的使用不会超过log n。
答案 1 :(得分:5)
有些语言有尾递归。这意味着如果你写f(x){... ... .. ... .. g(x)}那么最后的调用,到g(x),根本没有用函数调用来实现,但是有一个跳转,所以最后的调用不使用任何堆栈空间。
Quicksort将要分类的数据拆分为两个部分。如果您总是首先处理较短的部分,那么每个使用堆栈空间的调用都有一个要排序的数据部分,它最多只是调用它的递归调用的一半大小。因此,如果您从10个元素开始排序,最深的堆栈将有一个调用排序这10个元素,然后调用排序最多5个元素,然后调用排序最多2个元素,然后调用排序最多1个元素 - 然后,对于10个元素,堆栈不能更深 - 堆栈大小受数据大小日志的限制。
如果你不担心这个,你可能最终得到一个调用排序10个元素的堆栈,然后调用排序9个元素,然后调用排序8个元素,依此类推,以便堆栈与要排序的元素数量一样深。但是如果你首先对短节进行排序,这不会发生尾递归,因为虽然你可以将10个元素分成1个元素和9个元素,但最后完成调用排序9个元素并实现为跳转,这不是使用更多的堆栈空间 - 它重用其先前由其调用者使用的堆栈空间,该空间无论如何都要返回。
答案 2 :(得分:1)
理想情况下,列表分为两个大致相似的大小子列表。首先使用哪个子列表并不重要。
但是如果在糟糕的一天,列表以尽可能最不平衡的方式进行分区,则可以是两个或三个项目(可能是四个)的子列表,以及几乎与原始项目一样长的子列表。这可能是由于分区价值的错误选择或错误的人为设计数据。想象一下,如果您首先处理更大的子列表会发生什么。 Quicksort的第一次调用是在其堆栈帧中保存短列表的指针/索引,同时以递归方式调用快速排序以获取长列表。这也很糟糕地划分为一个非常短的列表和一个很长的列表,我们先做更长的子列表,重复...
最终,在最恶劣的恶劣数据的糟糕日子里,我们将建立与原始列表长度成比例的堆栈帧。这是quicksort的最坏情况行为,递归调用的O(n)深度。 (注意我们谈的是quicksort的递归深度,而不是性能。)
首先执行较短的子列表会很快消除它。我们仍然处理大量的小列表,与原始列表长度成比例,但是现在每个小列表都由一个浅的一个或两个递归调用处理。我们仍然进行O(n)调用(性能)但每个都是深度O(1)。
答案 3 :(得分:1)
令人惊讶的是,即使快速排序没有遇到非常不平衡的分区,甚至实际上正在使用内部排序时,这也很重要。
当被排序的容器中的值非常大时,问题就出现了(在C ++中)。通过这个,我并不是说他们指的是真正的大物体,但他们自己真的很大。在这种情况下,一些(可能很多)编译器也会使递归堆栈帧非常大,因为它需要至少一个临时值才能进行交换。交换在分区内部调用,它本身不是递归的,所以你会认为quicksort递归驱动程序不需要怪物堆栈框架;不幸的是,分区通常最终被内联,因为它很好而且很短,而且不会从其他任何地方调用。
通常情况下,20到40个堆栈帧之间的差异可以忽略不计,但如果值的重量为8kb,那么20到40个堆栈帧之间的差异可能意味着工作和堆栈溢出之间的差异,如果堆栈已经存在缩小尺寸以允许许多线程。
如果你使用"总是递归到较小的分区"算法,堆栈不能每超过log 2 N帧,其中N是向量中的元素数。此外,N不能超过可用内存量除以元素的大小。因此,在32位机器上,向量中只能有2个 19 8kb元素,并且快速呼叫深度不能超过19。
简而言之,正确编写快速排序使其堆栈使用可预测(只要您可以预测堆栈帧的大小)。不打扰优化(保存单个比较!)即使在非病理情况下也很容易导致堆栈深度加倍,并且在病理情况下它可能会变得更糟。