排序(下):如何用快排思想在O(n)内查找第K大元素?
冒泡排序、插入排序、选择排序它们的时间复杂度都是O(n2),适合小规模数据的排序。
大规模的数据排序可以用时间复杂度为O(nlogn)的排序算法,归并排序和快速排序。
归并排序的原理
将待排序的数组,从数组中间分成左右两部分,然后对左右两部分分别排序,再将排好序的凉部分数组合并在一起,这样就完成待排序数组的排序了。其利用的是分治思想,即将一个大问题分解成小的子问题来解决,分治算法一般都是使用递归来实现的。
static void Main(string[] args) { int[] iarray = new int[] { 11, 8, 3, 9, 7, 1, 2, 5 }; mergeSortInternally(iarray, 0, iarray.Length - 1); Console.WriteLine(string.Join(",", iarray)); Console.ReadKey(); } private static void mergeSortInternally(int[] array, int start, int end) { // 递归终止条件 if (start >= end) return; // 取start到end之间的中间位置mid,防止(start+end)的和超过int类型最大值 //int mid = (start + end) / 2; int mid = start + (end - start) / 2; // 分治递归 mergeSortInternally(array, start, mid); mergeSortInternally(array, mid + 1, end); // 将a[start...mid]和a[mid+1...end]合并为a[start...end] mergeBySentry(array, start, mid, end); } private static void merge(int[] array, int start, int mid, int end) { int p1 = start; int p2 = mid + 1; int k = 0; // 申请大小跟a[start...end]一样的临时数组 int[] tmp = new int[end - start + 1]; // 比较两个小集合的元素,依次放入大集合 while (p1 <= mid && p2 <= end) { if (array[p1] <= array[p2]) tmp[k++] = array[p1++]; else tmp[k++] = array[p2++]; } //左侧小集合还有剩余,依次放入大集合尾部 while (p1 <= mid) tmp[k++] = array[p1++]; //右侧小集合还有剩余,依次放入大集合尾部 while (p2 <= end) tmp[k++] = array[p2++]; // 把大集合的元素复制回原数组 for (int i = 0; i < tmp.Length; i++) { array[start + i] = tmp[i]; } }
可以利用哨兵简化merge合并代码:
private static void mergeBySentry(int[] array, int start, int mid, int end) { int[] leftArr = new int[mid - start + 2]; int[] rightArr = new int[end - mid + 1]; for (int i = 0; i <= mid - start; i++) { leftArr[i] = array[start + i]; } // 第一个数组添加哨兵(最大值) leftArr[mid - start + 1] = int.MaxValue; for (int i = 0; i < end - mid; i++) { rightArr[i] = array[mid + 1 + i]; } // 第二个数组添加哨兵(最大值) rightArr[end - mid] = int.MaxValue; int p1 = 0; int p2 = 0; int k = start; while (k <= end) { // 当左边数组到达哨兵值时,i不再增加,直到右边数组读完剩余值,同理右边数组也一样 if (leftArr[p1] <= rightArr[p2]) array[k++] = leftArr[p1++]; else array[k++] = rightArr[p2++]; } }
1.在合并过程中,左右两边的数组如果有值相同的元素,会优先排序左边数组,这样就保证了数组合并前后的先后顺序不变,所以归并排序是一个稳定的排序算法。
2.假设对n个元素进行归并排序需要的时间是T(n),那分解成两个子数组排序的时间都是T(n/2),merge()函数合并两个有序子数组的时间复杂度是O(n),所以归并排序的时间复杂度公式是:
T(n) = 2*T(n/2) + n = 2*(2*T(n/4) + n/2) + n = 4*T(n/4) + 2*n = 4*(2*T(n/8) + n/4) + 2*n = 8*T(n/8) + 3*n = 8*(2*T(n/16) + n/8) + 3*n = 16*T(n/16) + 4*n ...... = 2^k * T(n/2^k) + k * n ......
T(1) = C;n=1时,只需要常量级的执行时间,所以表示为C;
T(n) = 2kT(n/2k)+kn,当T(n/2k)=T(1)时,k=log2n,代入上面公式,T(n)=Cn+nlog2n,所以时间复杂度是O(nlogn)。
( 最后数据区间变成1的时候排序就完成了 我们看n经过了多少次分解会变成1)
3.递归代码每次合并操作都会申请额外的内存空间,但是在合并完成之后,临时开辟的内存空间就被释放掉了。在任意时刻,CPU只会有一个函数在执行,也就只会有一个临时的内存空间在使用。临时内存空间最大也不会超过n个数据的大小,所以空间复杂度是O(n)。