排序算法总结

算法

​ 算法是解某一特定问题的一组有穷规则的集合

​ 一个算法应该具有以下五个重要的特征:

1.有穷性
( Finiteness )
算法的有穷性是指算法必须能在执行有限个步骤之后终止;
2.确切性
(Definiteness)
算法的每-步骤必须有确切的定义;
3.输入项
(Input)
一个算法有0个或多个输入,以刻画运算对象的初始情况,所谓0个输入是指算法本身定出了初始条件;
4.输出项
(Output)
一个算法有一个或多个输出,以反映对输入数据加工后的结果。没有输出的算法是毫无意义的;
5.可行性.
(Effectiveness)
算法中执行的任何计算步骤都是可以被分解为基本的可执行的操作步骤,即每个计算步骤都可以在有限时间内完成.

时间复杂度与空间复杂度

时间复杂度(简单介绍一下)

在计算机科学中,算法的时间复杂度是一个函数,它定量描述了该算法的运行时间。

  1. 用常数1取代运行时间中的所有加法常数。
  2. 在修改后的运行次数函数中,只保留最高阶项。
  3. 如果最高阶项存在且系数不为1,则去除与这个项相乘的系数,得到的结果就是大O阶。

观察下列常见时间复杂度,帮助理解
在这里插入图片描述

时间复杂度越小越好

空间复杂度

​ 一个程序的空间复杂度是指运行完一个程序所需内存的大小。利用程序的空间复杂度,可以对程序的运行所需要的内存多少有个预先估计。一个程序执行时除了需要存储空间和存储本身所使用的指令、常数、变量和输入数据外,还需要一些对数据进行操作的工作单元和存储一些为现实计算所需信息的辅助空间。程序执行时所需存储空间包括以下两部分。
(1)固定部分。这部分空间的大小与输入/输出的数据的个数多少、数值无关。主要包括指令空间(即代码空间)、数据空间(常量、简单变量)等所占的空间。这部分属于静态空间。
(2)可变空间,这部分空间的主要包括动态分配的空间,以及递归栈所需的空间等。这部分的空间大小与算法有关。

除了在一些特殊情况下,我们都是更加注重时间复杂度,而不是空间复杂度。因此很多情况下可以考虑:牺牲空间换取时间

常见排序算法

在这里插入图片描述

一.选择排序

选择排序(Selection sort)是一种简单直观的排序算法。

1. 基本思想

首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

选择排序的思想其实和冒泡排序有点类似,都是在一次排序后把最小的元素放到最前面,或者将最大值放在最后面。但是过程不同,冒泡排序是通过相邻的比较和交换。而选择排序是通过对整体的选择,每一趟从前往后查找出无序区最小值,将最小值交换至无序区最前面的位置。

2. 实现逻辑
  • 第一轮从下标为 1 到下标为 n-1 的元素中选取最小值,若小于第一个数,则交换
  • 第二轮从下标为 2 到下标为 n-1 的元素中选取最小值,若小于第二个数,则交换
  • 依次类推下去……
    在这里插入图片描述
3.代码实现
 public void SelectSort(){
        int[] arr = {5,3,6,8,1,7,9,4,2};
        int minPos;//记录当前轮次最小值的下标

        for(int i=0;i<arr.length-1;i++) {
            minPos = i;
            for(int j=i+1;j<arr.length;j++) {
                minPos = arr[j] < arr[minPos] ? j : minPos;
            }
            int temp = arr[i];
            arr[i] = arr[minPos];
            arr[minPos] = temp;
        }

        for(int i=0;i<arr.length;i++) {
            System.out.print(arr[i]+" ");
        }

    }
    
总结

选择排序的主要优点与数据移动有关。如果某个元素位于正确的最终位置上,则它不会被移动。选择排序每次交换一对元素,它们当中至少有一个将被移到其最终位置上,因此对n个元素的表进行排序总共进行至多n-1次交换。在所有的完全依靠交换去移动元素的排序方法中,选择排序属于非常好的一种。

二.冒泡排序

1. 基本思想

冒泡排序是一种交换排序,核心是冒泡,把数组中最小的那个往上冒,冒的过程就是和他相邻的元素交换。

重复走访要排序的数列,通过两两比较相邻记录的排序码。排序过程中每次从后往前冒一个最小值,且每次能确定一个数在序列中的最终位置。若发生逆序,则交换;有俩种方式进行冒泡,一种是先把小的冒泡到前边去,另一种是把大的元素冒泡到后边。

趣味解释:
有一群泡泡,其中一个泡泡跑到一个泡小妹说,小妹小妹你过来咱俩比比谁大,小妹说哇你好大,于是他跑到了泡小妹前面,又跟前面的一个泡大哥说,大哥,咱俩比比谁大呗。泡大哥看了他一眼他就老实了。这就是内层的for,那个泡泡跟每个人都比一次。

话说那个泡泡刚老实下来,另一个泡泡又开始跟别人比谁大了,这就是外层的for,每个泡泡都会做一次跟其他泡泡比个没完的事。
在这里插入图片描述

2.思想描述:

1.冒泡排序属于一种典型的交换排序。

交换排序顾名思义就是通过元素的两两比较,判断是否符合要求,如过不符合就交换位置来达到排序的目的。冒泡排序名字的由来就是因为在交换过程中,类似水冒泡,小(大)的元素经过不断的交换由水底慢慢的浮到水的顶端。

冒泡排序的思想就是利用的比较交换,利用循环将第 i 小或者大的元素归位,归位操作利用的是对 n 个元素中相邻的两个进行比较,如果顺序正确就不交换,如果顺序错误就进行位置的交换。通过重复的循环访问数组,直到没有可以交换的元素,那么整个排序就已经完成了。

   //冒泡排序:
    @Test
    public void BubbleSort(){
        int[] arr = {3,4,1,5,2};

        for(int i=0;i<arr.length-1;i++) {
            for(int j=0;j<arr.length-i-1;j++) {
                if(arr[j]>arr[j+1]) {
                    int temp = arr[j];
                    arr[j] = arr[j+1];
                    arr[j+1] = temp;
                }
            }
        }

        for(int i=0;i<arr.length;i++) {
            System.out.print(arr[i]+" ");
        }
    }



	 //冒泡排序常规版
    @Test
    public void BubbleSortNormal() {
            int[] list = {3,4,1,5,2};
            int temp = 0; // 开辟一个临时空间, 存放交换的中间值
            // 要遍历的次数
            for (int i = 0; i < list.length-1; i++) {
                System.out.format("第 %d 遍:\n", i+1);
                //依次的比较相邻两个数的大小,遍历一次后,把数组中第i小的数放在第i个位置上
                for (int j = 0; j < list.length-1-i; j++) {
                    // 比较相邻的元素,如果前面的数小于后面的数,就交换
                    if (list[j] < list[j+1]) {
                        temp = list[j+1];
                        list[j+1] = list[j];
                        list[j] = temp;
                    }
                    System.out.format("第 %d 遍的第%d 次交换:", i+1,j+1);
                    for(int count:list) {
                        System.out.print(count);
                    }
                    System.out.println("");
                }
                System.out.format("第 %d 遍最终结果:", i+1);
                for(int count:list) {
                    System.out.print(count);
                }
                System.out.println("\n#########################");
            }
        }
//结果1 遍:
第 1 遍的第1 次交换:431521 遍的第2 次交换:431521 遍的第3 次交换:435121 遍的第4 次交换:435211 遍最终结果:43521
#########################
第 2 遍:
第 2 遍的第1 次交换:435212 遍的第2 次交换:453212 遍的第3 次交换:453212 遍最终结果:45321
#########################
第 3 遍:
第 3 遍的第1 次交换:543213 遍的第2 次交换:543213 遍最终结果:54321
#########################
第 4 遍:
第 4 遍的第1 次交换:543214 遍最终结果:54321
#########################

Process finished with exit code 0

2.下面详细分析一下常规版的冒泡排序,整个算法流程其实就是上面实例所分析的过程。可以看出,我们在进行每一次大循环的时候,还要进行一个小循环来遍历相邻元素并交换。所以我们的代码中首先要有两层循环。

外层循环:即主循环,需要辅助我们找到当前第 i 小的元素来让它归位。所以我们会一直遍历 n-2 次,这样可以保证前 n-1 个元素都在正确的位置上,那么最后一个也可以落在正确的位置上了。

内层循环:即副循环,需要辅助我们进行相邻元素之间的比较和换位,把大的或者小的浮到水面上。所以我们会一直遍历 n-1-i 次这样可以保证没有归位的尽量归位,而归位的就不用再比较了。

而上面的问题,出现的原因也来源于这两次无脑的循环,正是因为循环不顾一切的向下执行,所以会导致在一些特殊情况下得多余。例如 5,4,3,1,2 的情况下,常规版会进行四次循环,但实际上第一次就已经完成排序了。

算法的第一次优化

经过了上述的讨论和编码,常规的冒泡排序已经被我们实现了。那么接下来我们要讨论的就是刚刚分析时候提出的问题。

首先针对第一个问题,当我们进行完第三遍的时候,实际上整个排序都已经完成了,但是常规版还是会继续排序。

可能在上面这个示例下,可能看不出来效果,但是当数组是,5,4,3,1,2 的时候的时候就非常明显了,实际上在第一次循环的时候整个数组就已经完成排序,但是常规版的算法仍然会继续后面的流程,这就是多余的了。

​ 为了解决这个问题,我们可以设置一个标志位,用来表示当前第 i 趟是否有交换,如果有则要进行 i+1 趟,如果没有,则说明当前数组已经完成排序。实现代码如下:

@Test
    public void BubbleSoerOpt1(){
            int[] list = {5,4,3,1,2};
            int temp = 0; // 开辟一个临时空间, 存放交换的中间值
            // 要遍历的次数
            for (int i = 0; i < list.length-1; i++) {
                int flag = 1; //设置一个标志位
                //依次的比较相邻两个数的大小,遍历一次后,把数组中第i小的数放在第i个位置上
                for (int j = 0; j < list.length-1-i; j++) {
                    // 比较相邻的元素,如果前面的数小于后面的数,交换
                    if (list[j] < list[j+1]) {
                        temp = list[j+1];
                        list[j+1] = list[j];
                        list[j] = temp;
                        flag = 0;  //发生交换,标志位置0
                    }
                }
                System.out.format("第 %d 遍最终结果:", i+1);
                for(int count:list) {
                    System.out.print(count);
                }
                System.out.println("");
                if (flag == 1) {//如果没有交换过元素,则已经有序
                    return;
                }

            }
        }

//结果 可以看到优化效果非常明显,比正常情况下少了两次的循环。1 遍最终结果:543212 遍最终结果:54321

Process finished with exit code 0

这个时候我们就来讨论一下上面留下的一个小地方!没错就是最优时间复杂度为O(n)的问题,我们在进行了这一次算法优化之后,就可以做到了。

当给我们一个数列,5,4,3,2,1,让我们从大到小排序。没错,这是已经排好序的啊,也就是说因为标志位的存在,上面的循环只会进行一遍,flag没有变成1,整个算法就结束了,这也就是 O(n) 的来历了!

算法的第二次优化

除了上面这个问题,在冒泡排序中还有一个问题存在,就是第 i 趟排的第 i 小或者大的元素已经在第 i 位上了,甚至可能第 i-1 位也已经归位了,那么在内层循环的时候,有这种情况出现就会导致多余的比较出现。例如:6,4,7,5,1,3,2,当我们进行第一次排序的时候,结果为6,7,5,4,3,2,1,实际上后面有很多次交换比较都是多余的,因为没有产生交换操作。

我们用刚刚优化过一次的算法,跑一下这个数组。

@Test
    public void BubbleSoerOpt1For() {

            int[] list = {6,4,7,5,1,3,2};
            int len = list.length-1;
            int temp = 0; // 开辟一个临时空间, 存放交换的中间值
            // 要遍历的次数
            for (int i = 0; i < list.length-1; i++) {
                int flag = 1; //设置一个标志位
                //依次的比较相邻两个数的大小,遍历一次后,把数组中第i小的数放在第i个位置上
                for (int j = 0; j < len-i; j++) {
                    // 比较相邻的元素,如果前面的数小于后面的数,交换
                    if (list[j] < list[j+1]) {
                        temp = list[j+1];
                        list[j+1] = list[j];
                        list[j] = temp;
                        flag = 0;  //发生交换,标志位置0

                    }
                    System.out.format("第 %d 遍第%d 趟结果:", i+1, j+1);
                    for(int count:list) {
                        System.out.print(count);
                    }
                    System.out.println("");
                }

                System.out.format("第 %d 遍最终结果:", i+1);
                for(int count:list) {
                    System.out.print(count);
                }
                System.out.println("");
                if (flag == 1) {//如果没有交换过元素,则已经有序
                    return;
                }

            }
        }
//结果 可以看出,第三趟的多次比较实际上可以没有,因为中间几个位置在第二趟就没有过交换。1 遍第1 趟结果:64751321 遍第2 趟结果:67451321 遍第3 趟结果:67541321 遍第4 趟结果:67541321 遍第5 趟结果:67543121 遍第6 趟结果:67543211 遍最终结果:67543212 遍第1 趟结果:76543212 遍第2 趟结果:76543212 遍第3 趟结果:76543212 遍第4 趟结果:76543212 遍第5 趟结果:76543212 遍最终结果:76543213 遍第1 趟结果:76543213 遍第2 趟结果:76543213 遍第3 趟结果:76543213 遍第4 趟结果:76543213 遍最终结果:7654321

Process finished with exit code 0

​ 针对上述的问题,我们可以想到,利用一个标志位,记录一下当前第 i 趟所交换的最后一个位置的下标,在进行第 i+1 趟的时候,只需要内循环到这个下标的位置就可以了,因为后面位置上的元素在上一趟中没有换位,这一次也不可能会换位置了。基于这个原因,我们可以进一步优化我们的代码。

 /**
     * 冒泡排序优化第二版
     */
    @Test
    public void BubbleSoerOpt2() {
            int[] list = {6,4,7,5,1,3,2};
            int len = list.length-1;
            int temp = 0; // 开辟一个临时空间, 存放交换的中间值
            int tempPostion = 0;  // 记录最后一次交换的位置
            // 要遍历的次数
            for (int i = 0; i < list.length-1; i++) {
                int flag = 1; //设置一个标志位
                //依次的比较相邻两个数的大小,遍历一次后,把数组中第i小的数放在第i个位置上
                for (int j = 0; j < len; j++) {
                    // 比较相邻的元素,如果前面的数小于后面的数,交换
                    if (list[j] < list[j+1]) {
                        temp = list[j+1];
                        list[j+1] = list[j];
                        list[j] = temp;
                        flag = 0;  //发生交换,标志位置0
                        tempPostion = j;  //记录交换的位置
                    }
                    System.out.format("第 %d 遍第%d 趟结果:", i+1, j+1);
                    for(int count:list) {
                        System.out.print(count);
                    }
                    System.out.println("");
                }
                len = tempPostion; //把最后一次交换的位置给len,来缩减内循环的次数
                System.out.format("第 %d 遍最终结果:", i+1);
                for(int count:list) {
                    System.out.print(count);
                }
                System.out.println("");
                if (flag == 1) {//如果没有交换过元素,则已经有序
                    return;
                }

            }
        }

//结果 可以清楚的看到,部分内循环多余的比较已经被去掉了,算法得到了进一步的优化1 遍第1 趟结果:64751321 遍第2 趟结果:67451321 遍第3 趟结果:67541321 遍第4 趟结果:67541321 遍第5 趟结果:67543121 遍第6 趟结果:67543211 遍最终结果:67543212 遍第1 趟结果:76543212 遍第2 趟结果:76543212 遍第3 趟结果:76543212 遍第4 趟结果:76543212 遍第5 趟结果:76543212 遍最终结果:76543213 遍最终结果:7654321

Process finished with exit code 0

三.插入排序

​ 插入排序(Insertion-Sort)的算法描述是一种简单直观的排序算法。打过扑克牌的应该都会明白

在这里插入图片描述

1. 基本思想

​ 插入排序的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。

2.实现描述:

一般来说具体算法描述如下:

  • 从第一个元素开始,该元素可以认为已经被排序;
  • 取出下一个元素,在已经排序的元素序列中从后向前扫描;
  • 如果该元素(已排序)大于新元素,将该元素移到下一位置;
  • 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
  • 将新元素插入到该位置后;
  • 重复步骤2~5。
    在这里插入图片描述
 //插入排序
    @Test
    public  void insertionSort(){
        int[] arr = {6,9,6,3,4,5,7};
        int len = arr.length;
        int preIndex, current;
        for(int i = 1; i < len; i++) {
            preIndex = i - 1;
            current = arr[i];
            while(preIndex >= 0 && arr[preIndex] > current) {
                arr[preIndex + 1] = arr[preIndex];
                preIndex--;
            }
            arr[preIndex + 1] = current;
        }
        System.out.println(Arrays.toString(arr));
    }
总结

​ 插入排序不适合对于数据量比较大的排序应用。但是,如果需要排序的数据量很小,例如,量级小于千,那么插入排序还是一个不错的选择。尤其当数据基本有序时,采用插入排序可以明显减少数据交换和数据移动次数,进而提升排序效率。 在STL的sort算法和stdlib的qsort算法中,都将插入排序作为快速排序的补充,用于少量元素的排序。

四.快速排序

快速排序,又称划分交换排序(partition-exchange sort)

1.基本思想

通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。

2. 实现逻辑

快速排序使用分治法(Divide and conquer)策略来把一个序列(list)分为两个子序列(sub-lists)。

  • 1将排序的数据分为两部分,其中一部分比另一部分的数据大,然后再对两部分进行排序
  • 2首先挑出一个元素作为基准,左边的left和右边的right不动,然后从右往左遍历,然后从左往右遍历,并最终返回分割的标志(即下标)
  • 3调用方法分区,然后对数据小的一部分进行排序,接着对数据大的一部分进行排序
  • 4调用方法进行排序

递归到最底部时,数列的大小是零或一,也就是已经排序好了。这个算法一定会结束,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去。
在这里插入图片描述

3.代码实现

快排之双向扫描分区法

public class QuickSortTest {
    public static void main(String[] args) {
        int[] arr = {1, 34, 24, 89, 77, 65, 100};
        quickSort(arr,0,arr.length-1);
        System.out.println(Arrays.toString(arr));
    }
    public static void quickSort(int[] arr,int left,int right){
        if (left<=right){
            int mid = sort(arr, left, right);
            quickSort(arr,left,mid-1);
            quickSort(arr,mid+1,right);
        }
    }
    public static int sort(int[] arr,int left,int right){
        int k=arr[left],l=left+1,r=right;
        while (l<=r){
            while (l<=r&&arr[l]<=k)l++;
            while (l<=r&&arr[r]>k)r--;
            if (l<r){
                int temp=arr[l];
                arr[l]=arr[r];
                arr[r]=temp;
            }
        }
        arr[left]=arr[r];
        arr[r]=k;
        return r;
    }
}
package com.igeek.sort;
import java.util.Arrays;
public class QuickSort {
    public static void main(String[] args) {
        int[] arr = {1, 34, 24, 89, 77, 65, 100};
        quickSort(arr);
        System.out.println(Arrays.toString(arr));
    }

    public static void quickSort(int[] arr) {
        //判断一下防止出现空指针异常,或者没有意义的比较,提高程序的健壮性
        if (arr == null || arr.length == 0 || arr.length == 1) {
            return;
        }
        sort(arr, 0, arr.length - 1);
    }

    public static void sort(int[] arr, int left, int right) {
        //如果左边大于右边,跳出递归结束比较
        if (left > right) {
            return;
        }
        int i = left;
        int j = right;
        int temp = arr[i];
        //不知道具体次数的比较用while
        while (i < j) {
            //右往左移动,进行排序
            while (i < j && arr[j] >= temp) {
                j--;
            }
            arr[i] = arr[j];
            //左往右移动,进行排序
            while (i < j && arr[i] <= temp) {
                i++;
            }
            arr[j] = arr[i];
        }
        //如果i和j相遇,递归比较
        arr[i] = temp;
        sort(arr, left, i - 1);
        sort(arr, i + 1, right);
    }
}

总结

​ 快速排序在排序算法中具有排序速度快,而且是就地排序等优点,使得在许多编程语言的内部元素排序实现中采用的就是快速排序,很多面试题中也经常遇到。对于其算法的改进,除了刚刚上文中提到的意外,根据实际场景还有诸多改进方法,包括对小序列采用插入排序替代,三平均划分,三分区划分等改进方法(相关的改进方法就不一一说明,有兴趣的读者可上网查阅了解)。

五.希尔排序(Shell Sort)

​ 希尔排序的实质就是分组插入排序,该方法又称递减增量排序算法,因DL.Shell于1959年提出而得名。希尔排序是非稳定的排序算法。

1. 基本思想

先将整个待排元素序列分割成若干个子序列(由相隔某个“增量”的元素组成的)分别进行直接插入排序,然后依次缩减增量再进行排序,待整个序列中的元素基本有序(增量足够小)时,再对全体元素进行一次直接插入排序。

因为直接插入排序在元素基本有序的情况下(接近最好情况),效率是很高的,因此希尔排序在时间效率上比前两种方法有较大提高。

希尔排序是基于插入排序的以下两点性质而提出改进方法的:

插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-G4uJ4Mce-1644918299718)(img\希尔排序.gif)]

动图是分组执行,实际操作是多个分组交替执行

2.算法描述:

​ 选择增量gap=length/2,缩小增量继续以gap = gap/2的方式,这种增量选择我们可以用一个序列来表示,{n/2,(n/2)/2…1},称为**增量序列。**先将待排记录序列按增量序列分割成为若干子序列分别进行插入排序,待整个序列中的记录"基本有序"时,再对全体记录进行一次直接插入排序。
在这里插入图片描述

​ 在希尔排序的理解时,我们倾向于对于每一个分组,逐组进行处理,但在代码实现中,我们可以不用这么按部就班地处理完一组再调转回来处理下一组(这样还得加个for循环去处理分组)比如[5,4,3,2,1,0] ,首次增量设gap=length/2=3,则为3组[5,2] [4,1] [3,0],实现时不用循环按组处理,我们可以从第gap个元素开始,逐个跨组处理。同时,在插入数据时,可以采用元素交换法寻找最终位置,也可以采用数组元素移动法寻觅。希尔排序的代码比较简单,如下:

package com.igeek.sort;
import java.util.Arrays;
public class ShellSort {
    public static void main(String[] args) {
        int[] arr = {1, 4, 2, 7, 9, 8, 3, 6};
        sort(arr);
        System.out.println(Arrays.toString(arr));
        int[] arr1 = {1, 4, 2, 7, 9, 8, 3, 6};
        sort1(arr1);
        System.out.println(Arrays.toString(arr1));
    }

    /**
     * 希尔排序 针对有序序列在插入时采用交换法
     *
     * @param arr
     */
    public static void sort(int[] arr) {
        //增量gap,并逐步缩小增量
        for (int gap = arr.length / 2; gap > 0; gap /= 2) {
            //从第gap个元素,逐个对其所在组进行直接插入排序操作
            for (int i = gap; i < arr.length; i++) {
                int j = i;
                while (j - gap >= 0 && arr[j] < arr[j - gap]) {
                    //插入排序采用交换法
                    swap(arr, j, j - gap);
                    j -= gap;
                }
            }
        }
    }

    /**
     * 希尔排序 针对有序序列在插入时采用移动法。
     *
     * @param arr
     */
    public static void sort1(int[] arr) {
        //增量gap,并逐步缩小增量
        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];
                if (arr[j] < arr[j - gap]) {
                    while (j - gap >= 0 && temp < arr[j - gap]) {
                        //移动法
                        arr[j] = arr[j - gap];
                        j -= gap;
                    }
                    arr[j] = temp;
                }
            }
        }
    }

    /**
     * 交换数组元素
     *
     * @param arr
     * @param a
     * @param b
     */
    public static void swap(int[] arr, int a, int b) {
        arr[a] = arr[a] + arr[b];
        arr[b] = arr[a] - arr[b];
        arr[a] = arr[a] - arr[b];
    }
}

//结果 
[1, 2, 3, 4, 6, 7, 8, 9]
[1, 2, 3, 4, 6, 7, 8, 9]
总结

希尔排序的核心在于间隔序列的设定。既可以提前设定好间隔序列,也可以动态的定义间隔序列。动态定义间隔序列的算法是《算法(第4版)》的合著者Robert Sedgewick提出的。

六.归并排序

1.基本思想

归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。

2.补充:分治法:

​ 在求解一个输入规模为n,而n的取值又很大的问题时,直接求解往往非常困难。这时,可以先分析问题本身所具有的某些特性,然后从这些特性出发,选择某些适当的设计策略来求解。例如,如果把n个输入划分成k个子集,分别对这些子集进行求解,再把所得到的解组合起来,从而得到整个问题的解,这样,有时可以收到很好的效果。这种方法,就是所谓的分治法。
​ 一般来说,分治法是把问题划分成多个子问题来进行处理。这些子问题,在结构上和原来的问题一样,但在规模上比原来的小。如果所得到的子问题相对来说还太大,可以反复地使用分治策略,把这些子问题再划分成更小的、结构相同的子问题。这样,就可以使用递归的方法分别求解这些子问题,并把这些子问题的解组合起来,从而获得原来问题的解。

3.动图演示

在这里插入图片描述

4.算法描述
  • 把长度为n的输入序列分成两个长度为n/2的子序列;

  • 对这两个子序列分别采用归并排序;

  • 将两个排序好的子序列合并成一个最终的排序序列。

  • 如图:
    在这里插入图片描述

    ​ 可以看到这种结构很像一棵完全二叉树,本文的归并排序我们采用递归去实现(也可采用迭代的方式去实现)。阶段可以理解为就是递归拆分子序列的过程,递归深度为log2n。

    再来看看阶段,我们需要将两个已经有序的子序列合并成一个有序序列,比如上图中的最后一次合并,要将[4,5,7,8]和[1,2,3,6]两个已经有序的子序列,合并为最终序列[1,2,3,4,5,6,7,8],来看下实现步骤。

    在这里插入图片描述

package com.igeek.sort;
import java.util.Arrays;
public class MergeSort {
    public static void main(String []args){
        int []arr = {9,8,7,6,5,4,3,2,1};
        sort(arr);
        System.out.println(Arrays.toString(arr));
    }
    public static void sort(int []arr){
        int []temp = new int[arr.length];//在排序前,先建好一个长度等于原数组长度的临时数组,避免递归中频繁开辟空间
        sort(arr,0,arr.length-1,temp);
    }
    private static void sort(int[] arr,int left,int right,int []temp){
        if(left<right){
            int mid = (left+right)/2;//
            sort(arr,left,mid,temp);//左边归并排序,使得左子序列有序
            sort(arr,mid+1,right,temp);//右边归并排序,使得右子序列有序
            merge(arr,left,mid,right,temp);//将两个有序子数组合并操作
        }
    }
    private static void merge(int[] arr,int left,int mid,int right,int[] temp){
        int i = left;//左序列指针(指针就是一个保存了对象的存储地址的变量)->index;
        int j = mid+1;//右序列指针
        int t = 0;//临时数组指针
        while (i<=mid && j<=right){
            if(arr[i]<=arr[j]){
                temp[t++] = arr[i++];
            }else {
                temp[t++] = arr[j++];
            }
        }
        while(i<=mid){//将左边剩余元素填充进temp中
            temp[t++] = arr[i++];
        }
        while(j<=right){//将右序列剩余元素填充进temp中
            temp[t++] = arr[j++];
        }
        t = 0;
        //将temp中的元素全部拷贝到原数组中
        while(left <= right){
            arr[left++] = temp[t++];
        }
    }
}

总结

归并排序是稳定排序,它也是一种十分高效的排序,能利用完全二叉树特性的排序一般性能都不会太差。java中Arrays.sort()采用了一种名为TimSort的排序算法,就是归并排序的优化版本。从上文的图中可看出,每次合并操作的平均时间复杂度为O(n),而完全二叉树的深度为|log2n|。总的平均时间复杂度为O(nlogn)。而且,归并排序的最好,最坏,平均时间复杂度均为O(nlogn)。

七.堆排序

​ 堆的相关概念

堆一般指的是二叉堆,顾名思义,二叉堆是完全二叉树或者近似完全二叉树

1. 堆的性质
  • 是一棵完全二叉树
  • 每个节点的值都大于或等于其子节点的值,为最大堆;反之为最小堆。
    在这里插入图片描述
2. 堆的存储

一般用数组来表示堆,下标为 i 的结点的父结点下标为(i-1)/2;其左右子结点分别为 (2i + 1)、(2i + 2)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7xlZ3jx6-1644918299721)(img/heap_02.jpg)]

3. 堆的操作

在堆的数据结构中,堆中的最大值总是位于根节点(在优先队列中使用堆的话堆中的最小值位于根节点)。堆中定义以下几种操作:

  • 最大堆调整(Max_Heapify):将堆的末端子节点作调整,使得子节点永远小于父节点
  • 创建最大堆(Build_Max_Heap):将堆所有数据重新排序
  • 堆排序(HeapSort):移除位在第一个数据的根节点,并做最大堆调整的递归运算

堆排序(Heap Sort)

堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。

1. 基本思想

利用大顶堆(小顶堆)堆顶记录的是最大关键字(最小关键字)这一特性,使得每次从无序中选择最大记录(最小记录)变得简单。

  • 将待排序的序列构造成一个最大堆,此时序列的最大值为根节点
  • 依次将根节点与待排序序列的最后一个元素交换
  • 再维护从根节点到该元素的前一个节点为最大堆,如此往复,最终得到一个递增序列
2. 实现逻辑
  • 先将初始的R[0…n-1]建立成最大堆,此时是无序堆,而堆顶是最大元素。
  • 再将堆顶R[0]和无序区的最后一个记录R[n-1]交换,由此得到新的无序区R[0…n-2]和有序区R[n-1],且满足R[0…n-2].keys
    ≤ R[n-1].key
  • 由于交换后新的根R[1]可能违反堆性质,故应将当前无序区R[1…n-1]调整为堆。然后再次将R[1…n-1]中关键字最大的记录R[1]和该区间的最后一个记录R[n-1]交换,由此得到新的无序区R[1…n-2]和有序区R[n-1…n],且仍满足关系R[1…n-2].keys≤R[n-1…n].keys,同样要将R[1…n-2]调整为堆。
  • 直到无序区只有一个元素为止。
3. 动图演示

在这里插入图片描述

堆排序算法的演示。首先,将元素进行重排,以匹配堆的条件。图中排序过程之前简单的绘出了堆树的结构。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Jn2PS7JQ-1644918299721)(img/heap_sort_01.gif)]

分步解析说明:
实现堆排序需要解决两个问题:

  1. 如何由一个无序序列建成一个堆?
  2. 如何在输出堆顶元素之后,调整剩余元素成为一个新的堆?

假设给定一个组无序数列{100,5,3,11,6,8,7},带着问题,我们对其进行堆排序操作进行分步操作说明。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0ntXaRNP-1644918299722)(img/heap_sort_step00.png)]

3.1 创建最大堆

①首先我们将数组我们将数组从上至下按顺序排列,转换成二叉树:一个无序堆。每一个三角关系都是一个堆,上面是父节点,下面两个分叉是子节点,两个子节点俗称左孩子、右孩子;

②转换成无序堆之后,我们要努力让这个无序堆变成最大堆(或是最小堆),即每个堆里都实现父节点的值都大于任何一个子节点的值。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oQMsUdyI-1644918299722)(img/heap_sort_step02.png)]

③从最后一个堆开始,即左下角那个没有右孩子的那个堆开始;首先对比左右孩子,由于这个堆没有右孩子,所以只能用左孩子,左孩子的值比父节点的值小所以不需要交换。如果发生交换,要检测子节点是否为其他堆的父节点,如果是,递归进行同样的操作。

④第二次对比红色三角形内的堆,取较大的子节点,右孩子8胜出,和父节点比较,右孩子8大于父节点3,升级做父节点,与3交换位置,3的位置没有子节点,这个堆建成最大堆。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xsVtsggw-1644918299723)(img/heap_sort_step03.png)]

⑤对黄色三角形内堆进行排序,过程和上面一样,最终是右孩子33升为父节点,被交换的右孩子下面也没有子节点,所以直接结束对比。

⑥最顶部绿色的堆,堆顶100比左右孩子都大,所以不用交换,至此最大堆创建完成。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-A0LLSPT9-1644918299723)(img/heap_sort_step04.png)]

3.2 堆排序(最大堆调整)

①首先将堆顶元素100交换至最底部7的位置,7升至堆顶,100所在的底部位置即为有序区,有序区不参与之后的任何对比。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zn8jzdj3-1644918299723)(img/heap_sort_step11.png)]

②在7升至顶部之后,对顶部重新做最大堆调整,左孩子33代替7的位置。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-clGCW1uC-1644918299724)(img/heap_sort_step12.png)]

③在7被交换下来后,下面还有子节点,所以需要继续与子节点对比,左孩子11比7大,所以11与7交换位置,交换位置后7下面为有序区,不参与对比,所以本轮结束,无序区再次形成一个最大堆。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jCRWMMBi-1644918299724)(img/heap_sort_step13.png)]

④将最大堆堆顶33交换至堆末尾,扩大有序区;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RZ771GI8-1644918299724)(img/heap_sort_step14.png)]

⑤不断建立最大堆,并且扩大有序区,最终全部有序。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-o7c25OuK-1644918299724)(img/heap_sort_step15.png)]

4. 复杂度分析
  • 平均时间复杂度:O(nlogn)
  • 最佳时间复杂度:O(nlogn)
  • 最差时间复杂度:O(nlogn)
  • 稳定性:不稳定

堆排序其实也是一种选择排序,是一种树形选择排序。只不过直接选择排序中,为了从R[1…n]中选择最大记录,需比较n-1次,然后从R[1…n-2]中选择最大记录需比较n-2次。事实上这n-2次比较中有很多已经在前面的n-1次比较中已经做过,而树形选择排序恰好利用树形的特点保存了部分前面的比较结果,因此可以减少比较次数。对于n个关键字序列,最坏情况下每个节点需比较log2(n)次,因此其最坏情况下时间复杂度为nlogn。堆排序为不稳定排序,不适合记录较少的排序。

5. 代码实现
import java.util.Arrays;
public class HeapSort {

    private int[] arr;

    public HeapSort(int[] arr){
        this.arr = arr;
    }

    /**
     * 堆排序的主要入口方法,共两步。
     */
    public void sort(){
        /*
         *  第一步:将数组堆化
         *  beginIndex = 第一个非叶子节点。
         *  从第一个非叶子节点开始即可。无需从最后一个叶子节点开始。
         *  叶子节点可以看作已符合堆要求的节点,根节点就是它自己且自己以下值为最大。
         */
        int len = arr.length - 1;
        int beginIndex = (len - 1) >> 1;
        for(int i = beginIndex; i >= 0; i--){
            maxHeapify(i, len);
        }

        /*
         * 第二步:对堆化数据排序
         * 每次都是移出最顶层的根节点A[0],与最尾部节点位置调换,同时遍历长度 - 1。
         * 然后从新整理被换到根节点的末尾元素,使其符合堆的特性。
         * 直至未排序的堆长度为 0。
         */
        for(int i = len; i > 0; i--){
            swap(0, i);
            maxHeapify(0, i - 1);
        }
    }

    private void swap(int i,int j){
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }

    /**
     * 调整索引为 index 处的数据,使其符合堆的特性。
     *
     * @param index 需要堆化处理的数据的索引
     * @param len 未排序的堆(数组)的长度
     */
    private void maxHeapify(int index,int len){
        int li = (index << 1) + 1; // 左子节点索引
        int ri = li + 1;           // 右子节点索引
        int cMax = li;             // 子节点值最大索引,默认左子节点。

        if(li > len) return;       // 左子节点索引超出计算范围,直接返回。
        if(ri <= len && arr[ri] > arr[li]) // 先判断左右子节点,哪个较大。
            cMax = ri;
        if(arr[cMax] > arr[index]){
            swap(cMax, index);      // 如果父节点被子节点调换,
            maxHeapify(cMax, len);  // 则需要继续判断换下后的父节点是否符合堆的特性。
        }
    }

  
    public static void main(String[] args) {
        int[] arr = new int[]{3,5,3,0,8,6,1,5,8,6,2,4,9,4,7,0,1,8,9,7,3,1,2,5,9,7,4,0,2,6};        
        new HeapSort(arr).sort();        
        System.out.println(Arrays.toString(arr));
    }
}
  /**
     * 测试用例
     *
     * 输出:
     * [0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 5, 5, 5, 6, 6, 6, 7, 7, 7, 8, 8, 8, 9, 9, 9]
     */

总结

堆是一种很好做调整的结构,在算法题里面使用频度很高。常用于想知道最大值或最小值的情况,比如优先级队列,作业调度等场景。

堆排序相看似比较复杂(建堆的过程,堆调整的过程,堆排序等等),需要好好推敲揣摩理清思路。堆排序操作过程中其运行时间主要耗费在建初始堆和调整建新堆时进行的反复“筛选”上。

八.计数排序

计数排序(Counting sort)是一种稳定的线性时间排序算法。

1. 基本思想

计数排序使用一个额外的数组C,其中第i个元素是待排序数组A中值等于i的元素的个数。

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

用来计数的数组C的长度取决于待排序数组中数据的范围(等于待排序数组的最大值与最小值的差加上1),然后进行分配、收集处理:

  • 分配。扫描一遍原始数组,以当前值-minValue作为下标,将该下标的计数器增1。
  • 收集。扫描一遍计数器数组,按顺序把值收集起来。
2. 实现逻辑
  • 找出待排序的数组中最大和最小的元素
  • 统计数组中每个值为i的元素出现的次数,存入数组C的第i项
  • 对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加)
  • 反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1

举个例子,假设有无序数列nums=[2, 1, 3, 1, 5], 首先扫描一遍获取最小值和最大值,maxValue=5, minValue=1,于是开一个长度为5的计数器数组counter

(1) 分配
统计每个元素出现的频率,得到counter=[2, 1, 1, 0,
1],例如counter[0]表示值0+minValue=1出现了2次。

(2) 收集
counter[0]=2表示1出现了两次,那就向原始数组写入两个1,counter[1]=1表示2出现了1次,那就向原始数组写入一个2,依次类推,最终原始数组变为[1,1,2,3,5],排序好了。

3.动图演示

在这里插入图片描述

4. 复杂度分析
  • 平均时间复杂度:O(n + k)
  • 最佳时间复杂度:O(n + k)
  • 最差时间复杂度:O(n + k)
  • 空间复杂度:O(n + k)

当输入的元素是n 个0到k之间的整数时,它的运行时间是 O(n + k)。。在实际工作中,当k=O(n)时,我们一般会采用计数排序,这时的运行时间为O(n)。

计数排序需要两个额外的数组用来对元素进行计数和保存排序的输出结果,所以空间复杂度为O(k+n)。

计数排序的一个重要性质是它是稳定的:具有相同值的元素在输出数组中的相对次序与它们在输入数组中的相对次序是相同的。也就是说,对两个相同的数来说,在输入数组中先出现的数,在输出数组中也位于前面。

计数排序的稳定性很重要的一个原因是:计数排序经常会被用于基数排序算法的一个子过程。我们将在后面文章中介绍,为了使基数排序能够正确运行,计数排序必须是稳定的。

5. 代码实现
// 计数排序
public class CountingSort {
    public static void main(String[] argv) {
        int[] A = CountingSort.countingSort(new int[]{16, 4, 10, 14, 7, 9, 3, 2, 8, 1});
        Utils.print(A);
    }
    public static int[] countingSort(int[] A) {
        int[] B = new int[A.length];
        // 假设A中的数据a'有,0<=a' && a' < k并且k=100
        int k = 100;
        countingSort(A, B, k);
        return B;
    }
    private static void countingSort(int[] A, int[] B, int k) {
        int[] C = new int[k];
        // 计数
        for (int j = 0; j < A.length; j++) {
            int a = A[j];
            C[a] += 1;
        }
        Utils.print(C);
        // 求计数和
        for (int i = 1; i < k; i++) {
            C[i] = C[i] + C[i - 1];
        }
        Utils.print(C);
        // 整理
        for (int j = A.length - 1; j >= 0; j--) {
            int a = A[j];
            B[C[a] - 1] = a;
            C[a] -= 1;
        }
    }
}
6. 优化改进

6.1 场景分析
举个极端的例子:如果排序的数组有200W个元素,但是这200W个数的值都在1000000-1000100,也就说有100个数,总共重复了200W次,现在要排序,怎么办?

这种情况排序,计数排序应该是首选。但是这时候n的值为200W,如果按原来的算法,k的值10001000,但是此时c中真正用到的地方只有100个,这样对空间造成了极大的浪费。

6.2 改进思路
针对c数组的大小,优化计数排序

6.3 改进代码

package com.igeek.sort;
public class CountingSort {
        public static void main(String []args){
            //排序的数组
            int a[] = {100, 93, 97, 92, 96, 99, 92, 89, 93, 97, 90, 94, 92, 95};
            int b[] = countSort(a);
            for(int i : b){
                System.out.print(i + "  ");
            }
            System.out.println();
        }
        public static int[] countSort(int []a){
            int b[] = new int[a.length];
            int max = a[0], min = a[0];
            for(int i : a){
                if(i > max){
                    max = i;
                }
                if(i < min){
                    min = i;
                }
            }
            //这里k的大小是要排序的数组中,元素大小的极值差+1
            int k = max - min + 1;
            int c[] = new int[k];
            for(int i = 0; i < a.length; ++i){
                c[a[i]-min] += 1;//优化过的地方,减小了数组c的大小
            }
            for(int i = 1; i < c.length; ++i){
                c[i] = c[i] + c[i-1];
            }
            for(int i = a.length-1; i >= 0; --i){
                b[--c[a[i]-min]] = a[i];//按存取的方式取出c的元素
            }
            return b;
        }
    }

总结

计数算法只能使用在已知序列中的元素在0-k之间,且要求排序的复杂度在线性效率上。
Â
计数排序和基数排序很类似,都是非比较型排序算法。但是,它们的核心思想是不同的,基数排序主要是按照进制位对整数进行依次排序,而计数排序主要侧重于对有限范围内对象的统计。基数排序可以采用计数排序来实现。

九.桶排序(BucketSort)

桶排序(Bucket sort)或所谓的箱排序,是一个排序算法,工作的原理是将数组分到有限数量的桶里。每个桶再个别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序),最后依次把各个桶中的记录列出来记得到有序序列。桶排序是鸽巢排序的一种归纳结果。当要被排序的数组内的数值是均匀分配的时候,桶排序使用线性时间(Θ(n))。但桶排序并不是比较排序,他不受到O(n log n)下限的影响。

1. 基本思想

桶排序的思想近乎彻底的分治思想

桶排序假设待排序的一组数均匀独立的分布在一个范围中,并将这一范围划分成几个子范围(桶)。

然后基于某种映射函数f ,将待排序列的关键字 k 映射到第i个桶中 (即桶数组B 的下标i) ,那么该关键字k 就作为 B[i]中的元素 (每个桶B[i]都是一组大小为N/M 的序列 )。

接着将各个桶中的数据有序的合并起来 : 对每个桶B[i] 中的所有元素进行比较排序 (可以使用快排)。然后依次枚举输出 B[0]….B[M] 中的全部内容即是一个有序序列。

补充: 映射函数一般是 f = array[i] / k; k^2 = n; n是所有元素个数

为了使桶排序更加高效,我们需要做到这两点:

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

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

2. 实现描述
  • 设置一个定量的数组当作空桶子。
  • 寻访序列,并且把项目一个一个放到对应的桶子去。
  • 对每个不是空的桶子进行排序。
  • 从不是空的桶子里把项目再放回原来的序列中。
3.动图演示

在这里插入图片描述

package com.igeek.sort;
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");
    }
}
//结果


总结

桶排序是计数排序的变种,它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。把计数排序中相邻的m个”小桶”放到一个”大桶”中,在分完桶后,对每个桶进行排序(一般用快排),然后合并成最后的结果。

算法思想和散列中的开散列法差不多,当冲突时放入同一个桶中;可应用于数据量分布比较均匀,或比较侧重于区间数量时。

桶排序最关键的建桶,如果桶设计得不好的话桶排序是几乎没有作用的。通常情况下,上下界有两种取法,第一种是取一个10n或者是2n的数,方便实现。另一种是取数列的最大值和最小值然后均分作桶.

十.基数排序

基数排序(Radix sort)是一种非比较型整数排序算法。

1. 基本思想

原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。基数排序的方式可以采用LSD(Least significant digital)或MSD(Most significant digital),LSD的排序方式由键值的最右边开始,而MSD则相反,由键值的最左边开始。

  • MSD:先从高位开始进行排序,在每个关键字上,可采用计数排序
  • LSD:先从低位开始进行排序,在每个关键字上,可采用桶排序
2. 实现逻辑
  • 将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。
  • 从最低位开始,依次进行一次排序。
  • 这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。
3. 动图演示

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0dtmmIIz-1644918299725)(img/radix_sort.gif)]

分步图示说明:设有数组 array = {53, 3, 542, 748, 14, 214, 154, 63, 616},对其进行基数排序:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Z485bfdF-1644918299726)(img/radix_sort_01.jpg)]

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

  • 按照个位数进行排序。
  • 按照十位数进行排序。
  • 按照百位数进行排序。

排序后,数列就变成了一个有序序列。

4. 复杂度分析
  • 时间复杂度:O(k*N)
  • 空间复杂度:O(k + N)
  • 稳定性:稳定

设待排序的数组R[1…n],数组中最大的数是d位数,基数为r(如基数为10,即10进制,最大有10种可能,即最多需要10个桶来映射数组元素)。

处理一位数,需要将数组元素映射到r个桶中,映射完成后还需要收集,相当于遍历数组一遍,最多元素数为n,则时间复杂度为O(n+r)。所以,总的时间复杂度为O(d*(n+r))。

基数排序过程中,用到一个计数器数组,长度为r,还用到一个rn的二位数组来做为桶,所以空间复杂度为O(rn)。

基数排序基于分别排序,分别收集,所以是稳定的。

package com.igeek.sort;

public class RadixSort {
    public static void main(String[] args) {
        int[] nums = new int[]{53, 3, 542, 748, 14, 214, 154, 63, 616};
        radixSort(nums);
        for (int num : nums) {
            System.out.print(num + "\t");
        }
    }
    // 注意只能处理正整数,小数和负数需要一些额外处理
    public static void radixSort(int[] nums) {
        // 用来中转数据的桶
        int[][] bucket = new int[10][nums.length];
        // 用来存储每个桶中有多少个数据
        int[] bucketElementCount = new int[10];
        // 获得待排序数组中绝对值最大的数的长度,决定了要进行多少趟比较
        int maxLength = getMaxLength(nums);
        for (int i = 0; i < maxLength; i++) {
            // 标识桶的位置
            int bucketIndex;
            for (int j = 0; j < nums.length; j++) {
                // 计算当前数据应该放在哪个桶
                bucketIndex = (nums[j] / (int) Math.pow(10, i)) % 10;
                // 将当前数据放入该桶
                bucket[bucketIndex][bucketElementCount[bucketIndex]] = nums[j];
                bucketElementCount[bucketIndex]++;
            }
            // 当所有数据放入桶中后,又把这些数据从桶中按照顺序还原到要排序的数组中
            int index = 0;
            for (int j = 0; j < 10; j++) {
                for (int k = 0; k < bucketElementCount[j]; k++) {
                    nums[index++] = bucket[j][k];
                }
            }
            // 完成上一步后将bucket和bucketElementCount中的数据清空,避免干扰下一轮的操作
            for (int j = 0; j < 10; j++) {
                for (int k = 0; k < bucketElementCount[j]; k++) {
                    bucket[j][k] = 0;
                }
                bucketElementCount[j] = 0;
            }
        }
    }

    /**
     * 获取nums数组中数据的最大位数
     *
     * @param nums
     */
    public static int getMaxLength(int[] nums) {
        int max = Math.abs(nums[0]);
        for (int i = 1; i < nums.length; i++) {
            if (Math.abs(nums[i]) > max) {
                max = Math.abs(nums[i]);
            }
        }
        return (max + "").length();
    }
}
//
3	14	53	63	154	214	542	616	748	


总结

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

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

% 10;
// 将当前数据放入该桶
bucket[bucketIndex][bucketElementCount[bucketIndex]] = nums[j];
bucketElementCount[bucketIndex]++;
}
// 当所有数据放入桶中后,又把这些数据从桶中按照顺序还原到要排序的数组中
int index = 0;
for (int j = 0; j < 10; j++) {
for (int k = 0; k < bucketElementCount[j]; k++) {
nums[index++] = bucket[j][k];
}
}
// 完成上一步后将bucket和bucketElementCount中的数据清空,避免干扰下一轮的操作
for (int j = 0; j < 10; j++) {
for (int k = 0; k < bucketElementCount[j]; k++) {
bucket[j][k] = 0;
}
bucketElementCount[j] = 0;
}
}
}

/**
 * 获取nums数组中数据的最大位数
 *
 * @param nums
 */
public static int getMaxLength(int[] nums) {
    int max = Math.abs(nums[0]);
    for (int i = 1; i < nums.length; i++) {
        if (Math.abs(nums[i]) > max) {
            max = Math.abs(nums[i]);
        }
    }
    return (max + "").length();
}

}
//
3 14 53 63 154 214 542 616 748


#### 总结

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

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

基数排序不是直接根据元素整体的大小进行元素比较,而是将原始列表元素分成多个部分,对每一部分按一定的规则进行排序,进而形成最终的有序列表。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值