1. 插入排序基本思想

插入排序是一种简单直观的排序方法,其基本思想是每次将一个待排序的记录按其关键字大小插入到前面已排好序的子序列中,知道全部记录插入完成。由此可以引出三个重要的排序算法——直接插入排序、折半插入排序、希尔排序

2. 直接插入排序


要将元素L( i )插入到已有序的子序列L[ 1 ... i-1 ]中,则:

  1. 查找L( i )在L[ 1 ... i-1 ]中插入的位置k;
  2. 将L[ k ... i-1 ]中的所有元素依次向后移一位;
  3. 将L( i )复制到L( k )。

初始时,可以将L(1)看为已经有序的子序列,所以将上述步骤执行n-1次即可得到有序序列。
插入排序通常采用就地排序,空间复杂度为O(1)。

伪码(升序)

   INSERTION-SORT(A)
   for j=1 to A.length-1;
        key=A[j];
        i=j-1;
        while(i>-1 and key<A[i])
              A[i+1]=A[i];
              i=i-1;
        A[i+1]=key;

C++

   void InsertSort(Elemtype A[], int n)
        int i,j;
        for(i=2;i<=n;i++)
        {
              j=i-1;
              A[0]=A[i];//A[0]做暂存单元,存储A[i]
              while(A[0]<A[j])//从后往前寻找插入位置,A[0]有元素,所以不用加j>0
              {
                    A[j+1]=A[j];//将比A[j]大的元素向后挪
                    j--;
              }
              A[j+1]=A[0];//复制到插入位置
        }

性能分析:

  • 空间效率:
    • 仅用了常数个辅助单元,因而空间复杂度为O(1)
  • 时间效率:
    • 在最好情况下,表中元素已经有序,则每个元素只需要比较一次而不需要移动元素,所以时间复杂度为O(1)
    • 在最坏情况下,表中元素逆序,则比较次数达到最大,为;总的移动次数也达到最大,为:
    • 平均情况下,O(n²)
  • 稳定性:直接插入排序是一个稳定的算法
  • 适用性:直接插入排序适用于顺序存储和链式存储

3. 折半插入排序

从直接插入排序算法可以看出,每次插入的过程分两步:①从后往前寻找插入的位置。②将要插入位置之后的元素后移。直接插入排序是边比较边后移。下面将比较和后移分开,当排序表为顺序表时,先折半查找元素插入的位置,再整体往后移动元素。
C++

   void InsertSort(Elentype A[], int n)
   {
        int i,j,low,high,mid;
        for(i=2; i<=n; i++)
        {
              A[0]=A[i];
              low=1,high=i-1;
              while(low<=high)//折半查找
              {
                    mid=(low+high)/2;
                    if(A[mid]>A[0]) high=mid-1;
                    else low=mid+1;
              }
              for(j=i-1; j>=high+1; j--)//统一移动元素
              {
                    A[j+1]=A[j];
              }
              A[high+1]=A[0];//插入操作
        }
   }

性能分析:
折半查找减少了比较的次数,约为O(nlog2n),该比较次数与待排序序列的初始状态无关,仅取决于表中的元素个数n,而元素的移动次数并未改变,依旧与排序表的初始状态有关,因此折半插入排序的时间复杂度依旧为O(n²);但对于数据量不是很大的排序表,折半插入排序能表现出更好的性能。
折半插入排序是稳定的算法。

4. 希尔排序

基本思想:直接插入排序算法的时间复杂度为O(n²),但若待排序序列为“正序”时,其时间复杂度为O(n),由此可见它更适用于基本有序的排序表和数据量不大的排序表。所以先将待排序表分割成若干形如 L[ i , i+d , i+2d , i+3d , ... , i+kd ] 的特殊子表,即:把相隔某个增量的数据组成一个子表,对各子表进行直接插入排序,当整个表中的元素已呈“基本有序”时再对全体记录进行一次直接插入排序。

排序过程

  • 先取一个小于n的步长d₁,把表中的全部记录分成d₁组,所有距离为d₁的倍数的记录放在同一组,在各组中进行直接插入排序。
  • 然后取第二个步长d₂<d₁,重复上述操作,知道所取的d₁=1,即所有记录在同一组,再进行一次直接插入排序。
    (目前尚未得出一个最好的增量序列,希尔提出的方法是d₁=n/2,d₂=d₁/2向下取整)

C++

   void ShellSort(ElemType A[] , int n)
   {
        for(dk=n/2;dk>=1;dk=dk/2)                        //步长变化
        {
              for(i=dk+1;i<=n;i++)                              //i=dk+1从每个子表的第二个元素开始
              {
                    if(A[i]<A[i-dk])                              //需将A[i]插入有序增量子表
                    {
                          A[0]=A[i];
                          for(j=i-dk;j>0&&A[0]<A[j];j-=dk)//当j<0时,插入位置已到。
                          {
                                A[j+dk]=A[j];                  //记录后移,查找插入位置
                          }
                          A[j+dk]=A[0];                              //插入元素
                    }
              }
        }
   }

性能分析

  • 空间效率:仅使用了常数个辅助单元,因而空间复杂度为O(1);
  • 时间效率:由于希尔排序的时间复杂度依赖于增量序列的函数,涉及数学上尚未解决的难题,所以对其时间复杂度的分析比较困难。当n在某个特定的范围时,希尔排序的时间复杂度约为O(n的1.3次方),在最坏的情况下时间复杂度为O(n²)。
  • 稳定性:希尔排序是一种不稳定的排序算法
  • 适用性:仅适用于线性表为顺序存储的情况。