假设有这么一个数组:
int Array[20] = {15, 3, 6, 8, 2 ,11 ,12 ,19 ,1 ,7 ,17 ,20 ,4 ,13 ,18 ,10 ,5 ,16 ,14 ,9};
如下的柱状图,Index表示数组的下标,Value表示数组下表对应元素的值,即Array[0] = 15, Array[1] = 3, Array[2] = 6 , ……
插入法排序:
我们通常整理扑克牌的时候,习惯按照某个顺序(升序或者降序)。当抓牌的过程中正是按照顺序一张一张插入到已经排好的序列当中,这个过程像极了插入法。
具体的算法过程如下(升序):
- 外层循环从数组的第二个元素开始(即array[1]),内层循环结束后,外层循环加1,直到扫描到队列的最后一个元素
- 内层循环从外层循环的元素开始,同内循环的前一个元素比较,如果比前一个元素还要小,则两个数据发生交换,之后,内循环自减1,再次同前一个元素比较
- 所以跳出内循环的条件是2个,而且时必须同时满足,
- 第一个条件为:内循环的下标要大于0,即array[0]是数组的边界,不能取到array[-1]这样的元素,
- 第二个条件为:内循环的当前元素要比前一个元素要小,才会发生交换
第一次外循环开始的时候( i=1),
j=1,比较array[1]和array[0]的大小,若满足array[j] < array[j-1]的时候,数组里的2个元素发生交换;
若不满足以上条件,则数据不发生交换
第一次循环结束后,数据交换后的结果如下图:
第二次外循环开始的时候( i=2)
- j=2时;比较array[2]和array[1]的大小,满足交换条件,所以array[2]和array[1]的数据发生交换,交换后array[2] = 15, array[1] = 6,变量j自减1
- j=1时;比较array[1]和array[0]的大小(此时array[1] = 6, array[0] = 3 ),不满足交换条件,内层循环终止
- 变量i自加1,准备第三次外层循环
第二次循环结束后,数据交换后的结果如下图
第三次外循环开始的时候( i=3)
- j=3时;比较array[3]和array[2]的大小,满足交换条件,所以array[3]和array[2]的数据发生交换,交换后array[3] = 15, array[2] = 8,变量j自减1
- j=2时;比较array[2]和array[1]的大小(此时array[2] = 8, array[1] = 6 ),不满足交换条件,内层循环终止
- 变量i自加1,准备第四次外层循环
第三次循环结束后,数据交换后的结果如下图
第四次外循环开始的时候( i=4)
- j=4时;比较array[4]和array[3]的大小,满足交换条件,所以array[4]和array[3]的数据发生交换,交换后array[4] = 15, array[3] = 2,变量j自减1;
- j=3时;比较array[3]和array[2]的大小,交换后array[3] = 8,array[2] = 2;
- j=2时;比较array[2]和array[1]的大小,交换后array[2] = 6,array[1] = 2;
- j=1时;比较array[1]和array[0]的大小,交换后array[1] = 3,array[1] = 0;
- j=0,不满足j>0的循环条件,所以内循环结束,变量i自加1,准备第五次循环。
第四次内循环的交换过程如下图,因为2是最小的,所以要依次同前一个数据进行交换,具体过程如下图:
第四次循环结束后,数据交换后的结果如下图:
总结:
以上是插入法的前4次插入的逻辑图,从上面的图可以看出:
- 外循环是从图上蓝色的柱体开始的,然后依次扫描直至队列尾
- 内循环的起点位置也是图上蓝色的柱体,依次向队列头扫描,遇到比自己大的则进行交换
- 每一次新的外循环开始时,队列左边黑色的柱体是有序的(升序)
- 每一轮内循环的过程,本质就是把蓝色的柱体,插入到黑色有序的队列中
- 灰色的柱体是待插入的柱体,每一轮外循环开始后,内循环是不可能扫描到灰色柱体的(因为内循环是自减的)
插入排序的核心代码如下:
void InsertionArray(int* ptrArray)
{
if ( NULL == ptrArray || sizeof(ptrArray) < 2)
{
cout << "Array is NULL or no need to sort caused array length." << endl;
return;
}
//外层循环从array[1]开始,依次扫描到队列尾
for (int i = 1; i < sizeof(Array) / sizeof(int); i++)
{
//每次内层循环开始时,记录一下待插入数据的值,即图中蓝色的柱状图
int temp = ptrArray[i];
int j = 0;
//内层循环从插入数据开始,向队列头扫描,遇到比待插入数据(temp)大的值,则移动数据
for (j = i; j>0 && temp < ptrArray[j - 1]; j--)
{
ptrArray[j] = ptrArray[j - 1];
}
//最后一次循环j--之后,即不满足temp < ptrArray[j - 1]条件,所以把待插入的值填充到ptrArray[j]的位置
ptrArray[j] = temp;
}
};
从第四次排序的过程来分析,本次(第四次)排序的时间复杂度并不理想(最不理想的情况就是待插入的数据比前面的数据都要小),因为待插入的数据和所有的元素都发生了交换,如果一个数组部分有序,或者仅有几个元素的位置不确定,那么插入排序的效率是很高的。
对于一个大规模无序的随机数组,直接利用插入法进行排序时间复杂度并不理想,如果可以使这个数组经过几个简单的步骤后变成部分有序或者规则有序,再利用插入法排序将大大提高插入排序的效率,一种基于插入排序的快速排序算法应运而生----希尔排序
希尔排序
希尔排序是基于插入排序的一种快速排序,希尔排序的主要思想就是把随机无序的数组按增量分组,对每个增量的数组再分别利用插入法排序,当选取合适的因子之后,随着划分的增量数组排序后会逐渐减少,最终当增量减少到1时,最后一次利用插入法排序,循环体将全部结束。
还是之前1~20的一个无序的随机数组:
选取一个合适的步长,就是之前说的增量,本例中选用步长为3的因子
所以第一次的步长为20/3 =6
第二次的步长为6/3 = 2
第三次的步长为 2/3 < 1 ,所以第三次的步长也是最后一次的步长,即为1
第一次步长为6时的分组情况,未排序
当步长为6时,一共有6个分组,分别是上图的红色,黄色,绿色,蓝色,紫色和白色
对应的饿数据:
红色{15,12, 4, 14};
黄色{3,19,13,9};
绿色{6,1, 18};
蓝色{8,7,10};
紫色{2,17, 5};
白色{11,20,16};
分别对每个组进行插入排序,即
红色{4,12, 14, 15};
黄色{3,9,13,19};
绿色{1,6, 18};
蓝色{7,8,10};
紫色{2,5, 17};
白色{11,16,20};
第一次步长为6的分组情况,已排序;这也第二次步长为2排序的初始状态
第二次步长为2的分组情况,未排序;
数组里的数据没有发生变化,重新分组后重新用颜色标注了一下
当步长为2时,一共有2个分组,分别是上图的红色,黄色
对应的饿数据:
红色{4,1,2,12,6,5,14,18,17,15};
黄色{3,7,11,9,8,16,13,10,20,19};
分别对以上2组进行插入排序,得到新的序列应该为:
红色{1,2,4,5,6,12,14,15,17,18};
黄色{3,7,8,9,10,11,13,16,19,20};
第二次步长为2的分组情况,已排序;这也第三次步长为1排序的初始状态
当步长为1时,最后一次利用插入排序得到最终的排序结果
希尔排序的核心代码如下:
void ShellArray(int* ptrArray)
{
if (NULL == ptrArray || sizeof(ptrArray) < 2)
{
cout << "Array is NULL or no need to sort caused array length." << endl;
return;
}
//这里选择3作为因子
const int FACTOR = 3;
int iStep = (sizeof(Array) / sizeof(int)) / FACTOR;
//控制分组的变量,例如本例中第一次循环,iStepIncrease = 0 是红色的分组,通过iStepIncrease的增加依此指向黄色,绿色,蓝色,紫色,白色
int iStepIncrease = 0;
//因为选择3作为因子,iStep /FACTOR可能存在小于0的情况,此时还需要做最后一次插入排序
bool boLast = true;
//如果开始的步长就小于1,那么直接做一次插入排序就可以了
if (iStep < 1){
iStep = 1;
boLast = false;
}
//若步长小于或者等于0时,循环体结束
while (iStep > 0)
{
//该层循环控制分组里数据的移动
for (int i = iStep + iStepIncrease; i < sizeof(Array) / sizeof(int); i += iStep)
{
//该层循环比较分组里数据的大小,并进行交换
for (int j = i; j > 0 && ptrArray[j] < ptrArray[j - iStep]; j -= iStep)
{
//交换数据,这里可以简化为上面插入排序的代码
int temp = ptrArray[j];
ptrArray[j] = ptrArray[j - iStep];
ptrArray[j - iStep] = temp;
}
}
cout << "Step is : " << iStep << " Now the array is : " << endl;
for (int k = 0; k < sizeof(Array) / sizeof(int); k++)
{
cout << ptrArray[k] << " ";
}
cout << endl;
//当步长确定后,负责轮询每个分组,若没有访问到最后一个分组,则自加1
if (iStepIncrease < iStep - 1)
{
iStepIncrease++;
}
else//若所有的分组全部访问结束后,需要重新生成步长,即重新分组,若步长为0时,还需要进行最后一次插入排序
{
iStep = iStep / FACTOR;
iStepIncrease = 0;
if (iStep < 1 && boLast == true){
iStep = 1;
boLast = false;
}
}
}
}