直接插入排序(Straight Insertion Sort)
直接插入排序的基本操作是将一个元素插入到已经排序好的一个序列中,得到一个新的,元素个数增1的有序表。
例如有一个已经排序好的序列:
23, 45, 66, 88
我想要将55插入进去,如何做?
我可以从后向前扫描,发现88比55大,在发现66比55大,再发现45比55小,那么就将66以及88向后顺序移动一个位置,把55插入到原来66所在位置即可。
23, 45, 66, 88
23, 45, empty-hole(66), 66, 88
23, 45, 55, 66, 88
先来看看一个实现:
#include <stdio.h>
#include <stdlib.h>
/************************************************
* 函数名称:print_array
* 参数列表:1.int *parray:一个int类型指针,指向一个数组首地址
* 2.int n:一个int类型变量,指明数组大小
* 函数描述:输出数组中每个元素
* 返回值 :void
* Author :test1280
* History :2017/04/14
* *********************************************/
void print_array(int *parray, int n)
{
int i;
for (i=0; i<n; i++)
{
printf("%d ", parray[i]);
}
printf("\n");
}
/****************************************************
* 函数名称:insertSort
* 参数列表:1.int *parray:一个int类型指针,指向一个数组首地址
* 2.int n:一个int类型变量,指明数组大小
* 函数描述:直接插入排序
* 返回值 :void
* Author :test1280
* History :2017/04/17
* ***************************************************/
void insertSort(int *parray, int n)
{
int i, j;
// 临时存放变量
int tmp;
for (i=1; i<n; i++)
{
// 如果后一个比前面的i个元素都大(前i个元素已有序)则什么也不做
if (parray[i]<parray[i-1])
{
printf(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n");
printf("位置%d处的元素比前面的%d个元素最大的要小,需要插入\n", i, i);
print_array(parray, n);
tmp = parray[i];
// 这一句实际上可以与下面的for合并,只是在这里已经知道了
// 必定需要交换,不必要在下面再比较一次。
parray[i] = parray[i-1];
printf("tmp is %d\n", tmp);
printf("元素向后移动之前:\n");
print_array(parray, n);
for (j = i-2; tmp < parray[j] && j>=0; j--)
parray[j+1] = parray[j];
printf("元素向后移动之后:\n");
printf("j :%d\n", j);
print_array(parray, n);
parray[j+1] = tmp;
printf("tmp重置后:\n");
print_array(parray, n);
printf("<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n");
}
}
}
int main()
{
int arr[] = {49, 38, 65, 97, 76, 13, 27, 0};
printf("original:\n");
print_array(arr, 8);
insertSort(arr, 8);
printf("after sort:\n");
print_array(arr, 8);
return 0;
}
代码很大程度上都是调试输出使用,实际排序代码是很简洁很少量的。
输出如下:
[test1280@localhost sort]$ gcc -o main main.c
[test1280@localhost sort]$ ./main
original:
49 38 65 97 76 13 27 0
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
位置1处的元素比前面的1个元素最大的要小,需要插入
49 38 65 97 76 13 27 0
tmp is 38
元素向后移动之前:
49 49 65 97 76 13 27 0
元素向后移动之后:
j :-1
49 49 65 97 76 13 27 0
tmp重置后:
38 49 65 97 76 13 27 0
<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
位置4处的元素比前面的4个元素最大的要小,需要插入
38 49 65 97 76 13 27 0
tmp is 76
元素向后移动之前:
38 49 65 97 97 13 27 0
元素向后移动之后:
j :2
38 49 65 97 97 13 27 0
tmp重置后:
38 49 65 76 97 13 27 0
<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
位置5处的元素比前面的5个元素最大的要小,需要插入
38 49 65 76 97 13 27 0
tmp is 13
元素向后移动之前:
38 49 65 76 97 97 27 0
元素向后移动之后:
j :-1
38 38 49 65 76 97 27 0
tmp重置后:
13 38 49 65 76 97 27 0
<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
位置6处的元素比前面的6个元素最大的要小,需要插入
13 38 49 65 76 97 27 0
tmp is 27
元素向后移动之前:
13 38 49 65 76 97 97 0
元素向后移动之后:
j :0
13 38 38 49 65 76 97 0
tmp重置后:
13 27 38 49 65 76 97 0
<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
位置7处的元素比前面的7个元素最大的要小,需要插入
13 27 38 49 65 76 97 0
tmp is 0
元素向后移动之前:
13 27 38 49 65 76 97 97
元素向后移动之后:
j :-1
13 13 27 38 49 65 76 97
tmp重置后:
0 13 27 38 49 65 76 97
<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
after sort:
0 13 27 38 49 65 76 97
[test1280@localhost sort]$
一开始序列为:
49 38 65 97 76 13 27 0
这个时候可以想象成序列是:
49 empty empty …
当i=1时将38插入上述序列中,过程如下:
比较当前的元素(由外循环i指出位置)和已排好序的序列中最后一个,最后一个必然是最大值/最小值,即,比较49和38,其中49是最大值,38是当前元素,i=1指出了当前元素的位置;
如果当前元素和已排好序的序列最后一个元素满足一定关系,那么就执行:
现将当前元素保存在tmp中(挖坑),然后用已排好序的序列中最后一个元素去替换i指出的位置(填坑),然后进行循环(i-2),从之前的那个序列的最后的位置的前一个位置开始向前(向左)搜索,如果发现有满足条件的,那么就移动,最后移动不了的,直接用tmp的值填坑即可。
干说也不明白,看看示例。
现在有一个序列,49 empty empty …
要把38插入进去,首先比较38和49,发现38小于49,就把38保存在tmp中,然后把49填补到原来38所在的位置(i=1)。
然后将i减去2,这个时候i是-1,无法进行循环,则直接将38放置到比当前i(-1)大1处的位置,即0位置。
此时序列为:38 49 empty empty …
然后要插入65,过程如下:
发现65比之前已排好序的序列的最大值还要大,直接放置在最后一个即可。
由于是在同一个数组,当然就不用移动,即什么也不做。
同上,可以插入97。
此时序列为:
38 49 65 97 empty empty…
然后要插入位置4处的76。
首先比较76和之前已经有序的序列的最后一个值,也即之前有序序列的最大值,97,发现比97小,应该插入。
这个时候先将76存在tmp中(挖坑),然后将97置入位置4处(填坑),然后i-2(4-2)即指在位置2处,即已经有序的倒数第二个元素处,从此开始和76比较,发现是65,比76小,没有进入for循环后移,其实这个时候应该是这样子的:
38 49 65 empty(97) 97 empty empty…
i=2
然后把tmp置入i+1处,即:
38 49 65 76 97 empty empty…
然后要把13插入,过程如下:
首先比较13和97,发现比97小,则将13保存至tmp,然后将97放置到13的原位置处,然后从76处开始向前(向左)比较后移,13比76小,76后移:
38 49 65 76 76 97 empty…
然后13比65小:
38 49 65 65 76 97 empty …
然后13比49小:
38 49 49 65 76 97 empty …
然后13比38小:
38 38 49 65 76 97 empty …
此时j=-1,循环不了了,咋办?直接用tmp放置到i+1处,即0位置处。
然后插入27,过程如下:
27比97小,然后挖坑,填坑,结束后应该是:
tmp=27
13 38 49 65 76 97 97 empty …
然后依次后移:
13 38 49 65 76 76 97 empty …
13 38 49 65 65 76 97 empty …
13 38 46 46 65 76 97 empty …
13 38 38 46 65 76 97 empty …
此时j=0,发现27比13大,结束后移,将27置在i+1处,即1位置处:
13 27 38 46 65 76 97 empty …
至于最后一个0,类似13那次的循环,不再赘述。
附一个简洁版的吧,上面的代码乱哄哄的:
#include <stdio.h>
#include <stdlib.h>
/****************************************************
* 函数名称:insertSort
* 参数列表:1.int *parray:一个int类型指针,指向一个数组首地址
* 2.int n:一个int类型变量,指明数组大小
* 函数描述:直接插入排序
* 返回值 :void
* Author :test1280
* History :2017/04/17
* ***************************************************/
void insertSort(int *parray, int n)
{
int i, j;
int tmp;
for (i=1; i<n; i++)
{
if (parray[i]<parray[i-1])
{
tmp = parray[i];
parray[i] = parray[i-1];
for (j = i-2; tmp < parray[j] && j>=0; j--)
parray[j+1] = parray[j];
parray[j+1] = tmp;
}
}
}
int main()
{
int arr[] = {49, 38, 65, 97, 76, 13, 27, 0};
insertSort(arr, 8);
for (int i=0; i<8; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
有个地方解释下:
为啥当满足一定条件要先挖坑然后填坑?
其实这是由于:已经知道了有序序列最后一个元素和当前i处元素满足一定关系,那么这个有序序列最后一个元素必定要被后移啊!
接下来就是华华丽丽的大迁移,也就是那个for循环。
既然挖坑填坑和for实际是干一件事情,那能否合并到一起?
/****************************************************
* 函数名称:insertSort
* 参数列表:1.int *parray:一个int类型指针,指向一个数组首地址
* 2.int n:一个int类型变量,指明数组大小
* 函数描述:直接插入排序
* 返回值 :void
* Author :test1280
* History :2017/04/17
* ***************************************************/
void insertSort(int *parray, int n)
{
int i, j;
int tmp;
for (i=1; i<n; i++)
{
if (parray[i]<parray[i-1])
{
tmp = parray[i];
for (j = i-1; tmp < parray[j] && j>=0; j--)
parray[j+1] = parray[j];
parray[j+1] = tmp;
}
}
}
看到了吧?坑还是要挖的。。只不过是不填了,都和下面的for合并在一起了,只是这样子就多比较一次,在for循环时,第一次总是成功的。。。
为啥还是要挖坑呀?因为空间大小固定啊!然后你还要后移,当然需要先把对应的值保存一下,省的移动时被覆盖。
额,能不能从左边开始扫描?
/****************************************************
* 函数名称:insertSort
* 参数列表:1.int *parray:一个int类型指针,指向一个数组首地址
* 2.int n:一个int类型变量,指明数组大小
* 函数描述:直接插入排序
* 返回值 :void
* Author :test1280
* History :2017/04/17
* ***************************************************/
void insertSort(int *parray, int n)
{
int i;
int tmp;
for (i=1; i<n; i++)
{
if (parray[i]<parray[i-1])
{
tmp = parray[i];
// 指向要被移动的第一个元素(移动完后空余下的坑)
int pos = 0;
while (parray[pos]<=tmp)
{
pos++;
}
// 指向要被移动的最后的元素
int mvIdx = i-1;
for (; mvIdx >= pos; mvIdx--)
{
parray[mvIdx + 1] = parray[mvIdx];
}
parray[pos] = tmp;
}
}
}
代码描述了从左开始扫描,寻找第一个将要被移动的元素的位置。
注:之前从右向左扫描,是顺便将每个元素后移的。
仔细想想上面的代码,实际是有问题的。
在:while (parray[pos]<=tmp)处,可以对pos加个限制(与i)。
可不可以干掉那个if呢?
/****************************************************
* 函数名称:insertSort
* 参数列表:1.int *parray:一个int类型指针,指向一个数组首地址
* 2.int n:一个int类型变量,指明数组大小
* 函数描述:直接插入排序
* 返回值 :void
* Author :test1280
* History :2017/04/17
* ***************************************************/
void insertSort(int *parray, int n)
{
int i;
int tmp;
for (i=1; i<n; i++)
{
tmp = parray[i];
int pos = i-1;
while (pos >= 0 && parray[pos] > tmp)
{
pos--;
}
// 完全不需要插入,直接continue就好
if (pos == i-1)
{
continue;
}
// 指向要被移动的最后的元素
int mvIdx = i-1;
for (; mvIdx > pos; mvIdx--)
{
parray[mvIdx + 1] = parray[mvIdx];
}
parray[pos + 1] = tmp;
}
}
输出结果:
[test1280@localhost sort]$ ./main
original:
49 38 65 97 76 13 27 0
after sort:
0 13 27 38 49 65 76 97
[test1280@localhost sort]$
再看看我们最后书写的代码,是不是和一开始写的代码很像?