背景
继上一篇《冒泡排序》之后的第三篇,笔者准备在本篇介绍插入排序。
插入排序 (Insertion Sort )
本文要讲的排序算法是排序算法中最基本算法之一的插入排序(Insertion Sort ) 。
什么是插入排序?
插入排序,其实现方式,通过逐渐扩大数组头部有序部分的方式,逐渐令整个数组变得有序。具体是通过每一轮 “插入” 有序部分外的一个元素(下标:SortedBoundle + 1)到有序部分[0, SortedBoundle]的合适位置pos来实现,因为[0, SortedBoundle]这一部分是有序的,所以可以通过二分查找的方式来加快计算pos的所要时间。这样经过N次插入之后整个长度N的数组就变得有序了。
插入排序的空间时间复杂度
时间复杂度: O(n・㏒n) - O(n²),不稳定的排序算法,因为是非链表而是数组,所以插入操作如果是在有序部分头部,那么耗时会非常高。如果插入操作是在有序部分尾部,那么插入操作几乎不耗时。所以其复杂度是不稳定的。
空间复杂度: O(n),通常的实现是破坏性的in-memory实现,即在给定的原数组上做破坏性排序。
插入排序的实现
冒泡排序的样例代码和测试代码在笔者githubdemo仓库里能找到。
/**
* 插入排序
* @author toranekojp
*/
public final class InsertionAscendingSort extends AbstractAscendingSort {
@Override
protected void doSort(int[] nums) {
assert nums != null;
final int count = nums.length;
int sortedCount = 1; // 第一轮第一个永远是有序的。
while (sortedCount < count) {
// Assert [0, sortedCount)的区间 已经排序完毕
// 二分查找insertPosition。
final int targetIndex = sortedCount; // 需要做插入处理的元素的下标。
final int targetNum = nums[targetIndex];
final int sortedRightBoundle = sortedCount - 1; // 已排序部分的右边界index
final int insertPosition = binarySearch(nums, 0, sortedRightBoundle, targetNum);
// 移动与插入
if (insertPosition <= sortedRightBoundle) {
moveBackwardFor1Step(nums, insertPosition, sortedRightBoundle);
nums[insertPosition] = targetNum;
} else {
assert insertPosition == targetIndex: "此时不需要任何移动和插入。";
assert nums[insertPosition] >= nums[sortedRightBoundle]: "目标元素和已排序部分已经是有序的case。";
}
// 本轮结束,已排序计数+1。进入下一轮
sortedCount++;
}
}
/**
* 在[l,r]的有序升序区间内,递归地寻找能插入给定targetNum的位置pos。
* @return pos ∈ [l, r + 1],满足
* 当pos = l 时 targetNum <= nums[l]
* 当pos ∈ [l + 1, r] 时 nums[pos] >= targetNum
* 当pos = r + 1 时 nums[r] < targetNum.
*/
private int binarySearch(int[] nums, int l, int r, int targetNum) {
if (l == r) {
if (nums[l] < targetNum) return r + 1;
return r;
}
final int mid = (l + r + 1) / 2;
if (nums[mid] > targetNum)
return binarySearch(nums, l, mid - 1, targetNum);
// nums[mid] <= targetNum
return binarySearch(nums, mid, r, targetNum);
}
/**
* 把下标在range [l,r]的数组元素向后挪一位。
*
* 调用者需要保证
* 0 <= l <= r < nums.length - 1
*/
private void moveBackwardFor1Step(int[] nums, int l, int r) {
assert nums != null;
assert 0 <= l && l <= r && r < nums.length - 1;
int cursor = r + 1;
while (cursor > l) nums[cursor] = nums[--cursor]; // 往后挪一位
}
}
结语
插入排序,相对来说是一个吃力不讨好的算法。从其代码量也可以看到,代码相对复杂,而且用到了二分查找,对于初学者来说有点难,而且容易写错。但确实是一个可以用于理解二分查找的不错的排序算法。插入排序因插入操作的不稳定性,所以其时间复杂度是不稳定的从O(n・㏒n) 到 O(n²)。这在生产环境是不受欢迎的,不推荐使用,了解即可。