排序算法:二分法插入排序

ε祈祈猫儿з 提交于 2019-12-06 22:27:45

二分法插入排序:
      在上一篇文章排序算法:插入排序中,介绍了插入排序。其中,插入排序的“插入”二字体现于在寻找有序区间[R0,Ri-1]内第一个比Ri小的Rx,然后将Ri插入Rx之后,如果Ri>Ri-1,那正好,Ri不需要再与有序区间的其他元素比较。
      二分法插入排序沿用了插入排序的思路,而且由于[R0,Ri-1]是有序序列,在寻找Ri的插入位置时,可以采用二分查找法搜索有序区间中比第一个Ri小的元素,这样就减少了比较次数,提高了插入排序算法的性能。

以长度为6的序列 {6,3,5,4,1,2} 的二分法插入排序过程做示范:
第一趟排序:[3 6] 5 4 1 2 (3插入6之前)
第二趟排序:[3 5 6] 4 1 2 (5插入6之前)
第三趟排序:[3 4 5 6] 1 2 (4插入5之前)
第四趟排序:[1 3 4 5 6] 2 (1插入3之前)
第五趟排序:[1 2 3 4 5 6] (2插入3之前)
(注:用中括号括起来的是有序区间,之后的元素序列为待排区间)

可以看见二分法插入的排序过程和插入排序的过程是一摸一样的,但是其中寻找插入位置的过程是不一样的。

用第5趟排序过程作详细介绍第5趟排序过程需要寻找R5,即2的插入位置:
       第五趟排序前的序列:[1 3 4 5 6] 2
      首先,定义右边界right为R4,值为6,左边界为R0,值为1。[R0,R4]的中间元素为R(4+0)/2 = R2,值为4,2比中间元素小,因此元素2的插入位置一定在[R0,R2]之间,这时右边界置为中间元素的前一位,R1。
      [R0,R1]的中间元素为R(1+0)/2 = R0,值为1,2比1大,因此元素2的插入位置应该在R0之后,这时将左边界置位中间元素的后一位,即R1。
      此时搜寻的区间变为[R1,R1],中间元素也为R1,值为3,2小于R1,置右边届为中间元素前一位,即R0,左边界的位置大于了右边届,比较结束。此时的左边界R1即为2的插入位置。
      将2插入序号1的位置上,序列由:[1 3 4 5 6] 2 变为:[1 2 3 4 5 6]。
      使用二分查找法后,一共经过了3次比较找到插入位置。直接插入算法则需要5次比较(分别与6,5,4,3,1比较),可见二分查找法减少了插入发排序过程中比较的次数。

本文根据上述的二分法插入排序思路给出C++与Java的代码实现,并且使用Java对二分法插入排序算法和排序算法:插入排序中介绍的插入排序算法、以及排序算法:选择排序中介绍的选择排序算法,和排序算法:冒泡排序中介绍的两种冒泡排序算法进行性能比较。

C++代码:

void swap(int *a, int *b)
{
    int temp = *a;
    *a = *b;
    *b = temp;
}

void binarySort(int *arr, int length)
{
    if (arr == NULL || length <= 0)return;
    int left, right;
    for (int i = 1; i < length; ++i)
    {
        left = 0;
        right = i - 1;
        int temp = arr[i];
        while (left <= right)
        {
            int mid = (left + right) / 2;
            if (arr[mid]>arr[i])right = mid - 1;
            else left = mid + 1;
        }
        for (int j = i - 1; j >= left; --j)
        {
            arr[j + 1] = arr[j];
        }
        arr[left] = temp;
    }
}

Java代码:

private void binarySort(List<Integer> list) {
    int length = list.size();
    for (int i = 1; i < length; ++i) {
        int temp = list.get(i);
        // 寻找插入位置
        int position = findInsertPosition(list, temp, i - 1);
        for (int j = i; j > position; --j) {
            ++swapCount;
            list.set(j, list.get(j - 1));
        }
        if (position != i) {
            list.set(position, temp);
        }
        ++swapCount;
    }

}

private int findInsertPosition(List<Integer> list, int number,int rightIndex) {
    int left = 0;
    int right = rightIndex;
    while (left <= right) {
        int mid = (left + right) / 2;
        if (list.get(mid) > number) {
            right = mid - 1;
        } else
            left = mid + 1;
    }
    return left;
}

下面对findInsertPosition做详细解释:

    private int findInsertPosition(List<Integer> list, int number,int rightIndex) {
    int left = 0;
    int right = rightIndex;
    while (left <= right) {
        int mid = (left + right) / 2;
        if (list.get(mid) > number) {
            right = mid - 1;
        } else
            left = mid + 1;
    }
    return left;
}

      可以看见,findInsertPosition方法最后返回left作为最终的插入位置,那么Why left?记得在大学期间学到插入排序时,也没有多想,反正实现功能了就行。不过仔细想想,其实可以用倒推的方式来解决这个问题。
     首先,循环结束的条件是(left>right),那么left什么时候大于right呢?答案在循环内部的代码里,有两种可能,第一种是执行语句right = mid -1 后right小于了left,第二种是执行语句left = mid+1 后left大于了right。
     然后,由于left,mid,right都是整数,因此我们可以知道只有在left=right=mid的情况下,才能使得mid+1后刚好大于right,或者mid-1后刚好大于left(为什么是刚好?不然循环就已经结束了,循环结束的上一步正好是left=right的情况)。
     最后,第一种情况使得left>right的语句right = mid - 1,是在条件(list.get(mid) > number)下执行的,这代表的是当前元素是比待插元素大的元素中最小的元素,因此只需将当前元素和带插元素交换即可(为什么直接交换就行了?因为二分查找法保证了left之前的元素都比待插元素要小,而right之后的元素都比带插元素要大,因此当前的元素是最小的大于待插元素的元素)。第二种情况使得left>right的语句left = mid + 1,是在条件(list.get(mid) <= number)下执行的,这代表的是当前元素的值是比待插元素小的元素中值最大的,因此要将待插元素插入到当前元素之后(即mid+1)。
      这就是为什么要用left作为最终的插入位置的原因。

使用完全相同的元素为整数的List对二分法插入排序算法、插入排序算法、选择排序算法以及两种冒泡排序算法进行性能测试结果如下:

序列长度为1000:
这里写图片描述

序列长度为5000:
这里写图片描述

序列长度为10000:
这里写图片描述

序列长度为50000:
这里写图片描述

可以看到在数据量小的时候,几种算法效率相差无几,但当数据量足够大后,二分法插入排序的效率就体现出来了,要比直接插入排序的速度快很多,也比选择法快不少。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!