排序—时间复杂度为O(nlogn)的两种排序算法

上一个排序随笔中分析了三种时间复杂度为O(n2)的排序算法,它们适合小规模数据的排序;这次我们试着分析时间复杂为O(nlogn)的排序算法,它们比较适合大规模的数据排序。

1 归并排序

1.1 原理

 将待排序列划分为前后两部分,直到子序列的区间长度为1;对前后两部分分别进行排序,再将排好序的两部分合并在一起。

1.2 实现

 1 static void Merge(ElemType* pElem, int p, int q, int r)    //特别注意这个数组的细节问题
 2 {
 3     ElemType* temp = new ElemType[r - p + 1];    //临时数组
 4     int i = p, j = q + 1, k = 0;
 5 
 6     while (i <= q && j <= r)    //合并有序子序列
 7     {
 8         if (pElem[i] <= pElem[j])
 9             temp[k++] = pElem[i++];
10         else
11             temp[k++] = pElem[j++];
12     }
13     while (i <= q)
14         temp[k++] = pElem[i++];
15     while (j <= r)
16         temp[k++] = pElem[j++];
17 
18     for (k = 0, i = p; k < r - p + 1; )    //将数据元素重新放回
19         pElem[i++] = temp[k++];
20 }
21 
22 static void MergeSortInternal(ElemType* pElem, int p, int r)
23 {
24     if (p >= r)    return;    //序列区间为1
25 
26     int m = (p + r) / 2;    //区间中间点
27     MergeSortInternal(pElem, p, m);
28     MergeSortInternal(pElem, m + 1, r);
29     Merge(pElem, p, m, r);    //合并两子序列
30 }
31 
32 void MergeSort(ElemType* pElem, int n)    //归并排序
33 {
34     MergeSortInternal(pElem, 0, n -1);
35 }

测试结果:

1.3 算法分析

1.3.1 时间复杂度

归并排序算法由递归实现,所以进行时间复杂度分析时也可以通过递归公式分析。因为每次划分区域都选择中间点进行划分,所以递归公式可以写成:

T(n) = T(n/2) + T(n/2) + n, T(1) = C(常数)    //每次合并都要调用Merge()函数,它的时间复杂度为O(n)

等价于:T(n) = 2kT(n/2k) + k * n, 递归的最终状态为T(1)即n/2k = 1,所以k = log2n。

T(n) = nT(1) + n * logn。

所以,递归排序算法的时间复杂度为O(nlogn),不管待排序列的逆序度如何,时间复杂度不变。

1.3.2 空间复杂度

这里的空间复杂度其实是有分歧的,我就理解成王争老师的说法吧,因为归并排序在运行期间,同一时间段内只有一个Merge()在运行,Merge()函数的最大空间复杂度为O(n),所以归并排序的空间复杂度为O(n)。

1.3.3 稳定性

归并排序中会改变数据元素的相对位置的只有两子序列合并时,只要我们将前面的子序列中相等的数据元素放在临时数组的前面,那么相等数据元素的位置不会改变,所以归并排序是稳定的排序算法。

1.3.4 比较操作和交换操作的执行次数

比较操作和交换操作的执行次数可以由时间复杂度分析过程得出,Merge()中总的交换次数为n * logn,因为不管两个子序列的大小,子序列中的各个元素都会先放入临时数组temp中,再重新放回原序列;比较操作的次数小于等于交换操作次数,最大交换次数为n * logn。

2 快速排序

2.1 原理

快速排序和归并排序一样运用了分治的思想。选取分区值,将待排序列分为两个前后两部分,前部分数据元素的值小于等于分区值,后部分的数据元素的值大于等于分区值;继续对前后两部分分别进行分区,直到分区大小为1。

2.2 实现

 1 void swap(ElemType* elem1, ElemType* elem2)    //交换数据元素
 2 {
 3     ElemType temp;
 4     temp = *elem1;
 5     *elem1 = *elem2;
 6     *elem2 = temp;
 7 }
 8 
 9 static int Partition(ElemType* pElem, int p, int r)
10 {
11     //将首、中、尾三个位置的数据元素依序排列
12     int mid, pivot;    //分区值
13     mid = p + ((r - p) >> 1);    //取中间点
14     if (pElem[p] > pElem[mid])    swap(pElem + p, pElem + mid);
15     if (pElem[mid] > pElem[r])    swap(pElem + mid, pElem + r);
16     if (pElem[p] > pElem[mid])    swap(pElem + p, pElem + mid);
17 
18     //分区
19     int i = p, j = r;
20     pivot = pElem[mid];
21     while (i <= j)
22     {
23         while (pElem[i] <= pivot)
24             ++i;
25         while (pElem[j] >= pivot)
26             --j;
27         if (i <= j) 
28         {
29             swap(pElem + i, pElem + j);
30             ++i;
31             --j;
32         }    
33     }
34 
35     return i;
36 }
37 
38 static void QuickSortInternal(ElemType* pElem, int p, int r)
39 {
40     if (r <= p)    return;    //子序列长度为1
41 
42     int q = Partition(pElem, p, r);
43     QuickSortInternal(pElem, p, q - 1);
44     QuickSortInternal(pElem, q, r);
45     VisitArray(pElem, p, r);
46 }
47 
48 void QuickSort(ElemType* pElem, int n)    //快速排序
49 {
50     QuickSortInternal(pElem, 0, n - 1);    //为了统一操作
51 }

测试结果

2.3 算法分析

2.3.1 最坏情况时间复杂度、最好情况时间复杂度和平均情况时间复杂度

1)最坏情况时间复杂度:如果每次分区值选取的恰好是最大或最小值,那么就有n次分区操作,分区操作的时间复杂度为O(n),所以最坏情况时间复杂度为O(n2);

2)最好情况时间复杂度:如果每次分区值都为序列中数据元素的中位数,那么根据递归公式就可以写成T(n) = T(n/2) + T(n/2) + n;那么时间复杂度为O(nlogn);

3)平均情况时间复杂度:可以将区间划分为n / 10、9 * n / 10代入递推公式,可以得到时间复杂度为O(nlogn)。

2.3.2 空间复杂度

快速排序过程中,只用到了有限个数的临时变量,所以空间复杂度为O(1)。

2.3.3 稳定性

快速排序改变相对位置的操作为交换,这里的交换操作是在和分区值比较的基础上进行的,这样会导致相等数据元素的相对位置发生改变,所以快速排序算法是不稳定的。

2.3.4 比较操作和交换操作执行的次数

最坏情况下,待排序列为正序或逆序,需要执行n-1次递归调用,第i次需要执行n-i次比较操作,所以比较操作执行次数为n * (n - 1) / 2,交换操作的执行次数为0(对应正序情况)或n * (n - 1) / 2(对应逆序情况);最好情况下,递归调用执行logn次,第i次递归的比较次数为n / 2k,所以比较次数为n-1,最大交换次数也为n-1。

3 归并排序和快速排序的的适用场景

3.1 归并排序

归并排序在所有情况下的时间复杂度为O(nlogn)而且是稳定的,但是缺点也非常明显,它的空间复杂度为O(n)。所以,归并排序适合数据规模大、对多个关键字排序、内存限制小的场景。

3.2 快速排序

快速排序的缺点就是不是稳定的并且对分区值的选择依赖大,但是分区值的选取问题可以由方法改善。所以快速排序比归并排序更适合大规模数据的单一关键值排序,实际应用场景中快速排序也比归并排序应用的多。

猜你喜欢

转载自www.cnblogs.com/zpchya/p/10775866.html