排序算法的特性与实现汇总(Java)

写在前面

  • 代码实测

所有的代码,都经过了LeetCode 912的测试,有一部分时间复杂度为 O ( n 2 ) O(n^2) O(n2)的代码超时,超时的算法有:冒泡排序和选择排序,两者实现简单。
附图如下
所有代码的测试情况

  • 文章目的

    最近一直在准备秋招笔试,日常被各种算法题暴打,最近发现自己连一些排序最基础的概念都模糊不清,特地参考教材,总结了各个排序算法。

《数据结构教程(第五版)》李春葆

  • 如果有看不懂的代码,或注释,可以手动举几个样例,手算一遍会使思路更清晰,请原谅我这个菜鸡。。。

排序汇总

直接插入排序

  • 原理:

    将数组划分为有序区无序区,每一次将当前无序区开头元素插入到有序区适当位置(增量法:每一次有序区增加一个元素)

  • 特点:

    就地排序、稳定排序、 O ( n 2 ) O(n^2) O(n2)的时间复杂度

public void InsortSort(int[] nums,int n){
        /**
        对n个数进行直接插入排序
            例如 5 2 3 1,num数组中值的变化
                i=1:由于2和5逆序, 2 5 3 1
                i=2:2 5 5 1 -> 2 3 5 1
                i=3: 2 3 5 5->2 3 3 5 ->  2 2 3 5->插入1, 1 2 3 5

         */
         int temp,j;//temp暂存nums[i],j记录需要后移的位置
         for(int i=1;i<n;i++){
             if(nums[i]<nums[i-1]){
                 //只有当逆序时,才交换
                 //寻找位置
                 temp=nums[i];
                 j=i-1;
                 do{
                     nums[j+1]=nums[j];//后移
                     j--;
                 }while(j>=0&&nums[j]>temp);//直到nums[j]<=temp时,停止移动
                 nums[j+1]=temp;//填入nums[i],此时nums[j]处在正确位置nums[j+1]也向后移动了一位
             }
         }
    }

折半插入排序

  • 原理:

    • 在直接插入排序中,在有序区通过二分的方法找到无序区第一个元素应该存放位置index,然后统一将有序区index后元素后移一位
  • 特点:

    • 和直接插入排序相比性能没有改善, O ( n 2 ) 的 时 间 复 杂 度 O(n^2)的时间复杂度 O(n2),仅仅是减少了关键字比较次数
    • 稳定的排序
public void BinInsortSort(int[] nums,int n){
        /**
        对n个数进行折半插入排序

         */
         int temp;
         int lower,high;
         for(int i=1;i<n;i++){
             if(nums[i]<nums[i-1]){
                 //逆序时,排序
                 temp=nums[i];
                 lower=0;
                 high=i-1;//在[lower,high]做二分查找第一个大于等于num[i]的位置(j)
                 while(lower<high){
                     int mid=(lower+high)>>1;//此种二分要注意溢出问题,更普遍的方法是:lower+(high-lower)/2;
                     if(nums[mid]<temp){
                         lower=mid+1;//此种二分每次都能缩小区间,所以不可能出现死循环
                     }else{
                         high=mid;
                     }
                 }
                 //此时lower=high,第一个大于num[i]的位置j
                for(int j=i-1;j>=lower;j--){
                    //有序区lower后面的元素全部后移一位
                    nums[j+1]=nums[j];
                }
                //将lower位置赋值num[i]
                nums[lower]=temp;

             }
         }
    }

希尔排序

  • 一种分组插入排序

  • 选取一个小于数组长度n的整数 d 1 , 一 般 为 n / 2 d_1,一般为 n/2 d1,n/2,将所有距离为d1倍数的元素放在同一个组

    • 例如:1 2 3 4 5 6 7 8 9 10

    • 分为5组:

      1 6

      2 7

      3 8

      4 9

      5 10

      分别做直接插入排序,然后再分为2组、1组

    • 特点:

      • 希尔排序实现较复杂,时间复杂度难以计算,一般认为 O ( n 1.3 ) O(n^{1.3}) O(n1.3),
      • 是一种不稳定的排序
public void ShellSort(int[] nums,int n){
      /**
      希尔排序
          例子:5 1 1 2 0 0 n=6
          分3组:
              5      2        直接插入排序: 2    5
                1      0                    0     1
                  1      0                   0     1
              此时数组: 2 0 0 5 1 1
          分2组:
                      2    0   1      直接插入排序: 0 1 2
                         0   5    1               0 1 5
              此时数组: 0 0 1 1 5 2   //大部分已经有序
          分1组也就是数组本事,只有一个逆序,做一次插入排序即可
          
       */
      int temp;
      int d=n/2;//分组数量
      while(d>0){
          for(int i=d;i<n;i++){
              //对多组进行直接插入排序
              temp=nums[i];
              int j=i-d;//记录后移的位置
              while(j>=0&&temp<nums[j]){
                  //逆序就后移
                  nums[j+d]=nums[j];
                  j-=d;
              }
              //在正确位置上插入
              nums[j+d]=temp;
          }
          d/=2;
      }
    }

冒泡排序(超时)

  • 原理:

    • 在无序区中比较相邻的两个元素,逆序则交换
    • 注意:(冒泡排序元素的交换次数,就等于数组中的逆序对数,逆序对数 快速的求解方法为归并排序),这也是字节、微软考过的面试题,如果想了解此算法可以通过下面链接,在此就不贴代码了.
      逆序对数
public void BubbleSort(int[] nums,int n){
        /**
        超时!!!
        冒泡排序实现起来简单,但性能较低
         */
         if(n==1)
            return;
        boolean exchange=false;
        for(int i=0;i<n-1;i++){//n个元素只要找到前n-1大元素,就可以完成排序
            exchange=false;
            for(int j=n-1;j>i;j--){
                //每次把无序区最小值放到有序区的末尾
                if(nums[j]<nums[j-1]){
                    int temp=nums[j];//交换的临时变量
                    nums[j]=nums[j-1];
                    nums[j-1]=temp;
                    exchange=true;
                }
                
            }
            if(!exchange){
                    //如果遍历一次无序区,没有元素交换,则可以直接退出
                    return;
            }
        }
    }

快速排序

  • 注意快速排序是一个不稳定的排序算法,面试很可能会被问到如何实现稳定的快排

    • 首先快排的思想就是选取一个基准元素A,将所有小于A的元素放在一侧,大于A的元素放在另一侧,但由于传统快排实现中,只要小于或大于的基准元素就交换,很可能不满足稳定性

    稳定性: 大小相同的两个值在排序之前和排序之后的先后顺序不变

    • 稳定的快排需要借助一个辅助数组,遍历一遍数组,将小于基准元素加入数组中,再遍历一遍,将大于的加入数组中,最后将辅助数组转移到原数组中,稳定的快速排序因为需要多次遍历整个数组,因此效率会低很多。
  • 还要注意的是,快速排序空间复杂度: O ( l o g n ) O(logn) O(logn),因为递归树的高度: O ( l o g n ) O(logn) O(logn)所以需要需要栈空间 O ( l o g n ) O(logn) O(logn)

  • 快排常考第K大问题,快排思想可以实现 O ( n ) O(n) O(n)的时间复杂度

  • 传统快排算法

public void quickSort(int[] nums,int lower,int high){ //[lower,high)
        /**
         * 快速排序:
         *  从数组中取出一个数(通常第一个)
         *  将比这个数大的全放在右边,小的放在左边(得到某个位置,左边数都比这个数小,右边数都比它大)
         *  递归第二步得到的两个子数组
         *
         */
        if(lower<high){//结束条件
            int i=partition(nums,lower,high);
            quickSort(nums,lower,i);//[lower,high)右开
            quickSort(nums,i+1,high);
        }

    }
    public int partition(int[] nums,int lower,int high){

        int base=nums[lower];
        int i=lower;
        int j=high-1;   //[lower,high)右开
        while(i<j){
            while(i<j&&nums[j]>=base)
                j--;
            nums[i]=nums[j];
//            nums[j]=base;
            while(i<j&&base>=nums[i])
                i++;
            nums[j]=nums[i];
           
        }
        nums[i]=base;
        return i;

    }
  • 稳定的快速排序实现
    • 虽然理论上时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn),但是超时,没有通过样例
public void quickSort(int[] nums,int lower,int high,int[] temp){ //[lower,high)
        /**
         * 快速排序:
         *  从数组中取出一个数(通常第一个)
         *  将比这个数大的全放在右边,小的放在左边(得到某个位置,左边数都比这个数小,右边数都比它大)
         *  递归第二步得到的两个子数组
         *
         */
        if(lower+1<high){//结束条件
            int i=partition(nums,lower,high,temp);
            quickSort(nums,lower,i,temp);//[lower,high)右开
            quickSort(nums,i+1,high,temp);
        }

    }
    public int partition(int[] nums,int lower,int high,int[] temp){
        //[lower,high)排序
        int index;
        int base=nums[lower];
        int j=lower;//存储写入temp数组的索引
        for(int i=lower+1;i<high;i++){
            if(nums[i]<base){
                temp[j++]=nums[i];
            }
        }
        index=j;
        temp[j++]=base;
        for(int i=lower+1;i<high;i++){
            if(nums[i]>=base){//对于和base相等的元素,由于基准元素索引位lower,所以相等元素base放后面即可保证稳定性
                temp[j++]=nums[i];
            }
        }
        //将temp全部转移到nums即可
        for(int i=lower;i<high;i++){
            nums[i]=temp[i];
        }
        return index;

    }

选择排序(超时)

  • 思想:

    • 每一趟从待排序的元素中选出关键字最小的元素,放到已排好序的元素最后
    • 适合从大量元素中选择一部分排序元素,例如从10000个元素选择前10位(但是利用可以更快找到前K大元素)
public void SelectSort(int[] nums,int n){
        /**
        超时!!!
        选择排序:一次选择无序区最小值和无序区第一个交换
         */
        int index;//保存无序区的最小值索引
        for(int i=0;i<n-1;i++){
            index=i;
            for(int j=i+1;j<n;j++){
                if(nums[j]<nums[index]){
                    index=j;
                }
            }
            if(index!=i){
                //交换nums[i],nums[k]
                int temp=nums[index];
                nums[index]=nums[i];
                nums[i]=temp;
            }
        }
    }

堆排序

  • 由于建立初始堆所需比较次数较多,所以堆排序不适合元素数较少的排序表

  • 不稳定排序方法

  • 注意堆在优先队列中的应用

  • 注意:堆排序和选择排序一样,和初始序列顺序无关

  • 注意:

    • 以0为开始下标的两个子节点分别是:2i+1、2i+2
    • 以1为开始下标的两个子节点分别是:2i、2i+1
public void SelectSort(int[] nums,int n){
        /**
        堆排序:
            shit()方法:如果左右子树都是大根堆,插入当前结点后形成的树,继续保持大根堆的特性
            因此,只要从最后一个分支结点向前遍历,就可以建立一棵大根堆,这是因为当遍历到前面的一个结点i,以i为根节点的两颗子树2*i,2*i+1,都符合大根堆的特性!
          
         */
         for(int i=(n-2)/2;i>=0;i--){
             //注意数组下标从1开始
             shit(nums,i,n);
         }
         for(int i=n-1;i>0;i--){
             //将堆顶,也就是堆中最大元素,放到数组后面
             //交换
             int temp=nums[0];
             nums[0]=nums[i];
             nums[i]=temp;
             //堆顶元素变了,维护大根堆
             shit(nums,0,i);

         }
        
    }
    public void shit(int[] nums,int lower,int high){//维护[lower,high)的大根堆
        //以lower为根节点,维护大根堆特性
        int i=lower;//指向当前结点
        int j=2*lower+1;//当前节点的最大孩子节点
        int temp=nums[i];//
        while(j<high){
            if(j<high-1&&nums[j]<nums[j+1]){
                j=j+1;
            }
            if(temp<nums[j]){//如果根节点小于最大的孩子
                //交换
                nums[i]=nums[j];
                
                i=j;//递归到孩子节点
                j=2*i+1;
            }else{
                break;
            }
        }
        nums[i]=temp;

    }

归并排序

  • 思想:分治法

  • 应用:逆序对数

public void mergeSort(int[] nums,int lower,int high,int[] temp){   //排序[lower,high)
        /**
         * 归并排序:
         *      利用分治的思想,将数组分成两个小数组,直到数组长度为1(此时长度为1的数组本身就时排好序的)
         *      合并:将两个排好序的数组,合并成一个排序数组,时间复杂度 O(n)
         *          注意:因为排序数组为[lower,high) 所以 high-lower=1 时数组长度为1,因此high-lower>1 才需要分治
         */
        if(high-lower>1){
            int mid=lower+(high-lower+1)/2;
            mergeSort(nums,lower,mid,temp);
            mergeSort(nums,mid,high,temp);
            Merge(nums,lower,mid,high,temp);
        }

    }
    public void Merge(int[] num,int lower,int mid,int high,int[] temp){//[lower,high)
        //[lower,mid)和[mid,high)两个有序数组合并
        for(int i=lower;i<high;i++){
            temp[i]=num[i];
        }
        int i=lower;
        int j=mid;
        for(int k=lower;k<high;k++){
            if(i==mid){
                //如果[lower,mid)全部放入nums中
                num[k]=temp[j++];
            }else if(j==high){
                //如果[mid,high)全部放入nums中
                num[k]=temp[i++];
            }else if(temp[i]<temp[j]){
                num[k]=temp[i++];
            }else{
                num[k]=temp[j++];
            }
        }

    }

基数排序、桶排序

都可以实现 O ( n ) O(n) O(n)的时间复杂度,也是面试的常考点,这里没有实现,之后如果有例题啥的会补上。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值