【数据结构与算法】2.八大经典排序

news2024/12/24 3:17:16

文章目录

  • 简介
  • 1.分析排序算法
  • 2.插入排序
    • 2.1.直接插入排序
    • 2.2.希尔排序
  • 3.选择排序
    • 3.1.直接选择排序
    • 3.2.堆排序
      • 3.2.1.堆的数据结构
      • 3.2.2.算法实现
  • 4.交换排序
    • 4.1.冒泡排序
    • 4.2.快速排序
  • 5.归并排序
  • 6.基数排序
  • 7.八大排序算法总结

简介

      排序对于任何一个程序员来说,可能都不会陌生。我学的第一个算法就是冒泡排序。大部分编程语言中,也都提供了排序函数,如Java语言在Jdk1.8版本里Arrays.sort里使用排序算法根据不同数据量使用不同的算法,在数组元素小于47的时候用插入排序,大于47小于286用双轴快排,大于286用timsort归并排序;
比较经典和常用的就是以下八大排序。
在这里插入图片描述

推荐一个数据结构可视化的页面: https://visualgo.net/en

1.分析排序算法

排序算法的执行效率
      对于排序算法执行效率的分析,我们一般会从这几个方面来衡量:

  • 1.最好情况、最坏情况、平均情况时间复杂度:
    分析排序算法的时间复杂度时,要分别给出最好情况、最坏情况、平均情况下的时间复杂度。
  • 2.时间复杂度的系数、常数 、低阶
    时间复杂度反应的是数据规模n很大的时候的一个增长趋势,所以它表示的时候会忽略系数、常数、低阶。但是实际的软件开发中,我们排序的可能是10个、100个这种规模很小的数据,所以,在对同一阶时间复杂度的排序算法性能对比的时候,我们就要把系数、常数、低阶也考虑进来。
  • 3.比较次数和交换次数
    基于比较的排序算法的执行过程,会涉及两种操作,一种是元素比较大小,另一种是元素交换。所以,如果我们在分析排序算法的执行效率(时间复杂度)的时候,应该把比较次数和交换(或移动)次数也考虑进去。

排序算法的内存消耗
      算法的内存消耗可以通过空间复杂度来衡量,排序算法也不例外。不过,针对排序算法的空间复杂度,有一个新的概念原地排序(Sorted in place)。原地排序算法,就是特指空间复杂度是O(1)的排序算法,原地排序就是指不申请多余的空间来进行的排序,就是在原来的排序数据中比较和交换的排序,像归并排序和
属于原地排序算法:希尔排序、冒泡排序、直接插入排序、直接选择排序、堆排序、快速排序

排序算法的稳定性
      仅仅用执行效率和内存消耗来衡量排序算法的好坏是不够的。针对排序算法,我们还有一个重要的度量指标,稳定性。这个概念是说,如果待排序的序列中存在值相等的元素,经过排序之后,相等元素之间原有的先后顺序不变。
      例如我们有一组数据1,5,6,7,8,6,按照大小排序之后就是1,5,6,6,7,8。
这组数据里有两个6。经过某种排序算法排序之后,如果两个6的前后顺序没有改变,那我们就把这种排序算法叫作稳定的排序算法;如果前后顺序发生变化,那对应的排序算法就叫作不稳定的排序算法。

  • 稳定排序算法: 冒泡排序、直接插入排序、归并排序、基数排序
  • 不稳定排序算法: 希尔排序、堆排序、直接选择排序,快速排序

2.插入排序

2.1.直接插入排序

基本思想
      直接插入排序(Insertion Sort) 对于少量元素的排序,它是一种最简单的排序方法,它的基本思想是将一个记录插入到已经排好序的有序表中,从而一个新的、记录数增1的有序表。在其实现过程使用双层循环,外层循环对除了第一个元素之外的所有元素,内层循环对当前元素前面有序表进行待插入位置查找,并进行移动。
      例如我们在打牌玩斗地主的时候摸完牌然后整理顺序是一张一张的来,将每一张牌插入到其他已经有序的牌中的适当位置。在计算机的实现中,为了要给插入的元素腾出空间,我们需要将其余所有元素在插入之前都向右移动一位。

算法描述
一般来说,插入排序都采用in-place在数组上实现。具体算法描述如下:

  • 1.从第一个元素开始,该元素可以认为已经被排序
  • 2.取出下一个元素,在已经排序的元素序列中从后向前扫描
  • 3.如果该元素(已排序)大于新元素,将该元素移到下一位置
  • 4.重复步骤3,直到找到已排序的元素小于或者等于新元素的位置
  • 5.将新元素插入到该位置后
  • 6.重复步骤2~5

在这里插入图片描述

平均时间复杂度最好情况最坏情况空间复杂度
O(n²)O(n²)O(n²)O(1)

基于Java语言实现代码如下

    public static void sort(int nums[]) {
        for (int i = 1; i < nums.length; i++) {
            //若第 i 个元素大于 i-1 元素则直接插入;反之,需要找到适当的插入位置后在插入。
            if (nums[i] < nums[i - 1]) {
                int j = i - 1;
                int x = nums[i];
                //采用顺序查找方式找到插入的位置,在查找的同时,将数组中的元素进行后移操作,给插入元素腾出空间
                while (j > -1 && x < nums[j]) {
                    nums[j + 1] = nums[j];
                    j--;
                }
                //插入到正确位置
                nums[j + 1] = x;
            }
        }
    }
    //测试
    public static void main(String[] args) {
        int nums[] = new int[]{3, 44, 38, 5, 47, 15, 36, 26, 27,2,46,4,19,50,48};
        sort(nums);
        System.out.println(Arrays.toString(nums));
    }

在这里插入图片描述

总结: 插入排序所需的时间取决于输入元素的初始顺序。例如对一个很大且其中的元素已经有序(或接近有序)的数组进行排序将会比随机顺序的数组或是逆序数组进行排序要快得多。

2.2.希尔排序

基本思想
      希尔排序(SheII Sort),也称 递减增量排序算法,是插入排序的一种更高效的改进版本。希尔排序是 非稳定排序算法。

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

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

      希尔排序是先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序。
      将待排序数组按照步长gap进行分组,然后将每组的元素利用直接插入排序的方法进行排序;每次再将gap折半减小,循环上述操作;当gap=1时,利用直接插入,完成排序。

      可以看到步长的选择是希尔排序的重要部分。只要最终步长为1任何步长序列都可以工作。一般来说最简单的步长取值是初次取数组长度的一半为增量,之后每次再减半,直到增量为1。更好的步长序列取值可以参考维基百科。

算法描述

  • 1.选择一个增量序列 q1,q2,……,qk,其中 qi > qj, qk = 1;
  • 2.按增量序列个数 k,对序列进行 k 趟排序;
  • 3.每次排序,根据对应的增量 qi,将待排序列分割成若干长度为 m 的子序列,分别对各子表进行直接插入排序。仅增量因子为 1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。

在这里插入图片描述

平均时间复杂度最好情况最坏情况空间复杂度
O(nlog2 n)O(nlog2 n)O(nlog2 n)O(1)

基于Java语言实现代码如下

    public static void sort(int nums[]) {
        int gap = nums.length;
        while (true) {
            //增量每次减半
            gap /= 2;   
            for (int i = 0; i < gap; i++) {
                //下面循环是一个插入排序
                for (int j = i + gap; j < nums.length; j += gap) {
                    int k = j - gap;
                    while (k >= 0 && nums[k] > nums[k + gap]) {
                        int temp = nums[k];
                        nums[k] = nums[k + gap];
                        nums[k + gap] = temp;
                        k -= gap;
                    }
                }
            }
            if (gap == 1) {
                break;
            }
        }
    }
    public static void main(String[] args) {
        int nums[] = {86, 11, 54, 34, 53,12,45,81,19,65};
        sort(nums);
        System.out.println(Arrays.toString(nums));
    }

总结: 希尔排序更高效的原因是它权衡了子数组的规模和有序性。排序之初,各个子数组都很短,排序之后子数组都是部分有序的,这两种情况都很适合插入排序。
在这里插入图片描述

3.选择排序

3.1.直接选择排序

基本思想
      选择排序(Selection sort)是一种简单直观的排序算法。它的工作原理如下。首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

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

算法描述

  • 1.从未排序序列中,找到关键字最小的元素
  • 2.如果最小元素不是未排序序列的第一个元素,将其和未排序序列第一个元素互换
  • 3.重复1、2步,直到排序结束。
    在这里插入图片描述
平均时间复杂度最好情况最坏情况空间复杂度
O(n²)O(n²)O(n²)O(1)

基于Java语言实现代码如下

    public static void sort(int[] nums) {
        for (int i = 0; i < nums.length; i++) {
            int min = i;
            //选出之后待排序中值最小的位置
            for (int j = i + 1; j < nums.length; j++) {
                if (nums[j] < nums[min]) {
                    min = j;
                }
            }
            //最小值不等于当前值时进行交换
            if (min != i) {
                int temp = nums[i];
                nums[i] = nums[min];
                nums[min] = temp;
            }
        }
    }
    
    public static void main(String[] args) {
        int nums[] = new int[]{3, 44, 38, 5, 47, 15, 36, 26, 27,2,46,4,19,50,48};
        sort(nums);
        System.out.println(Arrays.toString(nums));
    }

在这里插入图片描述
总结:选择排序的简单和直观名副其实,这也造就了它”出了名的慢性子”,无论是哪种情况,哪怕原数组已排序完成,它也将花费将近n²/2次遍历来确认一遍。即便是这样,它的排序结果也还是不稳定的。 唯一值得高兴的是,它并不耗费额外的内存空间。

3.2.堆排序

3.2.1.堆的数据结构

      堆(HeapHeapHeap)一种特殊的树,堆这种数据结构的应用场景非常多,最经典的莫过于堆排序了。堆排序是一种原地的排序算法。
符合以下两点的树就是堆(二叉堆):

  • 1.堆是一个完全二叉树;
  • 2.堆中每一个节点的值都必须大于等于(或小于等于)其子树中每个节点的值。

第一点: 堆必须是一个完全二叉树,完全二叉树要求除了最后一层,其他层的节点个数都是满的,最后一层的节点都靠左排列。
第二点: 堆中的每个节点的值必须大于等于(或者小于等于)其子树中每个节点的值。实际上,我们还可以换一种说法,堆中每个节点的值都大于等于(或者小
于等于)其左右子节点的值。这两种表述是等价的。

      下图中其中第1个和第2是大顶堆,第3个是小顶堆,第4个不是堆,一般在应用中大顶堆用的比较多,可用参考这个数据结构可视化网站二叉堆: https://visualgo.net/zh/heap
在这里插入图片描述
      完全二叉树比较适合用数组来存储。用数组来存储完全二叉树是非常节省存储空间的。因为我们不需要存储左右子节点的指针,单纯地通过数组的下标,就可以找到一个节点的左右子节点和父节点。
以下用数组存储堆的例子
在这里插入图片描述

3.2.2.算法实现

基本思想
      此处以大顶堆为例,堆排序的过程就是将待排序的序列构造成一个堆,选出堆中最大的移走,再把剩余的元素调整成堆,找出最大的再移走,重复直至有序。

算法描述

  • 1.先将初始序列K[1…n]建成一个大顶堆, 那么此时第一个元素K1最大, 此堆为初始的无序区.
  • 2.再将关键字最大的记录K1 (即堆顶, 第一个元素)和无序区的最后一个记录 Kn 交换, 由此得到新的无序区K[1…n−1]和有序区K[n], 且满足K[1…n−1].keys⩽K[n].key
  • 3.交换K1 和 Kn 后, 堆顶可能违反堆性质, 因此需将K[1…n−1]调整为堆. 然后重复步骤2, 直到无序区只有一个元素时停止。

在这里插入图片描述

平均时间复杂度最好情况最坏情况空间复杂度
O(nlog2n)O(nlog2n)O(nlog2n)O(1)

代码实现
      从算法描述来看,堆排序需要两个过程,一是建立堆,二是堆顶与堆的最后一个元素交换位置。所以堆排序有两个函数组成。一是建堆函数,二是反复调用建堆函数以选择出剩余未排元素中最大的数来实现排序的函数。

创建堆和排序:

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

对于堆节点的访问:

  • 父节点i的左子节点在位置:(2*i+1);
  • 父节点i的右子节点在位置:(2*i+2);
  • 子节点i的父节点在位置:floor((i-1)/2);
    public static void sort(int[] nums) {
        for (int i = nums.length - 1; i > 0; i--) {
            max_heapify(nums, i);
            //堆顶元素(第一个元素)与Kn交换
            int temp = nums[0];
            nums[0] = nums[i];
            nums[i] = temp;
        }
    }

    /***
     *
     *  将数组堆化
     *  i = 第一个非叶子节点。
     *  从第一个非叶子节点开始即可。无需从最后一个叶子节点开始。
     *  叶子节点可以看作已符合堆要求的节点,根节点就是它自己且自己以下值为最大。
     */
    public static void max_heapify(int[] nums, int n) {
        int child;
        for (int i = (n - 1) / 2; i >= 0; i--) {
            //左子节点位置
            child = 2 * i + 1;
            //右子节点存在且大于左子节点,child变成右子节点
            if (child != n && nums[child] < nums[child + 1]) {
                child++;
            }
            //交换父节点与左右子节点中的最大值
            if (nums[i] < nums[child]) {
                int temp = nums[i];
                nums[i] = nums[child];
                nums[child] = temp;
            }
        }
    }

    public static void main(String[] args) {
        int[] nums = new int[]{91, 60, 96, 13, 35, 65, 46, 65, 10, 30, 20, 31, 77, 81, 22};
        sort(nums);
        System.out.println(Arrays.toString(nums));
    }

在这里插入图片描述
总结:由于堆排序中初始化堆的过程比较次数较多, 因此它不太适用于小序列。 同时由于多次任意下标相互交换位置, 相同元素之间原本相对的顺序被破坏了, 因此, 它是不稳定的排序。

4.交换排序

4.1.冒泡排序

基本思想
      冒泡排序(Bubble Sort)是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
算法描述
      比较相邻的元素。如果第一个比第二个大,就交换他们两个。对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。针对所有的元素重复以上的步骤,除了最后一个。持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

在这里插入图片描述

平均时间复杂度最好情况最坏情况空间复杂度
O(n²)O(n)O(n²)O(1)

基于Java语言实现代码如下

    public static void sort(int nums[]) {
        int temp;
        for (int i = 0; i < nums.length; i++) {
            for (int j = i + 1; j < nums.length; j++) {
                if (nums[i] > nums[j]) {
                    temp = nums[i];
                    nums[i] = nums[j];
                    nums[j] = temp;
                }
            }
        }
    }

    public static void main(String[] args) {
        int[] nums = new int[]{3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48};
        sort(nums);
        System.out.println(Arrays.toString(nums));
    }

总结:由于冒泡排序只在相邻元素大小不符合要求时才调换他们的位置, 它并不改变相同元素之间的相对顺序, 因此它是稳定的排序算法。

4.2.快速排序

基本思想
      快速排序(Quick Sort)的基本思想:挖坑填数+分治法。快速排序使用分治法(Divide and conquer)策略来把一个串行(list)分为两个子串行(sub-lists)。快速排序又是一种分而治之思想在排序算法上的典型应用。本质上来看,快速排序应该算是在冒泡排序基础上的递归分治法。
      快速排序的名字起的是简单粗暴,因为一听到这个名字你就知道它存在的意义,就是快,而且效率高!它是处理大数据最快的排序算法之一了。虽然 Worst Case 的时间复杂度达到了 O(n²),但是人家就是优秀,在大多数情况下都比平均时间复杂度为 O(n logn) 的排序算法表现要更好。

算法描述
快速排序使用分治策略来把一个序列(list)分为两个子序列(sub-lists)。步骤为:

  • 1.从数列中挑出一个元素,称为"基准"(pivot)。
  • 2.重新排序数列,所有比基准值小的元素摆放在基准前面,所有比基准值大的元素摆在基准后面(相同的数可以到任一边)。在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
  • 3.递归地(recursively)把小于基准值元素的子数列和大于基准值元素的子数列排序。

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

在这里插入图片描述

平均时间复杂度最好情况最坏情况空间复杂度
O(nlog₂n)O(nlog₂n)O(n²)O(1)(原地分区递归版)

基于Java语言实现代码如下

    /**
     * @param nums 
     * @param low  起始
     * @param high 结束
     */
    public static void sort(int[] nums, int low, int high) {
        //已经排完
        if (low >= high) {
            return;
        }
        int left = low;
        int right = high;

        //保存基准值
        int pivot = nums[left];
        while (left < right) {
            //从后向前找到比基准小的元素
            while (left < right && nums[right] >= pivot) {
                right--;
            }
            nums[left] = nums[right];
            //从前往后找到比基准大的元素
            while (left < right && nums[left] <= pivot) {
                left++;
            }
            nums[right] = nums[left];
        }
        // 放置基准值,准备分治递归快排
        nums[left] = pivot;
        sort(nums, low, left - 1);
        sort(nums, left + 1, high);
    }
    
    public static void main(String[] args) {
        int[] nums = new int[]{2, 5, 4, 3, 7, 1, 6, 2, 10};
        sort(nums,0,nums.length-1);
        System.out.println(Arrays.toString(nums));
    }

在这里插入图片描述
总结:快速排序和归并排序是两种稍微复杂的排序算法,它们用的都是分治的思想,代码都通过递归来实现,过程非常相似。归并排序算法是一种在任何情况下时间复杂度都比较稳定的排序算法,这也使它存在致命的缺点,即归并排序不是原地排序算法,空间复杂度比较高,是O(n)。正因为此,它也没有快排应用广泛。

5.归并排序

基本思想
      归并排序(Merge Sort)是建立在归并操作上的一种有效的排序算法,1945年由约翰·冯·诺伊曼首次提出。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用,且各层分治递归可以同时进行。
      归并排序算法是将两个(或两个以上)有序表合并成一个新的有序表,即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列

算法描述
归并排序可通过两种方式实现:

  • 自上而下的递归 (本文讲递归法)
  • 自下而上的迭代(请自行补充)

递归法(假设序列共有n个元素):

  • 1.将序列每相邻两个数字进行归并操作,形成 floor(n/2)个序列,排序后每个序列包含两个元素;
  • 2.将上述序列再次归并,形成 floor(n/4)个序列,每个序列包含四个元素;
  • 3.重复步骤2,直到所有元素排序完毕。

在这里插入图片描述

平均时间复杂度最好情况最坏情况空间复杂度
O(nlog₂n)O(nlog₂n)O(nlog₂n)O(n)

基于Java语言实现代码如下

    public static void sort(int [] nums,int lo,int hi){
         //判断是否是最后一个元素
        if(lo>=hi){
            return;
        }
        //重中间将数组分为两个部分
        int mid=lo+(hi-lo)/2;

        //分别递归将左右两半排好序
        sort(nums,lo,mid);
        sort(nums,mid+1,hi);

        //将排好序的左右两半合并
        merge(nums,lo,mid,hi);
    }

    /**
     *
     * @param nums
     * @param low 右偏移的数
     * @param mid 中位数      
     * @param high 查找的范围
     */
    private static void merge(int[] nums,int low,int mid,int high){
        //复制原来的数组
        int [] copy=nums.clone();
        //定义一个k指定表示重什么位置开始修改原来的数组,i指针左半边的位置,j表示右半边的位置
        int k=low,i=low,j=mid+1;
        while (k<=high){
            if(i>mid){
                // 左半边的数据处理完成,将右半边的数copy就行
                nums[k++]=copy[j++];
            }else if(j>high){
                // 右半边的数据处理完成,将左半边的数copy就行
                nums[k++]=copy[i++];
            }else if(copy[j]<copy[i]){
                //右边的数小于左边的数,将右边的数拷贝到合适的位置,j指针往前移动一位
                nums[k++]=copy[j++];
            }else{
                //左边的数小于右边的数,将左边的数拷贝到合适的位置,i指针往前移动一位
                nums[k++]=copy[i++];
            }
        }
    }

    public static void main(String[] args) {
        int[] nums=new int[]{1,3,4,5,11,6,7,5,28};
        sort(nums,0,nums.length-1);
        System.out.println(Arrays.toString(nums));
    }

在这里插入图片描述

总结: 从效率上看,归并排序可算是排序算法中的”佼佼者”. 假设数组长度为n,那么拆分数组共需logn,, 又每步都是一个普通的合并子数组的过程, 时间复杂度为O(n), 故其综合时间复杂度为O(nlogn)。另一方面, 归并排序多次递归过程中拆分的子数组需要保存在内存空间, 其空间复杂度为O(n)。

6.基数排序

基本思想
      基数排序(Radix sort)是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。
      基数排序将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列。

基数排序按照优先从高位或低位来排序有两种实现方案:

  • MSD(Most significant digital) 从最左侧高位开始进行排序。先按k1排序分组, 同一组中记录, 关键码k1相等, 再对各组按k2排序分成子组, 之后, 对后面的关键码继续这样的排序分组, 直到按最次位关键码kd对各子组排序后. 再将各组连接起来, 便得到一个有序序列。MSD方式适用于位数多的序列。

  • LSD (Least significant digital)从最右侧低位开始进行排序。先从kd开始排序,再对kd-1进行排序,依次重复,直到对k1排序后便得到一个有序序列。LSD方式适用于位数少的序列。

算法描述
我们以LSD为例,从最低位开始,具体算法描述如下:

  • 1.取得数组中的最大数,并取得位数;

  • 2.arr为原始数组,从最低位开始取每个位组成radix数组;

  • 3.对radix进行计数排序(利用计数排序适用于小范围数的特点);

在这里插入图片描述

平均时间复杂度最好情况最坏情况空间复杂度
O(d*(n+r))O(d*(n+r))O(d*(n+r))O(n+r)

基于Java语言实现代码如下

    public static void sort(int[] nums) {
        if (nums.length <= 1) return;

        //取得数组中的最大数,并取得位数
        int max = 0;
        for (int i = 0; i < nums.length; i++) {
            if (max < nums[i]) {
                max = nums[i];
            }
        }

        int maxDigit = 1;
        while (max / 10 > 0) {
            maxDigit++;
            max = max / 10;
        }
        //申请一个桶空间
        int[][] buckets = new int[10][nums.length];
        int base = 10;

        //从低位到高位,对每一位遍历,将所有元素分配到桶中
        for (int i = 0; i < maxDigit; i++) {
            //存储各个桶中存储元素的数量
            int[] bktLen = new int[10];

            //分配:将所有元素分配到桶中
            for (int j = 0; j < nums.length; j++) {
                int whichBucket = (nums[j] % base) / (base / 10);
                buckets[whichBucket][bktLen[whichBucket]] = nums[j];
                bktLen[whichBucket]++;
            }

            //收集:将不同桶里数据挨个捞出来,为下一轮高位排序做准备,由于靠近桶底的元素排名靠前,因此从桶底先捞
            int k = 0;
            for (int b = 0; b < buckets.length; b++) {
                for (int p = 0; p < bktLen[b]; p++) {
                    nums[k++] = buckets[b][p];
                }
            }
            base *= 10;
        }
    }

    public static void main(String[] args) {
		int[] nums = new int[]{3, 44, 38, 5, 47, 15, 36, 26, 27,2,46,4,19,50,48};
        sort(nums);
        System.out.println(Arrays.toString(nums));
    }

在这里插入图片描述

7.八大排序算法总结

各种排序性能对比如下:

排序类型平均情况最好情况最坏情况辅助空间稳定性
冒泡排序O(n²)O(n)O(n²)O(1)稳定
选择排序O(n²)O(n²)O(n²)O(1)不稳定
插入排序O(n²)O(n)O(n²)O(1)稳定
希尔排序O(n^1.3)O(nlogn)O(n²)O(1)不稳定
归并排序O(nlog₂n)O(nlog₂n)O(nlog₂n)O(n)稳定
快速排序O(nlog₂n)O(nlog₂n)O(n²)O(nlog₂n)不稳定
堆排序O(nlog₂n)O(nlog₂n)O(nlog₂n)O(1)不稳定
基数排序O(d(n+k))O(d(n+k))O(d(n+kd))O(n+kd)稳定

从时间复杂度来说:

  • 平方阶O(n²)排序:各类简单排序:直接插入、直接选择和冒泡排序
  • 线性对数阶O(nlog₂n)排序:快速排序、堆排序和归并排序
  • O(n1+§))排序,§是介于0和1之间的常数:希尔排序
  • 线性阶O(n)排序:基数排序,此外还有桶排序、计数排序

论是否有序的影响:

  • 当原表有序或基本有序时,直接插入排序和冒泡排序将大大减少比较次数和移动记录的次数,时间复杂度可降至O(n);
  • 而快速排序则相反,当原表基本有序时,将蜕化为冒泡排序,时间复杂度提高为O(n2);
  • 原表是否有序,对简单选择排序、堆排序、归并排序和基数排序的时间复杂度影响不大。

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/358952.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

从技术上来看,互联网技术开始衍生和蜕变出更多的新技术

很多人在看待产业互联网的问题上&#xff0c;一味地割裂它与互联网之间的联系&#xff0c;甚至还有人将产业互联网看成是对于传统互联网的颠覆。如果仅仅只是以这样的眼光来看待产业互联网&#xff0c;那么&#xff0c;他们势必是无法完整把握产业互联网的本质内涵和原始奥义的…

2023什么是分销商城?怎么搭建分销商城

当实体机构都接连探索线上营销模式的时候&#xff0c;分销也随着社交电商的兴起应运而生。 大家好&#xff0c;我是你们熟悉而又陌生的好朋友梦龙&#xff0c;一个创业期的年轻人 它借助裂变效率高的属性&#xff0c;建立更多用户触点&#xff0c;更好的提升企业运营的势能&am…

ros-sensor_msgs/PointCloud2消息内容解释

1.字段解释 header-----头文件&#xff0c;包含消息的序列号&#xff0c;时间戳(系统时间)和坐标系id&#xff0c;其中secs为秒&#xff0c;nsecs为去除秒数后剩余的纳秒数 height-----点云的高度&#xff0c;如果是无序点云&#xff0c;则为1&#xff0c;例子中的点云为有序点…

8 狗监控的封装

概述 为了保证嵌入式程序能够长时间稳定地运行,需要加入狗监控机制。狗监控的原理为:应用程序需要每隔一段时间来喂狗或保活,如果应用程序崩溃或者内核崩溃,导致长时间无法喂狗,则狗将超时,会自动重启系统。部分IPC芯片提供了硬件狗,对于没有硬件狗的,需要自行实现软件…

在代码质量和工作效率的矛盾间如何取舍?

这个问题的答案是&#xff0c;在很短的一段时期&#xff0c;编写高质量代码似乎会拖慢我们的进度。与按照头脑中首先闪现的念头编写代码相比&#xff0c;高质量的代码需要更多的思考和努力。但如果我们编写的不仅仅是运行一次就抛之脑后的小程序&#xff0c;而是更有实质性的软…

移动web(二)

her~~llo&#xff0c;我是你们的好朋友Lyle&#xff0c;是名梦想成为计算机大佬的男人&#xff01; 博客是为了记录自我的学习历程&#xff0c;加强记忆方便复习&#xff0c;如有不足之处还望多多包涵&#xff01;非常欢迎大家的批评指正。 目录 一、移动端特点 1.1 移动端和…

mysql调优参数

my.conf [client] port 端口 socket sokcet位置 [mysqld] basedir mysql位置 port 3306 socket sokcet位置 datadir data目录 pid_file mysqld.pid位置 bind_address 0.0.0.0 lower_case…

【Spark分布式内存计算框架——Spark SQL】14. 分布式SQL引擎

第八章 分布式SQL引擎 回顾一下&#xff0c;如何使用Hive进行数据分析的&#xff0c;提供哪些方式交互分析&#xff1f;&#xff1f;&#xff1f; 方式一&#xff1a;交互式命令行&#xff08;CLI&#xff09; bin/hive&#xff0c;编写SQL语句及DDL语句 方式二&#xff1a…

JdbcTemplate常用方法解析

文章目录1.JdbcTemplate简介2.JdbcTemplate主要方法&#xff1a;3.常用方法介绍update()方法增删改query()查询方法1.JdbcTemplate简介 JdbcTemplate是Spring JDBC的核心类&#xff0c;借助该类提供的方法可以很方便的实现数据的增删改查。 Spring对数据库的操作在jdbc上面做…

小波神经网络(WNN)的实现(Python,附源码及数据集)

文章目录一、理论基础1、小波神经网络结构2、前向传播过程3、反向传播过程4、建模步骤二、小波神经网络的实现1、训练过程&#xff08;WNN.py&#xff09;2、测试过程&#xff08;test.py&#xff09;3、测试结果4、参考源码及实验数据集一、理论基础 小波神经网络&#xff08…

Python实现性能自动化测试,还可以如此简单

Python实现性能自动化测试&#xff0c;还可以如此简单 目录&#xff1a;导读 一、思考❓❔ 二、基础操作&#x1f528;&#x1f528; 三、综合案例演练&#x1f528;&#x1f528; 四、总结&#x1f4a1;&#x1f4a1; 写在最后 一、思考❓❔ 1.什么是性能自动化测试? 性…

宁盾上榜第五版《CCSIP 2022 中国网络安全行业全景册》

2月1日&#xff0c;国内网络安全行业媒体Freebuf咨询正式发布《CCSIP&#xff08;China Cyber Security Panorama&#xff09;2022 中国网络安全行业全景册》第五版。宁盾作为国产身份安全厂商入驻身份识别和访问管理&#xff08;SSO、OTP、IDaaS&#xff09;及边界访问控制&am…

Unity毛发系统TressFX Exporter

Unity 数字人交流群&#xff1a;296041238 一&#xff1a;在Maya下的TressFX Exporter 插件安装步骤&#xff1a; 1. 下载Maya的TressFX Exporter插件 下载地址&#xff1a;TressFX Exporter 链接&#xff1a;https://github.com/Unity-China/cn.unity.hairfx.core/tree/m…

货仓选址 AcWing(JAVA)

在一条数轴上有 N家商店&#xff0c;它们的坐标分别为 A1∼AN。 现在需要在数轴上建立一家货仓&#xff0c;每天清晨&#xff0c;从货仓到每家商店都要运送商品。 为了提高效率&#xff0c;求把货仓建在何处&#xff0c;可以使得货仓到每家商店的距离之和最小。 输入格式&#…

Spring Cloud Alibaba--ActiveMQ微服务详解之消息队列(四)

上篇讲述高并发情况下的数据库处理方式&#xff1a;分布式事务管理机制。即使我们做到这一步并发情况只能稍微得到缓解&#xff0c;当然千万级别的问题不大&#xff0c;但在面对双十一淘宝这类的达上亿的并发的时候仅仅靠分布式事务管理还是远远不够&#xff0c;即使数据库可以…

基于Django和vue的微博用户情感分析系统

完整代码&#xff1a;https://download.csdn.net/download/weixin_55771290/87471350概述这里简单说明一下项目下下来直接跑起的方法。前提先搞好python环境和vue环境,保证你有一个账户密码连上数据库mysql。1、pip install requirements.txt 安装python包2、修改mysql数据库的…

Hadoop HDFS的主要架构与读写文件

一、Hadoop HDFS的架构 HDFS&#xff1a;Hadoop Distributed File System&#xff0c;分布式文件系统 &#xff11;&#xff0c;NameNode 存储文件的metadata&#xff0c;运行时所有数据都保存到内存&#xff0c;整个HDFS可存储的文件数受限于NameNode的内存大小一个Block在…

使用物联网进行智能能源管理的10大优势

如今&#xff0c;物联网推动了许多行业的自动化流程和运营效率&#xff0c;而物联网在能源领域的应用尤其受到消费者、企业甚至政府的关注。除了对电力供应链的诸多好处之外&#xff0c;物联网能源管理系统还让位于新的智能电网&#xff0c;并有望实现更高的安全性和效率。基于…

软件架构知识6-高性能数据库集群:读写分离

一、读写分离 读写分离原理&#xff1a;将数据库读写操作分散到不同的节点上&#xff1a; 读写分离的基本实现是&#xff1a; 1、数据库服务器搭建主从集群&#xff0c;一主一从&#xff0c;一主多从都可以&#xff1b; 2、数据库主机负责读写操作&#xff0c;从机只负责读操…

【2023-02-20】JS逆向之翼支付

提示&#xff1a;文章仅供参考&#xff0c;禁止用于非法途径 文章目录前言分析总结前言 真的好久没更了…… 提示&#xff1a;以下是本篇文章正文内容&#xff0c;下面案例可供参考 分析 进到网页&#xff0c;加载两个接口 applyLoginFactor 接口返回一个RSA公钥&#xff0…