今天阿粉就来谈一下这个 Java 中的各种排序的算法,因为之前遇到了一个面试高级开发,结果竟然出了一个 九九乘法表的题,阿粉当时听完读者说的,瞬间就明白是什么意思了,这感觉有点忽悠人,但是实际上却是面试官想要考察你的排序算法的事了,也有可能是真的无聊。
排序算法
什么是排序算法,实际上这个没有太多的说法,意思表达清楚就可以了,所谓排序,就是一串记录,按照其中的某个或某些关键字的大小,递增或递减地排列起来的操作。
排序算法的种类可以说是比较的多样化了,对时间,对空间的效率,不同的排序算法的优缺点是不一样的,有些是用时间换空间的,有些是拿空间换时间的,今天阿粉就来一一列举一下。
冒泡排序
什么是冒泡排序呢?
冒泡排序就是依次比较相邻的两个数,将小数放在前面,大数放在后面。
就像一个泡泡,一个泡泡一样,直接往起漂。
冒泡排序就是依次比较相邻的两个数,将小数放在前面,大数放在后面。
实际上比较的过程就是这个样子的:
第一次比较:
首先比较第1个和第2个数,将小数放前面,大数放后面,然后比较第2个数和第3个数,小数放前面,大数放后面,然后一直比较到最后,这样,最大的一个数就放到了最后面了。
第二次比较:
首先比较第1个和第2个数,将小数放前面,大数放后面,然后比较到倒数第二个数,然后第二次比较结束,这样在最后一个位置的就是最大的,然后倒数第二个位置的是第二大的数。
然后一直这样往下执行,第三次就是来取第三个数,这样依次循环,这样说大家肯定都是能理解的,假设需要排序的序列的个数是n,则需要经过n-1轮,最终完成排序。在第一轮中,比较的次数是n-1次,之后每轮减少1次。
我们接下来使用代码来实现一下:
- public static void main(int[] a) {
- int temp;
- //需要比较n-1轮
- for (int i = 0; i < a.length-1; i++) {
- //根据a.length-i-1,每轮需要比较的次数逐轮减少1次
- for (int j = 0; j < a.length-i-1 ; j++) {
- //相邻数进行比较,符合条件进行替换
- if (a[j] > a[j+1]) {
- temp = a[j];
- a[j] = a[j+1];
- a[j+1] = temp;
- }
- }
- }
- }
插入排序
插入排序的种类比较多
- 直接插入排序
- 二分插入排序
- 希尔排序
这些排序方式全都是属于插入式排序的,
我们先来看看直接插入排序:
- 比较的过程是这个样子的,数组的第二个数据开始往前比较,即一开始用第二个数和他前面的一个比较,如果 符合条件则让他们交换位置。
- 假如我们给定一个数组,我们要按照从小到大排序,数组为 [3,1,4,6,5,17,12,11] 这时候,第一步就是拿着 1 和 3 进行比较,如果 1 小于 3 ,这个时候,就把 1 和 3 换个位置,[1,3,4,6,5,17,12,11] .
接下来再用第三个数和第二个比较,符合则交换,但是此处还得继续往前比较 ,就是用 4 和 3 和 1 分别去比较,然后一直这么重复下去。直到把所有的数据都排列完之后,就得到了我们想要的结果,我们来写个代码看看是什么样子的。
- public static void basal(int[] array) {
- // 从第二项开始
- if (array == null || array.length < 2) {
- return;
- }
- for (int i = 1; i < array.length; i++) {
- int cur = array[i];
- // cur 落地标识,防止待插入的数最小
- boolean flag = false;
- // 倒序遍历,不断移位
- for (int j = i - 1; j > -1; j--) {
- if (cur < array[j]) {
- array[j + 1] = array[j];
- }else {
- array[j + 1] = cur;
- flag = true;
- break;
- }
- }
- if (!flag) {
- array[0] = cur;
- }
- }
- }
- }
既然我们之前都说了,插入排序的方法有很多种,那么我们也得说说其他的插入排序,然后在看看他们都有什么不同的地方,大家说对不对呢?
既然我们刚才都说了排序每次都要讲数组后移,但是我们之前的判断条件上却是可以优化出来的,这样优化优化的就衍生出来了其他的不同的插入排序,接下来我们就来看看这个二分查找插入排序。
二分查找法插入排序
优化直接插入排序的核心在于:快速定位当前数字待插入的位置。在一个有序数组中查找一个给定的值,最快的方法无疑是二分查找法,至少在阿粉的心中如果想到优化的时候肯定是第一时间选择的就是可不可以使用二分查找法呢?
我们先来看看代码实现,然后再看看他有什么缺点,为啥有很多人都不选择使用二分查找插入排序。
- // 利用系统自带的二分查找法,定位插入位置
- // 不稳定排序
- public static void optimized(int[] array) {
- if (array == null || array.length < 2) {
- return;
- }
- for (int i = 1; i < array.length; i++) { int cur = array[i];
- int[] sorted = Arrays.copyOf(array, i);
- int index = Arrays.binarySearch(sorted,cur); if (index < 0) {
- index = -(index + 1);
- }
- for (int j = i - 1; j > index - 1; j--) {
- array[j + 1] = array[j];
- }
- array[index] = cur;
- }
- }
在这其中,阿粉使用了系统自带的Arrays,然后进行了二分查找插入排序,为什么这么用呢?在JDK中提供了 Arrays.binarySearch(),方法的入参需要将有序数组传递进去 来进行实现,为什么不用这种方法呢?
假如在排序之前,有两个数相等,但是在排序结束之后,它们两个有可能改变顺序这就是说明该排序算法具有不稳定性。这就是大家所说的稳定性。
既然都有这种不稳定的排序,那是不是就应该存在稳定的排序呢?对没错,就是有,那么什么是稳定的排序呢?对,大家没有想错,冒泡排序就是稳定排序,因为冒泡排序只在相邻元素大小不符合要求时才调换他们的位置,它并不改变相同元素之间的相对顺序, 因此它是稳定的排序算法。
既然我们刚才说了这个排序中还有希尔排序,我们再来看看希尔排序。
希尔排序
说实话,阿粉第一次知道这个排序的时候,就想说,是不是一个叫做希尔的人改进的,结果,发现还真是,希尔排序,又叫做((缩小增量排序),因 D.L.Shell 于 1959 年提出而得名,实际上应该叫做Shell's Sort。
希尔排序是先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序。
实际上就相当于先进行了一个简单的分组,将待排序数组按照步长gap进行分组,然后将每组的元素利用直接插入排序的方法进行排序;每次再将gap折半减小,循环上述操作;当gap=1时,利用直接插入,完成排序。
代码实现是这个样子的
- public static void sort(int[] a) {
- int length = a.length;
- int h = 1;
- while (h < length / 3) h = 3 * h + 1;
- for (; h >= 1; h /= 3) {
- for (int i = 0; i < a.length - h; i += h) {
- for (int j = i + h; j > 0; j -= h) {
- if (a[j] < a[j - h]) {
- int temp = a[j];
- a[j] = a[j - h];
- a[j - h] = temp;
- }
- }
- }
- }
- }
选择排序
选择排序(Selection sort)是一种简单直观的排序算法。
这个就比较简单了,为什么这么说,是因为他就是直接从未排序的序列中去找最小或者最大的元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
实际上和冒泡排序在想法上有一丢丢的相似,但是就是不一样。
我们看看代码是否实现:
- public static void sort(int[] a) {
- for (int i = 0; i < a.length; i++) {
- int min = i;
- //选出之后待排序中值最小的位置
- for (int j = i + 1; j < a.length; j++) {
- if (a[j] < a[min]) {
- min = j;
- }
- }
- //最小值不等于当前值时进行交换
- if (min != i) {
- int temp = a[i];
- a[i] = a[min];
- a[min] = temp;
- }
- }
- }
既然阿粉都给大家列出了这几种排序,那么我们又回到一个问题上来了,排序这么多,怎么选择,为什么选择,这就又涉及到了时间复杂度和空间复杂度上面了,要么用空间换时间,要么用时间换空间。
总结
冒泡排序
平均时间复杂度 最好情况 最坏情况 空间复杂度 O(n²) O(n) O(n²) O(1)
直接插入排序
平均时间复杂度 最好情况 最坏情况 空间复杂度 O(n²) O(n²) O(n²) O(1)
二分查找排序
平均时间复杂度 最好情况 最坏情况 空间复杂度 O(n²) O(n²) O(n²) O(1)
希尔排序
平均时间复杂度 最好情况 最坏情况 空间复杂度 O(nlog2 n) O(nlog2 n) O(nlog2 n) O(1)
选择排序
平均时间复杂度 最好情况 最坏情况 空间复杂度 O(n²) O(n²) O(n²) O(1)
今天的排序就说到这里了,你学会了么?