zoukankan      html  css  js  c++  java
  • 排序(下):如何用快排思想在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)。

  • 相关阅读:
    C# 中字符串转换成日期
    c#中退出WinForm程序包括有很多方法,如:this.Close(); Application.Exit();Application.ExitThread(); System.Environment.Exit(0);
    c#获取程序版本号
    分分钟用上C#中的委托和事件
    【转载】C# 中的委托和事件(详解:简单易懂的讲解)
    C#什么时候需要使用构造函数
    15、生命周期-BeanPostProcessor-后置处理器
    13、生命周期-InitializingBean和DisposableBean
    11、组件注册-使用FactoryBean注册组件
    12、生命周期-@Bean指定初始化和销毁方法
  • 原文地址:https://www.cnblogs.com/zhengzc/p/11633877.html
Copyright © 2011-2022 走看看