常见排序算法

272 阅读7分钟

一、排序的基本概念

  1. 排序

    排序是按关键字的非递减或非递增顺序对一组记录重新进行排列的操作。

  2. 排序的稳定性

    简单来说,就是待排序的序列中存在值相等的元素,经过排序之后,相等元素之间原有的先后顺序不变。

    举个例子:有一个组数,1,3,5,9,4,5,按照大小排序之后就是1,3,4,5,5,9。这组数据里面有两个5,经过某种排序算法后,如果前后两个5的顺序没有改变,这个排序算法就是稳定的。

    为什么要考虑排序算法的稳定性?

    在实际开发过程中,需要排序的往往可能是一组对象,按照对象的某个值来排序。比如电商交易系统中的“订单”有两个属性,一个是下单时间,一个是下单金额。假设有几十万条数据,希望按照金额从小到大对订单数据进行排序,而对于金额相同的订单,希望按照下单时间从早到晚有序。

    对于这么一个排序需求,就需要借助稳定排序算法:先按照下单时间排序,排序完成后再用稳定排序算法按照订单金额再次排序。这样可以保证,相同金额的订单的时间依然是从早到晚排序的(因为稳定排序不会改变原来的相对位置)。

  3. 内部排序和外部排序

    由于待排序记录的数量不同,使得排序过程中数据所占用的存储设备也会有所不同,据此把排序方法分为两大类:一类是内部排序,指的是待排序记录全部存放在计算机内存中进行排序的过程;另一类是外部排序,指的是待排序记录的数量很大,以致于内存一次不能容纳全部记录,在排序过程尚需对外存进行访问的排序过程。

二、常见排序算法

  1. 插入排序

    • 直接插入排序

      基本思想:将一条记录插入到已排好序的有序表中,从而得到一个新的、记录数量+1的有序表

      算法步骤:1.设待排序的记录存放在数组r[1,n]中,r[1]是一个有序序列;2.循环n-1次,每次使用顺序查找法,查找r[i](i=2,...n)在已排序的序列[1,...i-1]中的插入位置,然后将r[i]插入表长为i-1的有序序列r[1,..i-1],知道将r[n]插入表长为n-1的有序序列r[1,...n-1],最后得到一个表长为n的有序序列

          public static void InsertS(int[] arr){
              int i,j;
              for( i=1;i<arr.length;i++){
                  int temp = arr[i];
                  for( j=i-1;j>=0&&arr[j]>temp;j--){
                      arr[j+1]=arr[j];
                  }
                  arr[j+1]=temp;
              }
          }
      

      算法分析:

      时间复杂度O(n²)、空间复杂度O(1)

      特点:适用于链式存储结构,只是在单链表上无需移动记录,只需修改相应指针;更适合初始记录基本有序(正序)的情况,当初始记录无序,n较大时,此算法复杂度高,不宜采用

  2. 交换排序

    基本思想:两两比较待排序记录的关键字,一旦发现两个记录不满足次序要求则进行交换,直到整个序列满足要求为止。

    • 冒泡排序

      (通过两两比较相邻记录的关键字,如果发生逆序,则进行交换,从而使关键字小的记录如气泡一般往上浮)

      算法步骤:比较简单,不说了

              for(int i=0;i<arr.length;i++){
                  for(int j=0;j<arr.length-1-i;j++){
                      if(arr[j]>arr[j+1]){
                          swap(arr,j,j+1);
                      }
                  }
              }
          }
      
      public static  void swap(int[] arr,int i,int j){
              int temp = arr[i];
              arr[i]=arr[j];
              arr[j]=temp;
          }
      
          小优化:标记一下每趟遍历是否有发生交换,如果没有发生交换就可以提前终止。
      

      算法分析:

      时间复杂度和空间复杂度同上。

      特点:稳定排序、可用于链式存储结构移动次数多,算法平均复杂度比直接插入排序差

    • 快速排序

      由冒泡排序改进而得,一次交换可能消除多个逆序。

      基本思想:

      在待排序的n个记录中任取一个记录作为privot。经过一趟排序后,把所有关键字小于privot的记录交换到前面,把所有关键字大于privot的记录交换到后面,结果将待排序记录分为两个子表,最后将privot放在分界处。然后,分别对左右子表重复上述过程,直至每一个子表只有一个记录时,排序完成。

              //终止条件
              if(low>=high)
                  return;
              int key=arr[low];
              int left=low;
              int right=high;
              while(left<right){
                  while(left<right&&arr[right]>=key){
                      right--;
                  }
                  arr[left]=arr[right];
                  while(left<right&&arr[left]<=key){
                      left++;
                  }
                  arr[right]=arr[left];
              }
              arr[left]=key;
              quikSort(arr,low,left-1);
              quikSort(arr,left+1,high);
          }
      

      算法分析:

      平均时间复杂度为O(nlog2n) ,空间复杂度最好的情况下O(log2n),最坏O(n²)

      特点:非稳定排序,需要定位表的上界和下界,适用于顺序结构;当n较大时,在平均情况下是所有内部排序最快的一种,适用于初始记录无序、n较大时的情况

    3.选择排序

    • 简单选择排序

      基本思想:每次从无序表中选出一个关键字最小的记录,放到有序表的末尾。

              for(int i=0;i<arr.length;i++){
                  //从未排序数组中选择一个最小的数插入到已排序数组的末尾
                 int index=i;
                 for(int j=i;j<arr.length;j++){
                     if(arr[index]>arr[j])
                         index=j;
                 }
                 //只有这个数不在当前的位置才交换
                  if(index!=i){
                      int temp = arr[index];
                      arr[index]=arr[i];
                      arr[i]=temp;
                  }
              }
          }
      

      算法分析:平均时间复杂度O(n²) ,空间复杂度O(1)

      特点:可以是稳定排序,可用于链式存储结构移动次数较少,当每一条记录占用的空间较多时,此方法比直接插入排序快

    • 堆排序

      堆排序是一种树形选择排序,在排序过程中,将待排序的记录r[1..n]看成是一颗完全二叉树的顺序存储结构,利用完全二叉树中双亲节点之间的内在关系,在当前无序的序列中选择关键字最大(或最小)的记录。

      基本步骤:

      1. 按堆的定义将待排序序列r[1..n]调整为大根堆(此过程也称为初建堆),交换r[1]和r[n],则r[n]为关键字最大的记录
      2. 将r[1..n-1]重新调整为堆,交换r[1]和r[n-1],则r[n-1]为关键字次大的记录
      3. 循环n-1次,知道交换了r[1]和r[2]为止,得到了一个非递减的有序序列r[1..n]

      核心思想:

      • 初建堆:如何将 一个无序序列建成一个堆?
      • 调整堆:去掉堆顶元素,在堆顶元素改变之后,如果调整剩余元素成为一个新的堆?
              for(int i=n/2;i>=1;i--){
                  heapify(a,n,i);
              }
          }
      
          public static void heapify(int[] a,int n,int i){
              int maxPos=i;
              while(true){
                  if(i*2<=n&&a[maxPos]<a[i*2])
                      maxPos=i*2;
                  if(i*2+1<=n&&a[maxPos]<a[i*2+1])
                      maxPos=i*2+1;
                  if(maxPos==i)
                      break;
                  swap(a,i,maxPos);
                  i=maxPos;
              }
          }
      

      算法分析:时间复杂度最坏情况下为O(nlog2n) ,空间复杂度为O(1) (平均时间复杂度接近最坏性能)

      特点:不稳定排序只能用于顺序结构;初建堆所需比较次数较多,因此记录较少时不宜采用;堆排序最坏情况下时间复杂度为O(nlog2n),相比快排最坏情况下的O(n²)是一个优点,当记录数较多时较为高效

    4.归并排序

    定义:将两个或两个以上的有序表合并成一个有序表的过程。

    基本思想(2路归并):

    假设初始序列有n个记录,可看成是n个有序的子序列,每个子序列的长度为1,然后两两归并,得到[n/2]个长度为2或1的有序子序列;再两两归并,····,如此重复,知道得到一个长度为n的有序序列为止。总的来说,就是先“分解”再“合并”。

    归并排序.drawio.png

    ```public  static void MSort(int[] T,int low, int high){
            //递归终止条件
            if(low>=high)
                return;
    
            int mid=(low+high)/2;
            MSort(T,low,mid);
            MSort(T,mid+1,high);
            Merge(T,low,mid,high);
    
        }
    
    public static void Merge(int[] T,int low,int mid,int high){
            //三个指针分别指向左区间、右区间、临时数组
            int i=low;
            int j=mid+1;
            int k=0;
            //临时数组
            int[] temp = new int[high-low+1];
            //合并两个有序数组
            while(i<=mid&&j<=high){
                //未分组的时候如果T[low,mid]和T[mid+1,high]有相等的元素
                //T[low,mid]的元素放在前面
                //要保持稳定性,如果相等就取左区间的元素
                if(T[i]<=T[j]){
                    temp[k++]=T[i++];
                }else {
                    temp[k++]=T[j++];
                }
            }
            //处理有剩余元素的一个数组
            int start=i;
            int end=mid;
            if(j<=high){
                start=j;
                end=high;
            }
            while(start<=end){
                temp[k++]=T[start++];
            }
            //把数组复制到原数组
            for(i=0;i<temp.length;i++)
                T[low+i]=temp[i];
    
        }
    ```
    算法分析:
    
    • 时间复杂度:当有n个记录时,需进行[log2n]趟归并排序,每一趟归并,关键字比较次数不超过n,元素移动次数都是n,因此,归并排序的时间复杂度是O(nlog2n)
    • 空间复杂度:需要和待排序记录个数相等的辅助存储空间,空间复杂度为O(n)

    特点:

    • 稳定排序
    • 可用于链式结构,且链式结构不需要辅助存储空间,但递归时仍需要递归工作栈