[排序算法] 插入排序的原理及其Java实现

背景


继上一篇《冒泡排序》之后的第三篇,笔者准备在本篇介绍插入排序。

插入排序 (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²)。这在生产环境是不受欢迎的,不推荐使用,了解即可。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值