zoukankan      html  css  js  c++  java
  • 九大基础排序总结与对比

    一、对比分析图
     
    • 均按从小到大排列

    • k代表数值中的”数位”个数

    • n代表数据规模

    • m代表数据的最大值减最小值 

    • 稳定性:稳定排序算法会让原本有相等键值的纪录维持相对次序。也就是如果一个排序算法是稳定的,当有两个相等键值的纪录R和S,且在原本的列表中R出现在S之前,在排序过的列表中R也将会是在S之前。

    ps:链表就是例如你要找链表的第10个元素,你就必须从第一个元素开始,一直往后找,不能像数组那样arry[n],马上找到,你要找第十个就必选找到第九个,要找第九个必选找到第八个。。。。。

    二、冒泡排序

     

    概述

      冒泡排序通过重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来,直到没有再需要交换的元素为止(对n个项目需要O(n^2)的比较次数)。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。

    实现步骤

    1. 比较相邻的元素。如果第一个比第二个大,就交换他们两个。 

    2. 对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。

    3. 针对所有的元素重复以上的步骤,除了最后一个。

    4. 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。 

    这里写图片描述

      冒泡排序为一列数字进行排序的过程 

    实现性能

    • 最差时间复杂度

    O(n^2)

    • 最优时间复杂度

    O(n) 

    • 平均时间复杂度

    O(n^2)

    • 最差空间复杂度

    总共O(n),需要辅助空间O(1)

     java实现:

    public static void main(String[] args) {
            int[] number = {95,45,15,78,84,51,24,12};
            bubble_sort(number);
            for(int i = 0; i < number.length; i++) {
                System.out.print(number[i] + " ");
            }
    }
    public static void bubble_sort(int[] arr) { int temp, len = arr.length; for (int i = 0; i < len - 1; i++) for (int j = 0; j < len - 1 - i; j++) if (arr[j] > arr[j + 1]) { temp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = temp; } }

     ps:为什么冒泡排序的时间复杂度是O(N^2)呢,明明是要比较n(n-1)/2次(等差数列:第一次循环比较n-1,第二次n-2,第三次n-3....)

    (10^8 - 10000) / 2来说,相对10^8,10000小的可以忽略不计了,这个式子的数量级就是10^8了,所以对应冒泡的数量级就是N^2了

    三、选择排序

    选择排序

      常用的选择排序方法有简单选择排序和堆排序,这里只说简单选择排序,堆排序后面再说。

    简单选择排序

      设所排序序列的记录个数为n,i 取 1,2,…,n-1 。 
      从所有n-i+1个记录(Ri,Ri+1,…,Rn)中找出排序码最小(或最大)的记录,与第i个记录交换。执行n-1趟 后就完成了记录序列的排序。

    以排序数组{3,2,1,4,6,5}为例

    这里写图片描述

    这里写图片描述

    简单选择排序性能

      在简单选择排序过程中,所需移动记录的次数比较少。最好情况下,即待排序记录初始状态就已经是正序排列了,则不需要移动记录。  
      最坏情况下,即待排序记录初始状态是按第一条记录最大,之后的记录从小到大顺序排列,则需要移动记录的次数最多为3(n-1)。

      简单选择排序过程中需要进行的比较次数与初始状态下待排序的记录序列的排列情况无关。 
      当i=1时,需进行n-1次比较;当i=2时,需进行n-2次比较;依次类推,共需要进行的比较次数是(n-1)+(n-2)+…+2+1=n(n-1)/2,即进行比较操作的时间复杂度为O(n^2),进行移动操作的时间复杂度为O(n)。 

      简单选择排序是不稳定排序。

     java实现:

    public static void main(String[] args) {
        int[] number = {3,1,2,8,4,5,24,12};
        SimpleSort(number);
        for(int i = 0; i < number.length; i++) {
            System.out.print(number[i] + " ");
            }
    }
    public static void SimpleSort(int[] arr) {
        int length=arr.length;
        int temp;
        for(int i=0;i<length-1;i++){
            int min=i;
            for(int j=i+1;j<length;j++){ //寻找最小的数
                if(arr[j]<arr[min]){
                    min =j;
                }
            }
            if(min!=i){
                 temp = arr[min];
                 arr[min]=arr[i];
                 arr[i]=temp;
            }
        }
    }   

     ps:为什么选择排序是不稳定的呢?这里有个稳定的定义就是,如果数组中有2个数是相同的,排序后他们的顺序发生了变化就称这个排序算法不稳定,反之稳定。想对于冒泡排序,选择排序算法的改进在于:先并不急于调换位置,每扫描一遍数组,只需要一次真正的交换,而冒泡可能需要很多次。比较的次数是一样的。

     

    四、希尔排序

    概述

      希尔排序法(缩小增量法) 属于插入类排序,是将整个无序列分割成若干小的子序列分别进行插入排序的方法。

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

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

    • 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率。

    • 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位。

    实现过程

      先取一个正整数d1小于n,把所有序号相隔d1的数组元素放一组,组内进行直接插入排序;然后取d2小于d1,重复上述分组和排序操作;直至di=1,即所有记录放进一个组中排序为止。

      例如,假设有这样一组数[ 13 14 94 33 82 25 59 94 65 23 45 27 73 25 39 10 ],如果我们以步长为5开始进行排序,我们可以通过将这列表放在有5列的表中来更好地描述算法,这样他们就应该看起来是这样:

    13 14 94 33 82 
    25 59 94 65 23 
    45 27 73 25 39 
    10

    然后我们对每列进行排序:

    10 14 73 25 23 
    13 27 94 33 39 
    25 59 94 65 82 
    45

      将上述四行数字,依序接在一起时我们得到:[ 10 14 73 25 23 13 27 94 33 39 25 59 94 65 82 45 ].这时10已经移至正确位置了,然后再以3为步长进行排序:

    10 14 73 
    25 23 13 
    27 94 33 
    39 25 59 
    94 65 82 
    45

    排序之后变为:

    10 14 13 
    25 23 33 
    27 25 59 
    39 65 73 
    45 94 82 
    94

    最后以1步长进行排序(此时就是简单的插入排序了)。

    实现效率

      希尔排序是一个不稳定的排序,其时间复杂度受步长(增量)的影响。

      空间复杂度: O(1)

      时间复杂度: 平均 O(n^1.3) 
             最好 O(n) 
             最坏 O(n^2)

    五、归并排序

    1.概述

      归并排序,是创建在归并操作上的一种有效的排序算法该算法是采用分治法(Divide and Conquer)的一个非常典型的应用,且各层分治递归可以同时进行。

      即先使每个子序列有序,再将两个已经排序的序列合并成一个序列的操作。若将两个有序表合并成一个有序表,称为二路归并。

    例如:

    设有数列{6,202,100,301,38,8,1} 
    初始状态:6,202,100,301,38,8,1 
    第一次归并后:{6,202},{100,301},{8,38},{1},比较次数:3; 
    第二次归并后:{6,100,202,301},{1,8,38},比较次数:4; 
    第三次归并后:{1,6,8,38,100,202,301},比较次数:4; 
    总的比较次数为:3+4+4=11,; 
    逆序数为14;

    这里写图片描述

          归并排序示意图

    2.效率

      归并排序速度仅次于快速排序,为稳定排序算法(即相等的元素的顺序不会改变),一般用于对总体无序,但是各子项相对有序的数列. 

      时间复杂度为O(nlogn)   
       
      空间复杂度为 O(n) 

    归并排序比较占用内存,但却是一种效率高且稳定的算法。

    3.迭代实现

    3.1实现原理

    ①申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列

    ②设定两个指针,最初位置分别为两个已经排序序列的起始位置

    ③比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置

    六、快速排序

    基本思想

      快速排序(Quicksort)是对冒泡排序的一种改进,又称划分交换排序(partition-exchange sort。

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

    步骤为:

    ①.从数列中挑出一个元素,称为”基准”(pivot)

    ②.重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。

    ③.递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序

    这里写图片描述 

    使用快速排序法对一列数字进行排序的过程

    排序效率

      在平均状况下,排序n个项目要Ο(n log n)次比较。在最坏状况下则需要Ο(n2)次比较,但这种状况并不常见。事实上,快速排序通常明显比其他Ο(n log n)算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来。

    最差时间复杂度 Ο(n^2) 

    最优时间复杂度 Ο(n log n) 

    平均时间复杂度Ο(n log n) 

    最差空间复杂度 根据实现的方式不同而不同

    ④重复步骤③直到某一指针到达序列尾

    ⑤将另一序列剩下的所有元素直接复制到合并序列尾

    4.递归实现

    4.1实现原理

    假设序列共有n个元素

    ①将序列每相邻两个数字进行归并操作,形成floor(n/2)个序列,排序后每个序列包含两个元素。

    ②将上述序列再次归并,形成floor(n/4)个序列,每个序列包含四个元素

    ③重复步骤②,直到所有元素排序完毕

    七、堆排序

    浅析堆

      堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法,它是选择排序的一种。可以利用数组的特点快速定位指定索引的元素。堆分为大根堆和小根堆,是完全二叉树。大根堆的要求是每个节点的值都不大于其父节点的值。

      由于堆中每次都只能删除第0个数据,通过 取出第0个数据再执行堆的删除操作、重建堆(实际的操作是将最后一个数据的值赋给根结点,然后再从根结点开始进行一次从上向下的调整。),然后再取,如此重复实现排序。

    堆的操作:

    这里写图片描述 

    在堆的数据结构中,堆中的最大值总是位于根节点。堆中定义以下几种操作:

    • 最大堆调整(Max_Heapify):将堆的末端子节点作调整,使得子节点永远小于父节点

    • 创建最大堆(Build_Max_Heap):将堆所有数据重新排序

    • 堆排序(HeapSort):移除位在第一个数据的根节点,并做最大堆调整的递归运算

    堆的存储:

    这里写图片描述

    通常堆是通过一维数组来实现的。在数组起始位置为0的情形中:

    • 父节点i的左子节点在位置(2*i+1); 

    • 父节点i的右子节点在位置(2*i+2);

    • 子节点i的父节点在位置floor((i-1)/2); 

    • 八、桶排序

      1.概念

      桶排序(Bucket sort)或所谓的箱排序,是一个排序算法。

        假设有一组长度为N的待排关键字序列K[1….n]。首先将这个序列划分成M个的子区间(桶) 。然后基于某种映射函数 ,将待排序列的关键字k映射到第i个桶中(即桶数组B的下标 i) ,那么该关键字k就作为B[i]中的元素。接着对每个桶B[i]中的所有元素进行比较排序(可以使用快排)。然后依次枚举输出B[0]….B[M]中的全部内容即是一个有序序列。

      桶排序的步骤:

      ①设置一个定量的数组当作空桶子。

      ②寻访序列,并且把项目一个一个放到对应的桶子去。

      ③对每个不是空的桶子进行排序。

      ④从不是空的桶子里把项目再放回原来的序列中。

      2.性能

      数据结构 数组  
      最差时间复杂度   O(n^2) 
      平均时间复杂度  O(n+k) 
      最差空间复杂度 O(n*k)

        平均情况下桶排序以线性时间运行,桶排序是稳定的,排序非常快,但是同时也非常耗空间,基本上是最耗空间的一种排序算法。

        对N个关键字进行桶排序的时间复杂度分为两个部分:

      ①循环计算每个关键字的桶映射函数,这个时间复杂度是O(N)。

      ②利用先进的比较排序算法对每个桶内的所有数据进行排序,其时间复杂度为 ∑ O(Ni*logNi) 。其中Ni 为第i个桶的数据量。

        很显然,第②部分是桶排序性能好坏的决定因素。尽量减少桶内数据的数量是提高效率的唯一办法(因为基于比较排序的最好平均时间复杂度只能达到O(N*logN)了)。因此,我们需要尽量做到下面两点:  

      ① 映射函数f(k)能够将N个数据平均的分配到M个桶中,这样每个桶就有[N/M]个数据量。  
         
      ②尽量的增大桶的数量。极限情况下每个桶只能得到一个数据,这样就完全避开了桶内数据的“比较”排序操作。 当然,做到这一点很不容易,数据量巨大的情况下,f(k)函数会使得桶集合的数量巨大,空间浪费严重。这就是一个时间代价和空间代价的权衡问题了。

    • 九、基数排序

      原理

        基数排序(Radix sort)是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。

        将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列。

      效率

        基数排序的时间复杂度是O(k·n),其中n是排序元素个数,k是数字位数。注意这不是说这个时间复杂度一定优于O(n·log(n)),k的大小取决于数字位的选择和待排序数据所属数据类型的全集的大小;k决定了进行多少轮处理,而n是每轮处理的操作数目。

        基数排序基本操作的代价较小,k一般不大于logn,所以基数排序一般要快过基于比较的排序,比如快速排序。

        最差空间复杂度是O(k·n)

  • 相关阅读:
    校验参考相关备份
    API接口设计
    redis 基础配置
    Apollo 统一配置中心
    http返回状态码记录
    ngnix实战
    OAuth2三方授权
    OAuth2授权协议记录
    KMP算法
    分治法
  • 原文地址:https://www.cnblogs.com/amiezhang/p/7422064.html
Copyright © 2011-2022 走看看