图解八大排序算法 - Java版

前言

排序算法是编程中最基础的也是一名合格的开发者必须掌握的算法,常见的排序算法大概分为八种:冒泡排序、快速排序、插入排序、希尔排序、选择排序、堆排序、归并排序、基数排序,据我所知很多开发者只是掌握了冒泡、快排、选择这几种比较简单的排序算法,因为其余几种略有难度也让很多人对其丧失了兴趣,所以特出此文章来帮助这部分人。本文会以画图的形式来详细描述以上八种排序算法,相信聪明的你一定会搞明白的。

注:由于堆排序会牵扯到二叉树,所以就不再本文中进行讲解,我会在以后的二叉树文章中重点描述

1. 冒泡排序

冒泡排序属于交换排序的一种,从数组的第一个角标开始逐个与后面的元素进行比较,如果小于就将其置换,语言描述略显空洞.....下面来看图:

bubble.PNG

首先取出第一个元素,与后面的元素挨个比较,如果大于后面的某个元素就将两个元素位置互换,然后继续比较直到最后一个。第二轮从第二个元素开始比较、第三轮从第三个以此类推,最后一轮比较完毕就会形成一个有序数组,下面我们拉看代码部分:

        int[] arr = {5,1,2,9,7};
        //轮数
        for (int i=0;i<arr.length-1;i++){
            //从第i个元素开始比较
            for (int j = i+1;j<arr.length;j++){
                if(arr[i]>arr[j]){
                    //交换位置
                    int temp = arr[i];
                    arr[i] = arr[j];
                    arr[j] = temp;
                }
            }
            System.out.println("第"+(i+1)+"轮:"+Arrays.toString(arr));
        }
        System.out.println(Arrays.toString(arr));

来看打印结果:

第1轮:[1, 5, 2, 9, 7]
第2轮:[1, 2, 5, 9, 7]
第3轮:[1, 2, 5, 9, 7]
第4轮:[1, 2, 5, 7, 9]
[1, 2, 5, 7, 9]

冒泡排序稳定程度还是挺高的,推荐大家使用

2. 快速排序

快速排序也属于交换排序的一种,要比冒泡略微复杂一些,排序原理:从数组中选取一个标准数,小于标准数的元素放在左边、大于标准数的元素放在右边,然后把分开的两个数组再进行以上操作,此步骤可通过递归来实现。下面来看图:

为节约画图时间,我会尽可能的把数组长度变短
quick.png

选择5为标准数进行分边,将1、2放在左边,7、9放在右边,通过递归将左右两个数组再次进行分边,当不能再分即数组块<2时跳出方法,以此类推最后会得出一个有序数组,下面来看代码部分:

public static void quickSort(int[] arr,int start,int end){
        //递归结束条件
        if(start>=end){
            return;
        }
        int standard = arr[start];//选取第start个元素为标准数
        int low = start;//低位指针
        int high = end;//高位指针
        //通过一个循环将小的数字分配到标准数左边、
        //大的放在右边,标准数放在中间
        //循环的结束条件为两个指针碰撞即low==high,
        while (low<high){
            //首先从高位开始找比标准数小的数字
            while (low<high&&standard<=arr[high]){
                //将高位角标+1继续比较
                high--;
            }
            //找到比标准数小的数字放在low角标处,此时的low即start
            arr[low] = arr[high];

            //从低位开始找比标准数大的数字
            while (low<high&&arr[low]<=standard){
                //将低位指针+1继续比较
                low++;
            }
            //找到比标准数大的数字放在high角标处
            arr[high] = arr[low];
        }
        //能执行到这说明low==high,将标准值放在low/high角标中
        arr[low] = standard;
        System.out.println("start:"+start+"--end:"+end+"--"+Arrays.toString(arr));
        //执行完以上步骤后就完成了第一轮数字分配,
        //通过递归将分开的两个数组再次进行数组分配
        quickSort(arr,start,low-1);//将左边数组进行数字分配
        quickSort(arr,low+1,end);//将右边数组进行数字分配
    }

然后执行如下代码:

 int[] arr = {5,1,7,9,2};
 quickSort(arr,0,arr.length-1);
 System.out.println("-------------");
 System.out.println(Arrays.toString(arr));

来看打印结果

start:0--end:4--[2, 1, 5, 9, 7]
start:0--end:1--[1, 2, 5, 9, 7]
start:3--end:4--[1, 2, 5, 7, 9]
-------------
[1, 2, 5, 7, 9]

注释写的很清楚就不多做解释。

3. 直接插入排序

插入排序也是一种比较简单的排序算法,虽然简单但是......图不好画啊!!!想了半天都不知道怎么画,所以本小节就不画图了,我尽量用文字描述的清晰一些。

原理:从第二个元素开始与第一个元素进行比较,如果小于第一个元素则交换位置。然后从第三个元素开始于第二个元素进行比较,如果小于第二个元素进行位置互换,再拿着第二个元素跟第一个元素比较,大于第一个元素就交换位置,以此类推,是不是很简单?相信你肯定明白了,下面来看代码部分:

     int[] arr = {5,1,7,9,2};
     for (int i=1;i<arr.length;i++){
            //如果当前遍历数字小于前一个数字
            if(arr[i]<arr[i-1]){
                int temp = arr[i];//记录下来当前遍历的数字
                int j;
                //将temp与前面数字进行比较,
                //如果小于前面数字将前一数字,
                //就将当前数字设置成与前一数组相同
                for (j = i-1;j>=0&&arr[j]>temp;j--){
                    arr[j+1] = arr[j];
                }
                //最后还要将temp放在j+1的位置
                arr[j+1] = temp;
            }
     }
    System.out.println(Arrays.toString(arr));

打印结果:

[1, 2, 5, 7, 9]

代码可能与上面的文字描述有些出入,但目的都是为了交换位置。

4. 希尔排序

上小节的直接插入排序还是存在很大的效率问题的,如果比较小的数字全在数组后面那么就会进行大量的遍历跟赋值,为了解决这个问题一名叫希尔的老哥推出了希尔排序,它会将数组中比较小的数字尽可能往前移,然后进行插入排序。

原理:首先获取到数组长度,将长度/2得到一个步长,我来举个例子来说明一下步长的作用,假如有一个长度为5的数组,那么步长就是2,将第4个元素和第2(4-步长)个元素进行比较,小于前面数字则交换位置,再将第2个元素与第0(2-步长)个元素比较,小于前面数字交换位置。然后再将第3个和第一个比较,以此类推,比较完一轮后将步长/2进行下一轮比较。下面来看图:

shell.png

  • 第一轮:将5、7、2和1、9进行比较得到第二轮
  • 第二轮:将2、1、5、9、7按步长为1进行比较

讲过以上两轮就可实现排序,下面来看代码部分:

        int[] arr = {5,1,7,9,2};
        //遍历步长,每遍历一轮步长除2,直到小于等于0跳出循环
        for (int d = arr.length/2;d>0;d/=2){
            
            //遍历每个元素
            for (int i = d;i<arr.length;i++){
                //遍历本步长组中的元素
                for (int j=i-d;j>=0;j-=d ){
                    //将当前元素与加上步长的元素比对
                    //如果当前元素大就交换位置
                    if (arr[j]>arr[j+d]){
                        //交换位置
                        int temp = arr[j];
                        arr[j] = arr[j+d];
                        arr[j+d] = temp;
                    }
                }
            }
            System.out.println("d:"+d+Arrays.toString(arr));
        }
        System.out.println(Arrays.toString(arr));

打印结果:

d:2[2, 1, 5, 9, 7]
d:1[1, 2, 5, 7, 9]
[1, 2, 5, 7, 9]

第一个for循环步长的轮数,第二个for循环从步长开始遍历后面每个元素,第三个for循环用于遍历一个步长组然后交换位置。

5. 简单选择排序

选择排序跟冒泡排序类似,从一个数字开始往后遍历,选出最小值并记录其角标然后第一位进行位置交换,再从第二个数字开始做以上操作以此类推,下面看图:


select.png

红色框内为遍历区域,很简单,甚至是八中排序里最简单的一种,下面来看代码:

    int[] arr = {2,3,5,1};
    for (int i=0;i<arr.length-1;i++){
            int index = i;
            for (int j=i+1;j<arr.length;j++){
                //记录本轮最小值角标
                if(arr[j]<arr[index]){
                    index = j;
                }
            }
            //交换位置
            if(i!=index) {
                int temp = arr[i];
                arr[i] = arr[index];
                arr[index] = temp;
            }
            System.out.println("第"+(i+1)+"轮:"+Arrays.toString(arr));
      }
     System.out.println("----------------");
     System.out.println(Arrays.toString(arr));

打印结果:

第1轮[1, 3, 5, 2]
第2轮[1, 2, 5, 3]
第3轮[1, 2, 3, 5]
----------------
[1, 2, 3, 5]

选择排序从逻辑到代码都非常简单,下面几种排序一种比一种难,你准备好了吗?

6. 归并排序

归并排序就是将一堆无序数字分成两部分,左右两边都保证有序,最后再将两边有序数字进行排序,下面来看图:


merge.png

通过递归的方式将数组拆分直到被拆分的数组长度<=1未知,原始数组可拆分为A和B,A数组又可拆分为C和D,D数组又可拆分为G和H,数组G和H长度都为1所以不能再往下进行拆分,因为数组G和H长度都为1所以可视为两个数组都是有序的,然后通过归并算法将G和H合并成一个有序数组,此时数组D就变成了[1,2],再通过归并算法将C和D合并成有序数组,此时A就变成了[1,2,5],依次类推最终就可以实现排序功能。下面我们先来看归并算法的实现:

//归并算法就是将左右两边的两个有序数组归并成一个有序数组
public static void merge(int[] arr,int low,int middle,int high){
        int[] temp = new int[high-low+1];//创建一个临时数组
        int i = low;//第一个数组需要遍历的角标
        int j = middle+1;//第二个数组需要遍历的角标
        int index = 0;//记录临时数组的下表
        //遍历两个数组,取出小的数字放入临时数组中
        while (i<=middle&&j<=high){
            //把小的数据放入临时数组中,小的一方角标+1
            if(arr[i]<arr[j]){
                temp[index] = arr[i];
                i++;
            }else {
                temp[index] = arr[j];
                j++;
            }
            //临时数组角标+1
            index++;
        }
        //处理右边多余的数据
        while (j<=high){
            temp[index] = arr[j];
            j++;
            index++;
        }
        //处理左边多余的数据
        while (i<=middle){
            temp[index] = arr[i];
            i++;
            index++;
        }

        //将临时排好序的临时数组放入到原数组
        for (int k=0;k<temp.length;k++){
            arr[low+k] = temp[k];
        }
    }

重点说一下下面的两个取多余数据的代码,首先这两个while循环是互斥的,什么时候会执行这两个while循环呢?假如左边的所有数据都小于右边的第一个数据,此时会将左边数组全部放到临时数组中,当放入最后一个元素后左边数组的角标i已经>middle了,会跳出第一个while循环,但是右面的元素还没有放入到临时数组,所以要将右边多余的数字放入到临时数组。其他部分注释标的都很清楚就不一一叙述了,下面来看数组拆分的算法:

    public static void mergeSort(int[] arr,int low,int high){
        //递归结束条件
        if(high<=low){
            return;
        }
        int middle = (high+low)/2;
        //处理左边
        mergeSort(arr,low,middle);
        //处理右边
        mergeSort(arr,middle+1,high);
        //归并
        merge(arr,low,middle,high);
    }
    ...
    //以下代码在main()方法中运行
    int[] arr = {2,3,5,1,11,2,15};
    mergeSort(arr,0,arr.length-1);
    System.out.println(Arrays.toString(arr));

通过递归的方式拆分数组,然后结合上面写的归并算法进行排序。下面来看代印结果:

[1, 2, 2, 3, 5, 11, 15]

归并算法还是略有难度的,当时我也看了好几遍才搞明白,所以看不懂没关系多看几遍就行了。

7. 基数排序

我认为基数排序是最有意思的一种排序。思路:创建一个长度为10的二维数组,遍历无序数组,将个位数为0 - 9的放在二维数组的第0-9个位置,然后按顺序将元素取出,再将十位数为0 - 9的放在二维数组的第0-9个位置,然后再取出,以此类推最后会得到一个有序数组,下面来看图:

radix.png

首先创建一个二维数组也就是图中中间的那个数组,然后遍历需要排序的数组将个位数为0 - 9的元素放入到二维数组中0 - 9位置,然后再从前到后将元素逐个取出,这样第一轮就完成了,然后再进行下一轮,进行的轮数就是最大数的长度。有些同学可能会有一个疑问,通过这种方式会实现排序功能吗?其实当时我也是这么想的,举个图中例子:在经历了第一轮放入-取出后形成的新数组在各位数上是有序的,会导致十位数相同个位数不同肯定是个位数大的在后面放着,比如图中的12和17,17肯定会在12后面,所以通过这种方式是可以实现排序的。下面来看代码部分:

 //基数排序
    public static void radixSort(int[] arr){
        //存数组中最大的数,目的获取其位数
        int max = Integer.MIN_VALUE;
        for (int x:arr){
            if(x>max){
                max = x;
            }
        }
        //最大值长度
        int maxLength = (max+"").length();
        //创建一个二维数组存储临时数据
        int[][] temp = new int[10][arr.length ];
        //创建一个数组,用来记录temp内层数组存储元素的个数
        int[] count = new int[10];
        //将数据放入二维数组中
        for (int i =0,n=1;i<maxLength;i++,n*=10){
            for (int j=0;j<arr.length;j++){
                int number = arr[j]/n%10;
                //将number放入指定数组的指定位置
                temp[number][count[number]] = arr[j];
                //将count数组中记录元素个数的元素+1
                count[number]++;
            }
            //从二维数组中取数据
            int index = 0;
            for (int x=0;x<count.length;x++){
                if(count[x]!=0){
                    for (int y=0;y<count[x];y++){
                        arr[index] = temp[x][y];
                        index++;
                    }
                    count[x] = 0;
                }
            }
        }
    }
    ...
    //以下代码在main()方法中运行
    int[] arr = {11,2,6,552,12,67,88,72,65,23,84,17};
    radixSort(arr);
    System.out.println(Arrays.toString(arr));
  • 首先遍历数组取出最大值,通过最大值确定轮数
  • 创建一个二维数组和一个存放二维数组每个角标中元素个数的数组
  • 将元素放入到二维数组中指定的位置
  • 从二维数组中逐个将元素取出

来看打印结果:

[2, 6, 11, 12, 17, 23, 65, 67, 72, 84, 88, 552]

原理通过图已经说清楚,代码部分注释写的也很清楚所以就不再进行赘述。

总结

以上几种排序难易程度大概可表示成(个人见解):选择<冒泡<插入<快速<希尔<基数<归并,每个算法在不同场景中都有各自的优点,所以搞清楚了每个算法的原理后,在实际的需求中你会快速的选择出一个效率最高、最合适的排序算法。本篇文章差不多就这些内容了,这也可能是年前我的最后一篇技术类文章,年后我可能会出一系列数据结构文章,是可能,大家期待一下吧。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 161,780评论 4 369
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,424评论 1 305
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 111,397评论 0 254
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,576评论 0 218
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,997评论 3 295
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,945评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,107评论 2 317
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,850评论 0 208
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,625评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,804评论 2 253
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,285评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,613评论 3 261
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,291评论 3 242
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,164评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,963评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,096评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,886评论 2 278

推荐阅读更多精彩内容

  • 排序算法中,经常见到的有八种排序算法,这里我们不包括在内存的外部进行排序。这八种排序算法分别是: 冒泡排序、选择排...
    teaGod阅读 1,273评论 0 7
  • 简单来说,时间复杂度指的是语句执行次数,空间复杂度指的是算法所占的存储空间 时间复杂度计算时间复杂度的方法: 用常...
    Teci阅读 1,009评论 0 1
  • 排序的基本概念 在计算机程序开发过程中,经常需要一组数据元素(或记录)按某个关键字进行排序,排序完成的序列可用于快...
    Jack921阅读 1,364评论 1 4
  • 知 识 点 / 超 人 数据结构算法排序是比较枯燥的知识,学习一定要耐着性子看,不然很容易理解错误。本文比较适合自...
    树下敲代码的超人阅读 5,135评论 9 58
  • 在我进入茶道的初期,有一次去一位前辈那里喝茶。 突然一位神神秘秘的人开了门, 拎着一个大袋子走进来。 到了前辈的身...
    额吖阅读 207评论 0 0