有没有人能够用“简单易懂”的方式,但又正式地解释快速排序算法为什么是O(n log n)的?据我所知,快速排序需要对n个元素进行一次遍历,并且它会重复执行log n次...我不确定如何用语言表达为什么它要重复执行log n次。
有没有人能够用“简单易懂”的方式,但又正式地解释快速排序算法为什么是O(n log n)的?据我所知,快速排序需要对n个元素进行一次遍历,并且它会重复执行log n次...我不确定如何用语言表达为什么它要重复执行log n次。
快速排序会首先将输入分成两个部分:它选择一个“枢轴”值,将输入分为小于枢轴值和大于枢轴值的两部分(当然,相等于枢轴值的元素也需要放到其中一部分,但是对于基本描述来说,这并不重要)。
因为输入(定义上)没有排序,所以为了进行分割,它必须查看输入中的每个元素,因此这是一个O(N)操作。在第一次分割完输入后,它递归地对这些“块”中的每一个排序。在这两次调用之间,每个递归调用都会检查它的所有输入,因此最终会访问每个输入值(再次)。所以在第一层分区时,我们有一个调用,它检查每个输入项。在第二级别上,我们有两个分区步骤,但在这两个步骤之间,它们(再次)检查每个输入项。每个后续级别都有更多的单独分区步骤,但总体来说,每个级别的调用都会查看所有输入项。
它会一直将输入分成越来越小的部分,直到达到某个分区大小的下限。可能的最小值是每个分区中只有一个元素。
在理想情况下,我们希望每次分区将输入分成两半。这些“半部分”可能不会完全相等,但如果我们选择好枢轴,它们应该非常接近。为了让数学计算简单,假设我们进行了完美的分区,因此每次都得到精确的一半。
在这种情况下,我们可以将其分成对数级别的部分,即以2为底,输入数量的对数。例如,给定128个输入,我们将得到64、32、16、8、4、2和1这几个分区大小。这是7个分区级别(是的,log2(128) = 7)。
因此,我们有log(N)个分区“级别”,每个级别都必须访问所有N个输入。 因此,log(N)个级别乘以每个级别的N次操作,使我们得到O(N log N)的总复杂度。
现在让我们重新考虑每个分区级别将输入“精确”分成一半的假设。 根据我们选择的分区元素的好坏程度,我们可能无法获得精确相等的两半。 那么最糟糕的情况是什么? 最糟糕的情况是枢轴实际上是输入中最小或最大的元素。 在这种情况下,我们进行了O(N)个分区级别,但与其得到两个相等大小的半部分,我们得到了一个包含一个元素和一个包含N-1个元素的分区。 如果每个分区级别都发生了这种情况,我们显然会在每个分区变为单个元素之前执行O(N)个分区级别。
这给出了Quicksort的技术上正确的大O时间复杂度(大O正式指代复杂度的上限)。由于我们有O(N)个分区级别,每个级别需要O(N)步骤,因此我们最终得到O(N * N)(即O(N2))的复杂度。
从实际角度考虑,真正的实现通常会在实际达到单个元素的分区之前停止分区。在典型情况下,当一个分区包含10个或更少的元素时,您将停止分区并使用诸如插入排序之类的东西(因为对于少量元素来说它通常更快)。
最近发明了Quicksort的其他修改方法(例如Introsort,PDQ Sort),这些方法防止了O(N2)的最坏情况。 Introsort通过跟踪当前的分区“级别”,并在/如果它过深时切换到堆排序来实现,堆排序比Quicksort对于典型输入来说更慢,但保证任何输入的O(N log N)的复杂度。
PDQ排序方法在这里又有了新的改进: 由于堆排序比较慢,因此它试图避免使用堆排序。为达到这个目的,如果看起来选择的枢轴值较差,它将在选择枢轴之前随机重排一些输入数据。只有当这种方法不能产生足够好的枢轴值时,它才会切换到使用堆排序。
嗯,它并不总是n(log n)。当所选的枢轴大约在中间时,这是性能时间。在最坏情况下,如果您选择最小或最大元素作为枢轴,则时间将为O(n^2)。
要想象'n log n',您可以假设枢轴是最接近要排序的所有元素平均值的元素。这将把数组分成大致相同长度的两部分。在这两个部分上都应用快速排序过程。
由于每一步都会将数组长度减半,因此您需要重复此操作log n(以2为底)次,直到达到长度= 1,即一个由1个元素组成的已排序数组。