二分法插入排序:
在上一篇文章排序算法:插入排序中,介绍了插入排序。其中,插入排序的“插入”二字体现于在寻找有序区间[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:
可以看到在数据量小的时候,几种算法效率相差无几,但当数据量足够大后,二分法插入排序的效率就体现出来了,要比直接插入排序的速度快很多,也比选择法快不少。
来源:CSDN
作者:Cavaing7
链接:https://blog.csdn.net/wangjinsu7/article/details/52904484