插入排序法

插入排序法

基础排序算法的另一种是插入排序,它的时间复杂度跟选择排序法一样都是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)

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

原文地址:https://www.cnblogs.com/jianzhihao/p/9336592.html