这里简单的介绍一下插入排序和希尔排序的算法实现,为简单起见,排序为升序且排序的数组是整形数组。
一、插入排序
(一)、算法思路
把数组里的第一个元素视为有序的,然后取第二个元素与前面的元素作比较,如果该元素小于第一个元素,则把第一个元素往后移,第二个元素往前移,这样,我们数组前两个元素就变得有序了。接着再取第三个元素,将其与前面的元素作比较,如果该元素小于其前面的元素,则将该元素往前移,将被比较的元素往后移,然后再将该元素与前一个元素作比较,如果当该元素大于其前面的元素,那么当前该元素所在的位置就是其正确的顺序,这个时候,数组有序的序列长度增加了一,其结果就是把一个元素插入到前面的已经有序的序列当中去,所以叫做插入排序,以此重复插入排序,数组变得有序。
下面是插入排序的动图:
(二)、算法的实现
void InsertSort(int* arr, int nums)
{
for (int i = 1; i < nums; i++)
{
int key = arr[i];//存储待插入排序的值
int j;
for (j = i; j > 0; j--)
{
//依次往前作比较进行移动
if (key < arr[j - 1])
arr[j] = arr[j - 1];
else
break;
}
//此时j的位置就是key插入排序的位置
arr[j] = key;
}
}
(三)、时间复杂度和稳定性
插入排序的时间复杂度为。当待排序的数组为完全或接近有序时,插入排序的时间复杂度为,而当待排序的数组完全或接近逆序时,插入排序的时间复杂度为。
插入排序是稳定的。
二、希尔排序
希尔排序(Shell's Sort)是插入排序的一种又称“缩小增量排序”(Diminishing Increment Sort),是直接插入排序算法的一种更高效的改进版本。希尔排序是非稳定排序算法。该方法因 D.L.Shell 于 1959 年提出而得名。
(一)、算法思路
希尔排序是从插入排序上改进的,先来看看改进了什么。假如有一组数组的序列是逆序的:[10,9,8,7,6,5,4,3,2,1],那么我们的插入排序在每次插入待排序的元素时,都要以一次移动一个数据这样(增量为一)依次移动到数组的开头位置,这样时间复杂度就为了。而我们的希尔排序一开始以一个增量开始跳跃式的进行插入排序,叫做预排序,将数组中较小的元素移动到数组的左边去,将数组中较大的元素移动到数组的右边去,排序一次后,数组左边差不多都是小的元素,数组的右边差不多都是大的元素。再缩小增量进行增量插入排序,跳跃进行插入排序的跳跃元素个数减小,执行该增量的插入排序,以此重复,每次执行完增量插入排序后数组的元素逐渐接近有序,当增量变为一时,执行的是朴素的插入排序,这个时候因为预排序的作用数组的元素已经非常接近有序了,执行完插入排序后,数组变为有序。
下面以缩小增量为除以2进行希尔排序来演示,假设待排序的数组为:
5 | 7 | 2 | 10 | 6 | 4 | 3 | 1 | 8 | 9 |
增量为5:
以增量为10 / 2 = 5 分组进行增量插入排序 | |||||||||
5 | 7 | 2 | 10 | 6 | 4 | 3 | 1 | 8 | 9 |
对第一组红色元素进行增量插入排序 | |||||||||
4 | 7 | 2 | 10 | 6 | 5 | 3 | 1 | 8 | 9 |
对第二组黄色元素进行增量插入排序 | |||||||||
4 | 3 | 2 | 10 | 6 | 5 | 7 | 1 | 8 | 9 |
对第三组绿色元素进行增量插入排序 | |||||||||
4 | 3 | 1 | 10 | 6 | 5 | 7 | 2 | 8 | 9 |
对第四组蓝色元素进行增量插入排序 | |||||||||
4 | 3 | 1 | 8 | 6 | 5 | 7 | 2 | 10 | 9 |
对第五组紫色元素进行增量插入排序 | |||||||||
4 | 3 | 1 | 8 | 6 | 5 | 7 | 2 | 10 | 9 |
增量为2:
以增量为5 / 2 = 2为增量分组进行插入排序 | |||||||||
4 | 3 | 1 | 8 | 6 | 5 | 7 | 2 | 10 | 9 |
对第一组红色元素进行增量插入排序 | |||||||||
1 | 3 | 4 | 8 | 6 | 5 | 7 | 2 | 10 | 9 |
对第二组黄色元素进行增量插入排序 | |||||||||
1 | 2 | 4 | 3 | 6 | 5 | 7 | 8 | 10 | 9 |
增量为1:
以增量为2 / 2 = 1为增量进行朴素插入排序 | |||||||||
1 | 2 | 4 | 3 | 6 | 5 | 7 | 8 | 10 | 9 |
对第一组红色元素进行增量插入排序 | |||||||||
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
我们可以看到,前两次的预排序使得最后一次的朴素插入排序非常轻松,因为待排序的数组经过预排序后变得十分接近有序了。
(二)、算法实现
//希尔排序
void SheelSort(int* arr, int nums)
{
int gap = nums;
while (gap > 1)
{
//缩小增量进行插入排序
gap /= 2;
//分组进行插入排序
//有gap组
for (int i = 0; i < gap; i++)
{
//增量为gap的插入排序
for (int j = i + gap; j < nums; j += gap)
{
int key = arr[j]; //存储待增量插入排序的值
int k;
//注意这里的结束条件是k >= gap 因为要保证k - gap >= 0
for (k = j; k >= gap; k -= gap)
{
//依次往前作比较进行移动
if (key < arr[k - gap])
arr[k] = arr[k - gap];
else
break;
}
//此时k的位置就是key插入的位置
arr[k] = key;
}
}
}
}
上面的代码有三个循环嵌套,我们还可以使其变得简单点。
我们可以在每次进行增量插入排序时,i从0遍历到nums - gap - 1的位置,然后依次将其遍历的位置的以增量为单位的下一个元素key进行增量插入排序。
比如上面的:
以增量为5 / 2 = 2为增量分组进行插入排序 | |||||||||
4 | 3 | 1 | 8 | 6 | 5 | 7 | 2 | 10 | 9 |
我们可以这样进行增量插入排序:
i = 0
将key = 1 插入到有序序列 4 中
结果:
i = 1
key = 8 插入到有序序列 3 中
结果:
i = 2
key = 6 插入到有序序列 1 4 中
结果:
i = 3
key = 5 插入到有序序列 3 8 中
结果:
i = 4
key = 7 插入到 有序序列 1 4 6 中
结果不变
i = 5
key = 2 插入到有序序列 3 5 8 中
结果:
i = 6
key =10 插入到 有序序列 1 4 6 7 中
结果不变
i = 7
key = 9 插入到有序序列 2 3 5 8 9 中
结果不变:
1 2 4 3 6 5 7 8 10 9
这就是通过从头遍历实现执行一次预排序的过程。
具体代码如下:
void SheelSort(int* arr, int nums)
{
int gap = nums;
while (gap > 1)
{
gap /= 2;
for (int i = 0; i < nums - gap; i++)
{
int j;
int key = arr[i + gap];//存储待插入排序的值
for (j = i; j >= 0; j -= gap)
{
if (key < arr[j])
arr[j + gap] = arr[j];
else
break;
}
//此时j + gap的位置为key插入的位置
arr[j + gap] = key;
}
}
}
(三)、时间复杂度和稳定性
希尔排序的时间复杂度比较复杂,大概为,其排序算法不稳定。
上面的代码中的缩小增量为除以2,其实还可以是其他值,如 3 4 5 .....,只要保证最后一次可以执行增量为1的朴素插入排序即可,如当缩小增量的倍率为3时,gap = gap / 3 + 1。