前言
插入排序具体有三种:直接插入排序、折半插入排序、希尔排序,陪女朋友复习数据结构,做个小总结吧。
直接插入排序
这个是插入排序中最简单的一个,算法原理直接用实例来介绍:
假设原始数组是:3 1 4 2 6 2 1 6 7
将数组分成两个区域,一个是有序区域一个是无序区域,最开始初始化有序区域(用黄色表示)为第一个数,无需区域为剩下部分。
第一次:3 1 4 2 6 2 1 6 7
将无序区域第一个数与有序区域作比较,判断该数适合插入的位置,这里就是1 和 3做比较,发现1应该插入3的前面
第二次:1 3 4 2 6 2 1 6 7
同理将4和 1 3 作比较确定插入位置
第三次:1 3 4 2 6 2 1 6 7
下面几次同理
第四次:1 2 3 4 6 2 1 6 7
第五次:1 2 3 4 6 2 1 6 7
第六次:1 2 2 3 4 6 1 6 7
第七次:1 1 2 2 3 4 6 6 7
第八次:1 1 2 2 3 4 6 6 7
第九次:1 1 2 2 3 4 6 6 7
直接插入排序算法稳定性分析
可以看到在直接插入排序中,我们判断插入在某个元素前面的条件是小于该值(注意不能等于),这样就不会破坏数组中两个相同数的前后位置关系,也就是说直接插入排序算法是一个稳定的算法
直接插入排序代码
void insertSort(int a[], int n)
{
int temp, j, k;
for (int i = 1; i < n; i++)
{
temp = a[i];//获取第一个无序区域的值
//下面是判断该值在有序区域的哪里插入
for (j = i - 1; j >= 0; j--)
{
if (a[j] < temp)
break;
}
//j就是插入的位置,如果就是自己当前位置直接不动
if (j != i - 1)
{
//插入位置往后所有有序区域的数都要后移腾出位置
for (k = i - 1; k > j; k--)
{
a[k + 1] = a[k];
}
//插入
a[k + 1] = temp;
}
}
}
算法时间复杂度分析
第一部分:程序最复杂的第一部分应该是查找插入位置,也就是:
if (a[j] < temp)
这一句,它的外层循环是n次,内层在最复杂的时候也是n - 1次就是,因此这句话的时间复杂度是O(n^2).
第二部分:程序最复杂的第二部分是数据的挪动部分,也就是:
a[k + 1] = a[k];
这一句话,直接插入排序最不理想的情况自然是整个数组降序排列例如:
6 5 4 3 2 1
这个时候每一次直接插入排序都要挪动整个有序区域,那么第一次需要移动0个数,第二次需要移动1个数,第三次需要移动2个数… …第n次需要移动n - 1个数,所以时间复杂度是(1 + 2 + 3 + … … + n - 1)所以是O(n^2)。
所以整个程序的时间复杂度是O(n^2)
直接插入排序总结
稳定性:稳定
时间复杂度:O(n^2)
折半插入排序
在查找算法的学习中我们知道有序数组的折半查找的时间复杂度是O(log2n)并且他是小于O(N)的,那么这里就能很直接的想到怎么去优化直接插入排序了,直接插入排序是有一个有序区域的,所以我们在找无序区域第一个数应该插入有序区域哪个位置的时候可以使用折半查找的方法,这样程序显然比直接插入排序快。
折半插入排序代码
void binaryInsertSort(int a[], int len)
{
int i, j, k;
for (i = 1; i < len; i++)
{
//为a[i]在前面的a[0...i-1]有序区间中找一个合适的位置
int left = 0, right = 0;
for (int p = 0; p < i; p++) {
//折半查找应该插入的位置
left = 0;
right = i - 1;
while (left <= right) {
int m = (left + right) / 2;
if (a[m] > a[i])
right = m - 1;
else
left = m + 1;
}
}
j = right;
printf("%d\n", right + 1);
//如找到了一个合适的位置
if (j != i - 1)
{
//将比a[i]大的数据向后移
int temp = a[i];
for (k = i - 1; k > j; k--)
a[k + 1] = a[k];
//将a[i]放到正确位置上
a[k + 1] = temp;
}
}
}
折半插入排序算法稳定性分析
折半插入的话稳定性也很好分析,会影响稳定性的地方就是查找插入有序区域位置的这个地方,那么和直接插入排序一样,只要判断插入某个位置前是小于该值(不可等于)就不会破坏稳定性。
折半插入排序时间复杂度分析
其实吧,他的时间复杂度很好分析的,折半查找的时间复杂度是O(log2n),然后折半查找的外层执行时n次,所以整个程序第一部分查找插入位置的时间复杂度是O(nlog2n)。
然后程序第二部分的数据挪动上是没有变化的,所以时间复杂度是O(n^2).
所以整个程序的时间复杂度是O(n^2)。
折半插入排序总结
稳定性:稳定
时间复杂度:O(n^2)
希尔排序
对于直接插入排序我们知道,主要影响时间复杂度的是数据的挪动,那我们就可以得出结论,如果原始数组是一个比较小的数组或者原始数组它的有序性非常高,那么他的算法速度就会非常的快,因此希尔排序也就是利用了这种思想。
一个很长的数组,我们可以将其分组处理这里我们也直接用例子说明:
第一次以步长为数组长一半分组
第一次:3 7 4 2 6 2 1 6,步长 8 / 2 = 4
分组结果:
每个分组进行插入排序:
3 2 1 2 6 7 4 6
这时要注意了,两个2的前后位置发生了变化,说明这个算法不稳定。
第二次步长为上一次步长一半
第二次:3 2 1 2 6 7 4 6,步长为4 / 2 = 2
分组结果:
每个分组进行插入排序:
1 2 3 2 4 6 6 7
第三次步长为上一次步长一半
第三次:1 2 3 2 4 6 6 7,步长为2 / 2 = 1
分组结果:
每个分组进行插入排序:
1 2 2 3 4 6 6 7
希尔排序代码
void shellSort(int arr[],int len)
{
for (int inc = len / 2; inc > 0; inc /= 2)
{
for (int i = inc; i < len; i++)
{
insertSort(arr, inc, i);
}
}
}
void insertSort(int arr[], int inc, int i)
{
int insertNumber = arr[i];
int j = i - inc;
for (; j >= 0 && insertNumber < arr[j]; j -= inc)
{
arr[j + inc] = arr[j];
}
arr[j + inc] = insertNumber;
}
希尔排序稳定性分析
在第一次排序的时候我们就以及发现两个2的位置发生了变化,由于多次插入排序,我们知道一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以shell排序是不稳定的。
希尔排序时间复杂度分析
大约为:O(n ^1.3)
最坏为:O(n ^2)
希尔排序总结
稳定性:不稳定
时间复杂度:O(n^1.3) ~ O(n ^2)
总结
稳定性:
稳定的算法:直接插入排序、折半插入排序
不稳定的算法:希尔排序
时间复杂度:
直接插入排序:O(n ^2)
折半插入排序:O(n ^2)
希尔排序:O(n^1.3) ~ O(n ^2)