上一个排序随笔中分析了三种时间复杂度为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 快速排序
快速排序的缺点就是不是稳定的并且对分区值的选择依赖大,但是分区值的选取问题可以由方法改善。所以快速排序比归并排序更适合大规模数据的单一关键值排序,实际应用场景中快速排序也比归并排序应用的多。
原文:https://www.cnblogs.com/zpchya/p/10775866.html