一、插入排序
1、基本思想
插入排序的基本思想,把待排序的一个序列,按照一定的大小顺序,将一个个数插入到一个已经排好序的有序序列中。
假设有一组无序序列,我们需要排一个升序的序列。
我们可以将第一个数,单独的看成是一个有序的序列。
然后把这个有序序列的右边一个值当作待插入数(如3),当3小于5就放在5的左边。
接下来让每个待插入的数,和已经有序的序列中的值进行比较,若小于其中一个值就放在左边,直到排成有序序列。
通过这个方法,我们可以很明显的看到它的时间复杂度是O(N^2),可见它的效率很低。
2、插入排序代码实现
首先我们需要一个变量,它能从有序序列最后一个值开始,方便和插入的值进行遍历比较。
并且每次需要插入比较的值,都是最开始end+1个位置的,而之后需要比较end–的位置会变化,所以可以通过一个tmp变量储存一开始待插入end+1的值,保存好待插入的值后,就可以对有序序列值的位置进行改变,再在最后将tmp插入到应该在的位置。
并且end的最大值应该是在最后一个数的前面一位,不然end+1会越界的。
//插入排序
void InsertSort(int* arr, int n)
{
for (int i = 0; i < n - 1; i++)//end位置范围第1到第n-1个数。
{
int end=i; //记录end位置
int tmp = arr[end + 1]; //记录待插入的数
while (end >= 0)
{
if (arr[end] > tmp)
{
arr[end + 1] = arr[end];//直接覆盖上一个值。
end--;
}
else
{
break;//当待插入的数大于或等于这个数就不需要往下了。
}
}
arr[end + 1] = tmp;//最后再赋待插入的值
}
/*print(arr, n);*/
}
值得注意的是:
如果排顺序
插入排序当序列接近顺序或者顺序的时候,时间复杂度只有O(N),而当序列接近逆序或者逆序的时候,时间复杂度为O(N*N)
从稳定性角度来看,直接插入排序中没有出现分组的情况,所有的数都在一个组里进行,相同的数之间顺序在有序后不会发生改变,所以稳定。
二、希尔排序
1、基本思想—预处理和插入排序
希尔排序在插入排序的基础上通过预处理,巧妙的大大提高了算法的效率。
假设需要对以下序列排升序。
现在对预处理中的一种情况进行处理:
希尔排序通过一个间隔(假设这种情况下间隔gap=3),从最开始的数开始依次将一个序列划分,一直到那个数的位置+3越界为止(如8的位置),并且也可以对划分进行分组(如5 2 1一组,7 3 0一组,4 8一组),每个数只会在一个组,所以间隔为1的时候就只有一个组。
从头开始,分别对每组进行插入排序,比如第一组5 2 1,先将5看成一个有序序列,并且令其位置为end,再用一个变量储存end+gap位置的数,end=end-gap后,如果end小于0,end+gap位置就放入待插的数。
直到这一组排成有序为止。
但是希尔排序的算法,并不是通过一组一组的单独排序,而是通过从第一个数开始,依次进行排序。
现在让我们来看看,不同的间距(gap) 对一个序列有多大的影响。
如果一个序列有n个数,那么最大间距就是n-1,而当间距为n-1的时候,无非就是第一个数和最后一个数进行排序。
如果一个序列的间距为1,那么就是插入排序。
所以从两种极端情况来看,当间距大的时候序列就越无序,当间距小的时候序列就越趋向有序。
所以在预处理,我们需要先通过大的间隔进行排序,再通过小的间隔进行排序,而大的间隔排序时,整个排序次数较低,在接近有序的基础上,通过小的间隔排序,因为更趋近有序,所以排序的次数似乎也变得低了,下面通过代码看看。
2、代码实现
希尔排序是直接在插入排序的基础上进行优化。
1、先通过预处理,让序列接近有序
2、再直接进行插入排序
gap > 1都是对序列进行预处理,而当gap = 1的时候,由于这个时候序列已经接近有序,所以在直接插入排序效率就很高。
第一种写法:齐头并进
//希尔排序
void ShellSort(int* arr, int n)
{
int gap = n;//因为数大、数小不确定,所以开始定义为n。
while (gap > 1)
{
//gap = gap / 2; //这也是一种取gap的方式
gap = gap / 3 + 1; //加1确保gap最后的间隔为1
//序列n-gap-1是分组最后的位置,当gap为1就是插入排序
for (int i = 0; i < n - gap; i++)
{
int end = i;
int tmp = arr[end + gap];
while (end >= 0)
{
if (arr[end] > tmp)
{
arr[end + gap] = arr[end];
end -= gap;
}
else
{
break;
}
}
arr[end + gap] = tmp;
}//end of for
}//end of while
/*print(arr, n);*/
}
另一种写法,它不同在于分组实现
void ShellSort(int* a, int n)
{
int gap = n;
int i = 0;
while (gap > 1)
{
gap = gap / 3 + 1;
//
for (int j = 0; j < gap; j++)
{
for (i = j; i < n - gap; i+=gap)
{
int end = i;
int tmp = a[end + gap];
for (; end >= 0; end -= gap)
{
if (tmp < a[end])
{
a[end + gap] = a[end];
}
else
{
break;
}
}
a[end + gap] = tmp;
}
}
}
}
希尔排序的时间复杂度不好计算,第一点在于gap的取值方式有多种,第二点在插入排序的过程中因为gap的改变,导致序列的有序情况不同,也就不能用最坏的情况去衡量每一次的排序,也就难以计算。最后可以保守在O(n^1.3)。
稳定性,希尔排序中,因为gap导致分组的存在,相同的数在排序后可能先后顺序会不一样,所以不稳定。