• 常用排序算法--合并排序和快速排序


    常用排序算法——合并排序

    分治

    分治(Divide and Conquer)是一场常见的算法策略。分治策略的基本思想就是对于一个问题规模为N的问题,将其划分为规模足够小的K个子问题,子问题由于规模足够小可以直接求解,最后将规模足够小的K的问题的解合并得出原问题的解。
    分治策略的问题的求解过程的一般套路就是:

    1. 判断问题规模,足够小进入步骤2,否者进入步骤3
    2. 直接对问题进行求解,返回问题的解。
    3. 将问题拆分成K个小规模的问题,进入步骤2。并将解合并,得到原问题的解。

    由于分治策略的步骤描述自带递归属性,因此分治策略的算法实现常采用递归来实现。递归的算法通常会含有三部分,第一部分是个基准情况(Base Case),第二部分是问题拆分,并调用自身,第三部分是合并并返回结果。

    分治策略中主要有两个关键过程:

    • 将原问题拆分Division
    • 将各子问题合并Combination

    分治策略的主要时间开销发生在这两个过程,通常分治策略有两类

    1. 拆分容易,合并困难
    2. 拆分困难,合并简单

    下文中的合并排序快速排序分别属于上面的第1和第2类。

    合并排序和快速排序

    在常见排序算法中,合并排序和快速排序是典型的分治算法,其时间复杂度的平均性能均为nlgn。合并排序的最坏时间复杂度也能保持nlgn,而快速排序的最坏时间复杂度却为n*n合并排序是稳定的,而快速排序不是稳定的。另外合并排序是拆分容易,合并困难,而快速排序则是拆分困难,合并容易。

    合并排序又称归并排序,主要的思想是:将待排序列拆分至数个足够小的子序列,然后将相邻子序列合并为一个有序子序列,重复合并相邻有序子序列直到整个序列有序。

    快速排序的主要思想则是,将待排序列拆分为左右两个子序列A,B,使得子序列A的元素都小于子序列B,然后对子序列A,B继续进行这种拆分,直到待拆分的序列足够小,最后整个序列变成有序。

    由于合并排序和快速排序每次元素的移动都不只移动了一个位置,因此每次元素的比较都不只消除了一个逆序对,因此对于插入排序这种每次比较只移动一个位置的算法,时间复杂度会得到改善。

    快速排序的递归实现

    int partition(int* a, int l, int h) {
        int pivot = a[l];
        int i,j;
        i = l - 1;
        j = h + 1;
        while(true) {
            while(a[--j] > pivot);
            while(a[++i] < pivot)
                if(i >= h)
                    break;
            //i 、j cross
            if(i >= j)
                break;
    
            int tmp = a[i];
            a[i] = a[j];
            a[j] = tmp;
        }
        a[j] = pivot;
        return j;
    }
    
    void quickSort(int* a, int l, int h) {
        if(l >= h)
            return;
        int p = partition(a, l, h);
        quickSort(a, l, p);
        quickSort(a, p+1, h);
    }

    合并排序的递归实现

    void merge(int* a, int l, int m, int h) {
        if(l == h)
            return;
    
        int* b = (int*)malloc(sizeof(int) * (h - l + 1));
        int i = l, j = m + 1;
        int k = 0;
    
        while(i <= m && j <= h)
            b[k++] = a[i] > a[j] ? a[j++] : a[i++];
    
        if(i <= m)
            while(i <= m)
                b[k++] = a[i++];
        if(j <= h)
            while(j <= h)
                b[k++] = a[j++];
        //回写
        i = l;
        k = 0;
    
        for(; i <= h;)
            a[i++] = b[k++];
    }
    
    void mergeSort(int* a, int l, int h) {
        if(l >= h)
            return;
        int m = (h + l) / 2;
        mergeSort(a, l, m);
        mergeSort(a, m+1, h);
        merge(a, l, m, h);
    }

    合并排序的非递归实现

    由于递归会导致函数调用栈的暴增,会引入额外的时间开销,例如现场保护和恢复之类的。通递归版本的算法都可以改写为迭代版本的算法。递归算法的优点是实现简单直观,易于理解,缺点是如果有时候调用深度过深,会带来栈内存溢出和额外的运行时间开销。迭代算法的有点是不会有额外的函数调用栈的增长, 缺点是难于理解且难于实现。

    合并排序的迭代版本的实现相对来讲是比较简单直观的,大致思路就是:第1轮,待排序列S中相邻的长度为1的子序列进行合并,第2轮,序列S中相邻的长度为2的子序列进行合并,第i轮,待排序列S中相邻的长度为2的i-1次方的子序列进行合并。直到待合并的子序列数为1。

    void merge(int* a, int l, int m, int h) {
        if(l == h)
            return;
    
        int* b = (int*)malloc(sizeof(int) * (h - l + 1));
        int i = l, j = m + 1;
        int k = 0;
    
        while(i <= m && j <= h)
            b[k++] = a[i] > a[j] ? a[j++] : a[i++];
    
        if(i <= m)
            while(i <= m)
                b[k++] = a[i++];
        if(j <= h)
            while(j <= h)
                b[k++] = a[j++];
        //回写
        i = l;
        k = 0;
    
        for(; i <= h;)
            a[i++] = b[k++];
    }
    
    //非递归版本
    void mergeIteation(int* a, int n) {
        int offset = 1;
        while(offset <= n) {
            int i = 0;
            for(;i < n;) {
                merge(a,i, i+offset-1,i+offset*2 - 1);
                i = i + offset * 2;
                if(n - 1 - i < offset)
                    break;
            }
            offset *= 2;
        }
    }
  • 相关阅读:
    洛谷3384树链剖分模板
    洛谷2672(前缀和技巧)
    普通平衡树与文艺平衡树的splay代码
    面向对象的几大原则
    面向对象的几大原则
    Struts标签、Ognl表达式、el表达式、jstl标签库这四者之间的关系和各自作
    Struts标签、Ognl表达式、el表达式、jstl标签库这四者之间的关系和各自作
    关于使用struts2跳转后css和js失效的解决方式
    关于使用struts2跳转后css和js失效的解决方式
    Hibernate——脏检查和缓存清理机制
  • 原文地址:https://www.cnblogs.com/Spground/p/8536157.html
Copyright © 2020-2023  润新知