JAVA 数据结构与算法(二)—— 排序算法(冒泡、选择、插入、希尔、快速、归并、计数、基数、堆排序和桶排序)

一、JAVA 算法介绍

1、排序算法(Sort Algorithm)

(1)排序的分类
① 内部排序:指将需要处理的所有数据都加在到内部存储器中进行排序(使用内存)。
② 外部排序:数据量过大,无法全部加载到内存中,需要借助外部储存进行排序(使用内存和外存结合)。
在这里插入图片描述

(2)算法的时间复杂度
一般情况下,算法中的基本操作语句的重复执行次数是问题规模n的某个函数,用T(n)表示,若有某个辅助函数f(n),使得当n趋近于无穷大时,T(n) / f(n)的极限值为不等于零的常数,则称f(n)是T(n)的同数量级函数。 记作T(n)=O(f(n)),称O(f(n)) 为算法的渐进时间复杂度,简称时间复杂度。T(n)不同,但时间复杂度可能相同。如: T(n)=n2+7n+6 与T(n)=3n2+2n+2它们的T(n)不同,但时间复杂度相同,都为0(n2)。

度量一个程序(算法)执行时间的两种方法:事后统计的方法、事前估算的方法。

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

常见的时间复杂度

  • 常数阶O(1)
  • 对数阶O(log2n)
  • 线性阶O(n)
  • 线性对数阶O(nlog2n)
  • 平方阶O(n^2)
  • 立方阶O(n^3)
  • k次方阶O(n^k)
  • 指数阶O(2^n)

常见的算法时间复杂度由小到大依次为: O(1)<O(log2n)<O(n)<o(nlog2n)<O(n^2)<0(n^3)< O(n^k)<0(2^n),随着问题规模n的不断增大,上述时间复杂度不断增大,算法的执行效率越低

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

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

在这里插入图片描述

(3)空间复杂度

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

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

二、常用排序算法示例

1、冒泡排序法

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

(2)图解冒泡法排序
在这里插入图片描述
在这里插入图片描述

(3)冒泡排序法示例

/*冒泡排序*/
public class BubbleSort {
    public static void main(String[] args) {
        /*随机定义一个数组*/
        int arr[] = {7,3,8,2,6,4,9,1,5};
        System.out.println("排序前的数组arr[] = " + Arrays.toString(arr));

        /*获取数组的长度*/
        int len = arr.length;
        /*第一层循环,代表要循环的次数*/
        for(int i = 0; i < len; i++){
            /*第二次循环,代表每次循环中排序的次数,每一次循环的排序次数会减少1*/
            for (int j = 0; j < len-1-i; j++){
                if(arr[j] > arr[j+1]){
                    int temp = arr[j];
                    arr[j] = arr[j+1];
                    arr[j+1] = temp;
                }
            }
            System.out.println("第" + (i+1) + "次循环排序后的结果为arr[] = " + Arrays.toString(arr));
        }
        System.out.println("排序完成后的数组arr[] = " + Arrays.toString(arr));
    }
}

--------
输出结果为
排序前的数组arr[] = [7, 3, 8, 2, 6, 4, 9, 1, 5]1次循环排序后的结果为arr[] = [3, 7, 2, 6, 4, 8, 1, 5, 9]2次循环排序后的结果为arr[] = [3, 2, 6, 4, 7, 1, 5, 8, 9]3次循环排序后的结果为arr[] = [2, 3, 4, 6, 1, 5, 7, 8, 9]4次循环排序后的结果为arr[] = [2, 3, 4, 1, 5, 6, 7, 8, 9]5次循环排序后的结果为arr[] = [2, 3, 1, 4, 5, 6, 7, 8, 9]6次循环排序后的结果为arr[] = [2, 1, 3, 4, 5, 6, 7, 8, 9]7次循环排序后的结果为arr[] = [1, 2, 3, 4, 5, 6, 7, 8, 9]8次循环排序后的结果为arr[] = [1, 2, 3, 4, 5, 6, 7, 8, 9]9次循环排序后的结果为arr[] = [1, 2, 3, 4, 5, 6, 7, 8, 9]
排序完成后的数组arr[] = [1, 2, 3, 4, 5, 6, 7, 8, 9]

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

/*冒泡排序优化*/
public class BubbleSort {
    public static void main(String[] args) {
        /*随机定义一个数组*/
        int arr[] = {1,6,2,7,3,8,4,5,9};
        System.out.println("排序前的数组arr[] = " + Arrays.toString(arr));

        /*获取数组的长度*/
        int len = arr.length;
        /*定义一个标记,默认为false*/
        boolean flag = false;
        /*第一层循环,代表要循环的次数*/
        for(int i = 0; i < len; i++){
            /*第二次循环,代表每次循环中排序的次数,每一次循环的排序次数会减少1*/
            for (int j = 0; j < len-1-i; j++){
                if(arr[j] > arr[j+1]){
                    int temp = arr[j];
                    arr[j] = arr[j+1];
                    arr[j+1] = temp;
                    //改变标记
                    flag = true;
                }
            }
            if(flag == false){
                break;
            }else{
                /*重置标记,便于下次循环*/
                flag = false;
            }
            System.out.println("第" + (i+1) + "次循环排序后的结果为arr[] = " + Arrays.toString(arr));
        }
        System.out.println("排序完成后的数组arr[] = " + Arrays.toString(arr));
    }
}

--------
输出结果为
排序前的数组arr[] = [1, 6, 2, 7, 3, 8, 4, 5, 9]1次循环排序后的结果为arr[] = [1, 2, 6, 3, 7, 4, 5, 8, 9]2次循环排序后的结果为arr[] = [1, 2, 3, 6, 4, 5, 7, 8, 9]3次循环排序后的结果为arr[] = [1, 2, 3, 4, 5, 6, 7, 8, 9]
排序完成后的数组arr[] = [1, 2, 3, 4, 5, 6, 7, 8, 9]

(5)冒泡排序分析
冒泡排序法分析的平均时间复杂度为O(n^2),最好的情况下时间复杂度为O(n),最坏的情况下时间复杂度为O(n^2),空间复杂度为O(1),是一种稳定的排序方式

2、选择排序法

(1)选择排序法介绍
选择排序法(Selection sort)是一种简单直观的排序算法。它的工作原理是:第一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,然后再从剩余的未排序元素中寻找到最小(大)元素,然后放到已排序的序列的末尾。以此类推,直到全部待排序的数据元素的个数为零。选择排序是不稳定的排序方法。

(2)选择排序法图解
在这里插入图片描述
在这里插入图片描述

(3)选择排序法示例

/*选择排序*/
public class SelectSort {
    public static void main(String[] args) {
        /*定义一个数组*/
        int arr[] = {7,4,8,2,9,3,6,1,5};
        System.out.println("排序前的数组arr[] = " + Arrays.toString(arr));
        /*获取数组的长度*/
        int len = arr.length;

        /*排序(从小到大)*/
        /*第一个循环代表要循环的次数*/
        for (int i = 0; i < len - 1; i++){
            /*默认第i个数为最小值,其下表为最小值对应的下标*/
            int minIndex = i;
            /*第二个循环代表每次循环中比较的次数,每轮需要比较的次数 N-i*/
            for (int j = i + 1; j < len; j++){
                if(arr[minIndex] > arr[j]){
                    /*得到当前最小值对应的下标*/
                    minIndex = j;
                }
            }
            /*数值交换*/
            if(minIndex != i){
                int temp = arr[i];
                arr[i] = arr[minIndex];
                arr[minIndex] = temp;
            }
            System.out.println("第"+(i+1)+"排序后的数组arr[] = " + Arrays.toString(arr));
        }
        System.out.println("排序完成后的数组arr[] = " + Arrays.toString(arr));
    }
}

---------------
输出结果为
排序前的数组arr[] = [7, 4, 8, 2, 9, 3, 6, 1, 5]1排序后的数组arr[] = [1, 4, 8, 2, 9, 3, 6, 7, 5]2排序后的数组arr[] = [1, 2, 8, 4, 9, 3, 6, 7, 5]3排序后的数组arr[] = [1, 2, 3, 4, 9, 8, 6, 7, 5]4排序后的数组arr[] = [1, 2, 3, 4, 9, 8, 6, 7, 5]5排序后的数组arr[] = [1, 2, 3, 4, 5, 8, 6, 7, 9]6排序后的数组arr[] = [1, 2, 3, 4, 5, 6, 8, 7, 9]7排序后的数组arr[] = [1, 2, 3, 4, 5, 6, 7, 8, 9]8排序后的数组arr[] = [1, 2, 3, 4, 5, 6, 7, 8, 9]
排序完成后的数组arr[] = [1, 2, 3, 4, 5, 6, 7, 8, 9]

(4)选择排序法分析
选择排序法的平均时间复杂度为O(n^2),最好的情况下时间复杂度为O(n^2),最坏的情况下时间复杂度为O(n^2),空间复杂度为O(1),是一种不稳定的排序方式

3、插入排序法

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

(2)插入排序法图解
在这里插入图片描述
在这里插入图片描述
(3)插入排序法示例
第一种写法

/*插入排序*/
public class InsertSort {
    public static void main(String[] args) {
        /*定义一个数组*/
        int arr[] = {7,4,8,2,9,3,6,1,5};
        System.out.println("排序前的数组arr[] = " + Arrays.toString(arr));
        /*获取数组的长度*/
        int len = arr.length;

        /*排序(从小到大)*/
        /*第一个循环代表要循环的次数*/
        for (int i = 1; i < len; i++){
            /*取出每次循环时要进行比较插入的数据*/
            int temp = arr[i];
            /*定义一个index,代表要插入的位置的下标*/
            int index = i;
            /*第二个循环代表每次循环中比较的次数,每轮需要比较的次数 N-i*/
            for (int j = i; j > 0; j--){
                if(temp < arr[j-1]){
                    arr[j] = arr[j-1];
                    /*获取要插入的下标*/
                    index--;
                }
            }
            /*插入数据*/
            if(index != i){
                arr[index] = temp;
            }
            System.out.println("第"+(i+1)+"排序后的数组arr[] = " + Arrays.toString(arr));
        }
        System.out.println("排序完成后的数组arr[] = " + Arrays.toString(arr));
    }
}

---------------
输出结果为
排序前的数组arr[] = [7, 4, 8, 2, 9, 3, 6, 1, 5]2排序后的数组arr[] = [4, 7, 8, 2, 9, 3, 6, 1, 5]3排序后的数组arr[] = [4, 7, 8, 2, 9, 3, 6, 1, 5]4排序后的数组arr[] = [2, 4, 7, 8, 9, 3, 6, 1, 5]5排序后的数组arr[] = [2, 4, 7, 8, 9, 3, 6, 1, 5]6排序后的数组arr[] = [2, 3, 4, 7, 8, 9, 6, 1, 5]7排序后的数组arr[] = [2, 3, 4, 6, 7, 8, 9, 1, 5]8排序后的数组arr[] = [1, 2, 3, 4, 6, 7, 8, 9, 5]9排序后的数组arr[] = [1, 2, 3, 4, 5, 6, 7, 8, 9]
排序完成后的数组arr[] = [1, 2, 3, 4, 5, 6, 7, 8, 9]

第二种写法

/*插入排序*/
public class InsertSort {
    public static void main(String[] args) {
        /*定义一个数组*/
        int arr[] = {7,4,8,2,9,3,6,1,5};
        System.out.println("排序前的数组arr[] = " + Arrays.toString(arr));
        /*获取数组的长度*/
        int len = arr.length;

        /*排序(从小到大)*/
        /*第一个循环代表要循环的次数*/
        for (int i = 1; i < len; i++){
            /*取出每次循环时要进行比较插入的数据*/
            int temp = arr[i];
            /*定义一个index,代表要插入的位置的下标*/
            int index = i;
            /*第二个循环代表每次循环中比较的次数,每轮需要比较的次数 N-i*/
            while (index > 0 && temp < arr[index - 1]){
                arr[index] = arr[index - 1];
                index--;
            }
            /*插入数据*/
            arr[index] = temp;
            System.out.println("第"+(i+1)+"排序后的数组arr[] = " + Arrays.toString(arr));
        }
        System.out.println("排序完成后的数组arr[] = " + Arrays.toString(arr));
    }
}

----------
输出结果为
排序前的数组arr[] = [7, 4, 8, 2, 9, 3, 6, 1, 5]2排序后的数组arr[] = [4, 7, 8, 2, 9, 3, 6, 1, 5]3排序后的数组arr[] = [4, 7, 8, 2, 9, 3, 6, 1, 5]4排序后的数组arr[] = [2, 4, 7, 8, 9, 3, 6, 1, 5]5排序后的数组arr[] = [2, 4, 7, 8, 9, 3, 6, 1, 5]6排序后的数组arr[] = [2, 3, 4, 7, 8, 9, 6, 1, 5]7排序后的数组arr[] = [2, 3, 4, 6, 7, 8, 9, 1, 5]8排序后的数组arr[] = [1, 2, 3, 4, 6, 7, 8, 9, 5]9排序后的数组arr[] = [1, 2, 3, 4, 5, 6, 7, 8, 9]
排序完成后的数组arr[] = [1, 2, 3, 4, 5, 6, 7, 8, 9]

(4)插入排序法分析
插入排序法的平均时间复杂度为O(n^2),最好的情况下时间复杂度为O(n),最坏的情况下时间复杂度为O(n^2),空间复杂度为O(1),是一种稳定的排序方式

4、希尔排序法

(1)希尔排序法介绍

  • 希尔排序也称递减增量排序算法,是插入排序的一种更高效的改进版本,是希尔(Donald Shell)于1959年提出的一种排序算法。
  • 希尔排序是非稳定排序算法。
  • 希尔排序法基本思想
    希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序,随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。即先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录"基本有序"时,再对全体记录进行依次直接插入排序。

(2)希尔排序法图解
在这里插入图片描述
在这里插入图片描述

(3)希尔排序法示例

/*希尔排序法*/
public class ShellSort {
    public static void main(String[] args) {
        /*定义一个数组*/
        int arr[] = {5,0,7,2,9,4,6,1,8,3};
        System.out.println("排序前的数组arr[] = " + Arrays.toString(arr));
        /*获取数组长度*/
        int len = arr.length;
        /*计数*/
        int count = 0;
        /*排序(从小到大)*/
        /*第一层循环,代表循环的次数*/
        for (int gap = len/2; gap > 0; gap /= 2){
            /*第二次循环,代表每次循环分组情况*/
            for (int i = gap; i < len; i++){
                /*第三次循环,代表具体比较情况*/
                for (int j = i - gap; j >= 0; j -= gap){
                    if(arr[j] > arr[j+gap]){
                        int temp = arr[j];
                        arr[j] = arr[j+gap];
                        arr[j+gap] = temp;
                    }
                }
            }
            count++;
            System.out.println("第"+count+"次排序后的数组arr[] = " + Arrays.toString(arr));
        }
        System.out.println("排序完成后的数组arr[] = " + Arrays.toString(arr));
    }
}

-------------
输出结果为
排序前的数组arr[] = [5, 0, 7, 2, 9, 4, 6, 1, 8, 3]1次排序后的数组arr[] = [4, 0, 1, 2, 3, 5, 6, 7, 8, 9]2次排序后的数组arr[] = [1, 0, 3, 2, 4, 5, 6, 7, 8, 9]3次排序后的数组arr[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
排序完成后的数组arr[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

(4)希尔排序法分析
希尔排序法的平均时间复杂度为O(n log n),最好的情况下时间复杂度为O(n log^2 n),最坏的情况下时间复杂度为O(n log^2 n),空间复杂度为O(1),是一种不稳定的排序方式。

5、快速排序法

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

(2)快速排序法图解
在这里插入图片描述
在这里插入图片描述

(3)快速排序法示例

/*快速排序法*/
public class QuickSort {
    public static void main(String[] args) {
        /*定义一个数组*/
        int arr[] = {5,0,7,2,9,4,6,1,8,3};
        System.out.println("排序前的数组arr[] = " + Arrays.toString(arr));
        /*获取数组的长度*/
        int len = arr.length;

        /*递归排序*/
        quickSort(arr, 0, len - 1);
        System.out.println("排序完成后的数组arr[] = " + Arrays.toString(arr));

    }
    /*快速排序*/
    public static void quickSort(int[] arr,int l, int r){
        /*left为左下标,right为右下标,min为中间值*/
        int left = l;
        int right = r;
        int min = arr[(l + r)/2];

        /*第一层循环:把比min大的梵高min右边,比min小的放到min左边*/
        while(left < right){
            /*在min的左边找到大于等于min的值对应的下标*/
            while(arr[left] < min){
                left += 1;
            }
            /*在min的右边边找到小于等于min的值对应的下标*/
            while (arr[right] > min){
                right -= 1;
            }
            /*如果left>=right说明min左边的值已经全部是小于min的,
                右边的值已经全部是大于min的*/
            if(left >= right){
                break;
            }
            /*交换比较的值*/
            int temp = arr[left];
            arr[left] = arr[right];
            arr[right] = temp;

            //如果交换完后,发现这个arr[left] == min值相等right--,前移
            if(arr[left] == min) {
                right -= 1;
            }
            //如果交换完后,发现这个arr[right] == min值相等1eft++,后移
            if(arr[right] == min) {
                left += 1;
            }
        }
        /*防止栈内存溢出*/
        if(left == right){
            left += 1;
            right -= 1  ;
        }
        /*向左递归*/
        if(l < right){
            quickSort(arr,l,right);
        }
        /*向左递归*/
        if(left < r){
            quickSort(arr,left,r);
        }
    }
}

------------
输出结果
排序前的数组arr[] = [5, 0, 7, 2, 9, 4, 6, 1, 8, 3]
排序完成后的数组arr[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

(4)快速排序法分析
快速排序法的平均时间复杂度为O(n log n),最好的情况下时间复杂度为O(n log n),最坏的情况下时间复杂度为O(n^2),空间复杂度为O(log n),是一种不稳定的排序方式。

6、归并排序法

(1)归并排序法介绍

  • 归并排序(MERGE- SORT)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。
  • 分治法将问题分成一些小的问题然后递归求解,而治(conquer)的阶段则将分的阶段得到的各答案修补”在一起,即分而治之。

(2)归并排序法图解
在这里插入图片描述
在这里插入图片描述

(3)归并排序法示例

/*归并排序*/
public class MergeSort {
    public static void main(String[] args) {
        /*定义一个数组*/
        int arr[] = {5,0,7,2,9,4,6,1,8,3};
        System.out.println("排序前的数组arr[] = " + Arrays.toString(arr));
        /*获取数组的长度*/
        int len = arr.length;
        int tempArr[] = new int[len];

        mergeSort(arr,0,len - 1,tempArr);
        System.out.println("排序完成后的数组arr[] = " + Arrays.toString(arr));
    }

    /*分解合并*/
    public static void mergeSort(int[] arr,int left, int right, int[] tempArr){
        if(left < right){
            int mid = (left + right) / 2;
            /*向左递归分解*/
            mergeSort(arr,left,mid,tempArr);
            /*向右递归分解*/
            mergeSort(arr,mid + 1,right,tempArr);

            /*合并*/
            merge(arr,left,mid,right,tempArr);
        }
    }

    /*合并*/
    public static void merge(int[] arr,int left, int mid, int right, int[] tempArr){
        /*left是左边有序序列的初始索引,min是中间有序序列的初始索引,
            right是右边有序序列的初始索引,tempArr是中间临时中转时用的数组*/
        int l = left;
        int r = mid + 1;
        int index = 0;  //指向tempArr数组的当前索引

        /*把左右两边有序的数组按照规则填充到tempArr数组,直到左右两边有一边处理完毕为止*/
        while(l <= mid && r <= right){
            /*如果左边的有序序列的当前元素值小于等于右边有序序列的当前元素值,
            	就将左边的拷贝到tempArr数组中*/
            if(arr[l] <= arr[r]){
                tempArr[index] = arr[l];
                index += 1;
                l += 1;
            }else{
                /*如果左边的有序序列的当前元素值大于等于右边有序序列的当前元素值,
                	就将右边的拷贝到tempArr数组中*/
                tempArr[index] = arr[r];
                index += 1;
                r += 1;
            }
        }

        /*把有剩余数据的一边的数据一次全部填充到tempArr数组中*/
        /*将左边有剩余的数据填充到tempArr数组*/
        while(l <= mid){
            tempArr[index] = arr[l];
            index += 1;
            l += 1;
        }
        /*将右边有剩余的数据填充到tempArr数组*/
        while(r <= right){
            tempArr[index] = arr[r];
            index += 1;
            r += 1;
        }

        /*将临时数组tempArr数组中的元素拷贝到arr数组*/
        int index1 = 0;
        int tempLeft = left;
        while(tempLeft <= right){
            arr[tempLeft] = tempArr[index1];
            index1 += 1;
            tempLeft += 1;
        }
    }
}

----------
输出结果为
排序前的数组arr[] = [5, 0, 7, 2, 9, 4, 6, 1, 8, 3]
排序完成后的数组arr[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

(4)归并排序法分析
归并排序法的平均时间复杂度为O(n log n),最好的情况下时间复杂度为O(n log n),最坏的情况下时间复杂度为O(n log n),空间复杂度为O(n),是一种稳定的排序方式。

7、堆排序法

(1)堆排序法介绍
堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。堆排序可以说是一种利用堆的概念来排序的选择排序。分为两种方法:

  • 大顶堆:每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列;
  • 小顶堆:每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列;

堆排序的平均时间复杂度为 Ο(nlogn)。

(2)堆排序法图解
在这里插入图片描述
在这里插入图片描述

(3)堆排序法示例

/*堆排序*/
public class HeapSort {
    public static void main(String[] args) {
        /*定义一个数组*/
        int arr[] = {5,0,7,2,9,4,6,1,8,3};
        System.out.println("排序前的数组arr[] = " + Arrays.toString(arr));
        /*获取数组的长度*/
        int len = arr.length;

        buildMaxHeap(arr, len);
        System.out.println("排序完成后的数组arr[] = " + Arrays.toString(arr));
    }

    public static void buildMaxHeap(int[] arr, int len) {
        for (int i = len / 2; i >= 0; i--) {
            heapify(arr, i, len);
        }
        for (int i = len - 1; i > 0; i--) {
            int temp = arr[0];
            arr[0] = arr[i];
            arr[i] = temp;
            len--;
            heapify(arr, 0, len);
        }
    }

    public static void heapify(int[] arr, int i, int len) {
        int left = 2 * i + 1;
        int right = 2 * i + 2;
        int largest = i;

        if (left < len && arr[left] > arr[largest]) {
            largest = left;
        }

        if (right < len && arr[right] > arr[largest]) {
            largest = right;
        }

        if (largest != i) {
            int temp = arr[i];
            arr[i] = arr[largest];
            arr[largest] = temp;
            heapify(arr, largest, len);
        }
    }
}

----------
输出结果为
排序前的数组arr[] = [5, 0, 7, 2, 9, 4, 6, 1, 8, 3]
排序完成后的数组arr[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

(4)堆排序法分析
堆排序法的平均时间复杂度为O(n log n),最好的情况下时间复杂度为O(n log n),最坏的情况下时间复杂度为O(n log n),空间复杂度为O(1),是一种不稳定的排序方式。

8、计数排序法

(1)计数排序法介绍

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

  • 计数排序的特征

    • 当输入的元素是 n 个 0 到 k 之间的整数时,它的运行时间是 Θ(n + k)。计数排序不是比较排序,排序的速度快于任何比较排序算法。
    • 由于用来计数的数组C的长度取决于待排序数组中数据的范围(等于待排序数组的最大值与最小值的差加上1),这使得计数排序对于数据范围很大的数组,需要大量时间和内存。例如:计数排序是用来排序0到100之间的数字的最好的算法,但是它不适合按字母顺序排序人名。但是,计数排序可以用在基数排序中的算法来排序数据范围很大的数组。
    • 通俗地理解,例如有 10 个年龄不同的人,统计出有 8 个人的年龄比 A 小,那 A 的年龄就排在第 9 位,用这个方法可以得到其他每个人的位置,也就排好了序。当然,年龄有重复时需要特殊处理(保证稳定性),这就是为什么最后要反向填充目标数组,以及将每个数字的统计减去 1 的原因。
  • 算法的步骤如下:

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

(2)计数排序法图解
在这里插入图片描述

(3)计数排序法示例

/*计数排序*/
public class CountSort {
    public static void main(String[] args) {
        /*定义一个数组*/
        int arr[] = {5,0,7,2,9,4,6,1,8,3};
        System.out.println("排序前的数组arr[] = " + Arrays.toString(arr));

        countingSort(arr);
        System.out.println("排序后的数组arr[] = " + Arrays.toString(arr));
    }

    private static void countingSort(int[] arr) {
        /*假定最大值为arr[0]*/
        int maxValue = arr[0];
        /*找到最大值*/
        for (int value : arr) {
            if (maxValue < value) {
                maxValue = value;
            }
        }

        /*定义一个长度比最大值大一的数组用于排序*/
        int[] bucket = new int[maxValue + 1];

        /*遍历原始数组中的每一个值,
            并将新的排序数组中对应的索引中存储值+1用于排序数组*/
        for (int value : arr) {
            bucket[value]++;
        }

        int sortedIndex = 0;
        for (int j = 0; j < bucket.length; j++) {
            while (bucket[j] > 0) {
                arr[sortedIndex++] = j;
                bucket[j]--;
            }
        }
    }
}

----------
输出结果为
排序前的数组arr[] = [5, 0, 7, 2, 9, 4, 6, 1, 8, 3]
排序后的数组arr[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

(4)计数排序法分析
计数排序法的平均时间复杂度为O(n+k),最好的情况下时间复杂度为O(n+k),最坏的情况下时间复杂度为O(n+k),空间复杂度为O(k),是一种稳定的排序方式。

9、桶排序法

(1)介绍
桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。为了使桶排序更加高效,我们需要做到这两点:

  • 在额外空间充足的情况下,尽量增大桶的数量
  • 使用的映射函数能够将输入的 N 个数据均匀的分配到 K 个桶中

同时,对于桶中元素的排序,选择何种比较排序算法对于性能的影响至关重要。

当输入的数据可以均匀的分配到每一个桶中时,桶排序效率最快;当输入的数据被分配到了同一个桶中时,桶排序效率最慢。

(2)图解
元素分布在各个桶中:
在这里插入图片描述
在每个桶中分别对元素进行排序
在这里插入图片描述

(3)示例

/*桶排序*/
public class BucketSort {
    public static void main(String[] args) {
        /*定义一个数组*/
        int arr[] = {5,0,7,2,9,4,6,1,8,3};
        System.out.println("排序前的数组arr[] = " + Arrays.toString(arr));

        bucketSort(arr, 5);
        System.out.println("排序后的数组arr[] = " + Arrays.toString(arr));
    }

    private static void bucketSort(int[] arr, int bucketSize){
        if (arr.length != 0) {
            int minValue = arr[0];
            int maxValue = arr[0];
            for (int value : arr) {
                if (value < minValue) {
                    minValue = value;
                } else if (value > maxValue) {
                    maxValue = value;
                }
            }

            int bucketCount = (maxValue - minValue) / bucketSize + 1;
            int[][] buckets = new int[bucketCount][0];

            // 利用映射函数将数据分配到各个桶中
            for (int i = 0; i < arr.length; i++) {
                int index = (arr[i] - minValue) / bucketSize;
                buckets[index] = arrAppend(buckets[index], arr[i]);
            }

            int arrIndex = 0;
            for (int[] bucket : buckets) {
                if (bucket.length <= 0) {
                    continue;
                }
                // 对每个桶进行排序,这里使用了插入排序
                bucket = insertSort(bucket);
                for (int value : bucket) {
                    arr[arrIndex++] = value;
                }
            }
        }
    }

    /*自动扩容,并保存数据*/
    private static int[] arrAppend(int[] arr, int value) {
        arr = Arrays.copyOf(arr, arr.length + 1);
        arr[arr.length - 1] = value;
        return arr;
    }

    /*插入排序*/
    public static int[] insertSort(int[] arr){
        int len = arr.length;
        for (int i = 1; i < len; i++){
            int temp = arr[i];
            int index = i;
            for (int j = i; j > 0; j--){
                if(temp < arr[j-1]){
                    arr[j] = arr[j-1];
                    index--;
                }
            }
            if(index != i){
                arr[index] = temp;
            }
        }
        return arr;
    }
}

----------
输出结果为
排序前的数组arr[] = [5, 0, 7, 2, 9, 4, 6, 1, 8, 3]
排序后的数组arr[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

(4)分析
桶排序法的平均时间复杂度为O(n+k),最好的情况下时间复杂度为O(n+k),最坏的情况下时间复杂度为O(n^2),空间复杂度为O(n+k),是一种稳定的排序方式。

10、基数排序法

(1)介绍

  • 基数排序是1887年赫尔曼.何乐礼发明的,它是这样实现的:将整数按位数切割成不同的数字,然后按每个位数分别比较。
  • 基数排序(Radix Sort)属于“分配式排序”(DistributionDort) ,又称“桶子法”( Bucket Sort)或BinSort,是桶排序的扩展, 顾名思义,它是通过键值的各个位的值,将要排序的元素分配至某些“桶”中,达到排序的作用。
  • 基数排序法属于稳定性的排序,是效率高的稳定性排序法。
  • 基数排序的基本思想
    将所有待比较数值统-一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列。

(2)图解
在这里插入图片描述
在这里插入图片描述

(3)示例

/*基数排序法*/
public class RadixSort {
    public static void main(String[] args) {
        /*定义一个数组*/
        int arr[] = {163,527,369,3,17,9,63,7,59,123};
        System.out.println("排序前的数组arr[] = " + Arrays.toString(arr));

        radixSort(arr);
        System.out.println("排序后的数组arr[] = " + Arrays.toString(arr));
    }

    public static void radixSort(int[] arr){
        /*假定最大值为arr[0]*/
        int max = arr[0];
        /*找到数组中实际的最大值*/
        for(int i = 1; i < arr.length; i++){
            if(arr[i] > max){
                max = arr[i];
            }
        }
        /*计算最大值是多少位数*/
        int maxLen = (max + "").length();

        /*定义一个二位数组,表示10个桶,每个桶中存放相应数据*/
        int[][] bucket = new int[10][arr.length];

        /*定义一个一维数组,用于记录每个桶每次放入的元素个数*/
        int[] count = new int[10];

        /*n每次循环完会乘以10,表示到了数值那一位(个位、十位、百位...)*/
        int n = 1;
        /*第一个循环:最大值有多少位数就进行几次循环*/
        for(int i = 0; i < maxLen; i++){
            /*第二轮循环:根据每个元素对应的位数的值进行排序*/
            for(int j = 0; j < arr.length; j++){
                /*去除每个元素的个位的值*/
                int element = arr[j] /n % 10;
                /*将元素放入到个位对应的桶中*/
                bucket[element][count[element]] = arr[j];
                count[element]++;
            }

            /*按照桶的顺序和一维数组的下标顺序依次取出数据放入到原来的数组中*/
            int index = 0;
            /*遍历每个桶并将桶中的数据取出放入到原数组*/
            for(int k = 0; k < count.length; k++){
                /*判断该桶是否有数据*/
                if(count[k] != 0){
                    /*循环第k个桶中的数据*/
                    for (int m = 0; m < count[k]; m++){
                        arr[index] = bucket[k][m];
                    index++;
                    }
                }
                /*每轮循环后,将桶中的元素清空*/
                count[k] = 0;
            }
            n *= 10;
            System.out.println("第" + (i+1) + "轮循环排序后数组arr[] = " + Arrays.toString(arr));
        }
    }
}

----------
输出结果为
排序前的数组arr[] = [163, 527, 369, 3, 17, 9, 63, 7, 59, 123]1轮循环排序后数组arr[] = [163, 3, 63, 123, 527, 17, 7, 369, 9, 59]2轮循环排序后数组arr[] = [3, 7, 9, 17, 123, 527, 59, 163, 63, 369]3轮循环排序后数组arr[] = [3, 7, 9, 17, 59, 63, 123, 163, 369, 527]
排序后的数组arr[] = [3, 7, 9, 17, 59, 63, 123, 163, 369, 527]

(4)分析

  • 基数排序法的平均时间复杂度为O(n*k),最好的情况下时间复杂度为O(n*k),最坏的情况下时间复杂度为O(n*k),空间复杂度为O(n+k),是一种稳定的排序方式。
  • 基数排序是对传统桶排序的扩展,速度很快。
  • 基数排序是经典的空间换时间的方式,占用内存很大,当对海量数据排序时,容易造成OutOfMemoryError。
  • 基数排序是稳定的。[注 :假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[i], 且r[i]在rj]之 前,而在排序后的序列中,r[i]仍在r[]之前,则称这种排序算法是稳定的,否则称为不稳定的。

(5)计数排序 、桶排序、基数排序比较
这三种排序算法都利用了桶的概念,但对桶的使用方法上有明显差异:

  • 基数排序:根据键值的每位数字来分配桶;
  • 计数排序:每个桶只存储单一键值;
  • 桶排序:每个桶存储一定范围的数值。

三、排序方式汇总比较

在这里插入图片描述
名词解释:

  • n:数据规模
  • k:"桶"的个数
  • In-place:占用常数内存,不占用额外内存
  • Out-place:占用额外内存
  • 稳定性:排序后 2 个相等键值的顺序和排序之前它们的顺序相同,如a,b两个数,如果a=b,排序后顺序依旧是a,b。
  • 不稳定性:排序后 2 个相等键值的顺序和排序之前它们的顺序不相同,如a,b两个数,如果a=b,排序后顺序有可能是b,a。
  • 时间复杂度:一个算法执行所耗费的时间。
  • 空间复杂度:运行完一个程序需要的内存大小。
  • 内排序:所有排序操作都在内存中进行。
  • 外排序:由于数据太大,因此把数据放在磁盘中,而排序通过磁盘和内存的数据传输才能进行。

在这里插入图片描述
总结:

  • 平方阶 (O(n2)) 排序 各类简单排序:直接插入、直接选择和冒泡排序。
  • 线性对数阶 (O(nlog2n)) 排序 快速排序、堆排序和归并排序。
  • O(n1+§)) 排序,§ 是介于 0 和 1 之间的常数。 希尔排序。
  • 线性阶 (O(n)) 排序 基数排序,此外还有桶、箱排序。
  • 稳定的排序算法:冒泡排序、插入排序、归并排序和基数排序
  • 不是稳定的排序算法:选择排序、快速排序、希尔排序、堆排序。
发布了104 篇原创文章 · 获赞 58 · 访问量 7505

猜你喜欢

转载自blog.csdn.net/baidu_27414099/article/details/104420782