zoukankan      html  css  js  c++  java
  • 插入排序法

    插入排序法

    基础排序算法的另一种是插入排序,它的时间复杂度跟选择排序法一样都是O(n^2)。

    工作原理

    通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用in-place排序(即只需用到 {displaystyle O(1)} {displaystyle O(1)}的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。

    例如:中打扑克牌时候整理扑克牌的思想就是插入排序法的核心思想。也就是说,看后面的牌大小,把小牌插入到前面合适的位置,直至牌有序为止。

    动画演示:

    使用插入排序为一列数字进行排序的过程

    插入排序动画;

    核心代码如下:

    /**
    * 实现两数交换
    * @param {Array} arr 数组
    * @param {Number} i 当前索引位置
    * @param {Number} minIndex 带交换的索引位置
    */
    const swap = (arr, i, minIndex) => {
        [arr[i], arr[minIndex]] = [arr[minIndex], arr[i]];
    }
    
    /**
    * 插入排序
    * @param {Array} arr 无序数组
    * @param {Number} n 数组长度
    */
    const insertionSort = (arr, n) => {
        for (let i = 1; i < n; i++) {
            //寻找arr[i]合适的插入位置(在前面)
            for (let j = i; j > 0; j--) {
                if (arr[j] < arr[j - 1]) {
                    swap(arr, j, j - 1);
                }else{
                    break;
                }
            }
        }
    }
    

    注意:

    • 上面的代码中,外层for循环索引是从1开始的,因为只考虑长度为1的数组,也就是索引为0的元素本身就是有序的。

    • 内层循环索引开始位置是第 i 个,循环结束的条件是 j > 0。这里因为当 j1时候,我们只需要比较索引为1与索引为0的元素大小即可。如果将结束条件改为 j>=0那么,循环最后一步将会比较0-1位置的元素大小。而负索引在某些语言是合法的,而另一些语言角度是不合法的操作会提示数组越界。

    • 这里的内层循环可以做一个小优化就是在于循环终止条件可以改为j > 0 && arr[j] < arr[j - 1]

        /**
        * 插入排序
        * @param {Array} arr 无序数组
        * @param {Number} n 数组长度
        */
        const insertionSort = (arr, n) => {
            for (let i = 1; i < n; i++) {
                //寻找arr[i]合适的插入位置(在前面)
                for (let j = i; j > 0 && arr[j] < arr[j - 1]; j--) {
                        swap(arr, j, j - 1);
                }
            }
        }
      

    思考:

    • 既然插入排序内层循环可以提前退出,相较于选择排序算法,理论上插入排序是快于选择排序的,但是测试结果显示: 1000条整型数据下,选择排序的耗时比插入排序的耗时短,而在10000条整型数据下,插入排序的耗时是选择排序的3倍有多。

    优化方案

    上面提到插入排序理论上是比选择排序的速度快,但是实测结果相反。接下来我们动手优化一下插入排序的算法。

    问题分析

    上面版本的插入排序算法,注意内层循环,当每执行一次循环都将交换两个元素的位置。而交换一次位置将对数组产生三次赋值操作(还要算上访问索引的时间)。那么优化点就在于提前缓存变量以及减少内层循环交换次数。

    实现思路:内层循环开始前,先缓存当前的比较值也就是缓存arr[i],循环终止条件改为arr[j-1] > ee是我们先前缓存好的arr[i]。这里我们相比未优化的版本,这里我们不贸然直接进行两数交换而是改为比较赋值,所以循环体是一次赋值操作(将位于j-1的元素后移一个位置),而未优化版本的循环体是三次赋值操作。

    const insertionSort = (arr, n) => {
        for (let i = 1; i < n; i++) {
    
            const e = arr[i];   //缓存当前值
            let j;              //保存元素e应该插入的位置
    
            //采用比较的方案,最后才赋值到数组
            //相比以前每比较一次有一次交换操作,一次交换操作要三次赋值
            //这里只有一次的赋值操作,性能更优
    
            for (j = i; j > 0 && arr[j - 1] > e; j--) {
                arr[j] = arr[j - 1];
            }
    
            arr[j] = e;
        }
    }
    

    这个时候有兴趣的朋友可以跑一下优化后的代码,会发现插入排序已经明显快于选择排序,原因插入排序可以提前终止内层循环。相比于选择排序需要将循环完全执行去去找到最小值,插入排序一旦找到合适的位置,就会终止循环

    总结

    • 在最优的情况下,也就是说排序内容是完全有序的情况下,插入排序的时间复杂度为O(n)

    • 当要排序的数组内容是将近有序的时候,选择插入排序法效率不比高级排序的算法差

  • 相关阅读:
    php接收二进制文件转换成图片
    浅述WinForm多线程编程与Control.Invoke的应用
    精典策略模式
    js数组的操作详解
    PHP常用类
    收藏:五种开源协议的比较(BSD,Apache,GPL,LGPL,MIT)
    在 Windows 上部署 Qt 应用程序
    解决 MinGW5.14 编译 QT4.4.3 报错的问题
    懒人制作 deb 包
    NET 自定义配置文件 Configuration
  • 原文地址:https://www.cnblogs.com/jianzhihao/p/9336592.html
Copyright © 2011-2022 走看看