手撕面试题算法<排序>(3)—— 插入排序及其优化实现

前言

讲完了冒泡排序和插入排序这两个暴力遍历型的排序后,接下来要讲讲不那么暴力,有点技巧的排序算法了~

手撕算法 - 排序系列

手撕面试题算法<排序>(1)—— 冒泡排序及其优化实现
手撕面试题算法<排序>(2)—— 选择排序
手撕面试题算法<排序>(3)—— 插入排序及其优化实现
手撕面试题算法<排序>(3.5)—— 希尔排序
手撕面试题算法<排序>(4)—— 归并排序
手撕面试题算法<排序>(5)—— 快速排序以及快排为什么快
手撕面试题算法<排序>(6)—— 堆 & 堆排序
手撕面试题算法<排序>(7)—— 箱排序 & 基数排序

源码

看完有收获别忘了点个star哦~

插入排序

思想

插入排序就像我们玩扑克的时候整理手牌一样,我们会在手上已经整理好的牌里找到合适的位置,将拿到的牌插入到那个合适的位置
在这里插入图片描述
没错,插入排序利用到了数组中已排序过的部分有序的特性,对排序进行了优化,进一步优化了排序

在这里插入图片描述

时间复杂度

最坏情况下,数组完全逆序,插入第2个元素时要考察前1个元素,插入第3个元素时,要考虑前2个元素,……,插入第N个元素,要考虑前 n - 1 个元素。因此,最坏情况下的比较次数是 1 + 2 + 3 + … + (n - 1),等差数列求和,结果为 n*(n-1) / 2,所以最坏情况下的复杂度为 O(n2)
最好情况下,数组已经是有序的,每插入一个元素,只需要考查前一个元素,因此最好情况下,插入排序的时间复杂度为O(n)
平均时间复杂度为O(n2)

实现

    public static void sort(int[] arr) {
        for (int i = 0; i < arr.length - 1; ++i) {
            // 每趟插入排序前,数组区间[0,i]经过上一趟插入排序处理后,是有序的
            for (int j = i + 1; j > 0 && arr[j - 1] > arr[j]; --j) {
                // 数组元素交换
                Swaper.exec(arr, j - 1, j);
            }
        }
    }

在每趟遍历开始时,取数组中[0, j]这个区间,通过逐个比对大小的方式,将arr[j]插入到数组中[0, i]的区间中,比对次数 = 交换次数 + 1,在最坏的情况下需要比对 j 次

测试

代码:

	// 生成长度为10的随机数组
	int[] tmp = Tester.randomArr(10);
	int[] arr = tmp.clone();
	// 输出未排序的数组
	System.out.println(Arrays.toString(arr));
	long start = System.currentTimeMillis();
	sort(arr);
	long end = System.currentTimeMillis();
	// 输出排序耗时和已排序的数组
	System.out.println("插入排序结束,耗时" + (end - start) + "ms");
	System.out.println(Arrays.toString(arr));

在这里插入图片描述

插入排序的优化

思考

前面说到,插入排序利用了已排序的数组中有序的特性,相比于冒泡排序,减少了遍历次数和交换次数,但是在坏情况下(排序了一堆较大的元素,接下来要一直插入小元素),每趟排序都需要逐个比对,交换很多次

我们在插入的过程实质上是一个查找合适的插入位置的过程,对于有序数组的查找,我们应该很容易就能想到二分查找

二分插入排序

时间复杂度分析

通过使用二分法,我们能够很快的找到适合插入的位置

遍历获得需要插入的元素需要的时间复杂度是O(n),之前的逐个比较查找法时间复杂度也是O(n),所以没有经过优化的插入排序的时间复杂度是O(n2)

而二分法可以做到O(log n)的时间复杂度,所以说在最好情况下(查到的插入位置正好在有序数组末尾,无需对数组元素进行移动)的时间复杂度为O(n*logn)

最坏情况下(需要插入到有序数组头,需要移动数组元素n次)还是O(n2)

平均时间复杂度O(n2)

二分查找算法实现

首先实现一个二分查找算法,返回一个key适合在一个数组中插入的索引

    private static int binarySearch(int[] arr, int start, int end, int key) {
    	// 当key <= 数组中最小元素 时,返回数组头的索引
        if (key <= arr[start]) {
            return start;
        // 当key >= 数组中最大元素时,返回数组尾的索引+1
        } else if (key >= arr[end]) {
            return end + 1;
        // 否则递归进行二分查找
        } else {
            int mid = start + (end - start) / 2;
            if (key > arr[mid]) {
                return binarySearch(arr, mid + 1, end, key);
            } else {
                return binarySearch(arr, start, mid, key);
            }
        }
    }

排序算法实现

需要注意的是,我们在获得合适的插入位置的索引insertIdx后,不需要再逐个交换,我们可以将需要插入的元素arr[j]缓存为tmp,再将数组区间(insertIdx, j)中的元素往后各移一位,最后将arr[insertIdx] = tmp即可

    public static void optSort(int[] arr) {
        for (int i = 0; i < arr.length - 1; ++i) {
            int j = i + 1;
            // 二分查找找到合适的插入位置
            int insertIdx = binarySearch(arr, 0, i, arr[j]);
            // 存储需要插入的元素
            int tmp = arr[j];
            // 将数组区间(insertIdx,j)中的元素往后各移一位
            for(;j>insertIdx;--j){
                arr[j]=arr[j-1];
            }
            // 为指定下标赋值
            arr[insertIdx] = tmp;
        }
    }

测试:性能对比

代码:

	int n = 100_000;
	int[] tmp = Tester.randomArr(n);
	int[] arr = tmp.clone();
	System.out.println("对有"+n+"个元素的随机数组进行排序:");
	long start = System.currentTimeMillis();
	sort(arr);
	long end = System.currentTimeMillis();
	System.out.println("插入排序结束,耗时" + (end - start) + "ms");
	
	arr = tmp.clone();
	start = System.currentTimeMillis();
	optSort(arr);
	end = System.currentTimeMillis();
	System.out.println("二分插入排序结束,耗时" + (end - start) + "ms");

在这里插入图片描述
可以看出,通过二分查找的加持,二分插入排序的耗时还是有了一定的减少

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值