73-直接插入排序

1. 插入排序

  插入排序的基本思想:每次将一个待排序的记录,按其关键字大小插入到前面已经排好序的子表中的适当位置,直到全部记录插入完成为止。


2. 直接插入排序

  假设待排序的记录存放在数组R[0 .. n-1]中,排序过程的某一中间时刻,R被划分成两个子区间R[0 … i-1]和R[i … n-1],其中已排好序的有序区,当前未排序的部分称其为无序区。

这里写图片描述
图1-直接插入排序

  直接插入排序的基本操作是将当前无序区的第1个记录R[i]插入到有序区R[0 … i-1]中适当的位置上,使R[0 … i]变为新的有序区。直接插入排序每次使有序区增加1个记录,又称为增量法


3. 直接插入排序过程

  现在有一组待排序的初始关键字序列为: {49 , 38 , 65 , 97 , 76 , 13 , 27 , 49} ,直接插入排序过程如下所示:

这里写图片描述
图2-第一趟

  首先将这个关键字序列的第一个关键字49放入有序区,其他关键字则全部都放在无序区中,在进行插入排序的时候,直接把无序区中的第一个关键字38提取出来,跟有序区中的关键字比较,由于38小于49,那么把关键字49往后移动一个位置,并把38插在49前面的位置。



这里写图片描述
图3-第二趟

  第二趟同理,提取无序区中的第一个关键字65,跟有序区中的关键字比较,由于49是有序区中的最大关键字,同时无序区中的关键字65比有序区中的关键字49都还要大的话,那么65就不用跟38比较了,直接把65插在49后面的位置。



这里写图片描述
图4-第三趟和第四趟

  第三趟的时候同理,提取关键字97跟有序区中的关键字依次比较,在进行比较时发现关键字97比关键字65要大,因此直接把97插入在65后面的位置即可。第四趟的时候,由于关键字76比关键字97要小,把关键字97往后移动,然后把76要插在97前面的位置。



这里写图片描述
图5-第五趟

  第五趟时,提取无序区中的第一个关键字13,然后跟有序区中的关键字按从后往前的顺序依次进行比较,13比97关键字要小,于是把97往后移动一个位置;13再跟76比较还是小,于是再把76往后移动一个位置,13继续跟65比较还是小,继续把65往后移动,13继续跟49比较还是小,继续把49往后移动,当13跟38比较还是小,把38往后移动,由于38已经是最后一个关键字了,当13跟38比较完后,13就插在当前这个位置。

  从这个过程我们发现,在插入关键字13时,把有序区中的所有关键字都往后移动了一个位置。



这里写图片描述
图6-第六趟和第七趟

  第六趟时同理,提取关键字27进行比较,把有序区中的所有比关键字27大的关键字都往后移动一个位置,由于13比27小,因此把27插在13后面的位置。

  第七趟时从无序区中提取关键字49,经过比较后,49比65小,于是把关键字49插入在65前面的位置,到此整个直接插入排序的过程基本上就结束了。

  注意,在关键字序列中有两个都是49的关键字,在图中用了黑色和红色两种不同的颜色来区分。在排序前,黑色的关键字49在红色的关键字49的前面,在排序后,黑色的关键字49仍然在红色的关键字49的前面,由此可知,直接插入排序是一个稳定的排序

  同时我们还可以发现,直接插入排序在进行比较,查找插入位置的时候是一个顺序查找的过程,每一趟的插入都是有序的


4. 直接插入排序算法

直接插入排序算法代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define MAXSIZE 8

typedef int KeyType;  //定义关键字类型
typedef int InfoType;  //其他数据项类型

typedef struct //记录类型
{
    KeyType key; //关键字项
    InfoType data; //其他数据项的类型InfoType
} RecType; //排序的记录类型定义


/*
直接插入排序

参数说明:
    arr:传入要排序的数组
    n:数组的大小
*/
void InsertSort(RecType R[] , int n)
{
    int i;
    int j;

    RecType temp;

    for(i = 1; i < n; i++)
    {
        //每次提取无序区的第一个关键字
         temp = R[i];

        /*
        R[j-1].key > temp.key:有序区中的关键字是否比无序区的第一个关键字大
        j--:从后往前,依次比较有序区中的每一个关键字
        */
        for(j = i; j > 0 && R[j-1].key > temp.key; j--)
        {
            //把有序区中的关键字依次往后移动
            R[j] = R[j-1];
        }


        /*
        如果无序区中的第一个关键字比有序区中的最后一个关键字大,
        那么无序区中的第一个关键字的位置插入到有序区最后的位置
        */
        R[j].key = temp.key;
    }
}

int main(void)
{
    RecType R[MAXSIZE] = {0};
    int arr[] = {49,38,65,97,76,13,27,49};
    int i;

    printf("--------排序前--------\n");
    for(i = 0; i < MAXSIZE; i++)
    {
        R[i].key = arr[i];
        printf("R[%d].key = %d\n" , i , R[i].key);
    }

    //插入排序
    InsertSort(R, MAXSIZE);

    printf("--------排序后--------\n");
    for(i = 0; i < MAXSIZE; i++)
    {
        printf("R[%d].key = %d\n" , i , R[i].key);
    }

    return 0;
}

测试结果:
这里写图片描述


5. 直接插入排序的性能分析

  现在我们来分析一下直接插入排序的性能,从空间复杂度来讲,直接插入排序只用了一个辅助空间变量,因此在这里我们重点关心时间复杂度。

这里写图片描述
图7-有序的关键字序列

  1. 对于最好的情况就是,待排关键字序列的顺序本身就是有序的(即从小到大的方式排列),这样的话,在排序进行比较的时候每次只比较一次就行了,那么总的“比较”次数为:

i = 1 n 1 1 = n 1



   在直接插入排序算法中每次都需要提取无序区中的关键字,即tmp=R[i];同时还需要把关键字插入到有序区中合适的位置,即R[j].key = temp.key 。那么总的移动次数为: 2 ( n 1 ) 因此在最好的情况下,直接插入排序算法的时间复杂度是: O ( n )



这里写图片描述
图8-逆序的关键字序列

   2. 对于最坏的情况就是,我们的需求是按从小到大的方式排序,但是关键字序列中的顺序是逆序的(即从大到小的方式排列),在这种情况下,每次都要把有序区中的所有关键字都比较一次,总的比较次数为:

i = 1 n 1 i = n ( n 1 ) 2

  在最坏的情况下,每次都要把关键字插入最前面的位置(即每次都要把有序区中所有关键字往后移动),那么总的移动次数为:

i = 1 n 1 ( i + 2 ) = ( n 1 ) ( n + 4 ) 2

  因此在最坏的情况下,直接插入排序算法的时间复杂度是 O ( n 2 )



  总的平均比较和移动次数大概是:

i = 1 n 1 ( i 2 + i 2 + 2 ) = i = 1 n 1 ( i + 2 ) = ( n 1 ) ( n + 4 ) 2 = O ( n 2 )

  也就是说,直接插入排序算法的平均时间复杂度还是 O ( n 2 ) ,从稳定性来说,直接插入排序是一个稳定排序。


6 . 折半插入排序

折半插入排序就是采用折半查找方法,称为二分插入排序或折半插入排序。

这里写图片描述
图9-折半插入排序



折半插入排序算法代码如下:

void InsertSort(RecType R[] , int n)
{
    RecType temp;
    int i;
    int j;
    int min;
    int high;
    int mid;
    for(i = 1; i < n; i++)
    {
        //提取无序区的关键字
        temp.key = R[i].key;

        //在有序区进行折半查找要插入的位置
        min = 0;
        high = i - 1;
        while(min <= high)
        {
            mid = (min + high)/2;
            if(temp.key > R[mid].key)
            {
                min = mid + 1;
            }
            else
            {
                high = mid - 1;
            }
        }


        //找high的位置,当找到要插入的位置时,从当前位置开始依次往后移
        //第一趟的时候由于high = mid - 1,high的值为负数,所以high要+1
        for(j = i; j > high+1; j--)
        {
            R[j] = R[j-1];
        }
        R[high+1].key = temp.key;
    }   
}


7. 折半插入排序的性能分析

  折半插入排序:在R[0] … R[i-1]中查找插入R[i]的位置,折半查找的平均关键字比较次数为 l o g 2 ( i + 1 ) 1 ,平均移动元素的次数为 i / 2 + 2 ,所以平均时间复杂度为:

i = 1 n 1 ( l o g 2 ( i + 1 ) 1 + 1 2 + 2 ) = O ( n 2 )

  由于折半插入排序是采用折半查找进行比较,查找效率有所提高,但元素移动次数不变,仅仅将分散移动改为集合移动,而不是一边比较一边移动关键字的位置。

  虽然平均时间复杂度仍然是 O ( n 2 ) ,但是平均性能要高于直接插入排序,从稳定性来说,折半插入排序是一种非稳定排序



  这里可能有同学会不解,直接插入排序和折半插入排序的性能都是 O ( n 2 ) ,其算法思想可以说,基本是一致的,但是为什么折半插入排序的性能要略高于直接插入排序呢?

  学过查找算法的同学知道,折半查找的效率要高于顺序查找,而直接插入排序算法在查找比较的过程中采用的是顺序查找;折半插入排序算法在查找比较的过程中是采用折半查找的,因此折半插入排序的性能略高一些。

猜你喜欢

转载自blog.csdn.net/qq_35733751/article/details/81713061