一、直接插入排序
1.1 基本思想
直接插入排序是一种简单的插入排序法,其基本思想是:
把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列 。
实际中我们玩扑克牌时,就用了插入排序的思想
1.2 代码实现
我们首先写出插入排序只排一趟的代码,如下图所示,假设数组[0,end]已经是有序数组,那么我们的待排序元素的下标就是end+1,该下标(end+1)元素存入tmp,然后与a[end]进行比较,只要tmp<a[end],就让下标为end的元素向后挪同时end左移,向后挪动是为了腾出空间让tmp插入,直到tmp不满足条件我们就停止循环,将tmp插入到合适的位置end+1中。
这样一趟插入排序的代码就可以写出来了
int end=i; //有序数组最后一个元素的下标
int tmp = a[end + 1]; //待插入元素存入临时变量tmp
while (end >= 0)
{
if (tmp < a[end]) //如果待插入元素比end下标的元素小
{
a[end + 1] = a[end]; //1.end下标元素向后挪方便tmp的插入
--end; //2.end左移进行下一轮的比较
}
else //如果待插入元素大于或等于end下标的元素退出循环
{
break;
}
}
a[end + 1] = tmp; //将tmp插入到数组当中
完整代码只需要让end动起来就可以实现插入排序了,我们默认end=0,即第一个元素是有序,那么只要让end向后移动,让每一个元素和前面排好顺序的元素比较就可以了,end的起始条件是从0开始,结束是end=n-2,因为当end=n-2时,待排序的元素下标就是n-1,即数组的最后一个元素下标,end的区间就是[0,n-2],通常我们喜欢将这个区间写为左闭右开,即[0,n-1)
// 插入排序
void InsertSort(int* a, int n)
{
//默认第一个数已经是有序
for (int i = 0; i < n-1; ++i)
{
int end=i; //有序数组最后一个元素的下标
int tmp = a[end + 1]; //待插入元素存入临时变量tmp
while (end >= 0)
{
if (tmp < a[end]) //如果待插入元素比end下标的元素小
{
a[end + 1] = a[end]; //1.end下标元素向后挪方便tmp的插入
--end; //2.end左移进行下一轮的比较
}
else //如果待插入元素大于或等于end下标的元素退出循环
{
break;
}
}
a[end + 1] = tmp; //将tmp插入到数组当中
}
}
测试结果
//打印数组
void PrintArr(int* a, int n)
{
for (int i = 0; i < n; ++i)
{
printf("%d ", a[i]);
}
printf("\n");
}
void TestInsertSort()
{
int a[] = { 2,4,7,2,6,4,5,7,8,32,33,11 };
int n = sizeof(a) / sizeof(a[0]);
PrintArr(a, n);
InsertSort(a, n);
PrintArr(a, n);
}
int main()
{
TestInsertSort();
system("pause");
return 0;
}
1.3 特性总结
直接插入排序的特性总结:
- 元素集合越接近有序,直接插入排序算法的时间效率越高
- 最好情况时间复杂度O(N)
最好情况即数组有序,每次排序都不进入内循环,只和end位置下标元素比较后就结束,比较n-1次, 元素无需移动,移动次数为0 - 最坏情况时间复杂度O(
N
2
N^2
N2)
最好情况每次都需要比较,第一次比较1次,第二次比较2次,直到n-1次,因此时间复杂度为 n ( n − 1 ) 2 \frac{n(n-1)}{2} 2n(n−1) - 平均时间复杂度为O( N 2 N^2 N2)接近最坏
- 空间复杂度:O(1),只有三个变量i,tmp和end,不需要辅助空间
- 稳定性:稳定
二、希尔排序
1.1 基本思想
希尔排序法又称缩小增量法。希尔排序法的基本思想是:
先选定一个整数gap(据计算gap=gap/3+1时最优),把待排序数组分成gap个组,所有距离为gap的元素分在同一组内,并对每一组内的元素进行排序。然后,重新选定一个整数 gap,重复上述分组和排序的工作。当gap=1时,则执行直接插入排序,保证所有元素的有序。
1.2 代码实现
同样的,我们假设数组长度n=10,gap=gap/3+1,那么我们先来写出一组排序的代码
- gap=4,数组被分为了四组,(9,7,3)、(1,4,5)、(2,8)、(5,6),将这四组分别执行插入排序,结果为(3,7,9)、(1,4,5)、(2,8)、(5,6)。
掌握了gap=4这一组排序的过程后,我们先写出这一组排序。
首先我们定义变量end=0从数组第一个元素9开始,下一个待插入的元素tmp就是end+gap下标的元素也就是7,我们可以发现规律,当end走到元素4时,4的下标为5,那么我们就可以确定end的区间下标为[0,5],通常我们喜欢将右边的区间变为开区间,你可以发现刚好是[0,n-gap),如果你不相信,可以继续看2,3步骤分析,这样我们就可以写出一组的排序代码了
gap = gap/3+1;
for (int i = 0; i < n - gap; ++i) //i从0开始到n-gap结束
{
int end = i;
int tmp = a[end + gap]; //每次增加gap的步长作为插入元素
while (end >= 0)
{
if (tmp < a[end]) //如果待插入元素比end下标的元素小
{
a[end + gap] = a[end]; //1.end下标元素向后挪方便tmp的插入,这里end挪动的步长是gap而不是1了
end -= gap; //2.end左移进行下一轮的比较,同理左移gap步长
}
else //如果待插入元素大于或等于end下标的元素退出循环
{
break;
}
a[end + gap] = tmp; //将tmp插入到数组当中
}
}
- gap=2,数组被分为了两组,(3,2,7,8,9)、(1,5,4,6,5),将这两组分别执行插入排序,结果为(2,3,7,8,9)、(1,4,5,5,6)。这一步同样可以发现end的区间为[0,8),8=n-gap=10-2
- gap=1,数组被分为了两组,(3,2,7,8,9)、(1,5,4,6,5),将这两组分别执行插入排序,结果为(2,3,7,8,9)、(1,4,5,5,6)。这一步同样可以发现end的区间为[0,9),9=n-gap=10-1,这一步也证明了插入排序中end的结束条件为n-1=10-1=9
理解了希尔排序的步骤后,我们只需要写出gap从4到2再到1的循环代码就可以了,这一步很简单,首先定义gap=n, 每次循环gap=gap/3+1, 最后+1是为了保证gap最后能等于1,也就是保证数组有序。
// 希尔排序 gap = gap/3+1
void ShellSort1(int* a, int n)
{
//1.gap>1时为预排序,让数列更接近有序
//2.gap=1为直接插入排序,保证有序
int gap = n;
while (gap > 1)
{
gap = gap / 3 + 1; //通常的
for (int i = 0; i < n - gap; ++i) //i从0开始到n-gap结束
{
int end = i;
int tmp = a[end + gap]; //每次增加gap的步长作为插入元素
while (end >= 0)
{
if (tmp < a[end]) //如果待插入元素比end下标的元素小
{
a[end + gap] = a[end]; //1.end下标元素向后挪方便tmp的插入,这里end挪动的不长是gap而不是1了
end -= gap; //2.end左移进行下一轮的比较,同理左移gap步长
}
else //如果待插入元素大于或等于end下标的元素退出循环
{
break;
}
a[end + gap] = tmp; //将tmp插入到数组当中
}
}
}
}
测试结果
void TestShellSort()
{
int a[] = { 2,4,7,2,6,4,5,7,8,32,33,11 };
int n = sizeof(a) / sizeof(a[0]);
PrintArr(a, n);
ShellSort1(a, n);
PrintArr(a, n);
}
int main()
{
//TestInsertSort();
TestShellSort();
system("pause");
return 0;
}
- 通常我们都将gap每次折半进行排序,则代码为
// 希尔排序
void ShellSort2(int* a, int n)
{
//1.gap>1时为预排序,让数列更接近有序
//2.gap=1为直接插入排序,保证有序
int gap = n;
while (gap > 1)
{
gap = gap / 2; //通常的
for (int i = 0; i < n - gap; ++i) //i从0开始到n-gap结束
{
int end = i;
int tmp = a[end + gap]; //每次增加gap的步长作为插入元素
while (end >= 0)
{
if (tmp < a[end]) //如果待插入元素比end下标的元素小
{
a[end + gap] = a[end]; //1.end下标元素向后挪方便tmp的插入,这里end挪动的不长是gap而不是1了
end -= gap; //2.end左移进行下一轮的比较,同理左移gap步长
}
else //如果待插入元素大于或等于end下标的元素退出循环
{
break;
}
a[end + gap] = tmp; //将tmp插入到数组当中
}
}
}
}
1.3 特性总结
希尔排序的特性总结:
- 希尔排序是对直接插入排序的优化。
- 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试的对比。
- 希尔排序的时间复杂度不好计算,需要进行推导,推导出来平均时间复杂度: O ( N 1.3 — N 2 ) O(N^{1.3}—N^2) O(N1.3—N2)
- 稳定性:不稳定