大家好,我是猿码叔叔,今天要复习的算法是插入排序,它是比较容易理解的排序算法之一,仅次于冒泡排序,如果大家对冒泡排序有疑问并与我一起学习和探讨,可以去阅读我之前的博文《今日算法解读之冒泡排序(bubble sort)》。
好了开始切入正题吧。
一、我对插入排序的理解
在理解插入排序之前,对冒泡排序的排序逻辑熟稔于心可以认为是掌握其它排序算法的基础。
冒泡排序简单理解就是从数组头到数组尾的元素进行链式比较,根据排序要求,将相邻元素进行位置交换,如果一次循环下来( O(n) )无法达到要求,就需要循环多次,最后我们得出冒泡排序的平均时间复杂度为( O(n²) )。
说了这么多,插入排序表示不服,这不就是生硬的线性遍历并对数据进行操作吗,能不能来点优雅的。所以插入排序为自己的排序算法增加了一些策略。后面我会结合代码细讲这些策略。结合策略,插入排序可以在每次循环中找出一个参照数(benchmark),然后从数组中划出一段元素,来与参照数进行比较,如果满足条件,就将被比较元素向前推进,本轮循环结束后将参照数放回数组。
大家可以参考一下面这张gif动图
二、插入排序策略
1、回溯(backtracking)
插入排序会向遍历的方向进行反向回溯。这个策略很符合一般人的思考逻辑,就是把过去我们认识到的一些事物进行分类分级,当接触新事物后我们需要从新对他们进行分类分级是一个道理。所以插入排序会把每一个新事物都当作一个参照数,来分类分级到自己熟悉的事物当中去。
举例:8、5、6、3、4、7、2、1(Increment)
8作为第一个数字,无需和自己比较,可以忽略不记,我们从第二个元素开始。5 向 8 反向遍历;6 向 8 反向遍历;3向 8 反向遍历,直到最后一个元素 1。
分析:当每个元素反向遍历时,他作为此次遍历的起点,因此 index 依次 -1,但这样做会面临一个问题,就是索引会超出正常索引范围(代码中会抛出IndexOutOfBoundsException)。为了阻止问题的发生我们需要对反向遍历进行控制,即 index > -1。
虽然每个元素在做回溯遍历时,整体的遍历其实是在前进的。
2、分段(segmentation)
分段其实与回溯相辅相成。与人的思考逻辑同样类似,如果我们不对新事物进行认知,就无法对其进行分类分级,所以有了回溯,就要对过去的经理分段并分类分级,即从当前新数字开始,反向遍历,到不满足条件的那个值的 (索引 + 1) 的位置就是一个段落。
举例:8、5、6、3、4、7、2、1(Increment)
此数组一共分了7段,即数组长度 arr.length - 1。分别为:
5 = [5, 8],
6 = [6, 8],
3 = [3, 5, 6, 8],
4 = [4, 5, 6, 8],
7 = [7, 8],
2 = [2, 3, 4, 5, 6, 7, 8]
1 = [1, 2, 3, 4, 5, 6, 7, 8]
分析:每个反向遍历的起点值,都会在完成遍历与比较后,放到段开端的位置。
我们仔细看,之所以放到这个位置,原因有二,① 该位置是分段的界限也是起点;②回溯不符合升序比较条件,即左侧的元素小于自己。
下面是分段图(每条横线即为一个分段)
三、Java代码实现
以下代码实现分为两个部分,一部分是优化后的代码,一部分是代码的优化过程。
1、优化后的的代码
private static void sort(int[] arr) {
int len = arr.length, benchmark, backtrackIdx;
for (int i = 1; i < len; i++) {
// backtrackIdx 为移动的回溯index,外循环每次走一步,那么它的值则比当前坐标 i - 1
backtrackIdx = i - 1;
// 参照数, 用来参照并比较的数字, 一般为当前外循环 i 坐标下的值
benchmark = arr[backtrackIdx + 1];
// 判断当移动坐标backtrackIdx 不为负数 && 它小于自己前一个坐标的值(当左向右升序排)
while (backtrackIdx >= 0 && benchmark < arr[backtrackIdx]) {
// 将回溯过程中满足条件的元素向右推进
arr[backtrackIdx + 1] = arr[backtrackIdx];
// 回溯索引 - 1, 继续回溯
backtrackIdx--;
}
// 本轮回溯结束, 把参照数放到段的头位置
arr[backtrackIdx + 1] = benchmark;
}
System.out.println(Arrays.toString(arr));
}
2、代码改进过程
第一次自己摸索实现的代码
private static void sortIOne(int[] arr) {
int len = arr.length, temp, b, a;
for (int i = 1; i < len; i++) {
a = i - 1;
b = i;
while (a >= 0 && arr[b] < arr[a]) {
temp = arr[b];
arr[b] = arr[a];
arr[a] = temp;
b = a;
a--;
}
}
System.out.println(Arrays.toString(arr));
}
第一次自己优化的代码
private static void sortITwo(int[] arr) {
int len = arr.length, temp, b, a;
for (int i = 1; i < len; i++) {
// b = i;
a = i - 1;
while (a >= 0 && arr[a + 1] < arr[a]) {
temp = arr[a + 1];
arr[a + 1] = arr[a];
arr[a] = temp;
a--;
}
}
System.out.println(Arrays.toString(arr));
}
第二次优化
private static void sortIThree(int[] arr) {
int len = arr.length, temp, benchmark, a;
for (int i = 1; i < len; i++) {
// b = i;
a = i - 1;
benchmark = arr[a + 1];
while (a >= 0 && benchmark < arr[a]) {
arr[a + 1] = arr[a];
arr[a] = benchmark;
a--;
}
}
System.out.println(Arrays.toString(arr));
}
最后一次优化的代码就是我在前面贴出的,大家有没有发现优化的位置呢,聪明的你也许早就看到了。之所以贴出自己的改进代码,是希望和大家一起分享在成长路上的遇到的坑,这样才能为自己的成长添砖加瓦。
谢谢大家的阅读,原创不易,欢迎大家一键三联,同时也不要忘了为我提出宝贵意见,特别是写博客与代码质量方面。