直接插入排序
直接插入排序的思想是:把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为
止,得到一个新的有序序列 。
我们可以首先了解弄清楚直接插入排序(升序)的单趟过程:
我们呢向一个有序序列中插入一个数字6,end定位到序列最末尾的位置,与待插入数字x进行比较,如果a[end]>x,end就向前移动一位,并将a[end]向后边挪动一位。
数字6的插入属于一种普通情况的插入(插到序列中间)。我们现在来试试数字1的插入(插到序列的头部)。
弄清楚这两中情况的插入,我们大概能写出直接插入排序的单趟过程了。
//直接插入排序的单趟过程
void InsertSort(int* a,int sz){
int end;//有序序列最末尾的位置
int x;//待插入的数字
while(end>=0){
if(a[end]>x){
a[end+1]=a[end]
end--;
}
else{
break;
}
}
a[end+1]=x;
}
直接插入排序的单趟过程我们弄清楚之后,我们现在在想想如果把给出的一组序列按照直接插入的方法排序呢?也就是如何控制我们写的end和x的位置。
我们先把end的位置置为0,x就置为end+1的位置,这就相当于end之前的数字都排好序了,再将end后边的数字一次插入end之前的序列。这就完成了直接插入排序的完整过程。
void insertSort(int* a, int sz) {
for (int i = 0; i < sz - 1; i++) {
int end = i;
int x = a[end + 1];
while (end >= 0) {
if (x < a[end]) {
a[end + 1] = a[end];
end--;
}
else {
break;
}
}
a[end + 1] = x;
}
}
直接插入排序的时间复杂度的最坏情况是O(N^2),就是序列是降序插入数值排升序的情况。
时间复杂度最好的情况是O(N),向一个序列是升序插入数值排升序的情况。
希尔排序
希尔排序是一种对直接插入排序的有一种优化。我们刚刚也有提到,直接插入排序时间复杂度最好的情况是O(N),这个时候序列是一个接近有序的序列。希尔排序呢就是先将序列进行预排序,使序列接近有序,再使用直接插入排序。
希尔排序使如何进行预排序的呢?
首先将序列按gap分组,对分组进行直接插入排序,使序列接近有序。
假如我们将gap的值设为3,那么上述序列将被分为3组,每一组进行直接插入排序。
我们还是先弄清楚其中一组是怎么完成直接插入排序的,我们以第一组为例。
我们把end置为0的位置,x就是end+gap的位置(就是分组的下一个位置),如果a[end]>x,就将a[end]向分组中的下一个位置挪动,并将end置为分组中的前一个位置。
void ShellSort(int* a, int sz) {
int gap = 3;
int end = 0;
int x = a[end + gap];
while (end >= 0) {
if (a[end] > x) {
a[end + gap] = a[end];
end -= gap;
}
else {
break;
}
}
a[end + gap] = x;
}
这样我们就完成了单组里边的直接插入排序,我们注意到这里和直接插入排序十分相似,当gap=1时,就是直接插入排序的逻辑了。现在我们需要控制end实现单组的排序。
我们观察上图,红色、绿色和蓝色的各组,每组的最后一个元素都在n-gap的后边,由直接插入排序的原则控制循环如下,则能完成单组的直接插入排序。
for (int i = 0; i < sz - gap; i += gap)
剩下的完成多组排序,则需要控制i了。
for (int j = 0; j < gap; j++)
for (int i = j; i < sz - gap; i += gap)
由上分析可以写出下边的完成预排的代码:
void ShellSort(int* a, int sz) {
int gap = 3;
//完成预排
for (int j = 0; j < gap; j++) {
for (int i = j; i < sz - gap; i += gap) {//i += gap是一组一组的排
int end = i;
int x = a[end + gap];
while (end >= 0) {
if (a[end] > x) {
a[end + gap] = a[end];
end -= gap;
}
else {
break;
}
}
a[end + gap] = x;
}
}
上边我们写了两个循环完成希尔排序的预排,下边我们使用一个循环完成希尔排序的预排。我们先上代码,再细细解释。
//完成预排
for (int i = 0; i < sz - gap; i++) {//i++是多组并排
int end = i;
int x = a[end + gap];
while (end >= 0) {
if (a[end] > x) {
a[end + gap] = a[end];
end -= gap;
}
else {
break;
}
}
a[end + gap] = x;
}
我们仔细观察上边代码会发现,我们只有一个地方发生了变化,就是i += gap变成了i++;
i += gap是一组排完了,再排下一组。
而i++是多组同时排。具体我们看下边的图解。
i等于0到2的时候红绿蓝三组里边只有一个数,都是和各组的下一个数字进行比较插入的。当i大于2的时候,第i位的数字都是和每组里边的[0,end]范围内的有序数进行比较插入的。下图中位置发生移动的数字都用浅绿的圈圈标注过的。乍一看比较糊,希望静下来,这里的逻辑并不绕。
这里的结果只是预排结果。是进行希尔排序的第一步,生成一个比较有序的序列。
进行预排序之后,我们控制gap的变化即可完成希尔排序的逻辑了。加油,各位!
gap越大,预排越快,预排后越不接近有序
gap越小,预排越慢,预排后越接近有序
gap=1就是直接插入排序
这里我们可以这么理解,当我们按gap=3预排,其结果就是5 1 2 5 6 3 8 7 4 9
而当我们gap=1的时候,其结果就是一个有序序列1 2 3 4 5 5 6 7 8 9
void ShellSort(int* a, int sz) {
int gap = 3;
while (gap > 1) {
gap /= 2;
//完成预排
for (int i = 0; i < sz - gap; i++) {//i++是多组并排
int end = i;
int x = a[end + gap];
while (end >= 0) {
if (a[end] > x) {
a[end + gap] = a[end];
end -= gap;
}
else {
break;
}
}
a[end + gap] = x;
}
}
}
我们按照 gap /= 2;的方式控制gap的变化,其结果会越来越小,最终为1,就能达到我们让一个序列接近有序,再按gap=1的直接插入排序排一次序,时间复杂度就接近O(N)。但是希尔排序的时间复杂度实际上是O(N^1.3)。
希尔排序是一款很优秀的排序算法,适合数据量大的时候使用的一种排序算法。