zoukankan      html  css  js  c++  java
  • 读书笔记-排序

    【选择排序】

    a[i++] —> a[n],从前往后看、选择最小值、一次交换到位

    1,完整循环找到数组中最小的元素;

    2,把这个最小的元素与a[0]交换;

    3,在a[i]-an的子数组中重复1-2步骤;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    
    for(int i = 0; i < n; i++) {
    
        int min = i;
    
        for(int j = i + 1; j < n; j++) {
    
            if(a[j] < min) 
    
                min = j;
    
        }
    
        swap(i, j, a);
    }
    

    简写:

    1
    2
    3
    4
    5
    
    for(int i =0; i < n; i++) {
        min = //本次循环的i...n中的最小的元素的index;
    
        //将min和本次循环的i两个元素交换;
    }
    

    特点:

    选择排序的扫描路线:a[i++] —> a[n]

    选择排序是在每个大循环下,通过完整子循环找到最小值后,退出子循环再进行交换,而不是一找到小于关系就交换,每次交换后,左侧的有序数组的位置是最终的;

    运行时间和输入无关,扫描数组的次数是固定的,因为大循环和子循环的次数是固定的,前一次扫描并不为下一次扫描提供信息;

    交换次数最少,因为是子循环完全结束后才进行一次交换,每次交换的结果都是最后的排序子结果,元素不用做二次挪动;

    选择排序是一截一截往后看,将子数组中的最小元素交换到最前头的位置;

    选择排序没有最好情况和最坏情况,扫描的次数是固定的,交换的次数已经是所有排序算法中最少的了;


    【插入排序】

    a[j—] —> a[0],从半路往前看、让元素尝试往前走不动为止

    插入排序是一截一截往前看,将倒置的元素交换;对于一个元素,总是尝试往前走,走到不能走为止,因为前面的元素已然有序了,所以走不动的时候左侧元素也是刚刚重新有序;总是相邻元素交换,所以交换次数频繁,一次交换的位置未必是最终位置;

    插入排序的扫描路线:a[j—] —> a[0]

    1
    2
    3
    4
    5
    6
    
    for(int i = 1; i < n; i++) {
    
        for(int j = i; j > 0 && a[j] < a[j-1]; j--) 
    
            swap(j, j -1, a);
    }
    

    每次子循环的结果,左侧的元素肯定是在已知(已经扫描)的数组中有序的,所以每次子循环发生的条件是,如果a[j]小于a[j-1],则交换,然后继续进行j—之后的扫描;但如果a[j]不小于a[j-1],因为左侧在已知数组中是有序的,这个时候该子循环就不会发生了;

    对已然有序的数组排序,插入排序是线性扫描,0次交换;

    插入排序就是解决倒置的两个元素,交换的次数就是倒置的元素个数;

    插入排序和选择排序都是平方级的运行时间,但插入排序通常比选择排序快一个常数,因为对于随机的数组来说,插入排序扫描的次数会由于数组中的部分有序而减少,但选择排序不会,必须全扫描;交换,则是插入排序比选择排序次数要多,选择排序交换次数最多不超过n;


    【归并排序】

    先2分排序子数组,再归并成原数组

    原地归并算法:

    1,先将所有元素赋值到一个新的数组中,此时数组是两截有序的数组;

    2, 在原数组上讲新数组中的数本地归并回来:左边用尽取右边;右边用尽取左边;右边当前元素比左边小取右边;右边当前元素大于等于左边取左边;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    
    void merge(Comparable[] a, int low, int mid, int hign) {
    
        int i = low, j = mid + 1;
    
        for(int k = low; k <= hign; k++) 
    
            aux[k] = a[k];
    
        for(int k = low; k <= hign; k++) 
    
            if(i > mid)                     a[k] = aux[j++];
    
            else if(j > hign)            a[k] = aux[i++];
    
            else if(aux[j] < aux[i])    a[k] = aux[j++);
    
            else                                  a[k] = aux[i++];
    }
    

    自顶向下的归并排序:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    
    sort(a, 0, a.length - 1);
    
    void sort(Comparable[] a, int low, int hign) {
    
        if(hign < = low)        return;
    
        int mid = low + (hign - low) / 2;
    
        sort(a, low, mid);
    
        sort(a, mid + 1, hign);
    
        merge(a, low, mid, hign);
    }
    

    归并算法的思路就是:两个单元素的数组是分别有序的,可以通过merge方法将其归并为一个2元素的有序数组,依此类推,两个a[low]到a[mid]和a[mid+1]到a[hign]的数组分别有序,可以通过merge方法将其归并为一个有序数组a[low]到a[hign];

    归并自上而下,先分拆(sort)直至单元素数组,再归并(merge)回到原数组;

    归并算法的几个优化策略:

    1,对小规模数组改用插入排序,比如sort方法中发现hign - low <= 10,则用插入排序;

    2, 测试数组是否已经有序,就是看a[mid]如果小于等于a[mid+1],就不用归并了;

    归并排序的时间总是NlogN;

    跟普通排序不需要额外空间不一样,归并排序是需要额外空间的,跟N成正比;

    归并排序是某种程度上的空间换时间算法;

    自底向上的归并:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
    void sort(Comparable[] a) {
    
        aux = new Comparable[a.length];
    
        for(int sz = 1; sz < a.length; sz = sz + sz) 
    
            for(int low = 0; low < N - sz; low += sz + sz) 
    
                merge(a, low, low + sz - 1, Math.min(low + sz + sz - 1, N -1);
    }
    

    自顶向下是 化整为零,自底向上是循序渐进;

    归并排序用了aux辅助数组,是空间复杂度不是最优的;

    任何比较排序算法的复杂度都不会低于lg(N!)~NlgN;


    【快速排序】

    每一次切分都使数组左右两边趋于有序

    特点:

    1, 快速排序是原地排序,只需要一个很小的辅助栈;归并排序无法做到;

    2, 复杂度是NlgN;插入、选择等交换排序无法做到;

    3, 快速排序的原理:将一个数组分成两个子数组,将两部分独立地排序。

    快速排序和归并排序是互补的:

    1, 归并排序将数组分成两个子数组分别排序,并将有序的子数组归并以将整个数组排序;快速排序是当两个子数组都有序时整个数组也就自然有序了。

    2, 归并排序中,递归调用发生在处理整个数组之前;快速排序中,递归调用发生在处理整个数组之后;

    3, 归并排序是数组被等分为两半;快速排序中,切分的位置取决于数组的内容;

    算法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    
    void sort(Comparable[] a) {
    
        random(a) //随机打乱数组,为了比避免每次的切分元素总是子数组中的最小元素
    
        sort(a, 0, a.length - 1);
    
    }
    
    void sort(Comparable[] a, int low, int hign) {
    
        if(hign <= low)
    
            return;
    
        int j = partition(a, low, hign);
    
        sort(a, low, j -1);
    
        sort(a, j + 1, hign);
    }
    

    递归调用切分实现排序的思路:

    如果左子数组和右子数组都是有序的,那么由左子数组(有序且所有元素都小于等于切分元素+切分元素+右子数组(有序且所有元素大于等于切分元素)组成的结果数组也一定是有序的;

    切分找j的条件

    1, 对于某个j,a[j]已经排定;

    2, a[low]到a[j - 1]中的所有元素都不大于a[j];

    3, a[j+1]到a[hign]中的所有元素都不小于a[j];

    注意,上面的2, 3说的都是j的左边和右边的元素分别不大于和不小于切分元素,但此时左右两边并不是有序的;

    切分算法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    
    int partition(Comparable[] a, int low, int hign) {
    
        int i = low, j = hign + 1;
    
        Comparable v = a[0];
    
        while(true) {
    
            while(a[++i]  < v)
    
                if(i == hign)
    
                        break;
    
            while(a[--j] > v)
    
                if(j == low)
    
                    break;
    
            if(i >= j)
    
                break;
    
            swap(a, i, j);
    
        }
    
        swap(a, low, j);
    
        return j;
    }
    

    两个小while分别做从左、从右往中间走的动作;

    从左边走遇见的比切分元素大的元素,跟从右边走遇见的比切分元素小的元素,交换之;

    因为一次大循环中用的都是a[lo],同一个参考值,那么通过这样的交换,左侧都小,右侧都大;

    i, j相遇的最后一次交换,是a[j]被认为小小于切分元素,a[i]被认为大于切分元素,然后他们交换了位置,这一次是相邻交换,—j和—i使得i和j的索引也交换了,所以此时j就是上一次的i的位置,已经被小于切分元素a[lo]的上一次的a[j]给占用了;同时a[i]则是 大于a[lo]的元素;

    所以,最后swap(lo, j),跟开始的切分元素=a[lo]相呼应;

    切分的轨迹是这样的 :

    lo….i….j…hi

    lo….ij…….hi

    lo….ji…….hi

    j…..loi…….hi

    归并排序是从底往上一层层合并有有序子数组;

    快速排序是从上往下,循序渐进,一层层切分下去,每一次切分都使得数组呈两边大小合适状态,切到单元素数组的时候,整个数组基于n多个两边大小合适的小数组,而有序了;

    切分元素不一定要选择a[low],随机选都行;

    打乱数组顺序的意义:random(a) //随机打乱数组,为了比避免每次的切分元素总是子数组中的最小元素。

    如果每次的切分元素总是最小的元素,那么每一次切分都只是分离成一个元素的数组和一个N-1长度的子数组,这使得数组会被切分N多次;

    对于小数组,切换到插入排序能提高效率;

    partition方法还可以用来找一个数组中【第k大的元素】:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
    int lo = 0, hi = a.length - 1;
    
    while(hi > lo) {
    
        int j = partition(a, lo, hi);
    
        if(j == k)
    
            return a[k];
    
        if(j > k)
    
            hi = j - 1;
    
        if(j < k)
    
            lo = j + 1;
    
        return a[k];
    

    【堆排序】

    先使堆有序,再一个个删除最大值,删除即把第一个最大元素往尾部交换以推出堆

    上浮和下沉算法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    
    void swim(int k) {
    
        while(k > 1 && pq[k/2] < pq[k]) {
    
            swap(pq, k/2, k);
    
            k = k / 2;
    }
    
    void sink(int k) {
    
        while(2 * k <= N) {
            int j = 2 * k;
    
            if(j < N && pq[j] < pq[j+1]) //选取两个子节点中较大的一个往上交换
    
                    j++;
    
            if(pq[k] >= pq[j])   //结束下沉,已经比字节点大了
    
                    break;
    
            swap(pq, k, j); //下沉
    
            k = j;
    
    }
    
    public static void sort(Comparable[] a) {
    
        int N = a.length;
    
        //先使得堆有序
    
        for(int k = N/2; k >= 1; k--)  //从右到左扫,因为最终堆有序是右边总比左边小;
    
                sink(a, k, N);             //只扫描一半,因为一半之后的元素都是叶节点,就是为1的堆
    
        //再进行下沉排序,销毁堆有序
    
       while(N > 1) {      //把最大元素删除,然后放入堆缩小后数组中空出的位置
            swap(a, 1, N--); //a[1]就是最大元素,把其交换到最后一个,就是从堆有序堆中删除它
    
            sink(a, 1, N);  //被交换到a[1]的元素,通过在子堆中下沉,使得子堆再次有序
    
    }                           //重复这个过程,最大元素总是往后一个一个走,最后整个数组就有序了
    

    堆排序的复杂度:N*lgN,而且是原地排序,无额外空间消耗;


    【SpaceToTime排序 空间换时间排序法】

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    
    int i = 0;
    
    int max = array[0];
    
    int len = array.length;
    
    for(int i = 1; i < len; i++)  //找出最大值,做为空间数组的length
    
        if(array[i] > max)
    
            max = array[i];
    
    int[] temp = new int[max + 1];
    
    for(int i = 0; i < len; i++)
    
        temp[array[i]] = array[i];
    
    int j = 0;
    
    int max1 = max + 1;
    
    for(i = 0; i < max1; i++) {
    
        if(temp[i] > 0]
    
            array[j++] = temp[i];
    }
    
    不计空间成本,把数组值映射到一个临时数组的下标,然后遍历临时数组,把大于0的数顺序放回原数组;
    
    注:temp[i] = i;
    
    array[i] > 0;
    

    【Java.util.Arrays.sort】

    对原始类型用三向切分的快速排序;

    对引用类型用归并排序;


    【Comparable Comparator】

    一个类实现了Comparable接口则表明这个类的对象之间是可以相互比较的,这个类对象组成的集合就可以直接使用sort方法排序。
    Comparator可以看成一种算法的实现,将算法和数据分离,Comparator也可以在下面两种环境下使用:
    1、类的设计师没有考虑到比较问题而没有实现Comparable,可以通过Comparator来实现排序而不必改变对象本身
    2、可以使用多种排序标准,比如升序、降序等

    多键排序,用Comparator;

  • 相关阅读:
    如何提高技术素养
    spoolsv.exe 无法启动
    太阳高度角和方位角的计算
    树莓派 3 alsa 声卡驱动
    PHP 7 Xdebug 深深的坑
    java 线性规划 和lingo 比较
    Python Microsoft Visual C++ Compiler Package for Python 2.7
    Node debug
    angular 调试 js (分 karms protractor / test e2e unit )
    hbase scan 的例子
  • 原文地址:https://www.cnblogs.com/mosthink/p/5288803.html
Copyright © 2011-2022 走看看