目录
前言:
嘿嘿,大家看到这篇文章的时候相信过了很久很久,是的,这篇文章差不多是作者耗时最长的一篇博客,字数也是最多的,文章里的所有的一切,除了代码思想和部分思路分析是借助于老师的之外,其他任何都是作者与c++资深人士共同完成的,如图解的完成(特此说明,没有照搬网上的动图,我不放心),我将会从一下几点开始阐述每一个排序算法.
排序算法的动图图解 🔜 排序算法的概述 🔜 排序算法的思想 🔜 排序算法的起源代码与分析 🔜 排序算法的最终代码实现与分析 🔜 排序算法的优化 🔜 排序算法的结论
ps:起源代码 到 最终代码 会有一个推导过程,让读者更好的理解代码的由来,而不是泛泛而谈!
起源代码会与思路紧紧联系,建议看完思路和起源代码尝试自己写一下最终代码(有些可能没有起源代码)
点赞+评论+关注+收藏 即可领取原版思维导图哦!记得私聊我
特别鸣谢:
c.Coder
文章注意事项:
本文是Java版,若要c++版请通过此传送门🚪在左边的门哦!
所有的图解都请勿随意搬运!若要搬运请联系作者
起源代码从图解出发,旨在读者更好理解代码
最终代码会与起源代码有所不同,旨在加深读者对排序算法的认识
时间复杂度与空间复杂度:
测量算法时间的方法:
事前估算法:
通过分析算法复杂度来判断更好的算法
事后统计法:
通过运行代码,计算一个算法所需要的总时间
问题:
需要消耗大量的时间,且代码的运行效率还与计算机硬件和操作系统有关,导致数不准确
时间频度
定义:
在Java中,代码的执行次数被称为时间频度,时间频度与代码执行次数成正比,时间频度即语句频度.记为T(n)
要点:
- 可以忽略常数项
- 可以忽略低次幂项
- 可以忽略最高次幂项的常数项
函数图解(解释要点)
1.常数项可以省略
解释:
从图可得,当x足够大的时候,两个函数几乎重合在了一起,说明里面的常数项是可以省略的
2.低次幂项可以省略
解释:
从图可得,当x足够大的时候,两个函数几乎重合在了一起,说明里面的低次幂项是可以省略的
3.最高次幂的常数项可以省略
解释:
从图可得,当x足够小的时候,两个函数几乎重合在了一起,说明里面的最高次幂项的系数是可以省略的.因为我们的代码不能能达到这么高的嵌套循环执行次数,所以认为是可以省略的
时间复杂度:
定义:
若一个时间频度T(n),假设建立一个辅助函数f(n),如果,当n->♾️,x为非0常数时,记作T(n) = O(f(n)),O(f(n))就是渐进时间复杂度,简称时间复杂度
计算方法:
- 将所有的常数项全部集中起来,换成一个1
- 只保留最高次幂项
- 将最高次幂项的系数省去
平均与最坏:
平均时间复杂度:
所有输入实例等概率出现的情况下,该算法所需要的平均时间
最坏时间复杂度:
在最坏情况下,该算法所需要的时间
意义:
在最坏时间复杂度下,代表的是最坏情况下所需要的运行时间,说明算法的任何一种情况所需要的时间都不可能比该时间要长
稳定度
如果两个相同的元素分别记为x与y,在排序前x在y前面,排序后x仍然在y前面,如果出现这种情况,我们称这个排序算法是稳定的
空间复杂度
- 该算法所需要占用的额外空间,他是问题规模n的函数,我们称之为空间复杂度
- 空间复杂度是对一个算法在运行过程中临时占用内存存储空间大小的一个度量
- (人话:空间复杂度越高,算法所占用内存越大)
- 一般都是空间换时间(如递归),不怎么关注空间复杂度
排序算法的时间复杂度
排序算法 | 平均时间 | 最坏时间 | 稳定度 | 额外空间 | 备注 |
冒泡 | O(n^2) | O(n^2) | 稳定 | O(1) | n较小时,排序较好 |
选择 | O(n^2) | O(n^2) | 不稳定 | O(1) | n较小时,排序较好 |
插入 | O(n^2) | O(n^2) | 不稳定 | O(1) | 大部分已排序时较好 |
希尔 | O(nlogn) | O(n^s)(1<s<2) | 不稳定 | O(1) | s是所选分组 |
归并 | O(nlogn) | O(nlogn) | 稳定 | O(1) | n大时较好 |
快速 | O(nlogn) | O(n^2) | 不稳定 | O(nlogn) | n大时较好 |
基数 | O(logR B) | O(logR B) | 稳定 | O(n) | B是真数(0~9) R是基数(个十百) |
冒泡排序
冒泡排序算法的图解:
冒泡排序的概述:
冒泡排序从前到后,依次比较相邻元素,如果发现逆序就交换,使较大的值往后移动,就像从水底的气泡逐渐上冒
冒泡排序的思想:
- 建立两个变量,分别指向相邻的两个元素(通过下标实现)
- 通过判断是否有序,若不符合排序规则,则交换
- 若有序,则将分别将两个变量自增一
- 重复上述操作,直到将最大的元素移动到最后
- 再重复上述操作,可以形成一个有序列表
注意的是,如果一趟下来一次都没有交换说明该列表已然有序,可以直接结束
冒泡排序的起源代码与分析:
public static void bubbleSort(int[] array) {
for(int j = 0;j < array.length-1;j++) {
if(array[j] > array[j+1]) {
temp = array[j];
array[j] = array[j+1];
array[j+1] = temp;
}
}
for(int j = 0;j < array.length-1-1;j++) {
if(array[j] > array[j+1]) {
temp = array[j];
array[j] = array[j+1];
array[j+1] = temp;
}
}
for(int j = 0;j < array.length-1-1-1;j++) {
if(array[j] > array[j+1]) {
temp = array[j];
array[j] = array[j+1];
array[j+1] = temp;
}
}
for(int j = 0;j < array.length-1-1-1-1;j++) {
if(array[j] > array[j+1]) {
temp = array[j];
array[j] = array[j+1];
array[j+1] = temp;
}
}
}
代码分析:
- 冒泡排序是从前到后的,所以我们的循环也要从前到后遍历
- array[j]代表的是第一个变量,array[j+1]代表的是第二个变量,根据思路,得出if语句的布尔表达式,即如果前面的元素大于后面的元素,是不符合排序规则的,我们需要将他们进行调换(交换的方式有很多,作者用的只是最普通的一个)
- 做完这一步之后,我们就可以让两个变量向后移动一步,即j++,
- 第一次for循环完成之后,我们发现最大的元素已然到达了最后,所以继续重复上述操作, 将第二个最大的元素移动到最后
注意:因为最后一个元素是最大的元素,是已然确定位置的元素,所以下次排序的时候可以排除它,这就是为什么每一次循环都是上一次-1的循环次数
第一次循环length-1的目的是:防止数组下标越界异常,因为j+1的缘故,j取得的最大值只能是length-2
冒泡排序的最终代码实现与分析:
public static void bubbleSort(int[] array) {
var temp = 0;
for (int i = 0; i < length-1 ; i--) {
for (int j = 0; j < array.length-1-i ; j++) {
if(array[j] > array[j+1]) {
temp = array[j];
array[j] = array[j+1];
array[j+1] = temp;
}
}
}
}
代码分析:
- 我们观察起源代码不难发现,起源代码中的每一个for循环的循环体都是一模一样的,所以,我们不妨把它封装起来,将其封装到一个for循环里面,以优化代码,避免写多个重复的for循环
- 其次,我们发现一共有三趟for循环,而且根据总元素是4个,for循环个数是3个,不难推出,循环个数是元素个数-1次,故外层循环需要有length-1次,即length-1趟排序
- 最后,我们在比较一下for循环里面的布尔表达式,发现下一次循环都比上一次循环少了1,所以在最终代码中会出现-i,因为第一次进去-1 第二次进去-2(-1-1),符合起源代码
根据以上的代码分析就可以创建出完整的冒泡排序代码
此刻,作为作者的我对这个代码很不满意,我决定要改进一下!!!
冒泡排序的最终代码的优化:
public static void bubbleSort(int[] array) {
var temp = 0;
for (int i = array.length - 1; i > 0 ; i--) {
// 在这里打布尔标记可以自动重置,进行下一次判断!!!
boolean flag = false;
for (int j = 0; j < i ; j++) {
if(array[j] > array[j+1]) {
temp = array[j];
array[j] = array[j+1];
array[j+1] = temp;
flag = true;
}
}
if(!flag) {
break;
}
}
}
代码分析:
1.在排序过程中可能会出现一种情况,会不会提前排序完成呢?答案是肯定的!
- 我们需要打一布尔标记,默认为false,如果在一趟一旦有元素发生了交换,就将布尔标记改一下,说明本趟排序还没完成,需要下一趟排序,对应着代码中if语句里面将布尔表达式更改
- 在一趟排序结束后,我们需要判断一下flag这个布尔标记,如果布尔标记已然为false,说明一次交换都没有,进一步说明已然有序,可以直接跳出循环,不需要继续进行下面几趟排序,否则,说明排序还没有完成,需要继续下一趟排序
2.我们发现,原来每一趟排序对应的for的布尔表达式太丑了(array.length-1-i),我要改变一下
- 我们直到一共有length-1趟排序,所以我们不妨初始化i = array.length-1,布尔表达式为i>0,这两个条件组合起来是和原来一样的,外层循环都是length-1次
- 接着,就是要改每一趟排序对应for的布尔表达式了,根据原始的(j < array.length-1-i),我们发现第一次是(j < array.length-1) 第二次是(j < array.length-2),因此,根据改良后的布尔表达式为(j < i),第一次i的值是array.length-1 第二次i的值是array.length-2,刚好符合原来的代码,所以可以用j<i替代
选择排序
选择排序算法的图解:
选择排序的概述:
选择排序从前到后,假定第一个元素为最小的元素,从后面的列表中找一个更小且最小的元素进行交换,然后假定第二个元素是最小的元素,,如此往复,得到一个有序列表
选择排序的思路:
- 建立一个索引,指向前面的假定最小的元素,建立第二个索引,指向后面跟他比较的元素(通过下标实现)
- 建立一个变量和一个索引,分别用于记录最小元素的下标和的数值
- 假设假定最小元素放于最小变量和下标处,如果比较发现有比他更小的,就将更小的元素及其下标替换进去,直到第一次遍历完
- 如果发现假定最小元素不在最小变量里,就根据下标进行交换,否则不交换
- 第一次选择排序可以确定第一个元素已然是最小元素且是确定位置的元素,我们可以向后移动一步去假设最小元素,重复上述操作,直到列表有序
选择排序的起源代码与分析:
public static void selectSort(int[] array) {\
int i = 0;
int minValue = array[i];
int minIndex = i;
for(int j = 1;j < array.length;j++) {
if(array[j] < minValue) {
minValue = array[j];
minIndex = j;
}
}
if(minIndex != i) {
array[minIndex] = array[i];
array[i] = minValue;
}
i = 1;
int minValue = array[i];
int minIndex = i;
for(int j = 2;j < array.length;j++) {
if(array[j] < minValue) {
minValue = array[j];
minIndex = j;
}
}
if(minIndex != i) {
array[minIndex] = array[i];
array[i] = minValue;
}
i = 2;
int minValue = array[i];
int minIndex = i;
for(int j = 3;j < array.length;j++) {
if(array[j] < minValue) {
minValue = array[j];
minIndex = j;
}
}
if(minIndex != i) {
array[minIndex] = array[i];
array[i] = minValue;
}
}
代码分析:
- i指向的是假定最小元素的索引,minValue与minIndex,分别用于存放最小元素的值与下标
- 因为i自己和自己比较无意义,说明从后一个元素开始比较,即j=1
- 里面的if语句,体现了如果发现后面有元素比他小,就将minVlue和minIndex改变,通过个语句块,就可以把最小的元素及其下标记录下来(只要比目前已知最小还要小就替换的原理)
- 接着判断,假定最小元素的下标是否和minIndex是否一样,如果一样说明假定正确,自己交换自己是无意义行为,故不交换,否则,说明需要交换,因为minIndex记录了最小值,minValue记录了最小值,将最小值对应下标的元素和假定元素交换即可,即arrray[i](将i的值给原来的最小值对应下标) array[i] = minValue(将最小值给i),这两步完成了交换!!!
注意:顺序不可以写反,写反了你可以试试什么情况
通过不断重复上述,就可以得到有序列表了
选择排序的最终代码实现与分析:
public static void selectSort(int[] array) {
var minValue = 0;
var minIndex = 0;
for (int i = 0; i < array.length - 1; i++) {
minValue = array[i];
minIndex = i;
for (int j = i + 1; j < array.length; j++) {
if(array[j] < minValue) {
minValue = array[j];
minIndex = j;
}
}
if(minIndex != i) {
array[minIndex] = array[i];
array[i] = minValue;
}
}
}
代码分析:
- 我们观察起源代码不难发现,每一次for循环的循环体都是一样的,我们不妨把他封装起来
- 我们观察亦可得,第一次下标是0,第二次下标是1,一共可以假设到array.length-2的下标的元素,即一共会进行length-1次这样的选择排序
- 因为每一次minValue与minIndex都要被放入假设值,所以将minValue和minIndex的赋值放到外层for里面(每一次的假设值都是动态的,所以要放入for循环里面动态获取)
- 因为从假设元素的后面一个元素开始去比较,所以j赋值为i+1
不禁又提出疑问,这个代码足够好了吗,我觉得还行,如果有能改进的地方请在评论区留言
插入排序
插入排序算法的图解:
插入排序的概述:
插入排序总体可以分为两部分,即有序列表和无序列表,通过不断地把无序列表中的第一个元素按照一定的顺序插入到有序列表中,就可以让有序列表不断增长,无序列表不断缩小,最终然整个列表变得有序
插入排序的思路:
- 我们要定义一个变量和一个索引,一个变量用于存放有序列表第一个元素的值,索引用于存放无序列表最后一个元素的下标
- 规定插入的位置默认是是索引后一个位置,若插入的值比索引的值大,所以默认位置符合,直接插入
- 若插入的值比索引的值小,说明,该索引的后一个位置不是我们改插入的位置,故要把索引对应的元素向后移动,索引向前移动,继续比较,直到找到适当位置
- 一共有length-1次插入(第一个不需要插)
插入排序的最终代码实现与分析:
public static void insertSort(int[] array) {
var insertedIndex = 0;
var insertValue = 0;
for (int i = 1; i < array.length; i++) {
insertedIndex = i - 1;
insertValue = array[i];
while (insertedIndex >= 0 && insertValue < array[insertedIndex]) {
array[insertedIndex + 1] = array[insertedIndex];
insertedIndex--;
}
array[insertedIndex + 1] = insertValue;
}
}
代码分析:
- insertedIndex为无序列表最后一个元素的下标,insertValue为有序列表第一个元素的值
- 设第一个元素为有序列表,所以循环从第二个元素开始即i=1
- insertedIndex>=0的目的是防止数组下标越界异常,且该布尔表达式必须写在前面
- insertValue < array[insertedIndex]若成立,说明该索引的后一个位置不是我们改插入的位置,将该索引对应的元素向后移动一步,并将索引向前移动
- 若不满足while的条件,说明找到了正确的位置,直接插入该索引的后一个位置即可
- 注意的是:一共有length-1次插入.所以i<array.length
希尔排序
希尔排序算法的图解:
希尔排序的概述:
在博主眼中,希尔排序最重要的是引入了分组的思想,通过分组解决问题,可以很大幅度的提高算法效率,此外,希尔排序也可以被理解为冒泡排序或者是插入排序的升级版
希尔排序的思路:
- 首先,我们需要对有序列表进行分组,第一次分组等于总元素/2个小组,后一次分组是上一次分组总数的一半,用gap记录组数
- 其次,我们需要通过组数来判断他们的步长,用于寻找他们的组员(中间是其他的组的元素,不能把别人组的成员算进来哦)
- 最后,听过冒泡排序的思想或者是插入排序的思想将各组组员进行排序
交换法:
- 我们需要从第gap个元素去开始(这样写,容易一些,如果不从第gap个元素开始,算法会变得复杂),通冒泡排序思想,与该组前一个元素(需要-gap,由于中间有其他组的元素)进行比较,若不符合则进行交换,否则继续向前比较
插入法:
- 定义一个索引和一个变量,用于记录下标为gap的元素的下标和值,通过与前一个元素进行比较,若前一个元素大于变量,则将前一个元素后退一步(因为中间有其他组的影响,所以不能只自减一,需要联系步长)
- 若前一个元素比变量小,所以该索引对应的位置就是插入的位置
希尔排序的代码实现与分析(交换法):
public static void shellSortBubble(int[] array) {
var temp = 0;
for (int gap = array.length/2; gap > 0; gap/=2) {
for (int i = gap; i < array.length; i++) {
for (int j = i-gap; j >= 0; j-=gap) {
if(array[j] > array[j+gap]) {
temp = array[j];
array[j] = array[j+gap];
array[j+gap] = temp;
}
}
}
}
}
代码分析:
- gap代表的就是组数,初始化表达式和更新表达式对应着思路的第一点
- 因为组数最少为1,所以gap要大于0
- 从第gap个元素开始,所以i=gap,i<array.length的意义是防止数组下标越界异常
- j=i-gap,说明j指向的是该组该元素中的前一个元素,如果发现逆序就进行交换
- j-=gap是为了向前移动一步(步长是gap,传统的冒泡排序步长是1)
- 注意:仔细观察可以发现,这是冒泡排序的逆过程哦
- i++是本个算法中最精髓的部分,当i++后,i指向的是另一组里面的元素,这让另一组也开始了冒泡排序,相当于同时多组进行冒泡排序!!!
- 形如(1组冒泡完二组冒泡然后再回到1组冒泡)
希尔排序的代码实现与分析(插入法):
public static void shellSortInsert(int[] array) {
for (int gap = array.length/2;gap > 0; gap/=2) {
for (int i = gap; i < array.length ; i++) {
var insertIndex = i;
var insertValue = array[insertIndex];
while(insertIndex - gap >= 0 && insertValue < array[insertIndex - gap]) {
array[insertIndex] = array[insertIndex-gap];
insertIndex -= gap;
}
array[insertIndex] = insertValue;
}
}
}
代码分析:
- gap代表的就是组数,初始化表达式和更新表达式对应着思路的第一点
- 因为组数最少为1,所以gap要大于0
- 从第gap个元素开始,所以i=gap,i<array.length的意义是防止数组下标越界异常
- 用insertIndex和insertValue记录gap的下标和值
- 和插入排序一样,前面insertIndex - gap >= 0是为了防止数组下标越界异常,前面的插入排序的赋值是不同的哦,所以表达起来不同,本质是一样的
- 因为前面插入排序的步长是1,所以要-=1,但是这里的步长是gap,所以要做出对应的改变
注意,只是最难理解的
快速排序
快速排序算法的图解:
快速排序的概述:
快速排序也用到了分组思想,左边那组的元素都要比右边那组的元素都要小,分完后继续对这两组进行以上操作,不断的分组,使得最终有序
快速排序的思想:
- 我们需要定义三个索引,左索引,右索引,中间索引,左索引为0,右索引为length-1,中间索引为(左索引+右索引)/2
- 中间索引对应的值被称为基准值
- 规定基准值左边的元素都小于基准值,基准值右边的元素都大于基准值
- 通过循环,发现左边一旦大于等于基准值就停下,发现右边一旦发现小于等于基准值就停下,将两个元素进行交换,不断重复,直到左索引大于等于右索引意味着结束
- 注意:若右索引对应的值等于基准值,左索引自增1.若左索引对应的值等于基准值,右索引自减1
- 最后,通过判断左索引是否等于右索引,若是,则将左索引自增1,右索引自减1(避免死递归)
- 最后比较索引值与边界的关系,判断是否还有组可分
快速排序的代码实现与分析:
public static void quickSort(int[] array,int iniLeft,int iniRight) {
var curLeft = iniLeft;
var curRight = iniRight;
var baseValue = array[(curLeft+curRight)/2];
var temp = 0;
// 若左索引大于右索引,根据逻辑又把元素换了换去,换了等于没换,故加此布尔表达式
while (curLeft < curRight) {
//在左组中找一个比基准值大的数进行交换
while (array[curLeft] < baseValue) {
curLeft++;
}
//在右组找一个比基准值小的数进行交换
while (array[curRight] > baseValue) {
curRight--;
}
//如果左索引大于等于右索引,说明交换结束
if(curLeft >= curRight) {
break;
}
//交换
temp = array[curLeft];
array[curLeft] = array[curRight];
array[curRight] = temp;
//如果左索引等于基准值 作用1.优化区间 2.若有相同数的情况下,让代码不陷入死循环
if(array[curLeft] == baseValue) {
curRight--;
}
if(array[curRight] == baseValue) {
curLeft++;
}
}
// 1.优化区间 2.使代码不会陷入死递归
if(curLeft == curRight) {
curLeft++;
curRight--;
}
// 若当前索引大于初识右索引,说明这组已经排序完
if(curLeft < iniRight) {
quickSort(array,curLeft,iniRight);
}
//同理
if(curRight > iniLeft) {
quickSort(array,iniLeft,curRight);
}
}
代码分析:
- iniLeft第一次应该被赋值为0,iniRight第一次应该被赋值为array.length-1
- 然后把他们分别赋值给curLeft和curRight
- 通过curLefe和curRight得到基准值的下标及其值
- curLeft < curRight说明这样的交换才有意义,如果大于了还交换,就把他们还回去了,白给了
- 第一个循环是找比基准值大的数,第二个循环是找比基准值小的数
- 此刻找到过后,可能会出现curLeft等于curRight的情况,这说明了交换结束了,可以直接退出循环优化代码
- 如果上面的情况为false,说明没交换完,就开始交换(相信大家都能看懂的咯)
- 特别注意的是,基准值可能被交换,但是由于该算法的强大性,没有影响
- 最重要:如果左索引对应的值等于基准值,就将右索引--,如果右索引对应的值等于基准值,就将左索引++
作用:优化区间 + 避免因相同元素造成的死循环
图解:
- 当本次弄完了之后,我们需要对这两组继续这样的操作,直到分的不能再分为止,这样就能有序啦
- 当跳出循环curLeft 等于 curRight 的时候,我们需要对其进行++或--,作用不仅可以优化区间,避 免多比较一个元素,还能有效防止死递归(不信你试试)
- 最后两个if,我取最特殊的一个位置去解释,假设iniRight是最后一个元素对应的下标,如果此刻的curLeft小于他,说明还能分组,如果curLeft>=他,只有两种情况,1只有一个元素,一个元素不能交换没意义,2越界,所以添加这个递归结束条件
- 特别说明的是:通过递归进行重复的操作
归并排序
归并排序算法的图解:
归并排序的概述:
归并排序使用到了分治思想,顾名思义,该算法是先分再治,这样也类似于分组思想,先治小组,再治大组,最终让序列有序
归并排序的思想:
- 创建分元素的方法,参数列表有,左索引,右索引,数组,临时数组,左索引初始化为0,右索引初始化length-1通过递归去完成,每一次传入的值,应该是动态变化的
- 通过左右索引除以2去定义中间索引
- 给递归增加一个判断,避免出现死递归的情况(中间索引用于递归的深入)
- 创建一个治理元素的方法,有数组,临时数组,左索引,右索引,中间索引
- 定义一个新的左右索引,左索引初始化为上面所述的,右索引为中间索引+1
- 通过判断左右索引对应的值,将值依次放入辅助数组中
- 最后将数组的数字放回原数组
归并排序的代码实现与分析:
public static void split(int[] array,int left,int right,int[] temp) {
if(left < right) {
var mid = (left + right)/2;
split(array,left,mid,temp);
split(array,mid+1,right,temp);
solve(array,left,right,mid,temp);
}
}
public static void solve(int[] array,int left,int right,int mid,int[] temp) {
var i = left;
var j = mid + 1;
var t = 0;
while(i <= mid && j <= right) {
if(array[i] <= array[j]) {
temp[t++] = array[i++];
}else {
temp[t++] = array[j++];
}
}
while (i <= mid) {
temp[t++] = array[i++];
}
while (j <= right) {
temp[t++] = array[j++];
}
var curLeft = left;
t = 0;
while (curLeft <= right) {
array[curLeft++] = temp[t++];
}
}
代码分析:
先分析分(split)方法
- 第一个判断left<right才能进去的原因是,如果left>=right,说明只有一个元素或者越界,分不了组了,所以要有这一个条件
- 将mid定义出来,实际上,mid其实是左边那个组的最后一个元素
- 注意的是,第一次的数组我们默认将他分为了两组,mid作为分界线,是第一组的最后一个元素,验证了第一个观点
- 然后通过递归去分组,下一下次分组以当前组的边界位置放进去,比如第一次是将原始数组的边界位置0和length-1放进去,下面同理得
- 当分完组后无法分组时,就会执行治(solve)的方法(第一次执行该方法是第一张图第三行开始执行的,通过回溯回到上面,所以我画成这样)
再分析治(split)方法(以前后两组的情况去阐释,即第一个途中的第二行,以小见大)
- left是左索引代表第一个组的第一个元素,把他赋值给i
- mid是第一个组的最后一个元素,mid+1就是第二个组的第一个元素,把他赋给j
- t是辅助数组的下标,默认为0,用于存储数据
- 他们结束的条件分别是mid和right(这是组的边界,再下去就要越界了,把别人的事干了,别人干什么)
- 分析i和j对应下标的值,把较小值放入辅助数组中
- 此刻会出现两种情况,要么是前面那一组提前放完,要么是后面一组提前放完,所以接下来的两个循环是用于把剩余元素全部放进去的(注意这里的两个组是有序的)
- 然后就是要将里面的元素拿回来了,我们可以定义一个curLeft,好看一点,curLeft = left
- 注意:curLeft = left很关键,不能乱写,因为这决定了辅助数组里面的元素放回到特定的位置,如果把他写死了,就没有效果了(如我把3~5下标的元素放到辅助数组,拿回来的时候也要放回到这些位置)
基数排序
基数排序算法的图解:
基数排序的概述:
基数排序还是较为不怎么常见的排序方式,但是其在排序算法中,有着举足轻重的作用,基数排序通过将个位,十位,百位....等依次放入桶中,在按顺序从桶里面拿出来,从而有序
基数排序的思想:
- 创造10个桶,分别为0~9,因为二维数组是一个特殊的一维数组,所以我们可以用二维数组去实现创建桶
- 从第一个元素开始,分别得到每一个元素的个位数,根据个位数将数字放入不同的桶中
- 当所有的元素都放入桶的时候,我们需要从0开始,按顺序将桶里面的元素依次拿出来放回数组中
- 注意:要清空桶里面的元素
- 继续从第一个元素开始,分别得到每一个元素的十位数,根据十位数将数字放入不同的桶中....
- 一共进行最大元素的位数次,就可以达到有序列表
基数排序的代码实现与分析:
public static void radixSort(int[] array) {
var bucket = new int[10][array.length];
var bucketElementCounts = new int[10];
var max = array[0];
for (int i = 1; i < array.length; i++) {
if (array[i] > max) {
max = array[i];
}
}
String s = max + "";
var maxLength = s.length();
for (int i = 0, n = 1; i < maxLength; i++, n *= 10) {
for (var arrays : array) {
var value = arrays / n % 10;
bucket[value][bucketElementCounts[value]++] = arrays;
}
var index = 0;
//限定作用
for (int j = 0; j < bucketElementCounts.length; j++) {
if (bucketElementCounts[j] != 0) {
for (int k = 0; k < bucketElementCounts[j]; k++) {
array[index++] = bucket[j][k];
}
}
bucketElementCounts[j] = 0;
}
}
}
代码分析:
- 我们先要创建10个桶,我们可以通过创建二维数组实现
- 因为是10个桶,所以行就是10(一行代表一个桶),列为array.length的原因是有可能所有的位数都一样,导致所有的数字去到同一个桶,所以为了安全,只能这样做
- 我们需要创建一个一维数组,用于记录每一个桶的元素的个数,方便我们后面去清空元素和添加元素,因为有10个桶,所以长度为10
- 第一个for循环是找最大值,通过下面的toString方法和length方法就可以得到最大位数(这里调用方法很妙哦)
- 第二个for循环正式开始了,用i<maxLength来表示一共有多少次放入和取出的操作
- n=1,n*=10的目的是为了取出各个位数,代码所示 arrays / n % 10,假若第一次进去,我们要取个位数,得出的结果是arrays%10,刚好取出原来的个位数,第二次进去时,我们要取十位数,得出的结果是arrays/10%10,刚好取出的是十位数字,非常的妙
- 把取出的值给变量value
- 因为知道了value,所以我们知道他应该放入哪个桶,所以能确定他在value行,知道他在value行后,我们需要知道在这一行有多少元素,所以将value放入一维数组的下标中就知道有多少个元素了
- 知道了多少个元素后,可以将该值放入列中,避免覆盖其他元素,采用后置++,因为,如果只有一个元素,里面记录的就是1,在下标1放置即可
- 当循环结束,意味着元素已经放完了,我们要到桶里面取出元素,取出元素时需要从第一个位置开始放,所以index = 0
- 因为有10个桶,所以j=0且j<bucketElementCounts.length,将里面的所有的元素都取出来
- 我们可以优化算法,如果该下标(桶)的值下不为0,说明有元素,把他拿出来(j代表桶,k代表元素下标)
- bucketElementCounts[j] = 0;这句话的意义是将桶里面的元素清空,把记录桶里面元素的一维数组里面的归0后,虽然没有真正清空桶里面的元素,但是,通过覆盖和一维数组的限定作用,不会再去到旧的元素
- 注意:该算法只能用于正整数
此刻,作为作者的我对这个代码很不满意,我决定要改进一下!!!
当我们无论是否发现负数的时候,我们可以将最小值找出来,然后所有的元素都加上该值的绝对值,那么这就能保证所有的元素都是正数,就能用上面的算法了,最后再把他们恢复回去即可
代码如下:
package datastructure.chapter03.sort.radixsorting;
import java.util.Arrays;
/**
* Created with IntelliJ IDEA.
* Description:
* User: 江骏杰
* Date: 2022-03-18
* Time: 15:56
*/
public class RadixSortingDemo {
public static void main(String[] args) {
var array = new int[]{-2, -1, -100, -102, -2, -44, -56};
radixSort(array);
System.out.println(Arrays.toString(array));
}
public static void radixSort(int[] array) {
var min = reverse(array);
var bucket = new int[10][array.length];
var bucketElementCounts = new int[10];
var max = array[0];
for (int i = 1; i < array.length; i++) {
if (array[i] > max) {
max = array[i];
}
}
String s = max + "";
var maxLength = s.length();
for (int i = 0, n = 1; i < maxLength; i++, n *= 10) {
for (var arrays : array) {
var value = arrays / n % 10;
bucket[value][bucketElementCounts[value]++] = arrays;
}
var index = 0;
for (int j = 0; j < bucketElementCounts.length; j++) {
if (bucketElementCounts[j] != 0) {
for (int k = 0; k < bucketElementCounts[j]; k++) {
array[index++] = bucket[j][k];
}
}
bucketElementCounts[j] = 0;
}
}
restore(array, min);
}
/**
* 这是基数排序时,当待排序数组中有负数,需要调用的方法,必须第一个调用
*
* @param array 待排序数组
* @return 最小值, 传入最后一个方法restore
*/
public static int reverse(int[] array) {
var min = array[0];
for (var element : array) {
if (element < min) {
min = element;
}
}
for (int i = 0; i < array.length; i++) {
array[i] += Math.abs(min);
}
return min;
}
/**
* 这是基数排序时,当待排序数组中有负数,需要调用的方法,必须最后一个调用
*
* @param array 待排序数组
* @param min 第一个方法返回的数字
*/
public static void restore(int[] array, int min) {
if(min < 0) {
for (int i = 0; i < array.length; i++) {
array[i] += min;
}
}else {
for (int i = 0; i < array.length; i++) {
array[i] -= min;
}
}
}
}
结论
哈哈,终于来到了最后,看到这里,咱们七大排序算法也是说完了的,回想创作路程,多少坎坷涌现出来,动图的抓耳挠腮,文字描述的纠结...
咱就来总结一下咱们的排序算法吧.
我认为,基数排序是最优的排序,但是他有个致命的缺陷,数据量太大时,会发生内存错误,内存溢出,其次是,基数,归并,快速这些用递归的排序都是典型的空间换时间的排序,小伙伴们可能会疑惑,为什么这样去做,这就是算法的魅力,我们只能去理解它
下一站:三大查找(二分,插值,斐波那契)