博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

常见排序算法的代码实现、性能分析及分类总结

Posted on 2020-08-03 13:13  池塘鱼  阅读(466)  评论(0)    收藏  举报

一、排序的分类

  • 稳定性:通俗的讲,就是能保证两个相等的数据在排序前和排序后的先后顺序不发生改变。
  • 类型
    • 插入排序:找到和适合的位置插入
    • 交换排序:边比较边交换
    • 选择排序:一趟比较完再交换

 

二、性能分析

  注意:以下提及的最好情况为全有序,最坏情况为全逆序。除了快速排序,最坏情况是基准值选了最大值或最小值,致使无法均匀划分序列。

  • 冒泡排序:
    • 整体思想:执行n-1次相邻数据的走访交换,每一次都能使当前剩余数字中最大的一个数字浮到最后。
    • 实现思路:进行n-1次循环,每次都访问除“已经浮到最后的元素”以外的元素,大小不符合则立即交换。n-1次循环过后,就已经使n-1个元素有序,即整体有序。
    • 性能分析:无论输入数据如何,比较次数不变,但数据输入的好坏,会影响交换次数。因此,最坏和平均时间复杂度均为O(n²),最好情况的时间复杂度为O(n)。
    • 特点:简单、稳定
    • 适用场景:在数据规模小,且基本有序的情况。
    • 实现代码:
      package com.ex.sort;
      
      import java.util.Arrays;
      
      /**
       * 冒泡排序的实现
       * 时间复杂度:O(n^2)
       */
      public class BubbleTest {
          public static void main(String[] args) {
              int[] arr=new int[]{1,4,2,6,3,7,9};
              bubble(arr);
              System.out.println(Arrays.toString(arr));
          }
      
          private static void bubble(int[] arr) {
              int length = arr.length;
              for (int i = 0; i <  length - 1; i++) {
                  //相邻元素作比较
                  for (int j = 0;j < length-1-i;j++){//记录的是前面元素的下标
                      if (arr[j]>arr[j+1]){
                          int temp=arr[j];
                          arr[j]=arr[j+1];
                          arr[j+1]=temp;
                      }
                  }
              }
          }
      }

 

  • 选择排序:
    • 整体思想:执行n-1次的寻找后面待排序中最小的那一个,并和当前元素做交换,每次都能使得最小的一个数字浮到前面。
    • 实现思路:执行n-1次循环,每一次循环都假定当前元素是最小元素,依次和后面的元素比较,并实时更新最小元素下标,一趟结束后如果当前元素确实不是最小元素就做交换。
    • 性能分析:
      • 该算法是冒泡算法的改进,依然是每趟都依次比较,只是不再立即交换,而是记录下标等一趟结束再交换。
      • 无论输入数据如何,都不影响比较次数,也不太影响交换次数。因此,最好、最坏、平均情况下的时间复杂度均为O(n²)。
    • 特点:简单
    • 适用场景:因为任何场景下时间复杂度均为O(n²),因此数据规模越小越好,没什么格外的优点,一般不用这个算法。
    • 实现代码:
      package com.ex.sort;
      
      import java.util.Arrays;
      
      /**
       * 实现选择排序
       * 时间复杂度:O(n^2)
       */
      public class SelectionTest {
          public static void main(String[] args) {
              int[] arr=new int[]{1,4,2,6,3,7,9};
              selection(arr);
              System.out.println(Arrays.toString(arr));
          }
      
          private static void selection(int[] arr) {
              int length = arr.length;
              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){
                      int temp=arr[i];
                      arr[i]=arr[min];
                      arr[min]=temp;
                  }
              }
          }
      }
      

 

  • 直接插入排序:
    • 整体思想:将待排序列视作一个有序表和一个无序表,最开始时有序表中只有一个元素,此后每次从无序表中拿出一个元素插入到有序表中的合适位置,重复n-1次完整整个的排序过程。
    • 实现思路:两层循环,第一层记录要插入的元素下标的变化,第二层记录要比较的元素的下标的变化,注意,寻找插入位置是倒着查找
    • 性能分析:输入数据影响比较次数和交换次数,如果在最好情况下,只需要比较n次且无需移动,在最坏情况下,则和选择排序运算量一样。因此,最好情况下时间复杂度为O(n),最坏和平均情况下均为O(n2)。
    • 特点:简单、稳定、适合有序情况
    • 适用场景:部分有序和小规模数据,越有序越快。
    • 实现代码:
      package com.ex.sort;
      
      import java.util.Arrays;
      
      /**
       * 实现插入排序
       * 时间复杂度:O(n^2)
       */
      public class InsertionTest {
          public static void main(String[] args) {
              int[] arr=new int[]{1,4,2,6,3,7,9};
              insertion(arr);
              System.out.println(Arrays.toString(arr));
          }
      
          private static void insertion(int[] arr) {
              int length = arr.length;
              for (int i = 1; i < length; i++) {//记录的是要插入的元素的下标
                  for (int j= i - 1;j >= 0;j--){//记录要比较的元素的下标
                      if (arr[i] < arr[j]){
                          int temp=arr[i];
                          arr[i]=arr[j];
                          arr[j]=temp;
                      }
                  }
              }
          }
      }
      

 

  • 希尔排序(缩小增量排序):
    • 整体思想:先将待排序列按照增量分为若干个组,对每一组元素进行直接插入排序,然后依次缩小增量再进行直接插入排序,待整个序列的元素基本有序时,再进行一次整体的直接插入排序。
    • 实现思路:三层循环,第一层改变步长,第二层和第三层对分组元素进行比较并交换。注意,这里的分组交换不是一组一组比较,而是合在一起依次(找到自己组的前一个元素)比较,第二层循环记录比较元素的移动。(这个过程如果不明白,可以看 https://www.bilibili.com/video/BV1BK4y1478X?from=search&seid=12600864908459859895 在 12:15 的讲解)
    • 性能分析:
      • 是对直接插入排序算法的改进,可以使得插入幅度提升,不必再顺序比较。
      • 时间复杂度分析及其复杂,记住结论即可。最好情况为O(n),最坏情况为O(n2),平均情况为O(n1.3)。
    • 特点:快,数据移动少
    • 适用场景:任何场景下都可以尝试使用这个算法
    • 实现代码:
      package com.ex.sort;
      
      import java.util.Arrays;
      
      /**
       * 实现希尔排序
       * 时间复杂度:O(n^2)
       */
      public class ShellTest {
          public static void main(String[] args) {
              int[] arr=new int[]{1,4,2,6,3,7,9,5,10,8};
              shell(arr);
              System.out.println(Arrays.toString(arr));
          }
      
          private static void shell(int[] arr) {
              //第一个循环改变增量大小,每次在原先的基础上除以2
              for (int step = arr.length / 2; step > 0; step /=2){
                  //第二个循环对分组元素进行排序,不是一组一组轮流排序,而是一起对每个组轮流排序,每个元素都和同组的前面的元素进行比较 [step,arr.length-1]
                  for (int i = step; i < arr.length; i++){//前者
                      //第三个循环来对每次比较的两个元素进行比较并交换
                      int value = arr[i],j;
                      for (j = i - step; j >= 0 && arr[j] > value; j-=step){
                          arr[j + step] = arr[j];
                      }
                      arr[j + step] = value;//注意这里的j+step 和 上面的j+step 不一样,上面交换完还又减了一次step
                  }
              }
          }
      
          /*
          * //step:步长
              for (int step = arr.length / 2; step > 0; step /= 2) {
                  //对一个步长区间进行比较 [step,arr.length)
                  for (int i = step; i < arr.length; i++) {
                      int value = arr[i];//比较中的后者
                      int j;
      
                      //对步长区间中具体的元素进行比较
                      for (j = i - step; j >= 0 && arr[j] > value; j -= step) {//比较中的前者
                          //j为左区间的取值,j+step为右区间与左区间的对应值。
                          arr[j + step] = arr[j];
                      }
                      //此时step为一个负数,[j + step]为左区间上的初始交换值
                      arr[j + step] = value;
                  }
              }*/
      }
      

 

  • 快速排序:
    • 整体思想:采用了分治思想。首先选取一个基准点,设两个标志low和high,然后从右往左扫描到比基准点小的就交换low和high指针,再从左往右扫描到比基准小的就交换low和high指针,直到low和high指针重合,此时基准左边比基准小,右边比基准大。然后分别对左右再这样递归排序。当前半部分和后半部分都有序时自然就均匀有序了。
    • 实现思路:分为具体排序代码和递归调用。具体排序分为两部分,一是左右移动找到不符合大小的元素并交换,二是交换基准值。
    • 性能分析:
      • 最好的情况是基准值选取得当,每次都能均匀的划分序列。最坏情况是基准值选了最大值或最小值,致使无法均匀划分序列。因为下面的版本是选取首个元素为基准值,所以实际上最坏情况是待排序列全正序、全逆序、全相同。
      • 因此,最好情况是O(n logn),最坏情况是O(n2),平均时间复杂度是O(n logn)。
    • 特点:非常快
    • 适用场景:一般应用中,比其他排序快得多。适用于数据量大和数据随机分布的情况,表现很好。对于数据量小的情况,没有插入排序好。
    • 实现代码:
      package com.ex.sort;
      
      import java.util.Arrays;
      
      /**
       * 实现快速排序
       *      不稳定
       *      5 3 3 4 3 8 9 10 11 第一趟排序时,把第一个5和第三个3交换,此时就是不稳定的
       * 时间复杂度:O(n^2)
       */
      public class QuickTest {
          public static void main(String[] args) {
              int[] arr=new int[]{1,4,2,6,3,7,9,5,10,8};
      //        int[] arr=new int[]{5,3,3,4,3,8,9,10,11};
              quick(arr,0,arr.length-1);
              System.out.println(Arrays.toString(arr));
          }
      
          private static void quick(int[] arr,int left,int right) {
              if (left<right){
                  //l:左指针 r:右指针
                  int l=left,r=right,ref=arr[left];
                  //第一趟排序
                  while(l!=r){
                      while (arr[r]>=ref && l<r)
                          r--;
                      while (arr[l]<=ref && l<r)
                          l++;
                      //找到一对就调换
                      if (l<r){
                          int temp=arr[l];
                          arr[l]=arr[r];
                          arr[r]=temp;
                      }
                  }
                  //把基准值换过来
                  arr[left]=arr[l];
                  arr[l]=ref;
                  //子序列递归排序
                  quick(arr,left,l-1);
                  quick(arr,l+1,right);
              }
          }
      }

       

  • 归并排序:
    • 整体思想:分治思想的典型应用。将待排序列分解为两个子序列,对每个子序列进行排序,合并两个排好序的子序列。每个子序列的排序也是递归进行这三步。
    • 实现思路:两个方法,一个负责对待排序列进行划分和调用“排序并合并”的方法,一个负责对子序列进行排序并合并。
    • 性能分析:
      • 和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是O(n logn)的时间复杂度。代价是需要额外的内存空间。
      • 最好、最坏、平均情况下时间复杂度都是O(n logn),空间复杂度是O(n)。或者可以将空间复杂度由 O(n) 降低至 O(1),然而相对的时间复杂度则由 O(nlogn) 升至 O(n²)。
    • 特点:稳定、需要额外空间
    • 适用场景:适合大规模数据
    • 实现代码:
      package com.ex.sort;
      
      import java.util.Arrays;
      
      /**
       * 实现归并排序
       *      稳定
       * 时间复杂度:O(n log n)
       */
      public class MergeTest {
          public static void main(String[] args) {
              int[] arr=new int[]{1,4,2,6,3,7,9};
              mergeSort(arr,0,arr.length-1);
              System.out.println(Arrays.toString(arr));
          }
      
          /**
           * 分治:递归划分子序列,并通过调用方法有序合并子序列
           * @param arr 数组
           * @param left 数组左边界
           * @param right 数组右边界
           */
          private static void mergeSort(int[] arr,int left,int right) {
              if (left < right){ //当只有一个元素的时候进入合并,这是结束“分”的递归进入“合”的条件
                  int mid = (left+right) / 2;
                  mergeSort(arr,left,mid);
                  mergeSort(arr,mid+1,right);
                  merge(arr,left,mid,right);
              }
          }
      
          /**
           * 合并子序列
           * @param arr 数组
           * @param left 子序列左边界
           * @param mid  子序列右边界
           * @param right 子序列右边界
           */
          private static void merge(int[] arr, int left,int mid, int right) {
              //1.合并
              //temp是辅助数组,p1和p2分别是左子序列和右子序列的检测指针,k是临时数组的存放指针
              int temp[] =new int[arr.length];
              int p1=left,p2=mid+1,k=left;
              while (p1<=mid && p2<=right){
                  if (arr[p1]<arr[p2])
                      temp[k++]=arr[p1++];
                  else
                      temp[k++]=arr[p2++];
              }
              //2.将有元素剩余的子序列中的剩余元素直接添加到数组
              while (p1<=mid){
                  temp[k++]=arr[p1++];
              }
              while (p2<=right){
                  temp[k++]=arr[p2++];
              }
              //3.将排好序的数据复制回原数组
              for (int i=left;i<=right;i++){
                  arr[i]=temp[i];
              }
      
          }
      }

       

三、总结

1.时间复杂度、空间复杂度、稳定性

 

2.适用场景

  • 数据量小(n≤50):
    • 基本有序:直接插入排序 或 冒泡排序,对数据要求不要但性能好快的快速排序也可。
    • 随机分布:直接插入、冒泡、选择、快速排序皆可。
    • 要求稳定:直接插入 或 冒泡。
  • 数据量大:
    • 随机分布:快速(内存皆可的话)、归并(内存阔绰的话)、堆排序(内存紧张的话)。
    • 要求稳定:堆排序。