数据结构与算法面试分享(六):排序算法

目录

一、排序的分类

二、算法的时间复杂度

度量一个程序(算法)执行时间的两种方法

时间频度

时间复杂度

常见的时间复杂度

平均时间复杂度和最坏时间复杂度

 三、算法的空间复杂度简介

四、冒泡排序

基本介绍

演示冒泡过程的例子

代码实现

五、选择排序

基本介绍

选择排序思想

选择排序思路分析图

选择排序应用实例

六、插入排序

基本介绍

插入排序思想

插入排序思路分析图

选择排序应用案例

七、希尔排序

简单插入排序存在的问题

希尔排序算法介绍

希尔排序算法基本思想

希尔排序算法的示意图

希尔排序应用案例

八、快速排序

基本介绍

快速排序法示意图

快速排序法应用案例


一、排序的分类

  1. 内部排序

    指将需要处理的所有数据都加载到内部存储器(内存)中进行排序。

  2. 外部排序法

    数据量过大,无法全部加载到内存中,需要借助**外部存储(文件等)**进行排序。

  3. 常见的排序算法分类

image

二、算法的时间复杂度

度量一个程序(算法)执行时间的两种方法

  1. 事后统计的方法

    这种方法可行,但是有两个问题:一是要想对设计的算法的运行性能进行评测,需要实际运行该程序;二是所得时间的统计量依赖于计算机的硬件、软件等环境因素,这种方式,要在同一台计算机的相同状态下运行,才能比较那个算法速度更快。

  2. 事前估算的方法

    通过分析某个算法的时间复杂度来判断哪个算法更优.

时间频度

  • 基本介绍

    时间频度:一个算法花费的时间与算法中语句的执行次数成正比例,哪个算法中语句执行次数多,它花费时间就多。一个算法中的语句执行次数称为语句频度或时间频度。记为T(n)。

  • 举例说明-基本案例

    比如计算.1-100所有数字之和,我们设计两种算法:

image

  • 举例说明-忽略常数项

image

结论:

  1. 2n+20 和 2n 随着 n 变大,执行曲线无限接近, 20 可以忽略
  2. 3n+10 和 3n 随着 n 变大,执行曲线无限接近, 10 可以忽略
  • 举例说明-忽略低次项

image

结论:

  1. 2n2+3n+10 和 2n2 随着 n 变大,执行曲线无限接近,可以忽略 3n+10
  2. n2+5n+20 和 n2 随着 n 变大,执行曲线无限接近,可以忽略 5n+20
  • 举例说明-忽略系数

image

结论:

  1. 随着 n 值变大,5n2+7n 和 3n2+2n,执行曲线重合,说明这种情况下,5 和 3 可以省略。
  2. 而 n3+5n 和 6n3+4n,执行曲线分离,说明多少次方式关键。

时间复杂度

  1. 一般情况下,算法中的基本操作语句的重复执行次数是问题规模n的某个函数,用 T(n) 表示,若有某个辅助函数 f(n) ,使得当 n 趋近于无穷大时,T(n)/ f(n)的极限值为不等于零的常数,则称 f(n)是 T(n)的同数量级函数。记作 T(n)=O( f(n)) ,称 o( f((n)) 为算法的渐进时间复杂度,简称时间复杂度。
  2. T(n) 不同,但时间复杂度可能相同。如: T(n)=n2+7n+6 与 T(n)=3n2+2n+2 它们的T(n)不同,但时间复杂度相同,都为O(n2)
  3. 计算时间复杂度的方法:
    • 用常数Ⅰ代替运行时间中的所有加法常数‘T(n)=n2+7n+6 => T(n)=n2+7n+1
    • 修改后的运行次数函数中,只保留最高阶项T(n)=n2+7n+1 => T(n)= n2
    • 去除最高阶项的系数‘T(n)= n2 => T(n)= n2  => O(n2)

常见的时间复杂度

  1. 常数阶 O(1)
  2. 对数阶 O(log2n)
  3. 线性阶 O(n)
  4. 线性对数阶 O(nlog2n)
  5. 平方阶 O(n2)
  6. 立方阶 O(n3)
  7. k 次方阶 O(nk)
  8. 指数阶 O(22)

常见的时间复杂度对应的图: 

image

说明

  1. 常见的算法时间复杂度由小到大依次为:O(1)<O(log2n)<O(n)<O(n2)<O(n3)<O(nk)<O(2n),随着问题规模 n 的不断增大,上述时间复杂度不断增大,算法的执行效率越低
  2. 从图中可见,我们应该尽可能避免使用指数阶的算法

常数阶 O(1)

无论代码执行了多少行,只要是没有循环等复杂结构,那这个代码的时间复杂度就都是O(1)

int i = 1;
int j = 2;
++i;
j++;
int m = i + j;

上述代码在执行的时候,它消耗的时候并不随着某个变量的增长而增长,那么无论这类代码有多长,即使有几万几十万行,都可以用O(1)来表示它的时间复杂度。

对数阶 O(log2n)

int i = 1;
while(i < n){
    i = i * 2;
}

说明:在while循环里面,每次都将 i 乘以 2 ,乘完之后,i 距离 n 就越来越近了。假设循环 x 次之后,i 就大于 2 了,此时这个循环就退出了,也就是说 2 的 x 次方等于 n ,那么x=logzn也就是说当循环 log2n 次以后,这个代码就结束了。因此这个代码的时间复杂度为: O(log2n)O(log2n) 的这个2时间上是根据代码变化的,i=i*3,则是O(log3n).

image

线性阶 O(n)

for(i = 1;i <= n; ++i) {
    j = 1;
    j++;
}

说明:这段代码,for 循环里面的代码会执行 n 遍,因此它消耗的时间是随着 n 的变化而变化的,因此这类代码都可以用 O(n) 来表示它的时间复杂度

线性对数阶 O(nlogN)

for(m = 1;m < n;m++) {
    i = 1;
    while(i < n) {
        i = i * 2;
    }
}

说明:线性对数阶 O(nlogN) 其实非常容易理解,将时间复杂度为O(logn)的代码循环N遍的话,那么它的时间复杂度就是 n * O(logN),也就是了o(nlogN)

平方阶 O(n2)

for(x = 1;i <= n;x++) {
    for(i = 1;i <= n;i++) {
        j = 1;
        j++;
    }
}

说明:平方阶 O(n2) 就更容易理解了,如果把 O(n) 的代码再嵌套循环一遍,它的时间复杂度就是 O(n2),这段代码其实就是嵌套了2层 n 循环,它的时间复杂度就是 O(n*n), 即**O(n2)**如果将其中一层循环的 n 改成 m ,那它的时间复杂度就变成了 O(m*n)

立方阶O(n³)、K次方阶O(nk)

平均时间复杂度和最坏时间复杂度

  1. 平均时间复杂度是指所有可能的输入实例均以等概率出现的情况下,该算法的运行时间。
  2. 最坏情况下的时间复杂度称最坏时间复杂度。一般讨论的时间复杂度均是最坏情况下的时间复杂度。这样做的原因是:最坏情况下的时间复杂度是算法在任何输入实例上运行时间的界限,这就保证了算法的运行时间不会比最坏情况更长。
  3. 平均时间复杂度和最坏时间复杂度是否一致,和算法有关(如图:)。

image

 三、算法的空间复杂度简介

  1. 类似于时间复杂度的讨论,一个算法的空间复杂度(Space Complexity)定义为该算法所耗费的存储空间,它也是问题规模n的函数。
  2. 空间复杂度(Space Complexity)是对一个算法在运行过程中临时占用存储空间大小的量度。有的算法需要占用的临时工作单元数与解决问题的规模n有关,它随着n的增大而增大,当n较大时,将占用较多的存储单元,例如快速排序和归并排序算法,基数排序就属于这种情况
  3. 在做算法分析时,主要讨论的是时间复杂度。从用户使用体验上看,更看重的程序执行的速度。一些缓存产品(redismemcache)和算法(基数排序)本质就是用空间换时间.

四、冒泡排序

基本介绍

冒泡排序(Bubble Sorting)的基本思想是:通过对待排序序列从前向后(从下标较小的元素开始〉,依次比较相邻元素的值,若发现逆序则交换,使值较大的元素逐渐从前移向后部,就象水底下的气泡一样逐渐向上冒。

优化:

因为排序的过程中,各元素不断接近自己的位置,如果一趟比较下来没有进行过交换,就说明序列有序,因此要在排序过程中设置一个标志 flag 判断元素是否进行过交换。从而减少不必要的比较。(这里说的优化,可以在冒泡排序写好后,在进行)

演示冒泡过程的例子

原始数组:3,9,-1,10,20

第一趟排序

(1) 3,9,-1,10,20 //如果相邻的元素逆序就交换

(2) 3,-1,9,10,20

(3) 3,-1,9,10,20

(4) 3,-1,9,10,20

第二趟排序

(1) -1,3,9,10,20

(2) -1,3,9,10,20

(3) -1,3,9,10,20

第三趟排序

(1) -1,3,9,10,20

(2) -1,3,9,10,20

第四趟排序

(2) -1,3,9,10,20

小结上面的排序过程:

  1. 一共进行数组的大小-1次大的循环。
  2. 每一趟排序的次数在逐渐的减少。
  3. 如果我们发现在某趟排序中,没有发生一次交换,可以提前结束冒泡排序。这个就是优化。

代码实现

我们举一个具体的案例来说明冒泡法。我们将五个无序的数: 3,9,-1,10,-2使用冒泡排序法将其排成一个从小到大的有序数列。

/**
 * @version 1.0
 * desc:冒泡排序
 */
public class BubbleSort {
    public static void main(String[] args) {
        int arr[] = {3, 9, -1, 10, -2};
        System.out.println("排序前:");
        System.out.println(Arrays.toString(arr));
        bubbleSort(arr);
        System.out.println("排序后:");
        System.out.println(Arrays.toString(arr));

        //测试一下冒泡排序的速度,给80000个数据
        //创建要给80000个随机的数组
        int[] array = new int[80000];
        for (int i = 0; i < 80000; i++) {
            array[i] = (int) (Math.random() * 8000000); //生成一个[0,8000000)数
        }
        Date date1 = new Date();
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String startTime = dateFormat.format(date1);
        System.out.println("排序前的时间=:" + startTime);

        //测试冒泡排序
        bubbleSort(array);
        Date date2 = new Date();
        String endTime = dateFormat.format(date2);
        System.out.println("排序后的时间=:" + endTime);


        //1.第一趟排序,就是将最大的数排在最后
        /*int temp = 0; //临时变量
        for (int i = 0; i < arr.length - 1; i++) {
            //如果前面的数比后面的数大,刚交换
            if (arr[i] > arr[i + 1]) {
                temp = arr[i];
                arr[i] = arr[i + 1];
                arr[i + 1] = temp;
            }
        }
        System.out.println("第一趟排序后的数组:"); //[3, -1, 9, -2, 10]
        System.out.println(Arrays.toString(arr));

        // 第二趟排序,就是将最二大的数排在最后,比较次数减一,因为最大数已经确定
        for (int i = 0; i < arr.length - 1 - 1; i++) {
            //如果前面的数比后面的数大,刚交换
            if (arr[i] > arr[i + 1]) {
                temp = arr[i];
                arr[i] = arr[i + 1];
                arr[i + 1] = temp;
            }
        }
        System.out.println("第二趟排序后的数组:"); //[-1, 3, -2, 9, 10]
        System.out.println(Arrays.toString(arr));

        //第三趟排序,就是将第三大的数排在倒数第三位
        for (int i = 0; i < arr.length - 1 - 2; i++) {
            //如果前面的数比后面的数大,刚交换
            if (arr[i] > arr[i + 1]) {
                temp = arr[i];
                arr[i] = arr[i + 1];
                arr[i + 1] = temp;
            }
        }

        System.out.println("第三趟排序后的数组:"); //[-1, -2, 3, 9, 10]
        System.out.println(Arrays.toString(arr));

        //第四趟排序,就是将第四大的数排在倒数第四位
        for (int i = 0; i < arr.length - 1 - 3; i++) {
            //如果前面的数比后面的数大,刚交换
            if (arr[i] > arr[i + 1]) {
                temp = arr[i];
                arr[i] = arr[i + 1];
                arr[i + 1] = temp;
            }
        }

        System.out.println("第四趟排序后的数组:"); //[-2, -1, 3, 9, 10]
        System.out.println(Arrays.toString(arr));*/

    }

    //不需要再进行第五趟排序
    //发现这四次for循环只有length-数字 不一样 ---->总结 length-i-1
    //封装到一个方法中
    //冒泡排序时间复杂度为O(n^2)
    //将冒泡排序封装到一个方法
    public static void bubbleSort(int[] arr) {
        int temp = 0;
        boolean flag = false; //标识变量,表示是否进行过交换
        for (int i = 0; i < arr.length - 1; i++) {
            for (int j = 0; j < arr.length - 1 - i; j++) {
                if (arr[j] > arr[j + 1]) {
                    flag = true;
                    temp = arr[j];
                    arr[j] = arr[j + 1];
                    arr[j + 1] = temp;
                }
            }
            //System.out.println("第" + (i + 1) + "趟排序后的数组");
            //System.out.println(Arrays.toString(arr));
            if (!flag) { //在一趟排序中,一趟都没有交换过
                break;
            } else {
                flag = false; //重置flag,进行下次判断
            }
        }
    }
}
  • 结果
排序前:
[3, 9, -1, 10, -2]
排序后:
[-2, -1, 3, 9, 10]
排序前的时间=:2022-12-30 21:30:36
排序后的时间=:2022-12-30 21:30:46

Process finished with exit code 0

五、选择排序

基本介绍

选择式排序也属于内部排序法,是从欲排序的数据中,按指定的规则选出某元素,再依规定交换位置后达到排序的目的。

选择排序思想

选择排序(select sorting)也是一种简单的排序方法。它的基本思想是:第一次从 arr[0]~arr[n-1]中选取最小值,与 arr[0]交换,第二次从 arr[1]~arr[n-1]中选取最小值,与 arr[1]交换,第三次从ar[2]~arr[n-1]中选取最小值,与ar[2]交换,…,第i次从 arr[i-1]~arr[n-1]中选取最小值,与arr[i-1]交换,…第n-1次从arr[n-2]~arr[n-1]中选取最小值,与arr[n-2]交换,总共通过n-1次,得到一个按排序码从小到大排列的有序序列。

选择排序思路分析图

image

image

选择排序应用实例

有一群牛﹐颜值分别是10,34,1,19请使用选择排序从低到高进行排序[10,34,1,19]

image

  • 代码实现
/**
 * @version 1.0
 * desc:选择排序
 */
public class SelectSort {
    public static void main(String[] args) {
        int arr[] = {10, 34, 1, 19};
        selectSort(arr);
        System.out.println(Arrays.toString(arr));
        //测试一下选择排序的速度,给80000个数据
        //创建要给80000个随机的数组
        int[] array = new int[80000];
        for (int i = 0; i < 80000; i++) {
            array[i] = (int) (Math.random() * 8000000); //生成一个[0,8000000)数
        }
        Date date1 = new Date();
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String startTime = dateFormat.format(date1);
        System.out.println("排序前的时间=:" + startTime);

        selectSort(array);
        Date date2 = new Date();
        String endTime = dateFormat.format(date2);
        System.out.println("排序后的时间=:" + endTime);
    }

    //选择排序
    //每一轮排序都把最小的放到最前面,把最小的放在第0位,把第二小的放在第1位,把第三小的放在第2位...
    //需要arr.length-1次排序
    public static void selectSort(int[] arr) {

        //使用逐步推导的方式
        for (int i = 0; i < arr.length - 1; i++) {
            int minIndex = i;
            int min = arr[i];
            for (int j = i + 1; j < arr.length; j++) {
                if (min > arr[j]) {
                    min = arr[j];
                    minIndex = j;
                }
            }
            if (minIndex != i) {
                //将最小值,放在arr[i],即交换
                arr[minIndex] = arr[i];
                arr[i] = min;
            }
        }

    }
}
  • 结果
[1, 10, 19, 34]
排序前的时间=:2023-01-01 19:33:31
排序后的时间=:2023-01-01 19:33:34

Process finished with exit code 0

六、插入排序

基本介绍

插入式排序属于内部排序法,是对于欲排序的元素以插入的方式找寻该元素的适当位置,以达到排序的目的。

插入排序思想

插入排序(Insertion Sorting)的基本思想是:把n个待排序的元素看成为一个有序表和一个无序表,开始时有序表中只包含一个元素,无序表中包含有n-1个元素,排序过程中每次从无序表中取出第一个元素,把它的排序码依次与有序表元素的排序码进行比较,将它插入到有序表中的适当位置,使之成为新的有序表。

插入排序思路分析图

image

选择排序应用案例

有一群小牛,考试成绩分别是 101,34,119,1请从小到大排序

/**
 * @version 1.0
 * desc:插入排序
 */
public class InsertSort {
    public static void main(String[] args) {
        int[] arr = {101, 34, 119, 1, 61, 89};
        insertSort(arr);
        System.out.println(Arrays.toString(arr));
        //测试一下选择排序的速度,给80000个数据
        //创建要给80000个随机的数组
        int[] array = new int[80000];
        for (int i = 0; i < 80000; i++) {
            array[i] = (int) (Math.random() * 8000000); //生成一个[0,8000000)数
        }
        Date date1 = new Date();
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String startTime = dateFormat.format(date1);
        System.out.println("排序前的时间=:" + startTime);

        insertSort(array);
        Date date2 = new Date();
        String endTime = dateFormat.format(date2);
        System.out.println("排序后的时间=:" + endTime);
    }

    //插入排序
    public static void insertSort(int[] arr) {
        //第一轮 {101, 34, 119, 1} ==> {34, 101, 119, 1}

        int insertValue = 0;
        int insertIndex = 0;
        for (int i = 1; i < arr.length; i++) {
            //定义待插入的数
            insertValue = arr[i];
            insertIndex = i - 1; //即arr[i]的前面这个数的下标

            //给insertValue 找到插入的位置
            //说明
            //1.insertIndex >= 0 保证在给insertValue 找插入位置时,不越界
            //2.insertValue < arr[insertIndex] 待插入的数,还没有找到插入的位置
            //3.就需要将 arr[insertIndex] 值后移
            while (insertIndex >= 0 && insertValue < arr[insertIndex]) {
                arr[insertIndex + 1] = arr[insertIndex];
                insertIndex--;
            }
            //直到插入的数不比前面的数小并且插入的索引大于等于0
            //就退出while循环,说明插入的位置找到,insertIndex + 1
            arr[insertIndex + 1] = insertValue;

            //System.out.println("第" + i + "轮插入");
            //System.out.println(Arrays.toString(arr));
        }

        //第2轮
        /*insertValue = arr[2];
        insertIndex= 2 - 1;
        while (insertIndex >= 0 && insertValue < arr[insertIndex]) {
            arr[insertIndex + 1] = arr[insertIndex];
            insertIndex--;
        }
        //当退出while循环时,说明插入的位置找到,insertIndex + 1
        arr[insertIndex + 1] = insertValue;
        System.out.println("第二轮插入");
        System.out.println(Arrays.toString(arr));

        //第3轮
        insertValue = arr[3];
        insertIndex= 3 - 1;
        while (insertIndex >= 0 && insertValue < arr[insertIndex]) {
            arr[insertIndex + 1] = arr[insertIndex];
            insertIndex--;
        }
        //当退出while循环时,说明插入的位置找到,insertIndex + 1
        arr[insertIndex + 1] = insertValue;
        System.out.println("第三轮插入");
        System.out.println(Arrays.toString(arr));*/

    }
}
  • 结果
[1, 34, 61, 89, 101, 119]
排序前的时间=:2023-01-01 22:38:48
排序后的时间=:2023-01-01 22:38:49

Process finished with exit code 0

七、希尔排序

简单插入排序存在的问题

我们看简单的插入排序可能存在的问题.

数组arr = {2,3,4,5,6,1}这时需要插入的数1(最小),这样的过程是:

{2,3,4,5,6,6}

{2,3,4,5,5,6}

{2,3,4,4,5,6}

{2,3,3,4,5,6}

{2,2,3,4,5,6}

{1,2,3,4,5,6}

结论:当需要插入的数是较小的数时,后移的次数明显增多,对效率有影响.

希尔排序算法介绍

希尔排序是希尔〈Donald ShelI)于1959年提出的一种排序算法。希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序

希尔排序算法基本思想

希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。

希尔排序算法的示意图

image

经过上面的“宏观调控”,整个数组的有序化程度成果喜人。

此时,仅仅需要对以上数列简单微调,无需大量移动操作即可完成整个数组的排序。

image

希尔排序应用案例

有一群小牛,考试成绩分别是 {8,9,1,7,2,3,5,4,6,0}请从小到大排序.请分别使用

希尔排序时,对有序序列在插入时采用交换法,并测试排序速度.

希尔排序时,对有序序列在插入时采用移动法,并测试排序速度

/**
 * @version 1.0
 * desc:希尔排序
 */
public class ShellSort {
    public static void main(String[] args) {
        int[] arr = {8, 9, 1, 7, 2, 3, 5, 4, 6, 0};
        System.out.println(Arrays.toString(arr));
        int[] array = new int[80000];
        for (int i = 0; i < 80000; i++) {
            array[i] = (int) (Math.random() * 8000000); //生成一个[0,8000000)数
        }
        Date date1 = new Date();
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String startTime = dateFormat.format(date1);
        System.out.println("交换希尔排序前的时间=:" + startTime);

        shellSort(array);
        Date date2 = new Date();
        String endTime = dateFormat.format(date2);
        System.out.println("交换希尔排序后的时间=:" + endTime);

        int[] array1 = new int[80000];
        for (int i = 0; i < 80000; i++) {
            array1[i] = (int) (Math.random() * 8000000); //生成一个[0,8000000)数
        }

        Date date3 = new Date();
        String startTime2 = dateFormat.format(date3);
        System.out.println("移动希尔排序前的时间=:" + startTime2);

        shellSort2(array1);
        Date date4 = new Date();
        String endTime2 = dateFormat.format(date4);
        System.out.println("移动希尔排序后的时间=:" + endTime2);
    }

    //使用逐步推导的方式来编写希尔排序
    public static void shellSort(int[] arr) {

        int temp = 0;
        //根据逐步分析,得到循环处理
        for (int gap = arr.length / 2; gap > 0; gap /= 2) {
            for (int i = gap; i < arr.length; i++) {
                //遍历各组中所有的元素(共5组,每组有2个元素),步长为gap
                for (int j = i - gap; j >= 0; j -= gap) {
                    //如果当前元素大于加上步长后的那个元素,说明交换
                    if (arr[j] > arr[j + gap]) {
                        temp = arr[j];
                        arr[j] = arr[j + gap];
                        arr[j + gap] = temp;
                    }
                }
            }
        }

        arr = new int[]{8, 9, 1, 7, 2, 3, 5, 4, 6, 0};
        //希尔排序的第1轮排序
        //因为第1轮排序,是将10个数据分成了5组
        for (int i = 5; i < arr.length; i++) {
            //遍历各组中所有的元素(共5组,每组有2个元素)
            for (int j = i - 5; j >= 0; j -= 5) {
                //如果当前元素大于加上步长后的那个元素,说明交换
                if (arr[j] > arr[j + 5]) {
                    temp = arr[j];
                    arr[j] = arr[j + 5];
                    arr[j + 5] = temp;
                }
            }
        }
        System.out.println("希尔排序1轮后=" + Arrays.toString(arr));

        //希尔排序第二轮排序
        //因为第2轮排序,是将10个数据分成了5/2 = 2组
        for (int i = 2; i < arr.length; i++) {
            for (int j = i - 2; j >= 0; j -= 2) {
                if (arr[j] > arr[j + 2]) {
                    temp = arr[j];
                    arr[j] = arr[j + 2];
                    arr[j + 2] = temp;
                }
            }
        }
        System.out.println("希尔排序2轮后=" + Arrays.toString(arr));

        //希尔排序第二轮排序
        //因为第2轮排序,是将10个数据分成了2/2 = 1组
        for (int i = 1; i < arr.length; i++) {
            for (int j = i - 1; j >= 0; j -= 1) {
                if (arr[j] > arr[j + 1]) {
                    temp = arr[j];
                    arr[j] = arr[j + 1];
                    arr[j + 1] = temp;
                }
            }
        }
        System.out.println("希尔排序3轮后=" + Arrays.toString(arr));
    }

    public static void shellSort2(int[] arr) {
        for (int gap = arr.length / 2; gap > 0; gap /= 2) {
            //从gap个元素,逐个对其所在的组进行直接插入排序
            for (int i = gap; i < arr.length; i++) {
                int j = i;
                int temp = arr[j];
                while (j - gap >= 0 && temp < arr[j - gap]) {
                    //移动
                    arr[j] = arr[j - gap];
                    j -= gap;
                }
                //当退出while后,就给temp找到插入的位置
                arr[j] = temp;
            }
        }
    }
}
  • 测试
[8, 9, 1, 7, 2, 3, 5, 4, 6, 0]
交换希尔排序前的时间=:2023-01-03 13:24:20
希尔排序1轮后=[3, 5, 1, 6, 0, 8, 9, 4, 7, 2]
希尔排序2轮后=[0, 2, 1, 4, 3, 5, 7, 6, 9, 8]
希尔排序3轮后=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
交换希尔排序后的时间=:2023-01-03 13:24:25
移动希尔排序前的时间=:2023-01-03 13:24:25
移动希尔排序后的时间=:2023-01-03 13:24:25

Process finished with exit code 0

八、快速排序

基本介绍

快速排序(Quicksort)是对冒泡排序的一种改进。基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

快速排序法示意图

image

image

快速排序法应用案例

要求:对[-9,78,0,23,-567,70]进行从小到大的排序,要求使用快速排序法。【测试8w 和 800w】

说明[验证分析]:

  1. 如果取消左右递归,结果是 -9 -567 0 23 78 70
  2. 如果取消右递归,结果是 -567 -90 23 78 70
  3. 如果取消左递归,结果是 -9 -567 0 23 70 78
/**
 * @version 1.0
 * desc: 快速排序
 */
public class QuickSort {
    public static void main(String[] args) {
        int[] arr = {-9, 78, 0, 23, -567, 70};
        quickSort(arr, 0, arr.length - 1);
        System.out.println(Arrays.toString(arr));

        //测试一下快速排序的速度,给80000个数据
        //创建要给80000个随机的数组
        int[] array = new int[80000];
        for (int i = 0; i < 80000; i++) {
            array[i] = (int) (Math.random() * 8000000); //生成一个[0,8000000)数
        }
        Date date1 = new Date();
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String startTime = dateFormat.format(date1);
        System.out.println("排序前的时间=:" + startTime);

        //测试快速排序
        quickSort(array, 0, array.length - 1);
        Date date2 = new Date();
        String endTime = dateFormat.format(date2);
        System.out.println("排序后的时间=:" + endTime);
    }

    public static void quickSort(int[] arr, int left, int right) {

        int l = left;//左边下标
        int r = right;//右边下标
        //pivot 中轴值
        int pivot = arr[(left + right) / 2];
        int temp = 0;//临时变量,作为交换时使用
        //while循环的目的是让比 pivot 值小的放到它的左边
        //比 pivot 值大的放到右边
        while (l < r) {

            //在 pivot 的左边一直找,找到大于等于 pivot 的值,才推出
            while (arr[l] < pivot) {
                l += 1;
            }
            //在 pivot 的右边一直找,找到小于等于 pivot 的值,才推出
            while (arr[r] > pivot) {
                r -= 1;
            }
            //如果 l>=r 成立说明 pivot 的左右两边的值,已经按照左边全部是
            if (l >= r) {
                break;
            }
            //交换
            temp = arr[l];
            arr[l] = arr[r];
            arr[r] = temp;

            //如果交换完后,发现这个arr[l] == pivot 值相等 r--,前移
            if (arr[l] == pivot) {
                r -= 1;
            }
            //如果交换完后,发现这个arr[r] == pivot 值相等 l++,后移
            if (arr[r] == pivot) {
                l += 1;
            }

        }

        //如果l==r,必须l++,r--,否则会出现栈溢出
        if (l == r) {
            l += 1;
            r -= 1;
        }

        //向左递归
        if (left < r) {
            quickSort(arr, left, r);
        }

        //向右递归
        if (right > l) {
            quickSort(arr, l, right);
        }
    }
}
  • 结果
[-567, -9, 0, 23, 70, 78]
排序前的时间=:2023-01-08 21:11:09
排序后的时间=:2023-01-08 21:11:09

Process finished with exit code 0

九、堆排序(Heap Sort) 

堆排序介绍

学习堆排序之前,有必要了解堆!若读者不熟悉堆,建议先了解堆(建议可以通过二叉堆,左倾堆,斜堆,二项堆或斐波那契堆等文章进行了解),然后再来学习本章。

我们知道,堆分为"最大堆"和"最小堆"。最大堆通常被用来进行"升序"排序,而最小堆通常被用来进行"降序"排序。 鉴于最大堆和最小堆是对称关系,理解其中一种即可。本文将对最大堆实现的升序排序进行详细说明。

最大堆进行升序排序的基本思想: ① 初始化堆: 将数列a[1...n]构造成最大堆。 ② 交换数据: 将a[1]和a[n]交换,使a[n]是a[1...n]中的最大值;然后将a[1...n-1]重新调整为最大堆。 接着,将a[1]和a[n-1]交换,使a[n-1]是a[1...n-1]中的最大值;然后将a[1...n-2]重新调整为最大值。 依次类推,直到整个数列都是有序的。

下面,通过图文来解析堆排序的实现过程。注意实现中用到了"数组实现的二叉堆的性质"。 在第一个元素的索引为 0 的情形中:

  • 性质一: 索引为i的左孩子的索引是 (2*i+1);
  • 性质二: 索引为i的右孩子的索引是 (2*i+2);
  • 性质三: 索引为i的父结点的索引是 floor((i-1)/2);

例如,对于最大堆{110,100,90,40,80,20,60,10,30,50,70}而言: 索引为0的左孩子的所有是1;索引为0的右孩子是2;索引为8的父节点是3。

堆排序实现

下面演示heap_sort_asc(a, n)对a={20,30,90,40,70,110,60,10,100,50,80}, n=11进行堆排序过程。下面是数组a对应的初始化结构:

初始化堆

在堆排序算法中,首先要将待排序的数组转化成二叉堆。 下面演示将数组{20,30,90,40,70,110,60,10,100,50,80}转换为最大堆{110,100,90,40,80,20,60,10,30,50,70}的步骤。

  • 1.1 i=11/2-1,即i=4

上面是maxheap_down(a, 4, 9)调整过程。maxheap_down(a, 4, 9)的作用是将a[4...9]进行下调;a[4]的左孩子是a[9],右孩子是a[10]。调整时,选择左右孩子中较大的一个(即a[10])和a[4]交换。

  • 1.2 i=3

上面是maxheap_down(a, 3, 9)调整过程。maxheap_down(a, 3, 9)的作用是将a[3...9]进行下调;a[3]的左孩子是a[7],右孩子是a[8]。调整时,选择左右孩子中较大的一个(即a[8])和a[4]交换。

  • 1.3 i=2

上面是maxheap_down(a, 2, 9)调整过程。maxheap_down(a, 2, 9)的作用是将a[2...9]进行下调;a[2]的左孩子是a[5],右孩子是a[6]。调整时,选择左右孩子中较大的一个(即a[5])和a[2]交换。

  • 1.4 i=1

上面是maxheap_down(a, 1, 9)调整过程。maxheap_down(a, 1, 9)的作用是将a[1...9]进行下调;a[1]的左孩子是a[3],右孩子是a[4]。调整时,选择左右孩子中较大的一个(即a[3])和a[1]交换。交换之后,a[3]为30,它比它的右孩子a[8]要大,接着,再将它们交换。

  • 1.5 i=0

上面是maxheap_down(a, 0, 9)调整过程。maxheap_down(a, 0, 9)的作用是将a[0...9]进行下调;a[0]的左孩子是a[1],右孩子是a[2]。调整时,选择左右孩子中较大的一个(即a[2])和a[0]交换。交换之后,a[2]为20,它比它的左右孩子要大,选择较大的孩子(即左孩子)和a[2]交换。

调整完毕,就得到了最大堆。此时,数组{20,30,90,40,70,110,60,10,100,50,80}也就变成了{110,100,90,40,80,20,60,10,30,50,70}。

交换数据

在将数组转换成最大堆之后,接着要进行交换数据,从而使数组成为一个真正的有序数组。 交换数据部分相对比较简单,下面仅仅给出将最大值放在数组末尾的示意图。

上面是当n=10时,交换数据的示意图。 当n=10时,首先交换a[0]和a[10],使得a[10]是a[0...10]之间的最大值;然后,调整a[0...9]使它称为最大堆。交换之后: a[10]是有序的! 当n=9时, 首先交换a[0]和a[9],使得a[9]是a[0...9]之间的最大值;然后,调整a[0...8]使它称为最大堆。交换之后: a[9...10]是有序的! ... 依此类推,直到a[0...10]是有序的。

堆排序时间复杂度

堆排序的时间复杂度是O(N*lgN)。

假设被排序的数列中有N个数。遍历一趟的时间复杂度是O(N),需要遍历多少次呢? 堆排序是采用的二叉堆进行排序的,二叉堆就是一棵二叉树,它需要遍历的次数就是二叉树的深度,而根据完全二叉树的定义,它的深度至少是lg(N+1)。最多是多少呢? 由于二叉堆是完全二叉树,因此,它的深度最多也不会超过lg(2N)。因此,遍历一趟的时间复杂度是O(N),而遍历次数介于lg(N+1)和lg(2N)之间;因此得出它的时间复杂度是O(N*lgN)。

堆排序稳定性

堆排序是不稳定的算法,它不满足稳定算法的定义。它在交换数据的时候,是比较父结点和子节点之间的数据,所以,即便是存在两个数值相等的兄弟节点,它们的相对顺序在排序也可能发生变化。

算法稳定性 -- 假设在数列中存在a[i]=a[j],若在排序之前,a[i]在a[j]前面;并且排序之后,a[i]仍然在a[j]前面。则这个排序算法是稳定的!

代码实现

/**
 * 堆排序: Java
 *
 */

public class HeapSort {

    /* 
     * (最大)堆的向下调整算法
     *
     * 注: 数组实现的堆中,第N个节点的左孩子的索引值是(2N+1),右孩子的索引是(2N+2)。
     *     其中,N为数组下标索引值,如数组中第1个数对应的N为0。
     *
     * 参数说明: 
     *     a -- 待排序的数组
     *     start -- 被下调节点的起始位置(一般为0,表示从第1个开始)
     *     end   -- 截至范围(一般为数组中最后一个元素的索引)
     */
    public static void maxHeapDown(int[] a, int start, int end) {
        int c = start;            // 当前(current)节点的位置
        int l = 2*c + 1;        // 左(left)孩子的位置
        int tmp = a[c];            // 当前(current)节点的大小

        for (; l <= end; c=l,l=2*l+1) {
            // "l"是左孩子,"l+1"是右孩子
            if ( l < end && a[l] < a[l+1])
                l++;        // 左右两孩子中选择较大者,即m_heap[l+1]
            if (tmp >= a[l])
                break;        // 调整结束
            else {            // 交换值
                a[c] = a[l];
                a[l]= tmp;
            }
        }
    }

    /*
     * 堆排序(从小到大)
     *
     * 参数说明: 
     *     a -- 待排序的数组
     *     n -- 数组的长度
     */
    public static void heapSortAsc(int[] a, int n) {
        int i,tmp;

        // 从(n/2-1) --> 0逐次遍历。遍历之后,得到的数组实际上是一个(最大)二叉堆。
        for (i = n / 2 - 1; i >= 0; i--)
            maxHeapDown(a, i, n-1);

        // 从最后一个元素开始对序列进行调整,不断的缩小调整的范围直到第一个元素
        for (i = n - 1; i > 0; i--) {
            // 交换a[0]和a[i]。交换后,a[i]是a[0...i]中最大的。
            tmp = a[0];
            a[0] = a[i];
            a[i] = tmp;
            // 调整a[0...i-1],使得a[0...i-1]仍然是一个最大堆。
            // 即,保证a[i-1]是a[0...i-1]中的最大值。
            maxHeapDown(a, 0, i-1);
        }
    }

    /* 
     * (最小)堆的向下调整算法
     *
     * 注: 数组实现的堆中,第N个节点的左孩子的索引值是(2N+1),右孩子的索引是(2N+2)。
     *     其中,N为数组下标索引值,如数组中第1个数对应的N为0。
     *
     * 参数说明: 
     *     a -- 待排序的数组
     *     start -- 被下调节点的起始位置(一般为0,表示从第1个开始)
     *     end   -- 截至范围(一般为数组中最后一个元素的索引)
     */
    public static void minHeapDown(int[] a, int start, int end) {
        int c = start;            // 当前(current)节点的位置
        int l = 2*c + 1;        // 左(left)孩子的位置
        int tmp = a[c];            // 当前(current)节点的大小

        for (; l <= end; c=l,l=2*l+1) {
            // "l"是左孩子,"l+1"是右孩子
            if ( l < end && a[l] > a[l+1])
                l++;        // 左右两孩子中选择较小者
            if (tmp <= a[l])
                break;        // 调整结束
            else {            // 交换值
                a[c] = a[l];
                a[l]= tmp;
            }
        }
    }

    /*
     * 堆排序(从大到小)
     *
     * 参数说明: 
     *     a -- 待排序的数组
     *     n -- 数组的长度
     */
    public static void heapSortDesc(int[] a, int n) {
        int i,tmp;

        // 从(n/2-1) --> 0逐次遍历每。遍历之后,得到的数组实际上是一个最小堆。
        for (i = n / 2 - 1; i >= 0; i--)
            minHeapDown(a, i, n-1);

        // 从最后一个元素开始对序列进行调整,不断的缩小调整的范围直到第一个元素
        for (i = n - 1; i > 0; i--) {
            // 交换a[0]和a[i]。交换后,a[i]是a[0...i]中最小的。
            tmp = a[0];
            a[0] = a[i];
            a[i] = tmp;
            // 调整a[0...i-1],使得a[0...i-1]仍然是一个最小堆。
            // 即,保证a[i-1]是a[0...i-1]中的最小值。
            minHeapDown(a, 0, i-1);
        }
    }

    public static void main(String[] args) {
        int i;
        int a[] = {20,30,90,40,70,110,60,10,100,50,80};

        System.out.printf("before sort:");
        for (i=0; i<a.length; i++)
            System.out.printf("%d ", a[i]);
        System.out.printf("\n");

        heapSortAsc(a, a.length);            // 升序排列
        //heapSortDesc(a, a.length);        // 降序排列

        System.out.printf("after  sort:");
        for (i=0; i<a.length; i++)
            System.out.printf("%d ", a[i]);
        System.out.printf("\n");
    }
}

十、归并排序(Merge Sort) 

归并排序介绍

根据具体的实现,归并排序包括"从上往下"和"从下往上"2种方式。

从下往上的归并排序

将待排序的数列分成若干个长度为1的子数列,然后将这些数列两两合并;得到若干个长度为2的有序数列,再将这些数列两两合并;得到若干个长度为4的有序数列,再将它们两两合并;直接合并成一个数列为止。这样就得到了我们想要的排序结果。

 从下往上的归并排序的思想正好与"从下往上的归并排序"相反。如下图:

通过"从下往上的归并排序"来对数组{80,30,60,40,20,10,50,70}进行排序时:

  • 将数组{80,30,60,40,20,10,50,70}看作由8个有序的子数组{80},{30},{60},{40},{20},{10},{50}和{70}组成。
  • 将这8个有序的子数列两两合并。得到4个有序的子树列{30,80},{40,60},{10,20}和{50,70}。
  • 将这4个有序的子数列两两合并。得到2个有序的子树列{30,40,60,80}和{10,20,50,70}。
  • 将这2个有序的子数列两两合并。得到1个有序的子树列{10,20,30,40,50,60,70,80}。

从上往下的归并排序

它与"从下往上"在排序上是反方向的。它基本包括3步:

  • 分解 -- 将当前区间一分为二,即求分裂点 mid = (low + high)/2;
  • 求解 -- 递归地对两个子区间a[low...mid] 和 a[mid+1...high]进行归并排序。递归的终结条件是子区间长度为1。
  • 合并 -- 将已排序的两个子区间a[low...mid]和 a[mid+1...high]归并为一个有序的区间a[low...high]。

从上往下的归并排序采用了递归的方式实现。它的原理非常简单,如下图:

通过"从上往下的归并排序"来对数组{80,30,60,40,20,10,50,70}进行排序时:

  • 将数组{80,30,60,40,20,10,50,70}看作由两个有序的子数组{80,30,60,40}和{20,10,50,70}组成。对两个有序子树组进行排序即可。
  • 将子数组{80,30,60,40}看作由两个有序的子数组{80,30}和{60,40}组成。
    • 将子数组{20,10,50,70}看作由两个有序的子数组{20,10}和{50,70}组成。
  • 将子数组{80,30}看作由两个有序的子数组{80}和{30}组成。
    • 将子数组{60,40}看作由两个有序的子数组{60}和{40}组成。
    • 将子数组{20,10}看作由两个有序的子数组{20}和{10}组成。
    • 将子数组{50,70}看作由两个有序的子数组{50}和{70}组成。

归并排序时间复杂度

归并排序的时间复杂度是O(N*lgN)。

假设被排序的数列中有N个数。遍历一趟的时间复杂度是O(N),需要遍历多少次呢? 归并排序的形式就是一棵二叉树,它需要遍历的次数就是二叉树的深度,而根据完全二叉树的可以得出它的时间复杂度是O(N*lgN)。

归并排序稳定性

归并排序是稳定的算法,它满足稳定算法的定义。

算法稳定性 -- 假设在数列中存在a[i]=a[j],若在排序之前,a[i]在a[j]前面;并且排序之后,a[i]仍然在a[j]前面。则这个排序算法是稳定的!

代码实现

/**
 * 归并排序: Java
 *
 */

public class MergeSort {

    /*
     * 将一个数组中的两个相邻有序区间合并成一个
     *
     * 参数说明: 
     *     a -- 包含两个有序区间的数组
     *     start -- 第1个有序区间的起始地址。
     *     mid   -- 第1个有序区间的结束地址。也是第2个有序区间的起始地址。
     *     end   -- 第2个有序区间的结束地址。
     */
    public static void merge(int[] a, int start, int mid, int end) {
        int[] tmp = new int[end-start+1];    // tmp是汇总2个有序区的临时区域
        int i = start;            // 第1个有序区的索引
        int j = mid + 1;        // 第2个有序区的索引
        int k = 0;                // 临时区域的索引

        while(i <= mid && j <= end) {
            if (a[i] <= a[j])
                tmp[k++] = a[i++];
            else
                tmp[k++] = a[j++];
        }

        while(i <= mid)
            tmp[k++] = a[i++];

        while(j <= end)
            tmp[k++] = a[j++];

        // 将排序后的元素,全部都整合到数组a中。
        for (i = 0; i < k; i++)
            a[start + i] = tmp[i];

        tmp=null;
    }

    /*
     * 归并排序(从上往下)
     *
     * 参数说明: 
     *     a -- 待排序的数组
     *     start -- 数组的起始地址
     *     endi -- 数组的结束地址
     */
    public static void mergeSortUp2Down(int[] a, int start, int end) {
        if(a==null || start >= end)
            return ;

        int mid = (end + start)/2;
        mergeSortUp2Down(a, start, mid); // 递归排序a[start...mid]
        mergeSortUp2Down(a, mid+1, end); // 递归排序a[mid+1...end]

        // a[start...mid] 和 a[mid...end]是两个有序空间,
        // 将它们排序成一个有序空间a[start...end]
        merge(a, start, mid, end);
    }


    /*
     * 对数组a做若干次合并: 数组a的总长度为len,将它分为若干个长度为gap的子数组;
     *             将"每2个相邻的子数组" 进行合并排序。
     *
     * 参数说明: 
     *     a -- 待排序的数组
     *     len -- 数组的长度
     *     gap -- 子数组的长度
     */
    public static void mergeGroups(int[] a, int len, int gap) {
        int i;
        int twolen = 2 * gap;    // 两个相邻的子数组的长度

        // 将"每2个相邻的子数组" 进行合并排序。
        for(i = 0; i+2*gap-1 < len; i+=(2*gap))
            merge(a, i, i+gap-1, i+2*gap-1);

        // 若 i+gap-1 < len-1,则剩余一个子数组没有配对。
        // 将该子数组合并到已排序的数组中。
        if ( i+gap-1 < len-1)
            merge(a, i, i + gap - 1, len - 1);
    }

    /*
     * 归并排序(从下往上)
     *
     * 参数说明: 
     *     a -- 待排序的数组
     */
    public static void mergeSortDown2Up(int[] a) {
        if (a==null)
            return ;

        for(int n = 1; n < a.length; n*=2)
            mergeGroups(a, a.length, n);
    }

    public static void main(String[] args) {
        int i;
        int a[] = {80,30,60,40,20,10,50,70};

        System.out.printf("before sort:");
        for (i=0; i<a.length; i++)
            System.out.printf("%d ", a[i]);
        System.out.printf("\n");

        mergeSortUp2Down(a, 0, a.length-1);        // 归并排序(从上往下)
        //mergeSortDown2Up(a);                    // 归并排序(从下往上)

        System.out.printf("after  sort:");
        for (i=0; i<a.length; i++)
            System.out.printf("%d ", a[i]);
        System.out.printf("\n");
    }
}

十一、桶排序(Bucket Sort)

桶排序介绍

假设待排序的数组a中共有N个整数,并且已知数组a中数据的范围[0, MAX)。在桶排序时,创建容量为MAX的桶数组r,并将桶数组元素都初始化为0;将容量为MAX的桶数组中的每一个单元都看作一个"桶"。

在排序时,逐个遍历数组a,将数组a的值,作为"桶数组r"的下标。当a中数据被读取时,就将桶的值加1。例如,读取到数组a[3]=5,则将r[5]的值+1。

桶排序实现

假设a={8,2,3,4,3,6,6,3,9}, max=10。此时,将数组a的所有数据都放到需要为0-9的桶中。如下图:

在将数据放到桶中之后,再通过一定的算法,将桶中的数据提出出来并转换成有序数组。就得到我们想要的结果了。

桶排序复杂度

  • 平均时间复杂度: O(n + k)
  • 最佳时间复杂度: O(n + k)
  • 最差时间复杂度: O(n ^ 2)
  • 空间复杂度: O(n * k)

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

桶排序稳定性

稳定性: 稳定

代码实现

/**
 * 桶排序: Java
 *
 */

public class BucketSort {

    /*
     * 桶排序
     *
     * 参数说明: 
     *     a -- 待排序数组
     *     max -- 数组a中最大值的范围
     */
    public static void bucketSort(int[] a, int max) {
        int[] buckets;

        if (a==null || max<1)
            return ;

        // 创建一个容量为max的数组buckets,并且将buckets中的所有数据都初始化为0。
        buckets = new int[max];

        // 1. 计数
        for(int i = 0; i < a.length; i++) 
            buckets[a[i]]++; 

        // 2. 排序
        for (int i = 0, j = 0; i < max; i++) {
            while( (buckets[i]--) >0 ) {
                a[j++] = i;
            }
        }

        buckets = null;
    }

    public static void main(String[] args) {
        int i;
        int a[] = {8,2,3,4,3,6,6,3,9};

        System.out.printf("before sort:");
        for (i=0; i<a.length; i++)
            System.out.printf("%d ", a[i]);
        System.out.printf("\n");

        bucketSort(a, 10); // 桶排序

        System.out.printf("after  sort:");
        for (i=0; i<a.length; i++)
            System.out.printf("%d ", a[i]);
        System.out.printf("\n");
    }
}

十二、基数排序(Radix Sort)

基数排序介绍

它的基本思想是: 将整数按位数切割成不同的数字,然后按每个位数分别比较。 具体做法是: 将所有待比较数值统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。

基数排序实现

通过基数排序对数组{53, 3, 542, 748, 14, 214, 154, 63, 616},它的示意图如下:

在上图中,首先将所有待比较树脂统一为统一位数长度,接着从最低位开始,依次进行排序。

  1. 按照个位数进行排序。
  2. 按照十位数进行排序。
  3. 按照百位数进行排序。 排序后,数列就变成了一个有序序列。

下面简单介绍一下对数组{53, 3, 542, 748, 14, 214, 154, 63, 616}按个位数进行排序的流程。

  • 个位的数值范围是[0,10)。因此,参见桶数组buckets[],将数组按照个位数值添加到桶中。

  • 接着是根据桶数组buckets[]来进行排序。假设将排序后的数组存在output[]中;找出output[]和buckets[]之间的联系就可以对数据进行排序了。

代码实现

/**
 * 基数排序: Java
 *
 */

public class RadixSort {

    /*
     * 获取数组a中最大值
     *
     * 参数说明: 
     *     a -- 数组
     *     n -- 数组长度
     */
    private static int getMax(int[] a) {
        int max;

        max = a[0];
        for (int i = 1; i < a.length; i++)
            if (a[i] > max)
                max = a[i];

        return max;
    }

    /*
     * 对数组按照"某个位数"进行排序(桶排序)
     *
     * 参数说明: 
     *     a -- 数组
     *     exp -- 指数。对数组a按照该指数进行排序。
     *
     * 例如,对于数组a={50, 3, 542, 745, 2014, 154, 63, 616};
     *    (01) 当exp=1表示按照"个位"对数组a进行排序
     *    (02) 当exp=10表示按照"十位"对数组a进行排序
     *    (03) 当exp=100表示按照"百位"对数组a进行排序
     *    ...
     */
    private static void countSort(int[] a, int exp) {
        //int output[a.length];    // 存储"被排序数据"的临时数组
        int[] output = new int[a.length];    // 存储"被排序数据"的临时数组
        int[] buckets = new int[10];

        // 将数据出现的次数存储在buckets[]中
        for (int i = 0; i < a.length; i++)
            buckets[ (a[i]/exp)%10 ]++;

        // 更改buckets[i]。目的是让更改后的buckets[i]的值,是该数据在output[]中的位置。
        for (int i = 1; i < 10; i++)
            buckets[i] += buckets[i - 1];

        // 将数据存储到临时数组output[]中
        for (int i = a.length - 1; i >= 0; i--) {
            output[buckets[ (a[i]/exp)%10 ] - 1] = a[i];
            buckets[ (a[i]/exp)%10 ]--;
        }

        // 将排序好的数据赋值给a[]
        for (int i = 0; i < a.length; i++)
            a[i] = output[i];

        output = null;
        buckets = null;
    }

    /*
     * 基数排序
     *
     * 参数说明: 
     *     a -- 数组
     */
    public static void radixSort(int[] a) {
        int exp;    // 指数。当对数组按各位进行排序时,exp=1;按十位进行排序时,exp=10;...
        int max = getMax(a);    // 数组a中的最大值

        // 从个位开始,对数组a按"指数"进行排序
        for (exp = 1; max/exp > 0; exp *= 10)
            countSort(a, exp);
    }

    public static void main(String[] args) {
        int i;
        int a[] = {53, 3, 542, 748, 14, 214, 154, 63, 616};

        System.out.printf("before sort:");
        for (i=0; i<a.length; i++)
            System.out.printf("%d ", a[i]);
        System.out.printf("\n");

        radixSort(a);    // 基数排序

        System.out.printf("after  sort:");
        for (i=0; i<a.length; i++)
            System.out.printf("%d ", a[i]);
        System.out.printf("\n");
    }
}
  • 17
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

之乎者也·

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值