美文网首页
从零开始养成算法·篇二十二:十大经典排序算法(2)

从零开始养成算法·篇二十二:十大经典排序算法(2)

作者: 文竹_自然 | 来源:发表于2020-05-22 16:45 被阅读0次

    \color{orange}{6、归并排序:}

    归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。

    \color{orange}{算法描述:}
    把长度为n的输入序列分成两个长度为n/2的子序列;
    对这两个子序列分别采用归并排序;
    将两个排序好的子序列合并成一个最终的排序序列。

    \color{orange}{算法分析:}
    1、时间复杂度:O(nlogn) 2、空间复杂度:O(n) 3、稳定排序 4、非原地排序

    \color{orange}{递归:}

    void BubbleSort2(SqList *L){
    //③ 将有序的SR[i..mid]和SR[mid+1..n]归并为有序的TR[i..n]
    void Merge(int SR[],int TR[],int i,int m,int n)
    {
        int j,k,l;
        //1.将SR中记录由小到大地并入TR
        for(j=m+1,k=i;i<=m && j<=n;k++)
        {
            if (SR[i]<SR[j])
                TR[k]=SR[i++];
            else
                TR[k]=SR[j++];
        }
        
        //2.将剩余的SR[i..mid]复制到TR
        if(i<=m)
        {
            for(l=0;l<=m-i;l++)
                TR[k+l]=SR[i+l];
        }
        
        //3.将剩余的SR[j..mid]复制到TR
        if(j<=n)
        {
            for(l=0;l<=n-j;l++)
                TR[k+l]=SR[j+l];
        }
    }
    
    
    //② 将SR[s...t] 归并排序为 TR1[s...t];
    void MSort(int SR[],int TR1[],int low, int hight){
        int mid;
        int TR2[MAXSIZE+1];
        
        if(low == hight)
            TR1[low] = SR[low];
        else{
            //1.将SR[low...hight] 平分成 SR[low...mid] 和 SR[mid+1,hight];
            mid = (low + hight)/2;
            //2. 递归将SR[low,mid]归并为有序的TR2[low,mid];
            MSort(SR, TR2, low, mid);
            //3. 递归将SR[mid+1,hight]归并为有序的TR2[mid+1,hight];
            MSort(SR, TR2, mid+1, hight);
            //4. 将TR2[low,mid] 与 TR2[mid+1,hight], 归并到TR1[low,hight]中
            Merge(TR2, TR1, low, mid, hight);
        }
    }
    
    //① 对顺序表L进行归并排序
    void MergeSort(SqList *L){
       
        MSort(L->r,L->r,1,L->length);
    }
    

    \color{orange}{非递归:}

    void MergePass(int SR[],int TR[],int s,int length){
      
        int i = 1;
        int j;
        
        //①合并数组
        //s=1 循环结束位置:8 (9-2*1+1=8)
        //s=2 循环结束位置:6 (9-2*2+1=6)
        //s=4 循环结束位置:2 (9-2*4+1=2)
        //s=8 循环结束位置:-6(9-2*8+1=-6) s = 8时,不会进入到循环;
        while (i<= length-2*s+1) {
            //两两归并(合并相邻的2段数据)
            Merge(SR, TR, i, i+s-1, i+2*s-1);
            i = i+2*s;
            
            /*
             s = 1,i = 1,Merge(SR,TR,1,1,2);
             s = 1,i = 3,Merge(SR,TR,3,3,4);
             s = 1,i = 5,Merge(SR,TR,5,5,6);
             s = 1,i = 7,Merge(SR,TR,7,7,8);
             s = 1,i = 9,退出循环;
             */
            
            /*
             s = 2,i = 1,Merge(SR,TR,1,2,4);
             s = 2,i = 5,Merge(SR,TR,5,6,8);
             s = 2,i = 9,退出循环;
             */
            
            /*
             s = 4,i = 1,Merge(SR,TR,1,4,8);
             s = 4,i = 9,退出循环;
             */
        }
        
        //②如果i<length-s+1,表示有2个长度不等的子序列. 其中一个长度为length,另一个小于length
        // 1 < (9-8+1)(2)
        //s = 8时, 1 < (9-8+1)
        if(i < length-s+1){
            //Merge(SR,TR,1,8,9)
            Merge(SR, TR, i, i+s-1, length);
        }else{
            //③只剩下一个子序列;
            for (j = i; j <=length; j++) {
                TR[j] = SR[j];
            }
        }
    }
    
    void MergeSort2(SqList *L){
        int *TR = (int *)malloc(sizeof(int) * L->length);
        int k = 1;
        //k的拆分变换是 1,2,4,8;
        while (k < L->length) {
            //将SR数组按照s=2的长度进行拆分合并,结果存储到TR数组中;
            //注意:此时经过第一轮的归并排序的结果是存储到TR数组了;
            MergePass(L->r, TR, k, L->length);
            k = 2*k;
            //将刚刚归并排序后的TR数组,按照s = 2k的长度进行拆分合并. 结果存储到L->r数组中;
            //注意:因为上一轮的排序的结果是存储到TR数组,所以这次排序的数据应该是再次对TR数组排序;
            MergePass(TR, L->r, k, L->length);
            k = 2*k;
            
        }
    }
    

    \color{orange}{7、快速排序:}

    快速排序的基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。

    \color{orange}{算法描述:}
    快速排序使用分治法来把一个串(list)分为两个子串(sub-lists)。具体算法描述如下:

    从数列中挑出一个元素,称为 “基准”(pivot);
    重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
    递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。

    \color{orange}{算法分析:}
    1、时间复杂度:O(nlogn) 2、空间复杂度:O(logn) 3、非稳定排序 4、原地排序

    int Partition2(SqList *L,int low,int high){
       
        int pivotkey;
        
        /**1.优化选择枢轴**/
        //① 计算数组中间的元素的下标值;
        int m = low + (high - low)/2;
        //② 将数组中的L->r[low] 是整个序列中左中右3个关键字的中间值;
        //交换左端与右端的数据,保证左端较小;[9,1,5,8,3,7,4,6,2]
        if(L->r[low]>L->r[high])
            swap(L, low, high);
        //交换中间与右端的数据,保证中间较小; [2,1,5,8,3,7,4,6,9];
        if(L->r[m]>L->r[high])
            swap(L, high, m);
        //交换中间与左端,保证左端较小;[2,1,5,8,3,7,4,6,9]
        if(L->r[m]>L->r[low])
            swap(L, m, low);
        //交换后的序列:3,1,5,8,2,7,4,6,9
        //此时low = 3; 那么此时一定比选择 9,2更合适;
        
        
        /**2.优化不必要的交换**/
        //pivokey 保存子表中第1个记录作为枢轴记录;
        pivotkey = L->r[low];
        //将枢轴关键字备份到L->r[0];
        L->r[0] = pivotkey;
        
        //① 从表的两端交替地向中间扫描;
        while (low < high) {
            //② 比较,从高位开始,找到比pivokey更小的值的下标位置;
            while (low < high &&  L->r[high] >= pivotkey)
                high--;
            
            //③ 将比枢轴值小的记录交换到低端;
            //swap(L, low, high);
            //③ 采用替换的方式将比枢轴值小的记录替换到低端
            L->r[low] = L->r[high];
            
            //④ 比较,从低位开始,找到比pivokey更大的值的下标位置;
            while (low < high && L->r[low] <= pivotkey)
                low++;
            
            //⑤ 将比枢轴值大的记录交换到高端;
            //swap(L, low, high);
            //⑤ 采样替换的方式将比枢轴值大的记录替换到高端
            L->r[high] = L->r[low];
        }
        //将枢轴数值替换会L->r[low]
        L->r[low] = L->r[0];
        
        //返回枢轴pivokey 所在位置;
        return low;
    }
    

    \color{orange}{8、计数排序:}

    计数排序不是基于比较的排序算法,其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。

    \color{orange}{算法描述:}
    找出待排序的数组中最大和最小的元素;
    统计数组中每个值为i的元素出现的次数,存入数组C的第i项;
    对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加);
    反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1。

    \color{orange}{算法分析:}
    计数排序是一个稳定的排序算法。当输入的元素是 n 个 0到 k 之间的整数时,时间复杂度是O(n+k),空间复杂度也是O(n+k),其排序速度快于任何比较排序算法。当k不是很大并且序列比较集中时,计数排序是一个很有效的排序算法。

    \color{orange}{9、桶排序:}

    桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。桶排序 (Bucket sort)的工作的原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排)。

    \color{orange}{算法描述:}
    设置一个定量的数组当作空桶;
    遍历输入数据,并且把数据一个一个放到对应的桶里去;
    对每个不是空的桶进行排序;
    从不是空的桶里把排好序的数据拼接起来。

    \color{orange}{算法分析:}
    桶排序最好情况下使用线性时间O(n),桶排序的时间复杂度,取决与对各个桶之间数据进行排序的时间复杂度,因为其它部分的时间复杂度都为O(n)。很显然,桶划分的越小,各个桶之间的数据越少,排序所用的时间也会越少。但相应的空间消耗就会增大。

    \color{orange}{10、基数排序:}

    基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。

    \color{orange}{算法描述:}
    取得数组中的最大数,并取得位数;
    arr为原始数组,从最低位开始取每个位组成radix数组;
    对radix进行计数排序(利用计数排序适用于小范围数的特点);

    \color{orange}{算法分析:}
    基数排序基于分别排序,分别收集,所以是稳定的。但基数排序的性能比桶排序要略差,每一次关键字的桶分配都需要O(n)的时间复杂度,而且分配之后得到新的关键字序列又需要O(n)的时间复杂度。假如待排数据可以分为d个关键字,则基数排序的时间复杂度将是O(d*2n) ,当然d要远远小于n,因此基本上还是线性级别的。

    基数排序的空间复杂度为O(n+k),其中k为桶的数量。一般来说n>>k,因此额外空间需要大概n个左右。

    相关文章

      网友评论

          本文标题:从零开始养成算法·篇二十二:十大经典排序算法(2)

          本文链接:https://www.haomeiwen.com/subject/zptcahtx.html