排序(下):如何用快排思想在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)。

 

posted @ 2019-10-08 10:13  Z大山  阅读(558)  评论(0编辑  收藏  举报