快速排序和堆排序都是原地排序算法。哪个更好?在什么应用场景下,两者中的哪一个更受青睐?
堆排序的时间复杂度为O(N log N),比快速排序最坏情况下的时间复杂度要好得多。堆排序不需要额外的数组来存储有序数据,因此比归并排序更省内存。那么为什么商业应用程序还是坚持使用快速排序?快速排序有什么特别之处,超过其他实现吗?
我亲自测试过算法,并且发现快速排序确实有一些特别之处。它运行速度快得多,比堆排序和归并排序都快。
快速排序的秘密在于:几乎不进行不必要的元素交换。交换非常耗时。
对于堆排序,即使所有数据已经有序,您也需要交换100%的元素才能将数组排序。
对于归并排序,情况更糟糕。您需要将100%的元素写入另一个数组,然后将其写回原始数组,即使数据已经有序。
在快速排序中,您不会交换已经有序的元素。如果您的数据完全有序,则几乎不需要交换任何内容!尽管人们对最坏情况十分担心,但选择一个好的枢轴(除了获取数组的第一个或最后一个元素)可以避免最坏情况。如果从第一个、最后一个和中间元素之间的中间元素获取枢轴,则足以避免最坏情况。
快速排序的优势不在于最坏情况,而在于最好情况!在最好情况下,您进行相同数量的比较,但交换的内容很少。在平均情况下,您交换部分元素,但不像堆排序和归并排序那样交换所有元素。这就是为什么快速排序需要的时间最短,交换次数更少,速度更快。
在我的电脑上,以C#实现的以下代码(在发布模式下运行)使用中间枢轴优化比Array.Sort快3秒钟,使用改进的枢轴则快2秒钟(是的,获取良好的枢轴确实有一些开销)。
static void Main(string[] args)
{
int[] arrToSort = new int[100000000];
var r = new Random();
for (int i = 0; i < arrToSort.Length; i++) arrToSort[i] = r.Next(1, arrToSort.Length);
Console.WriteLine("Press q to quick sort, s to Array.Sort");
while (true)
{
var k = Console.ReadKey(true);
if (k.KeyChar == 'q')
{
// quick sort
Console.WriteLine("Beg quick sort at " + DateTime.Now.ToString("HH:mm:ss.ffffff"));
QuickSort(arrToSort, 0, arrToSort.Length - 1);
Console.WriteLine("End quick sort at " + DateTime.Now.ToString("HH:mm:ss.ffffff"));
for (int i = 0; i < arrToSort.Length; i++) arrToSort[i] = r.Next(1, arrToSort.Length);
}
else if (k.KeyChar == 's')
{
Console.WriteLine("Beg Array.Sort at " + DateTime.Now.ToString("HH:mm:ss.ffffff"));
Array.Sort(arrToSort);
Console.WriteLine("End Array.Sort at " + DateTime.Now.ToString("HH:mm:ss.ffffff"));
for (int i = 0; i < arrToSort.Length; i++) arrToSort[i] = r.Next(1, arrToSort.Length);
}
}
}
static public void QuickSort(int[] arr, int left, int right)
{
int begin = left
, end = right
, pivot
// get middle element pivot
//= arr[(left + right) / 2]
;
//improved pivot
int middle = (left + right) / 2;
int
LM = arr[left].CompareTo(arr[middle])
, MR = arr[middle].CompareTo(arr[right])
, LR = arr[left].CompareTo(arr[right])
;
if (-1 * LM == LR)
pivot = arr[left];
else
if (MR == -1 * LR)
pivot = arr[right];
else
pivot = arr[middle];
do
{
while (arr[left] < pivot) left++;
while (arr[right] > pivot) right--;
if(left <= right)
{
int temp = arr[right];
arr[right] = arr[left];
arr[left] = temp;
left++;
right--;
}
} while (left <= right);
if (left < end) QuickSort(arr, left, end);
if (begin < right) QuickSort(arr, begin, right);
}
这篇论文有一些分析。
另外,来自维基百科:
快速排序的最直接竞争者是堆排序。堆排序通常比快速排序略慢,但最坏情况下的运行时间始终是 Θ(nlogn)。快速排序通常更快,但仍有可能出现最坏情况,除非采用introsort变体,在检测到糟糕情况时切换到堆排序。如果预先知道需要使用堆排序,则直接使用它将比等待introsort切换更快。
对于大多数情况来说,快速排序与稍微更快的排序并没有什么区别......您只是永远不希望它偶尔变得非常慢。虽然您可以调整快速排序以避免非常慢的情况,但您会失去基本快速排序的优雅。因此,在大多数情况下,我实际上更喜欢堆排序......您可以完全实现其简单优雅性,并且永远不会出现慢速排序。
对于需要在大多数情况下实现最大速度的情况,快速排序可能优于堆排序,但两者都可能不是正确的答案。对于速度至关重要的情况,值得仔细研究情况的细节。例如,在我的一些速度关键代码中,数据已经排序或接近排序(正在索引多个相关字段,这些字段通常同时向上或向下移动,或相反地向上或向下移动,因此一旦您按一个字段排序,则其他字段就被排序或反向排序或接近...任何一种情况都可能导致快速排序失败)。对于这种情况,我都没有实施快排或堆排......相反,我实施了Dijkstra的SmoothSort......它是一种堆排序变体,当数据已排序或接近排序时,它的时间复杂度为O(N)......它不太优雅,也不太容易理解,但很快......如果您想要挑战一些更具挑战性的代码,请阅读 http://www.cs.utexas.edu/users/EWD/ewd07xx/EWD796a.PDF。
堆排序具有最坏情况下运行时间为O(n*log(n))的优点,因此在快速排序可能表现不佳的情况下(通常是大部分已排序数据集),堆排序更受欢迎。
如果你深入到架构层面,我们在缓存内存中使用队列数据结构。所以无论队列中有什么,都会被排序。在快速排序中,我们没有将数组划分为任何长度的问题。但是在堆排序(通过使用数组)中,可能会出现父级不在可用于缓存的子数组中的情况,然后它必须将其带入缓存内存...这需要时间。
这就是为什么快速排序是最好的选择!
快速排序和归并排序之间的比较,因为两者都是原地排序的一种类型,所以它们之间存在差异。最坏情况下快速排序的运行时间为O(n^2)
,而堆排序的最坏运行时间仍为O(n*log(n))
,对于平均数量的数据,快速排序将更加有用。由于它是随机算法,因此在较短的时间内得到正确的答案的概率将取决于您选择的轴元素的位置。
因此:
好的选择: L和G的大小都小于3s/4
不好的选择: L和G中有一个的大小大于3s/4
对于少量数据,我们可以选择插入排序;对于非常大量的数据,我们可以选择堆排序。