1、我们知道,所有的插入排序的基本思想是:通过构建有序表,并找出待插入数据在有序表中的插入位置,接着移动记录以便空出插入位置,最后将待插入数据放入该位置,至此完成一个数据的插入。而前篇文章介绍的折半插入排序算法主要是在查找插入位置时使用二分法。
2、本文介绍的2-路插入排序算法是为了减少移动记录的次数,但是需要更多的辅助空间。再说的直接一点,这些空间指的就是另一个用于临时存储排序数据的数组。
3、2-路插入排序能够减少移动数据的次数,那么移动次数是如何减少的呢?或者说,减少的是原来的哪一部分移动数据的过程?
4、我们知道,插入排序就是找出位置,然后空出位置(通过移动数据),插入数据的过程。2-路插入排序设置两个指针(这里这样说可能不准确),也可以说是两个保存数组有效数据最值下标的变量first和final。将原本在一个数组中的排序过程放到两个数组中进行,原数组只负责提供排序数据:从原数组中逐个读取数据,并判断它在临时数组中的位置并进行插入。
5、举例来说,一组数据{3, 2, 4, 6, 5}进行排序,如下定义两个数组:
a[5] = {3, 2, 4, 6, 5};// 保存待排序数据
b[5] = {0};// 用0初始化数据,无意义
下面介绍2-路插入排序的过程(用min和max代替first和final,这样可能好理解一点):
(1)取出第1个待排序数据(这里是a[0]),即数据3;
(2)将数据3放入临时数组b的任意位置,因为此时数组b的任意位置都是没有有效数据的;
(3)注:一般来说,都会将第一个待排序数据放入临时数组的第一个位置,即本例的b[0]处;
(4)以将数据3放入b[0]为例,即b[0] = 3;
(5)那么此时,min和max都等于0(min和max可以看做数组b中某个元素的下标);
(6)现在,取出第2个待排序数据a[1],将其与b[min]和b[max]进行比较;
(7)b[max]大于等于b[min],所以(6)的比较结果有三种:小于b[min],大于b[max],或是在此之间;
(8)因为a[1]等于2,b[0]等于3,所以a[1]小于b[min];
(9)数据2比b中最小的有效数据b[min]还小,因此数据2应放到b[min]的左边,也就是b[min - 1]的位置;
(10)但是,我们知道现在min等于0,那么min-1就等于-1了,这显然是不合理的,数组越界了;
(11)我们可以这样去做:将数组b看成是首尾相连的环,也就是说b[0]的左边为b[4];
(12)也就是说,将第二个待排序数据2,放入b[4],即b[4]等于2;
(13)所以b[min]现在应该等于2,即min等于4(注意:max不一定大于min,但是b[max]一定不小于b[min]);
(14)现在,取出第3个待排序数据a[2],将其与b[min]和b[max]进行比较;
(15)a[2]大于b[max],所以b[max + 1]等于a[2],即b[1]等于a[2];
(16)所以现在max等于1;
(17)现在,取出第4个待排序数据a[3],将其与b[min]和b[max]进行比较;
(18)a[3]大于b[max],所以b[max + 1]等于a[3],即b[2]等于a[3];
(19)所以现在max等于2;
(20)现在,取出第5个待排序数据a[4],将其与b[min]和b[max]进行比较;
(21)a[4]大于b[min],小于b[max];这时就需要通过循环判断和移动记录来确定和空出插入位置;
(22)... ...
6、哎,一不小心就是这么一大段,想用文字表述清楚一个过程还真不简单,下面列个表吧:
b[0] | b[1] | b[2] | b[3] | b[4] |
0 | 0 | 0 | 0 | 0 |
3 | 0 | 0 | 0 | 0 |
3 | 0 | 0 | 0 | 2 |
3 | 4 | 0 | 0 | 2 |
3 | 4 | 6 | 0 | 2 |
3 | 4 | 6 | 5 | 2 |
7、由上表可知,最终数组b中的元素为{3, 4, 6, 5, 2},而min等于4,max等于2;最后一步就是将数据正确的复制会数组a中;现在正确的顺序应为b[min],b[min + 1],... ,b[max],但是,现在min(4)大于max(2),这样min自加永远也不可能等于max;下面引出一种巧妙的方法,那就是取余。
8、比较取余与否:
(1)不取余:
数据顺序应为:b[4],b[5],b[6],b[7],b[8],显然越界了;
(2)取余:
数据顺序为:b[4 % 5],b[5 % 5],b[6 % 5],b[7 % 5],b[8 % 5]
即:b[4],b[0],b[1],b[2],b[3]
9、实现代码:
//************************************
// Parameter1: 待排序数组首地址
// Parameter2: 临时数组首地址
// Parameter3: 数组大小
// Attention:a[0]非哨兵
//************************************
void
TwoInsertionSort1(
int
* a,
int
* b,
int
n)
{
int
nMin = 0;
int
nMax = 0;
b[0] = a[0];
for
(
int
i = 1; i < n; i++)
{
if
(a[i] < b[nMin])
{
nMin = (nMin - 1 + n) % n;
b[nMin] = a[i];
}
else
if
(a[i] >= b[nMax])
{
nMax = (nMax + 1 + n) % n;
b[nMax] = a[i];
}
else
{
int
j = nMax;
nMax = (nMax + 1 + n) % n;
while
(a[i] < b[j])
{
b[(j + 1 + n) % n] = b[j];
j = (j - 1 + n) % n;
}
b[(j + 1 + n) % n] = a[i];
}
}
for
(
int
i = 0; i < n; i++)
{
a[i] = b[(i + nMin) % n];
}
}
|
10、关于等号的问题不是任意的,考虑到算法的稳定性:a[i] >= b[nMax]可以加等号,但是a[i] < b[nMin]不能加等号。
11、前面提到的以数组b的任意位置为起点,可以这样测试:
void
TwoInsertionSort2(
int
* a,
int
* b,
int
n)
{
const
int
nBegin = 2;
int
nMin = nBegin;
int
nMax = nBegin;
b[nBegin] = a[0];
for
(
int
i = 1; i < n; i++)
{
if
(a[i] < b[nMin])
{
nMin = (nMin - 1 + n) % n;
b[nMin] = a[i];
}
else
if
(a[i] >= b[nMax])
{
nMax = (nMax + 1 + n) % n;
b[nMax] = a[i];
}
else
{
int
j = nMax;
nMax = (nMax + 1 + n) % n;
while
(a[i] < b[j])
{
b[(j + 1 + n) % n] = b[j];
j = (j - 1 + n) % n;
}
b[(j + 1 + n) % n] = a[i];
}
}
for
(
int
i = 0; i < n; i++)
{
a[i] = b[(i + nMin) % n];
}
}
|
可以将nBegin的值改为任意不使数组越界的值,你会发现排序算法依然正确,这样多少就能体会取余的巧妙之处了。