一、概述
基础排序算法包括:桶排序、冒泡排序、选择排序、插入排序等
二、桶排序
2.1 算法介绍
桶排序可以算是最简单快速的排序算法了,只是限定条件要多一点,需要事先知晓待排序列的极限值或范围来准备足够的桶。
2.2 算法原理
桶排序的原理就是准备足够数量的有序桶(一般用数组实现),用于标记待排序列的每个元素,用元素值对应桶下标,桶里的值代表的是元素对应的值出现的次数,有一次就在原值上加1(初始值为0)。当将所有的待排序列中的元素遍历一遍后,将其全部标记到桶中,这时候其实就已经排好序了。如果我们需要正序排序,则对桶进行正序遍历,排除值为0 的桶,按顺序和桶中值的个数输出桶的下标值,即为正序列,倒序反之。
2.3 算法实现
1 public classBusketSort {2
3 public static void sort(int[] ints, Boolean isAsc) {4 int[] basket = new int[101]; //定义足够大的数组桶
5 for (inti : ints) {6 basket[i]++; //对应元素的桶下标自增
7 }8 if(isAsc) {9 for (int i = 0; i < basket.length - 1; i++) {10 if (basket[i] > 0) {11 for (int j = 1; j <= basket[i]; j++) {12 System.out.print(i + " "); //循环输出桶元素不为0的下标值
13 }14 }15 }16 } else{17 for (int i = basket.length - 1; i > 0; i--) {18 if (basket[i] > 0) {19 for (int j = 1; j <= basket[i]; j++) {20 System.out.print(i + " "); //循环输出桶元素不为0的下标值
21 }22 }23 }24 }25
26 }27
28 public static voidmain(String[] args) {29 int[] ints = {2, 6, 4, 9, 12, 98, 5, 32, 90, 33, 24, 65, 37, 12, 4};30 sort(ints, false);31 }32 }
2.4 算法解析
首先我们需要准备一个桶数组,桶数组的大小为已知的待排序列的范围,比如我们要对学生的单科成绩(满分100)进行排序,那么我们就要准备101个桶(即定义一个长度为101的数组)。
然后遍历待排序列,将与待排元素值一致的桶下标对应的桶的值加1(初始值为0),遍历结束,其实排序也就完成了。
最后,我们需要正序排序则,正序遍历桶输出桶下标,倒序则倒序遍历桶输出桶下标(需要注意的就是下标输出的次数为下标对应的桶值)。
桶排序的时间复杂度是O(m+n),其中m为待排序元素个数,n为桶数。
桶排序的空间复杂度极大,与待排序的最大元素相关。
从代码实现中我们也可以看出,桶排序是及其简单的,唯一的缺陷就是桶的存在及其占用空间,如果我们要排序的数列只有很少的个数,但是元素之间相差极大的话,那么我们就需要准备一个极大的桶,及其浪费空间资源。一般情况下我们会在一个桶中存放一个范围之内的元素,桶中的元素我们可以采用其他的排序算法实现排序。这样可以极大的降低空间消耗。
桶排序一般适用于如下场景:
数据分布相对较为均匀或者数据范围不大的情况下
特殊场景,比如需要统计元素的个数的情况,或者希望通过哈希映射快速获取某些值的情况
三、冒泡排序
3.1 算法介绍
冒牌排序是很耳熟的排序方式,虽然它使用的很少,但是经常会出现在面试中,冒泡的意思是渐进式的意思,即渐进式排序。
3.2 算法原理
冒泡排序就是通过相邻元素的两两比较交换的方式实现排序,每次比较的两个元素中都包含上一次比较的后一位元素,也就是每次比较替换之间都是存在关联的,那个关联的元素一步步向后移,就好比冒泡一般,直到水面(排好序)。
每次冒泡的结束都意味着一个元素的归位,即一个元素被排好序,一般我们都是采用向后冒泡的方式来实现排序的,那么也就意味着,元素归位的顺序是从尾部开始的。
而每次冒泡又都是从首位元素开始的,到未被排序的最后一位结束。那么也就意味着每次冒泡的比较次数要比上一次冒泡少1次,因为每结束一次冒泡就会在末尾完成一个元素的排序,而未排序的元素相应的就会少一位。
了解上面的这些,这有利于我们实现编程算法,但初学者必定是一头雾水,不要急,你需要在参考下面实现源码的基础上再理解上面的内容,那么你会加深理解。
3.3 算法实现
1 public classBubbleSort {2
3 public static voidmain(String[] args) {4 int[] ints = {2, 6, 4, 9, 12, 98, 5, 32, 90, 33, 24, 65, 37, 12, 4};5 sort(ints);6 }7
8 public static void sort(int[] ints){9 for(int i = ints.length - 1; i > 0; i--) {10 for(int j = 0; j < i; j++) {11 if(ints[j] > ints[j + 1]){12 int temp =ints[j];13 ints[j] = ints[j + 1];14 ints[j + 1] =temp;15 }16 }17 }18 for(inti : ints) {19 System.out.print(i + " ");20 }21 }22 }
3.4 算法解析
从上面的代码中我们进行原理的解析,这样可以加深对原理的理解,我们学习算法并不是为了背诵这么一段简单的代码,我们学的是原理,是思维,虽然我也无法完全做到,但努力中,因为在我们完全理解了其原理之后,代码实现起来就会相当相当的容易,再也不用去背代码。
冒泡排序的实现代码中第9行为外层循环,用于控制冒泡的次数,正如原理中所说,冒泡排序是从尾部开始的,所以我们的循环变量i从尾部开始循环,末尾元素下标即为ints.length-1,i的变化范围为i>0,这里不包含0 的原因是因为当我们把序列的ints.length-1个元素排好序之后,只剩余一位元素,那么这一位元素自动就已经排好序了,无需再次冒泡,若为从小到大排序,那么最后剩余的元素就是首位元素,它即为最小的元素,不用再次执行冒泡排序。那么我们就得知外层循环控制的冒泡的次数为序列的长度减1次。
第10行的内容为内层循环,由于每次冒泡我们都是从首位元素开始,慢慢右移到末位(这里的末位并非末尾),一次冒泡是由有数次的两两比较交换(第11到14行代码)实现的,所以我们的代码中内循环变量j=0,表示每次都是从0下标元素开始冒泡,j的变化范围为j
忘了说一点,这里j
冒泡排序的精髓就在这两层循环之内,完全理解其意之后,实现起来就很简单了。
冒泡排序外层循环执行次数为n-1次,这里n代表序列元素个数,内循环次数为n(n-1)/2次,冒泡排序算法的时间复杂度为O(n2)。
冒泡排序算法中会用到临时空间用于元素交换,通过优化,我们可以实现,空间复杂度为O(1)。
冒泡排序现在更多的是出现在一些面试题里面,用于查看面试者的只是扩展度,实际使用中很少用到了,因为这种算法的时间复杂度还是不如人意。现在使用较多的还是快速排序算法。
四、选择排序
4.1 算法介绍
选择排序,就是选择,我们在一堆元素中进行有目的的挑选,然后用挑选到的元素实现排序。
4.2 算法原理
接上述,如果我们要实现序列正序排序,那么我们可以在元素堆中选择最小的元素将其放到首位,然后在剩余的元素中再次挑选最小的元素,将其放到第二位,以此类推,直到倒数第二个元素被挑选出来,整个序列也就是先了正序排序。因为每次都是选择最小的元素,那么剩余的最后一个元素一定是最大的元素,所以最后一个元素无需再进行挑选替换。
这里说到替换,其实每次将选到的元素放到正确的位置都是与其位元素进行替换实现。
每选择替换一次则意味着排好一位元素,由此可知,元素是从前往后实现排序的。
4.3 算法实现
1 public classSelectSort {2
3 public static voidmain(String[] args) {4 int[] ints = {2, 6, 4, 9, 12, 98, 5, 32, 90, 33, 24, 65, 37, 12, 4};5 sort(ints);6 }7
8 public static void sort(int[] ints){9 for(int i = 0; i < ints.length - 1; i++) {10 for(int j = i + 1; j < ints.length; j++){11 if(ints[i] >ints[j]) {12 int temp =ints[j];13 ints[j] =ints[i];14 ints[i] =temp;15 }16 }17 }18 for(inti : ints) {19 System.out.print(i + " ");20 }21 }22
23 }
4.4 算法解析
选择排序就是每次在剩余元素堆中选择最小的元素,但在实现方式上是通过比较替换的方式实现的,每次再剩余的元素中选择首位元素,这个元素的位置其实就是当次循环的最小元素归位位置,用这个元素逐个与其后的元素进行比较替换,直到将该位变成当前最小元素。
外层循环每完成一次,就可以归位一个元素,内层循环则是用于通过比较替换来实现归位,外层循环从首位开始进行排序,所以i=0,其循环的次数为元素个数-1次,是因为只剩最后一个元素的时候其实就已经完成了所有元素的排序工作了,最后一个元素无需再次循环。内层循环的开始是i+1,表示每次内循环都是在外循环开始的循环变量指代的位置的下一位开始与外循环开始的循环变量指代的位置进行比较替换,这种比较操作需要持续到最后一位元素。所以j
时间复杂度还是O(n2)。
空间复杂度可达O(1)。
五、插入排序
5.1 算法介绍
插入排序,也是一种简单的排序方法,通过顺序选择元素与已排好序的元素进行比较替换实现全部排序。
5.2 算法原理
插入排序从第二个元素开始进行,因为第一个元素默认排好序,因为它只有一个元素,自动有序,从第二个元素开始与之前的元素逐个比较替换进行排序。这个比较替换遵循以下规则:
如果该元素比前一个元素(已有序的最后一个元素,即当前最大元素)大,则自动排在末尾,不再与之前的元素进行比较替换,充当当前的最大元素;
如果该元素比前一个元素小,则将该元素提取出来,开始与之前的元素进行逐个比较替换,直到找到比当前元素小的元素或者到达第一位为止;
如果找到了比当前元素小的元素,说明当前元素是整个序列中非首和尾元素(即不是最小元素也不是最大元素),则将找到的元素之后已排序元素全部后移一位,将当前元素置于找到的元素后一位
如果比较直到首位仍然未发生替换,则说明,该元素是当前最小元素,应该将其置于首位,原有序列需要全部后移一位,来为新的最小元素空位。
5.3 算法实现
1 public classInsertSort {2
3 public static voidmain(String[] args) {4 int[] ints = {2, 6, 4, 9, 12, 98, 5, 32, 90, 33, 24, 65, 37, 12, 4};5 sort(ints);6 }7
8 public static void sort(int[] ints) {9 for(int i = 1; i < ints.length; i++) {10 if(ints[i] < ints[i - 1]) {11 int temp =ints[i];12 int j = i - 1;13 for(; j >= 0; j--) {14 if(temp >ints[j]){15 ints[j + 1] =temp;16 break;17 }else{18 ints[j + 1] =ints[j];19 }20 }21 if(j == -1) {22 ints[0] =temp;23 }24 }25 }26 for(inti : ints) {27 System.out.print(i + " ");28 }29 }30 }
5.4 算法解析
说句实话,每次实现这个算法的时候总会出点幺蛾子,我毕竟没有背代码的习惯,每次都是按照原理来手动实现,总会出现一些问题,总结了一下,其实还是原理理解的有点不透彻。出问题的重点部位就在内循环的实现上。
外层循环从1开始,即从第二个元素开始循环。表示排第二个元素,依次类推,直到最后一个元素,依次进行排序。
内存循环的执行还有一个条件,那就是当前元素的值要小于其前一位元素,也就是小于已排序元素的最大值。如果大于最大值,那么新元素就充当新的最大值,置于原位即可。
内循环从i-1开始,i-1位的元素即为原最大值元素,因为涉及到移位操作,故此处在进行一次比较,i的范围为i-1到0,0位的元素位当前最小值元素,故j>=0。
如果在内循环过程中找到了比该元素小的已排序元素位j,则将j+1到i-1的元素全部后移一位,将新元素置于j+1位。
如果再内循环结束之后没能找到比该元素小的以排序元素,则表明该元素是当前的最小元素,需要置于首位(0位),那么从0到i-1位的元素全部都要后移一位来让位。
代码中我们把内循环的第一句int j = i - 1提取出for循环,在外面定义,目的就是为了在最后循环结束后能借用j的值进行判断是否找到比该元素小的已排序元素,也就是说判断该元素是否为当前最小元素。
当j最后值为-1的时候,说明j=0的循环也正常执行了,未遭遇到break结束循环,也就是说循环结束后还没找到要替换的位置,也就是说所有已排序元素都大于或等于该元素,那么就将其最后置于首位即可。
时间复杂度为O(n2)。
空间复杂度为O(1)。
六、总结
这四种排序算法都属于简单排序算法,原理很简单,本文中实现方式虽然原始,但是足以说明其原理,当然这些算法都有优化实现方式,喜欢的可以自己试试。