八大排序算法详细分析,通俗易懂,基于Java实现

排序

时间复杂度先不展开讲,等到了实际的算法中,再慢慢的分析,这样就懂了.

排序分类

排序其实是一种算法,分为内部排序和外部排序,所谓内部排序是指在内存中进行排序,外部排序则需要借助一些外部的空间进行排序,比如磁盘文件.
我们一般学习的是内部排序,面试问的最多的也是内部排序.
内部排序的顺序图:

时间复杂度表:
image.png
时间的平均复杂度越低,性能整体上越好.比如o(n2)和o(nlogn),如果你知道这两个曲线的雏形就会了解,当n越大,n2就会便会变化得很快,而且是往大的方向增长.而后者的logn就不一样了,n越大,值会趋于平缓,时间复杂度趋于平缓(不是没有增长),且肯定没有n^2大,所以当n变大的时候,性能的优越性logn便更能体现出来.

冒泡排序

分析

闻如其名,冒泡,就是规定将大的值给排到最后,下一次冒泡就去掉上一次冒泡的值,这样每一次都取出本次冒泡的最大值,就是排序的一系列值了.

我们先分析它是如何冒泡出最大值的,首先拿到元数据,然后从下面分析,-1<136,理论上冒泡,但是我们按照大的往上冒,所以-1不动.第二次比较136>121,将136往上冒,第三次比较136>78大,继续往上冒,第四次比较136>99,继续往上冒.然后136没得比较了,就是本次冒泡最大的.

那为什么要多次冒泡?因为我们可以看到,经过一轮冒泡只是将本次冒泡的最大值拿到,但是后面121才是第二次最大的啊,所以还要进行多轮冒泡,直到最后只剩下一个泡泡要冒,就不进行该泡泡的冒出工作,因为没必要了,剩下的最后一个就是最小值了.

那么我们可以总结,进行一次冒泡,要经过四次循环比较,进行完全部冒泡,要经过4轮冒泡.所以可以得出以下结论:
有n个元素进行冒泡排序比较时,要进行n-1轮冒泡,每轮冒泡都有n-1次比较.

所以我们在写代码的时候,就需要两个for循环因为每次for循环都是n-1次,那么时间复杂度不就是o((n-1)2)了吗?这个(n-1)2等于n2-2*n-1,又因为时间复杂度计算的规则,当有指数项的时候,要去除掉系数项和常数项,所以这里的时间复杂度近似等于o(n2).

但这只是最大的时间复杂度,其实本质没有这么多,认真分析你会发现,笔者忽略了一件事.在每次冒泡的时候,我们是不需要将上次冒泡得到的最大值加入到本次冒泡的过程里的,所以本次冒泡的的迭代次数是上一次迭代次数-1,那么每次循环最多是n-1,不是每次循环都是n-1,真实的时间复杂度就比不上上面的分析的.

代码实现

package com.hyb.ds.排序.冒泡排序;


import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class BubbleSort<E> {

    public List<E> BubbleMaxSort(List<E> sort){

        int length = sort.size();
        //代码优化,如果当前冒泡这一轮一次都没有发生过冒泡,说明已经是顺序的了,后面用再排序了.
        boolean f=false;
        for (int i = 0; i < length - 1; i++) {
            for (int j = 0; j < length-i-1; j++) {
                E o1 = sort.get(j);
                E o2 = sort.get(j+1);
                if (o1 instanceof Integer && o2 instanceof Integer){
                    if (((Integer) o1).compareTo((Integer) o2) > 0){
                        f=true;
                        sort.set(j+1, o1);
                        sort.set(j,o2);
                    }
                }
            }
            if (!f){
                break;
            }
            f=false;
        }
        return sort;

    }
}

class Test{
    public static void main(String[] args) {
        BubbleSort<Integer> integerBubbleSort = new BubbleSort<>();
        /*
        * Arrays.asList(11, 2, -1, 33, 4, 55, 6) 返回的是
        * java.util.Arrays的静态内部类,
        * 并不是import java.util.List里的ArrayList
        * 所以要利用import java.util.List里的ArrayList进行初始化
        * */
        List<Integer> integers = integerBubbleSort.BubbleMaxSort(new ArrayList<>(Arrays.asList(11, 2, -1, 33, 4, 55, 6)));
        integers.forEach(System.out::println);

    }
}

快速排序**

快速排序是冒泡排序的一种升级,因为冒泡排序要将每两个相邻的值都进行比较,耗费的时间是比较久,所以产生了快速排序.

分析

  1. 找到一个基数,让基数放在数组中间某个位置,规定左边的数一定要小于该基数,右边的数一定要大于该基数.
  2. 只是这个基数怎么找是一个问题,甚至大多数人都不知道快速排序的原理是什么,请看下面这张图:

先选出一个基数,为6,也就是我们要将6放在中间某个位置,那么得左指针得往右找,找到第一个比6大的数停下来,这里是7,右指针往左找,找到第一个比6小的数为0,之后两个值交换位置:

  1. 之后左指针继续往右找,右指针继续往左找,规则一致,找到并交换,直到两个指针重叠,就达到了第一轮放基数的目的.
  2. 但这样还不行,虽然知道了6的左边是全小于它的,右边是全大于它的,但是左边的还是错乱的,右边的还是错乱的,所以以同样的方式分别处理两边,最后得到一个完整的从小到大的数组.

代码实现

  1. 首先指定基数,作为判断标准.
  2. 整体有一个大的循环,就是当左指针位置小于右指针位置的时候
  3. 在这个大循环内,分别有一个左循环和一个右循环,左循环负责寻找大于基数的工作,右循环负责找小于基数的工作,各自找到了第一个后,直接进行交换.
  4. 在大循环内注意若是左右循环各自找到的值相等,直接跳过,不进行交换.
  5. 大循环完毕后,注意,两个指针的位置一定是相等的,这个时候可以进行下一轮,调用两次本函数,实现左右递归
public class QuickSort {

    public void quickSort(int[] a,int leftIndex,int rightIndex){
        int i=leftIndex;
        int j=rightIndex;
        int key=a[i];
        while (leftIndex<rightIndex){

            while ((leftIndex<rightIndex)&&a[leftIndex]<key){
                leftIndex++;
            }

            while ((leftIndex<rightIndex)&&a[rightIndex]>key){
                rightIndex--;
            }
            if ((a[leftIndex]==a[rightIndex])&&(leftIndex<rightIndex)) {
                leftIndex++;
            }else{
                int t=a[leftIndex];
                a[leftIndex]=a[rightIndex];
                a[rightIndex]=t;
            }
        }
        if (leftIndex-1>i)
            quickSort(a,i,leftIndex-1);
        if (rightIndex+1<j)
            quickSort(a,rightIndex+1,j);


    }




    public static void main(String[] args) {
        QuickSort quickSort = new QuickSort();
        int [] ints=new int[]{6, 6, 2, 7, 9, 3, 4, 5, 10, 6,8};
        quickSort.quickSort(ints,  0, 10);
        for (int s :
                ints) {
            System.out.println(s);
        }

    }
}

选择排序

分析


加入有八个元素-1,3,6,1,13,123,2,12
那么第n次排序,就拿第n-1号位元素与后面所有元素(size-n个)的最小值做对比,如果自己最小,原地不动,如果自己最大,与之交换位置.(注意我图中的箭头是代表下一次交换的两个数),最多进行length-1次排序
如此迭代,直到最后仅剩123一个元素,就不用排序,这个值便是最大.

代码实现

package com.hyb.ds.排序.选择排序;

import java.util.Arrays;

public class SelectSort {

    public int[] selectSort(int[] sort){

        for (int i=1;i<=sort.length-1;i++){
            //假定p1就是最小的
            int p1=sort[i-1];
            int index=-1;
            for (int j=i+1;j<=sort.length-1;j++){
                int p3=sort[j-1];
                //如果后面有值比p1小
                if (p3<p1){
                    //让p1重新复制
                   p1=p3;
                   //拿到最小值的下标
                   index=j-1;
                }
            }
            //交换
            //index=-1代表后面没有比p1还小的值,那就是p1就是最小值
            if (index!=-1){
                //先让后面最小值的位置填入当前位置
                sort[index]=sort[i-1];
                //然后让当前位置填入后面的最小值
                sort[i-1]=p1;
            }

        }
        return sort;
    }
}

class Test{
    public static void main(String[] args) {
        SelectSort selectSort = new SelectSort();
        int[] p=new int[]{22,-1,3,32,12,90};
        int[] ints = selectSort.selectSort(p);
        System.out.println(Arrays.toString(ints));
    }
}

插入排序

分析


代码分析

从小到大排序:
前面的图可以看出,从1开始,发现123比它大就,就交换位置,每次都是这样,先比较前一位数据,但仅仅比较前一位数据还不行,比如到了88与256比较后交换位置会发现88所在位置的前一个位置的值还是比它大,所以又要交换位置,依次类推,直到88所在位置的前一个位置的值比88小,才说明88前面所有的值都比它小,这个时候就可以进行下一轮.

public class InsertSort {

    public int[] insertSort(int [] sort){

        for (int i = 1; i <= sort.length-1; i++) {
            int n=i;
            while (true){

                int f=sort[n-1];
                int e=sort[n];
                if (f>e){
                    sort[n]=f;
                    sort[n-1]=e;
                }else if (f<e)
                    break;
                n--;
                if (n==0)
                    break;


            }

        }
        return sort;
    }

    public static void main(String[] args) {
        InsertSort insertSort = new InsertSort();
        int[] ints = insertSort.insertSort(new int[]{123, 650, 15555, 34, 890, 4003});
        for (int i = 0; i < ints.length; i++) {
            System.out.println(ints[i]);
        }
    }

}

希尔排序

分析

![image.png](https://img-blog.csdnimg.cn/img_convert/a96ec6642ef2b874bcccea37e8cdb310.png#clientId=u9f221bb5-61bd-4&crop=0&crop=0&crop=1&crop=1&from=paste&id=u74953eee&margin=[object Object]&name=image.png&originHeight=502&originWidth=534&originalType=url&ratio=1&rotation=0&showTitle=false&size=172319&status=done&style=none&taskId=u7940fe1f-0a6a-4918-92a8-19b1e67e382&title=)
自己画的没那个意思,所以截取网上的一张图片,很清晰明了.

  1. 首次取数组长度n/2的值为步长进行一个交换排序,如第一轮十个数,所以步长是5,所以第一个和第六个数交换,依次类推,第二个和第七个交换…
  2. 进行完一轮之后取(n/2)/2的值,也就是上一个值的1/2,以这个步长进行交换排序.
  3. 直到步长为1,进行排序.

代码

public class ShellSort {

    public int[] shellSort(int[] a){
        int n=a.length;
        do {
            n = n / 2;
            if (n==1){
                InsertSort insertSort = new InsertSort();
                a=insertSort.insertSort(a);
            }
            for (int i = 0; i < n; i++) {
                if (a[i] > a[i + n]) {
                    int t = a[i + n];
                    a[i + n] = a[i];
                    a[i] = t;
                }
            }
        } while (n != 1);
        return a;
    }

    public static void main(String[] args) {
        ShellSort shellSort = new ShellSort();
        int[] ints = shellSort.shellSort(new int[]{9, 6,8,9,45,78,23, 7, 4, 3, 1});
        for (int i :
                ints) {
            System.out.println(i);
        }
    }
}

归并排序**

带了**的排序就有点难了,反正小编做不出来,所以觉得难.

分析

归并排序主要采用了一种分治思想,你可以理解为分合思想.
![image.png](https://img-blog.csdnimg.cn/img_convert/f71b620c4777758d511ac8c25a51ee17.png#clientId=u2fa86d18-f8c7-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=426&id=uc0d4a0ef&margin=[object Object]&name=image.png&originHeight=533&originWidth=674&originalType=binary&ratio=1&rotation=0&showTitle=false&size=256764&status=done&style=none&taskId=u8c2a63c8-82ff-4c01-85e8-47142baa3a5&title=&width=539.2)
先看尚硅谷这张图片,这样子一下子豁然开朗了吧.
归并排序主要的难点在于这个治,拆分倒是容易,主要是拆分过后如果进行合并,这里的合并是要按照规则合并,也就是我们要保证拆分后各个分组里必须是有序的,才能将各个分组合并,最终合并成一个有序的.

那么,我们不妨想一下,这个合并过程该怎么去做,请看下面这张图片:
![image.png](https://img-blog.csdnimg.cn/img_convert/f23869ae468a861eeafd4c55cdcd523d.png#clientId=u2fa86d18-f8c7-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=312&id=u7d0c9a11&margin=[object Object]&name=image.png&originHeight=390&originWidth=1228&originalType=binary&ratio=1&rotation=0&showTitle=false&size=261069&status=done&style=none&taskId=uca59ac2c-5387-4d0e-8d1f-7f3818ac0ed&title=&width=982.4)
合并过程用到了另一个新数组,这也是归并排序的缺点,就是浪费空间,但是效率是极高的,因为各个分组内是有序的,如果某个分组排完,另一个分组是可以直接转移的.

代码分析

  1. 使用递归进行拆分,分为左右两边拆分.
  2. 拆到左指针到达右指针为止,注意这里的左指针和右指针值得是每个分组里的.
  3. 之后就合并,合并的重点在于新数组如何将值复制给旧数组,不是简单的转移引用就行了,因为这个新数组是每次递归都会产生的引用,而旧数组只有一个数组,所以如果直接拷贝因为,就会让每次新的数组引用直接覆盖了原数组,所以只能将对应下表的值复制过去.
public class MergeSort {

    public static void mergeSort(int left,int right,int[] a,int[] b){

        if (left<right){
            //不能直接用长度计算mid,这要根据每个分组情况计算mid
            int mid=(left+right)/2;
            //左边递归
            mergeSort(left,mid,a,b);
            //右边递归
            mergeSort(mid+1,right,a,b);
            //合并
            merge(left,right,mid,a,b);
        }


    }

    private static void merge(int left,int right,int mid, int[] a,int [] b) {
        int i=left;
        int j=mid+1;
        int t=0;

        //只能循环左右两边,不能越过mid,或者越过right
        while (i<=mid && j<=right){
            if (a[i]<=a[j]){
                b[t]=a[i];
                i++;
                t++;
            }else{
                b[t]=a[j];
                j++;
                t++;
            }

        }


        //如果循环完毕,i不等于mid,j不等于right
        //说明两边都有剩余,直接将剩余的转移到b数组中
        while (i<=mid){
            b[t]=a[i];
            i++;
            t++;
        }
        while (j<=right){
            b[t]=a[j];;
            j++;
            t++;
        }

        //这一步和关键,要将新数组的值复制到原数组中
        //这里有人可能会问,能否直接返回新数组?
        //不行,因为这里是递归,有可能新数组针对的是旧数组某些分组进行排序,如果直接返回,就代替了原数组
        //所以这里只能一一复制,没有别的选择
        // 初始化新数组下标
        t=0;
        //初始化left,因为这个left是不能改变,切记一定要用给一个变量去接,因为在循环中可能被改变
        int bleft=left;
        while (bleft<=right){
            a[bleft]=b[t];
            t++;
            bleft++;
        }
    }


    public static void main(String[] args) {
        int[] a=new int[]{9,3,10,5,89,0,4};
        int[] b=new int[a.length];
        MergeSort.mergeSort(0,a.length-1,a,b);
        for (int i :
                a) {
            System.out.println(i);
        }
    }
}

基数排序

分析

基数排序的思想很简单,就是将每个数的位数进行拆分,然后按规律放在桶里,这样说可能有些抽象,可以看下面这张图:

遍历待排序的数组,从左到右,先到第一个数字11,发现个位是1,放在第一个桶内,碰到234,发现其个位数是4,放在第四个桶内…,如果出现个位数相同的,比如1234和234是相同的,那么也是放在第四个桶内,但是要往后放入.遍历一次后,再从桶内顺序拿出数字,如果某个桶含多个数,从上往下先遍历完该桶内数字,才能进入下一个桶内遍历.这里进行一轮后得到的顺序是: 11,234,1234,15,6,7,908.你会发现这个还不是最有序的,所以要进入下一轮,这个时候,是看十位数字,规律都一样,等最大数字的高位都遍历完后,才能完全结束,此刻数字一定有序,比如1234,这个数字是四位的,也就是要进行四轮放桶.如果中间过程出现有些数字没有高位,比如这里遍历1234的1的时候,11这个数就没有千位的1,只有个位和十位,这个时候将11放入第0个桶.

代码

  1. 一定要十个桶,0-9编号,因为所有出现的数位范围是0-9.
  2. 总的轮次是最大数的数位量,比如1234,就要四个轮次.
  3. 若是出现没有该数位的数字,则放入第0个桶内,比如11在第四轮遍历或者第三轮遍历的时候,由于没有百位,所以放入第0个桶.
  4. 要记录每个桶每轮放入的数量,这样方便从桶内拿出数据,这个放入的数量可以是一个数组,保存着每个桶的实际存放数量.
  5. 也可以巧妙的保存: 舍去每个桶的第0个位置,该位置用来存放本桶的实际存放数量,那么实际存放元素的是第1位开始的,这样节省重新创建数组保存每个桶实际存放数量的空间.
public class BucketSort {

    public static void bucketSort(int [] a){

        //10为桶数量,a.length代表每个桶存放的最多数量
        int[][] bucket=new int[10][a.length];
        //求出最大值
        int max = maxValue(a);
        //计算最大值位数
        int count = count(max);

        int mod=1;

        for (int i = 0; i < count; i++) {
            for (int j = 0; j < a.length; j++) {
                int k=(a[j]/mod)%10;
                //从1开始放
                int index=++bucket[k][0];
                bucket[k][index]=a[j];

            }
            //取出元素放到原数组中
            int index=0;
            for (int m=0;m<10;m++){
                if (bucket[m][0]!=0){
                    for (int n=1;n<=bucket[m][0];n++){
                        a[index]=bucket[m][n];
                        index++;
                    }
                    //相等于情况,实际没清空,但这一步很重要
                    bucket[m][0]=0;
                }
            }


            mod=mod*10;
        }
    }

    private static int count(int value){
        int v=value;
        int count=0;
        while (v!=0){
            v=v/10;
            count++;
        }
        return count;
    }

    private static int maxValue(int[]a){

        int max=a[0];
        for (int i = 1; i < a.length; i++) {
            if (max< a[i]){
                max=a[i];
            }
        }
        return max;

    }


    public static void main(String[] args) {
        int[] a=new int[]{10,18,9,923,69842,90,1781978};
        long l = System.currentTimeMillis();
        System.out.println(l);
        BucketSort.bucketSort(a);
        System.out.println(System.currentTimeMillis());
        for (int i = 0; i < a.length; i++) {
            System.out.println(a[i]);
        }
    }
}

堆排序

堆排序要理解堆的结构,请翻看我们的主页查阅.

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值