六大排序算法精讲(SelectSort、BubbleSort、InsertSort、MergeSort、QuickSort、HeapSort)

一、前言

        最近在看算法公开课,听了很多老师讲了不少很有用的东西,但是只听不练很快就忘了,所以我还是选择以博客的方式巩固自己的学习。

二、排序算法概述

        今天主要讲的是比较经典的六大排序算法:选择排序、冒泡排序、插入排序、归并排序、快速排序和堆排序,除此之外还有一个比较重点的基数排序,但是因为它是不基于比较的排序,所以用法不是很广泛,所以我也只讲解它的实现原理,不多做阐述,并且,在讲解之余,我也会穿插一些其他内容。

三、选择排序

        选择排序是最基础的一种排序之一,它的实现方式是:对一个无序数组,先通过遍历查找整个数组的最大值放到最后一个,然后再对未排序部分继续查找最大值,直到查找完毕为止(当然也可选择最小值)。

        举个例子就是:

        有一个无序数组长度N=7,我所做的第一件事就是将数组从0到N-1遍历一遍,然后找到最大值7, 然后和第N-1,也就是最后一个数交换,之后变成了:

        此时,最后一个数就已经排好了,所以在接下来的排序中,我们就可以不用管这个数了。现在我们再对前N-2也就是前6个数进行相同操作,将前六个数最大值5和最后第六个数交换,如下:

        然后,第六个数也排好了,可以不用管了,再对前五个数进行操作:

        重复操作直到对前0个数进行排序。

        代码如下:

    public static void selectsort(int[]arr){
        if(arr==null||arr.length<2)
            return;
        for(int i=0;i<arr.length-1;i++){
            int minindex=i;
            for(int j=i+1;j<arr.length;j++){
                minindex=arr[j]<arr[minindex]?j:minindex;
            }
            swap(arr,i,minindex);
        }
    }
    public static void swap(int[]arr,int i,int j){
        arr[i]=arr[i]^arr[j];
        arr[j]=arr[i]^arr[j];
        arr[i]=arr[i]^arr[j];
    }

         这里的交换操作,我用的是异或运算,异或运算的详细解析,见这份博客:异或运算

        分析:我们最后看一下选择排序的时间复杂度:假设数组长度是N,那么我们外层for循环的复杂度就是O(N),所以里面需要进行的循环次数从N到N-1,到N-2,是一个等差数组。所以一共循环的次数是N+N-1+N-2+...+1,复杂度是O(N^2)。

四、冒泡排序

        ·冒泡排序也是一种比较基础的排序,它的实现方法也是遍历数组,然后相邻元素一一进行比较,如果要升序排序的话,每次比较的时候都把大的那个放在后面,这样一轮循环下来,最后一个也就被排好了,然后和选择排序一样,固定排序好的那个数,对没有排序好的数继续进行操作,直到数组的每个数都被放到正确的位置。

        流程如下:

 

 

 

 

 

        一轮结束后,最大值已经放在最后一个,然后对前六个数继续操作。

代码:

    public static void bubblesort(int[]arr){
        if(arr==null||arr.length<2)
            return;
        for(int i=arr.length-1;i>=0;i--){
            for(int j=0;j<i;j++){
                if(arr[j]>arr[j+1])
                    swap(arr,j,j+1);
            }
        }
    }
    public static void swap(int[]arr,int i,int j){
        arr[i]=arr[i]^arr[j];
        arr[j]=arr[i]^arr[j];
        arr[i]=arr[i]^arr[j];
    }

 分析:冒泡排序依然是要对数组进行遍历,依然是第一次遍历的长度是N,第二次是N-1,和选择一样,时间复杂度为O(N^2)。

五、插入排序

        插入排序我认为和冒泡排序还是挺像的,它的实现方法是,给定一个无序数组arr,我们假设它最开始有序的长度是1,也就是arr的前1个数时有序的,这是肯定成立的。

        之后从第二个数开始,我想要前2个数变的有序,我就需要将第二个数插入到前1个数都某个地方,使得前2 个数正好有序,如图:

        

        4要想和3构成的数组有序,就要选择合适的地方插入,4和3比较,4比较大,所以原地不动就可以。然后又来了个1: 

 

        现在已知前2个数时有序的,那么1插入之后要想有序,必须插入到合适的位置上,1和4比较,4更大,所以1应该插入到4之前,于是1再和3比较,比3小,应该插入到3之前,但是3之前没有了,所以就把1放在第一个。

         那么前三个数也有序了,与此同理:下面的操作如下:

 

 

代码:

 public static void insertsort(int[]arr){
        if(arr==null||arr.length<2)
            return;
        for(int i=1;i<arr.length;i++){
            for(int j=i-1;j>=0;j--){
                if(arr[j]>arr[j+1])
                    swap(arr,j,j+1);
            }
        }
    }
    public static void swap(int[]arr,int i,int j){
        arr[i]=arr[i]^arr[j];
        arr[j]=arr[i]^arr[j];
        arr[i]=arr[i]^arr[j];
    }

         分析:插入排序与前面两个排序的不同之处在于,它排序的好坏取决于数据的整齐程度,而冒泡排序和选择排序,不管原本的数组是什么样的,都要老老实实的遍历,最终结果也总是O(N^2)。但是插入排序的不同之处在于,在最优情况下,给定的数组的排好序的,也就是说每次只需要查看一下当前数是否比前一个数要大即可,复杂度是O(N),而最差情况下给定的数组的逆序给定的,那么它需要执行的次数就和选择和冒泡一样了,是N+N-1+N-1+...1,为O(N^2)的复杂度。所以插入排序平均来说还是优于前两个排序的,但是由于我们说时间复杂度都是按照最坏的情况下计算的,所以我们依然将插入排序归类为O(N^2)的排序。

六、归并排序

        从这个排序开始,都是比O(N^2)快的排序的,当然也相对前面三种排序更难理解一些。

        归并排序的方法是:我想要排序一个数组,那我就先将数组等分成两部分,分别排序两个部分,然后再对排序好的两个部分再进行个排序不就好了?那么就递归调用,直到被划分的小数组只有两个的时候,我们开始排序。这样一来,很容易就得到两个排好序的子数组,现在对他们进行合并:怎么合并呢?我们需要一个长度为两个数组之和的辅助数组,通过双指针解决:

        

        首先两个指针分别指向两个数组的开头,比较大小,将小的那个放入数组,然后指针右移:

 

重复操作直到有一边越界为止:

        然后有一边越界之后,退出循环,对没有将没有越界的一边的剩余的数加到辅助数组里去。

 

        这样两个小数组就排好序了,接下来我们只需要将辅助数组一一对应的赋值给我们真实数组即可。然后把排好序的数组和另外一个等大的数组继续进行归并,直到整个数组都有序为止。

         代码:

 public static void mergesort(int[]arr){
        if(arr==null||arr.length<2)
            return;
        process(arr,0,arr.length-1);
    }
    public static void process(int []arr,int l,int r){
        if(l==r)
            return;
        int mid=l+((r-l)>>1);
        process(arr,l,mid);
        process(arr,mid+1,r);
        merge(arr,l,mid,r);
    }
    public static void merge(int[]arr,int l,int m,int r){
        int[]help=new int[r-l+1];
        int p1=l,p2=m+1;
        int i=0;
        while(p1<=m&&p2<=r){
            help[i++]=arr[p1]<=arr[p2]?arr[p1++]:arr[p2++];
        }
        while(p1<=m){
            help[i++]=arr[p1++];
        }
        while(p2<=m){
            help[i++]=arr[p2++];
        }
        for(i=0;i<help.length;i++){
            arr[l+i]=help[i];
        }
    }

        分析:它的时间复杂度怎么计算呢?前面说了,我们将数组依次对半分开,然后对分开的子数组计算,那么分开之后看起来其实是一个二叉树,计算的次数就是二叉树的高logN,然后进行的merge又要遍历一整个长度的数组,所以算法时间复杂度应该是O(NlogN),额外空间复杂度是辅助数组的长度O(N)。

七、快速排序

        快速排序也是一个很经典的排序,它有几个不一样的版本,其实区别并不大。我们直接所3,0版本吧。3.0版本的快排就是在无序数组中任意选择一个数和最后一个数交换,然后将小于这个数的数放在数组的左边,大于这个数的数放在数组的右边,等于这个数的数放在数组的中间,递归着实现即可。所谓1.0版本就是选择最后一个值作为划分值,将小于等于它的放在左边,大于它的放在右边;所谓2.0版本就是依然选择最后一个数进行划分,小于它的放左边,大于它的放右边,等于它的放中间。

        我们为了简便计算,通过2.0版本演示一下:

        

 代码:

        

    public static void quicksort1(int []arr){
        if(arr==null||arr.length<2)
            return;
        quicksort(arr,0,arr.length-1);
    }
 public static void quicksort(int[]arr,int l,int r){
        if(l<r){
            swap(arr,l+(int)(Math.random())*(r-l+1),r);
            int[]p=partition(arr,l,r);
            quicksort(arr,l,p[0]-1);
            quicksort(arr,p[1]+1,r);
        }
    }
    public static int[] partition(int[]arr,int l,int r){
        int less=l-1;
        int more=r;
        while(l<more){
            if(arr[l]<arr[r]){
                swap(arr,++less,l++);
            }else if(arr[l]>arr[r]){
                swap(arr,--more,l);
            }else {
                l++;
            }
        }
        swap(arr,more,r);
        return new int[]{less+1,more};
    }
    public static void swap(int[]arr,int l,int r){
        arr[l]=arr[l]^arr[r];
        arr[r]=arr[l]^arr[r];
        arr[l]=arr[l]^arr[r];
    }

        复杂度依然和树结构相似,是O(NlogN)。

 八、堆排序

        在介绍堆排序之前,我们先了解一下堆,堆其实就是一个二叉树,堆分为大根堆和小根堆,所谓大根堆就是指任何一颗树头结点都是最大的,小根堆同理,如下就是一个大根堆:

        

         任何一棵树都比它子节点的值大,这就是大根堆,那么堆有哪些操作呢?

        堆主要操作有两个,一个是插入一个元素后重新把大根堆转化为大根堆,一个是将堆中任何一个元素变化后,重新构建大根堆,具体方式在这就不多说了,直接上代码吧。

        

    public static void main(String[] args) {
        int []arr={2,5,1,6,1,6,3,61,6,1,2,3};
        heapsort(arr);
        for(int a:arr){
            System.out.println(a);
        }
    }
    public static int[] heapsort(int[]arr){
        //安排成大根堆
//        for(int i=0;i< arr.length;i++){
//            heapInsert(arr,i);
//        }
        //下面过程和上面一样,稍微快了一点
        //变大根堆应该从下开始
        for(int i=arr.length-1;i>=0;i--){
            heapify(arr,i, arr.length);
        }
        int heapsize= arr.length;
        swap(arr,0, --heapsize);
        while(heapsize>0){
            heapify(arr,0,heapsize);
            swap(arr,0,--heapsize);
        }
        return arr;
    }
    public static void heapInsert(int[]arr, int index){
        //循环停止的条件是两个,一个是当前节点并不比父亲节点要打,循环停止
        //或者是到达头结点的时候,index和(index-1)/2都是0,那么依然不满足循环,退出循环

        while(arr[index]>arr[(index-1)/2]){
            swap(arr,index,(index-1)/2);
            index=(index-1)/2;
        }
    }
    public static void heapify(int []arr,int index,int heapsize){
        int leftson = index*2+1;
        while(leftson<heapsize){
            int max=leftson+1<heapsize&&arr[leftson+1]>arr[leftson]?leftson+1 : leftson;
            max = arr[max]>arr[index]?max:index;
            if(max==index)
                break;
            swap(arr,index,max);
            index=max;
            leftson=index*2+1;
        }
    }
    public static void swap(int[]arr,int i,int j){
        arr[i]=arr[i]^arr[j];
        arr[j]=arr[i]^arr[j];
        arr[i]=arr[i]^arr[j];

    }

 

 

 

  • 5
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

少๑渊

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值