微信公众号:"算法与编程之美",欢迎关注,及时了解更多此系列文章

作者|杨旭

 


问题描述


上次的博客讨论了排序算法中的插入排序和交换排序两个大类,今天将剩下的常见排序算法全部梳理出来。


选择排序

简单选择排序


基本思想:每一趟排序从待排序的序列中选择出最小的元素,顺序放入到元素序列中,直到排序完成。该算法是一个不稳定的算法并且效率与初始数据顺序无关。

空间复杂度为O(1)

时间复杂度最高,平均,最低都为O(n2)


public static int[] selectInsert(int[] n) {

    int minPos,  temp;

    for (int i =  0; i < n.length; i++) {

        minPos =  i;

        //遍历找出最小数的位置

        for (int  j = i + 1; j < n.length; j++) {

            if  (n[j] < n[minPos]) {

                 minPos = j;

            }

        }

        //如果最小数位置不等于当前位置指针,就交换位置,即把数按照从小到大的顺序依次排列

        if  (minPos != i) {

             temp  = n[i];

            n[i]  = n[minPos];

             n[minPos] = temp;

        }

    }

    return n;

}


堆排序

基本原理:堆排序是一种树形选择排序算法,其原理是将R[1…n]看成一棵完全二叉树的顺序存储结构。利用完全二叉树中双亲节点和孩子结点的关系,在当前无序区中选择关键字最大(最小)的元素构建成大根堆(小根堆)。堆排序的主要流程便是,建立大(小)根堆,然后输出元素(顶部和底部元素进行交换),再调整堆,直到元素全部输出。堆排序是一个不稳定的算法。

堆的定义为:n个关键字序列R[1…n]称为堆,堆通常可以被看做一棵树的数组对象。

当且仅当序列满足R[i≤R[2i]且R[i]≤R[2i+1]时称为该堆为大根堆,其中1≤i≤⌊ n/2⌋

当且仅当序列满足R[i]≥R[2i]且R[i]≥R[2i+1]时称为该堆为小根堆,其中1≤i≤⌊n/2⌋

比如在大根堆中,最大的元素放在根节点中,且对于任一非根节点,它的值小于等于其双亲节点的值。

对于堆排序来说关键在于构造堆,而建堆是一个反复调整筛选的过程。首先从树的最后一个非叶子节点开始调整,按照堆的性质移动元素位置。如下图便是一个大顶推的调整过程。

Java实现常见排序算法(二)_排序算法

初始堆建立完成后,就是进行排序操作,排序操作的主要步骤是:以大根堆为例,每一次排序将堆顶元素与堆尾元素进行交换,然后再调用调整堆的算法使除了堆尾元素以外剩下的堆再次调整成一个大根堆,这样循环length-1次就可以将元素调整为从小到大的顺序,完成排序。

空间复杂度为O(1)

时间复杂度的最高,平均,最低都为O(nlog2n)

Java实现:


//堆排序

private static int[] heapSort(int[] n) {

    //建立堆,从最后一个非叶子节点开始

    for (int i =  (n.length - 2) / 2; i >= 0; i--) {

         adjustHeap(n, i, n.length);

    }

    int temp;

    //完成排序,从最后一个元素进行输出,每次循环确定一个元素的位置

    for (int i =  n.length - 1; i >= 1; i--) {

        temp =  n[0];

        n[0] =  n[i];

        n[i] =  temp;

        // 筛选 R[0] 结点,得到i个结点的堆

         adjustHeap(n, 0, i);

    }

    return n;

}


//调整堆

public static void adjustHeap(int[] n, int k, int  length) {

    //设置堆顶的值

    int temp =  n[k];

     //从左孩子开始判断

    for (int i =  k * 2 + 1; i <= length - 1; i = 2 * i + 1) {

        //如果左孩子小于右孩子

        if (i + 1  < length && n[i] < n[i + 1]) {

             //取右孩子

             i++;

        }

        //如果堆顶的值大于左右孩子中的较大者,无需调整

        if (temp  >= n[i]) {

             break;

        } else {

            //否则的话,将左右孩子中的较大者换到双亲节点

            n[k]  = n[i];

            //然后将k值更新,方便继续向下调整

            k =  i;

        }


    }

    //将原堆顶位置当入最后调整出来的地方

    n[k] = temp;

}



归并排序和基数排序


归并排序

基本思想:“归并”的意思是指将两个或者两个以上的有序表合并成一个新的有序表。假设一个待排序的序列长度为n,首先我们可以将其视为n个有序表,即每个表中元素个数为1,然后我们将n个有序表进行两两归并,得到⌈ \lceil⌈n/2⌉ \rceil⌉个长度为2或者1的有序表然后再再次基础上进行两两归并直到得到一个长度为n的有序表为止。这种方法就称为2路归并排序。归并排序是一个稳定的排序算法。PS.归并排序的发明者是约翰·冯·诺伊曼,其速度仅次于快速排序(平均状况下)

例如我们有一个初始值为[22,11,32,2,12,83,10]的序列,才有2路归并排序

第一趟归并后:[11,22],[2,32],[12,83],[10]

第二趟归并后:[2,11,22,32],[10,12,83]

第三趟归并后:[2,10,11,12,22,32,83]

空间复杂度为O(n)

时间复杂度的最高,平均,最低都为O(nlog2n)

Java实现:


//归并排序

public static int[] mergeSort(int[] n, int low, int  high) {

    int mid =  (low + high) / 2;

    if (low <  high) {

         mergeSort(n, low, mid);

         mergeSort(n, mid + 1, high);

        merge(n,  low, mid, high);

    }

    return n;

}


//调整

public static void merge(int[] n, int low, int mid, int  high) {

    int[]  tempArr;

    int i = low,  j = mid + 1, k = 0;

    tempArr =  Arrays.copyOf(n, high - low + 1);

    // 把较小的数先移到新数组中

    while (i  <= mid && j <= high) {

        if (n[i]  < n[j]) {

            tempArr[k++] = n[i++];

        } else {

             tempArr[k++] = n[j++];

        }

    }

    // 把左边剩余的数移入数组

    while (i  <= mid) {

         tempArr[k++] = n[i++];

    }

    // 把右边边剩余的数移入数组

    while (j  <= high) {

         tempArr[k++] = n[j++];

    }

    // 把新数组中的数覆盖nums数组

     System.arraycopy(tempArr, 0, n, low, tempArr.length);

}


基数排序(桶子排序)

基本思想:基数排序是一种不基于比较的排序,而采用多关键字排序思想,即基于关键字各位的大小进行排序,主要使用“分配”和“收集”两种基本操作对单逻辑关键字进行排序。基数排序又分为最高位优先(MSD)排序和最低位优先(LSD)排序。他的主要思想是将待排序的整数按位数切割成不同的数字,然后对每一位的数进行单独的比较,具体做法是:将所有待比较数值统一为同样的数位长度,如有不同位数则在前面进行补零操作,然后从最低位开始,依次进行一次排序,这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。

举个栗子:

待排序的数据为[222,123,580,996,965,854,775],使用最低位优先排序

第一趟将最低位进行排序:

[580,222,123,854,965,775,996]

第二趟将中间位进行排序:

[222,123,854,965,775,580,996]

第三趟将最高位进行排序:

[123,222,580,775,854,965,996]

空间复杂度:一趟排序需要的辅助空间为O®(r个队列或桶)用于存放d待排序的数

时间复杂度:O(d(n+r)),d趟的分配和收集,一趟分配为O(n),一趟收集为O®。

Java实现(代码参考百度百科)


//d表示最大的数有多少位

public static int[] radixSort(int[] number, int d) {

    //控制键值排序依据在哪一位

    int k = 0,n =  1,m = 1;

    //数组的第一维表示可能的余数0-9

    int[][] temp  = new int[10][number.length];

    //数组orderp[i]用来表示该位是i的数的个数

    int[] order =  new int[10];

    while (m  <= d) {

        for (int  value : number) {

            int  lsd = ((value / n) % 10);

             temp[lsd][order[lsd]] = value;

            order[lsd]++;

        }

        for (int  i = 0; i < 10; i++) {

            if  (order[i] != 0)

                 for (int j = 0; j < order[i]; j++) {

                     number[k] = temp[i][j];

                     k++;

                }

            order[i]  = 0;

        }

        n *= 10;

        k = 0;

        m++;

    }

    return  number;

}


总结

就此常见的几个排序算法就总结得差不多了,下一次的博客准备写一下JDK8中默认的排序算法是如何实现的以及介绍的这几种排序算法的实际使用案例和场景。

END


主  编   |   张祯悦

责  编   |   杨   旭


 where2go 团队




微信号:算法与编程之美          

Java实现常见排序算法(二)_归并排序_02


长按识别二维码关注我们!

写留言”发表评论,期待您的参与!期待您的转发!