我正在阅读Robert Sedgewick的书算法第4版,他有以下练习题:大小为0,1和2时预期的子阵列数是多少quicksort用于使用不同的键对N个项目的数组进行排序?
然后他说,如果你在数学上有倾向,做数学,如果不是,运行实验,我已经进行了实验,看起来大小为0和1的数组具有完全相同的出现次数和大小为2的数组只是发生的一半。
有问题的快速排序版本是双向分区的版本。
据我所知,当分区项目是子阵列中最小/最大的分区项目时,我们得到大小为0的子数组,因此后续的2个排序调用将是
sort(a, lo, j-1); // here if j-1 < lo, we have an array of size 0
sort(a, j+1, hi); // here if j+1 > hi, we have an array of size 0
当分区项目是第二个到第一个最小/最大项目时,大小为1的数组,当它是第三个到第一个最小/最大项目时,大小为2。
那么,我究竟如何得出一个数学公式?
以下是C#中的代码
class QuickSort
{
private static int zero = 0, one = 0, two = 0;
private static int Partition<T>(T[] a, int lo, int hi) where T : IComparable<T>
{
T v = a[lo];
int i = lo, j = hi + 1;
while(true)
{
while(Alg.Less(a[++i], v)) if(i == hi) break;
while(Alg.Less(v, a[--j])) if(j == lo) break;
if(i >= j) break;
Alg.Swap(ref a[i], ref a[j]);
}
Alg.Swap(ref a[lo], ref a[j]);
return j;
}
private static void Sort<T>(T[] a, int lo, int hi) where T : IComparable<T>
{
if(hi < lo) zero++;
if(hi == lo) one++;
if(hi - lo == 1) two++;
if(hi <= lo) return;
int j = Partition(a, lo, hi);
Sort(a, lo, j - 1);
Sort(a, j + 1, hi);
}
public static void Sort<T>(T[] a) where T : IComparable<T>
{
Alg.Shuffle(a);
int N = a.Length;
Sort(a, 0, N - 1);
Console.WriteLine("zero = {0}, one = {1}, two = {2}", zero, one, two);
}
}
有一个证据表明平均快速排序使用2NlnN~1.39NlgN比较排序长度为N且具有不同键的数组。
我想我们可以想到1.39NlgN,因为我们进行N次比较~lgN次,所以平均而言我们将数组分成两半,因此在某些时候我们将留下成对进行比较,并且因为有只有要比较的对,例如:&lt; 1,2&gt;,&lt; 3,4&gt;,&lt; 5,6&gt;等等...,我们将在分区后得到大小为0和1的子数组,这只能证明0和1的大小更频繁,但我仍然不明白为什么2的大小几乎只有一半的频率。
答案 0 :(得分:4)
QuickSort将递归地将数组分区为位置k处的两个较小的数组。 k可以是1到n。每个k具有相同的发生概率。让C0(n)
为0大小子集的平均出现次数,C1(n)
,C2(n)
对于1个大小和2个大小的子集是相同的。
除了初始条件,每个都满足:
C(n) = 1/n sum(C(k-1) + C(n-k) for k=1..n)
总和的两个部分是相同的,但是以相反的顺序求和,所以:
C(n) = 2/n sum(C(k-1) for k=1..n)
或
n*C(n) = 2*sum(C(k-1) for k=1..n)
假设n
和n-1
都不是初始条件的一部分,我们可以通过从双方减去(n-1)C(n-1)
来简化:
n*C(n) - (n-1)C(n-1) = 2*C(n-1)
或
C(n) = (n+1)/n * C(n-1)
从递归关系中导出结果
我们现在有一个递归关系C(n)
,它同样适用于C0
,C1
和C2
。
对于C0
,我们有初始条件C0(0)=1
,C0(1)=0
。我们计算C0(2)
以获得1
,然后我们可以为n> 2应用简化的递归关系C0(n) = (n+1)/n * C0(n-1)
以获得一般结果C0(n)=(n+1)/3
。
对于C1
,我们有初始条件C1(0)=0
,C1(1)=1
。与以前一样,我们计算C1(2)
以获取1
,并应用与C0
相同的过程来获得一般结果C1(n)=(n+1)/3
。
对于C2
,我们有初始条件C2(0)=C2(1)=0
和C2(2)=1
。这次我们计算C2(3) = 1/3 * 2 * (C2(0) + C2(1) + C2(2)) = 2/3
。然后应用简化的递归关系推断一般结果C2(n)=(n+1)/4 * C2(3) = (n+1)/4 * 2/3 = (n+1)/6
。
<强>结论强>
我们已经显示了在两种情况下(n + 1)/ 3快速分配大小为n的数组时,0个大小和1个大小的子阵列的平均出现次数。对于2个大小的子阵列,我们已经显示它是(n + 1)/ 6。
这证实了您的原始观察结果,即2个大小的子集恰好是0和1个大小的子集的一半,并给出了精确的均值公式。
答案 1 :(得分:3)
不考虑数学,有一点是完全清楚的:当使用双元素分区调用quicksort时,它将发出两个递归调用,其中一个具有零元素分区,另一个具有一个 - 元素分区。
因此,对于计算的每个双元素分区,肯定会有一个单元素分区和一个零元素分区。
此外,当所选分区元素是当前分区中的最大/最小或第二大/最小元素时,单元素和零元素分区可以自发地发生,而没有双元素父元素。粗略地说,这些应该和对方一样可能,并且也可能出现两元素分区。
所以观察到的结果并不出乎意料。