基于二分查找的折半插入排序
时间复杂度:O(n²),但减少了比较元素的次数。
稳定性:稳定,无发生相对位置改变。
这个算法的核心思想还是插入排序思想,
唯一不同的一点是:
直接插入排序是比较一次,挪一次位置,直到找到目标位置,
并且挪的过程是逐个的;
而折半插入排序在找位置的时候,
先不比较,而是采用二分查找先把要插入的位置提前找出来,
然后统一地移动插入位置之后的所有元素。
于是开始了今夜的DEBUG旅程
首先,我先写了一个二分查找算法:
int myBinarySearch(T array[], int len, int x)
{
T low = 0;
T high = len - 1;
while (low <= high)
{
T mid = (low + high) / 2;
if (x < array[mid])
{
high = mid - 1;
}
else if (x > array[mid])
{
low = mid + 1;
}
else
{
return mid;
}
}
return -1;
}
这个二分查找是基于升序的。
测试后,无误:
int b[] = {1,2,3,4,5,6,7,8,9,10};
int len = sizeof(b) / sizeof(b[0]);
cout << len << endl;
cout << myBinarySearch<int>(b, len, 2);
接下来,我准备应用这个算法到折半插入排序中:
我举了一个例子:
一个整型数组,4,3,2,1。
i还是从1开始,代表数组下标为1的元素即将进行排序。
为了不影响i,我们定义一个j = i,
接下来的操作都是基于j的。
首先创建一个临时变量temp存放待排元素,
然后通过二分查找算法,
查找数组长度为j+1的子数组,
查找元素为temp。
然后定义一个下标down = myBinarySearch(array, j + 1, temp);
比如第一趟,
j = 1;
down = 0;
然后开始排序:
这里使用的是while循环,
当j>down时,进行循环体。
while (j > down)
{
array[j] = array[j - 1];
j--;
}
开始挪位置;
挪好后,
将array[down] = temp;
然后进行下一次循环即可。
当前代码:
template<typename T>
void myBinaryInsertSort(T array[], int len)
{
for (int i = 1; i < len; i++)
{
int j = i;
T temp = array[j];
int num = 0;
int down = myBinarySearch(array, j + 1, temp);
cout << "元素" << temp << "应该放在本待插入序列的第" << down + 1 << "个位置" << endl;
while (j > down)
{
array[j] = array[j - 1];
j--;
}
array[down] = temp;
cout << "这是第" << i << "趟排序" << endl;
}
}
运行程序:
居然报了一个异常。
我又仔细地看了我的代码,
逻辑上没有问题,
于是按F11进入函数体逐个分析。
终于发现了我的问题:
由于待排序数组里的空格子放了一个数,
即便他是无意义的,
但如果他比他左边的数字要小的话,
子序列就不再是一个升序的序列,
也就不满足二分查找算法了。
所以现在需要解决的问题是:
如何将待排序列变成一个有序数组。
很明显,造成乱序的元素就是起始空格子里的元素,
他左边的序列已经是升序。
比如第一趟排序中:
我们要在{4,3}中找3,
但是{4,3}是一个逆序。
不过,如果我们单独看{4}的话,
4是一个升序,
所以我决定以此为突破口,
写一个新的二分查找算法。
这个新二分查找算法的功能如下:
对一个升序数组查找指定元素:
如果找到了,则返回其下标;
如果没找到,则返回若其在数组里,升序后的下标。
比如{1,2,4,5},找3,
函数会return 2。
开始实现:
int a[] = {1,2,4,5}
第一个例子:查找数在中间
x = 3
第一趟:
a[low] = 1,
a[high] = 5,
a[mid] = 2;
x>mid;
low = 2;
第二趟:
a[low] = 4,
a[high] = 5;
a[mid] = 4;
x<mid;
high = 2;
第三趟:
a[high] = 4,
a[low] = 4,
a[mid] = 4;
x<mid;
high = 1;
跳出循环。
看一下此时low,high,mid的下标:
low:2,
high:1。
也就是说,此时low&high会错位挨在一起,
而中间正好差一个值为x的数。
再举一个例子:查找数在最左侧
x = 0
第一趟:
a[low] = 1,
a[high] = 5,
a[mid] = 2,
x<mid;
high = 0;
第二趟:
a[high] = 1,
a[low] = 1,
a[mid] = 1,
x<mid;
high = -1;
跳出循环,
此时low = 0,
high = -1。
第三个例子:查找数在最右侧
x = 6
第一趟:
a[low] = 1,
a[high] = 5,
a[mid] = 2,
x>mid;
low = 2;
第二趟:
a[high] = 5,
a[low] = 4,
a[mid] = 4,
x>mid;
low = 5;
第三趟:
a[high] = 5,
a[low] = 5,
a[mid] = 5,
x>mid;
low = 4;
跳出循环,
此时low = 4,
high = 3。
第四种情况:要找的数在序列里。
这种情况return mid即可。
通过以上现象,我们得出结论:
low的值即为将要插入的值。
int myBinarySearch(T array[], int len, int x)
{
T low = 0;
T high = len - 1;
while (low <= high)
{
T mid = (low + high) / 2;
if (x < array[mid])
{
high = mid - 1;
}
else if (x > array[mid])
{
low = mid + 1;
}
else
{
return mid;
}
}
return low;
}
排序代码:
template<typename T>
void myBinaryInsertSort(T array[], int len)
{
for (int i = 1; i < len; i++)
{
int j = i;
T temp = array[j];
int num = 0;
int down = myBinarySearch(array, j, temp);
cout << "元素" << temp << "应该放在本待插入序列的第" << down + 1 << "个位置" << endl;
while (j > down)
{
array[j] = array[j - 1];
j--;
}
array[down] = temp;
cout << "这是第" << i << "趟排序" << endl;
}
}
测试代码:
int b[] = {5,21,6,8,4,3,2,1};
int len = sizeof(b) / sizeof(b[0]);
cout << len << endl;
//cout << myBinarySearch<int>(b, len, 2);
myBinaryInsertSort<int>(b, len);
for (int i = 0; i < len; i++)
{
cout << b[i] << endl;
}
运行截图:
大功告成!
仍需改进的地方
虽然成功排好了顺序,
但是和直接插入不同的是,
如果第一次就找到了该元素,
但该元素后面跟着同样值的元素,
这样新插入的元素就会把先进来的同值元素挤到后面去,
造成了不稳定。
这个问题暂时还没想好解决办法,
想出来了会回来解决。