文章目录
前言
开始介绍排序,如下图是我们常见的排序算法,本文介绍插入排序部分。
一、排序的概念
所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
生活中也常见排序的运用:
二、插入排序
1.直接插入排序(InsertSort)
1.1插入思想
把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列 。
太过书面的说法,其实实际中我们玩扑克牌时,就用了插入排序的思想:
我们用右手从牌堆中抽出一张牌,将它放入我们左手中的有序牌组中(放入后依旧保持有序)。
摸牌过程就用了插入的思想,那我们便将左手中拿着的已经有序的牌组看做是一个数组,并像数组一样用下标编号[0,end],最后一张牌的下标为end,右手从牌堆中摸的牌为一个变量(tmp),我们做一次插入的过程便是要将tmp插入到这个有序数组中去。
我们先来实现插入一个数据的代码:
//插入一个数据进入有序的数组中
{
int end;
int tmp;
while (end >= 0)
{
if (tmp < a[end])
{
a[end + 1] = a[end];
end--;
}
else
{
break;
}
}
a[end + 1] = tmp;
}
1.2实现插入算法
在实际情况中,我们会传入一个数组(a)和数组的大小(int n)给InsertSort函数,要求函数内部实现对该数组a的排序。
我们还是像插入一个数据那样,用下标[0,end]标注已经排好序的数据,用tmp表示我们即将要插入的数据。
所以我们这时我们的InsertSort函数如何对待我们的数组a便变得很明确了,我们就用end分割数组,end之前的数据[0,end]便是已经排好的数据,end之后的数据便是我们未排好的数据(牌堆),而我们将要插入的数据tmp便每次end后面的那一个数据。
于是我们的插入排序便实现好了:
void InsertSort(Datatype* a, int n)
{
for (int i = 1; i < n; i++)
{
int end=i-1;
int tmp=a[i];
while (end >= 0)
{
if (tmp < a[end])
{
a[end + 1] = a[end];
end--;
}
else
{
break;
}
}
a[end + 1] = tmp;
}
}
2.希尔排序(ShellSort)
2.1希尔排序思想
开始我们会发现:当用InsertSort函数时,传入的数组约接近有序,InsertSort的效率便越高,于是我们的希尔排序便应运而生了。
希尔排序:用InsertSort的思想先将数组进行预排序,让较小的数更快速的跳到前面,更大的数更快的跳到后面,从而避免中间过多的遍历比较的过程,然后最后在用InsertSort完成最后的排序。
那么我们希尔排序的预排思路便是:先选定一个整数间隙gap,将数组a中间隔为gap的数据都分为一组,对一组做InsertSort,便可让较小的数更快的跳到前面。
我们还是先写一次插入的代码。
//写一次插入
{
int gap = 3;
int end;
int tmp=a[end+gap];
while (end>=0)
{
if (tmp < a[end])
{
a[end + gap] = a[end];
end -= gap;
}
else
{
break;
}
}
a[end + gap] = tmp;
}
然后再写对一组的排序
{
int gap = 3;
for (int i = 0; i < n - gap; i += gap)
{
int end=i;
int tmp = a[end + gap];
while (end >= 0)
{
if (tmp < a[end])
{
a[end + gap] = a[end];
end -= gap;
}
else
{
break;
}
}
a[end + gap] = tmp;
}
}
最后完成对所有组的排序
{
int gap = 3;
for (int j=0;j<gap;j++)
{
for (int i = j; i < n - gap; i += gap)
{
int end = i;
int tmp = a[end + gap];
while (end >= 0)
{
if (tmp < a[end])
{
a[end + gap] = a[end];
end -= gap;
}
else
{
break;
}
}
a[end + gap] = tmp;
}
}
}
我们这里有两层for循环,但其实只要我们对其优化一下,就能减少一层:我们原先的思路是一组排完再排另一组,但是因为间隔为gap的元素一定是一组中的,所以我们没必要先排完一组,只要将一个元素前后gap的元素都给排好就行,这就是多组并排,来走走看我们的代码逻辑:
{
int gap = 3;
for (int i = 0; i < n - gap; i++)
{
int end = i;
int tmp = a[end + gap];
while (end >= 0)
{
if (tmp < a[end])
{
a[end + gap] = a[end];
end -= gap;
}
else
{
break;
}
}
a[end + gap] = tmp;
}
}
2.2那么我们的gap是多少合适呢?
gap越大,跳的越快,越不接近有序。
gap越小,跳的越慢,越接近有序。
这个gap其实也就决定了一次预排序的分组,当我们的数据个数过多时,gap还取3,并不能体现希尔对比插入排序的优势。
所以我们需要多次的预排序。 将希尔思想的特性发挥到极致:我们这里再建立一个循环,每一次循环都是一次预排序,每次预排序的gap又都不同,用gap的大小来做这个循环的结束条件。
2.2.1希尔排序完整代码
//希尔排序
void ShellSort(Datatype* a, int n)
{
int gap = n;
while(gap>=1)//一次进入循环便是一次预排
{
gap /= 2;
for (int i = 0; i < n - gap; i++)//每次预排都有gap个分组要进行排序,使较小的数更快的跳到前面。
{
int end = i;
int tmp = a[end + gap];
while (end >= 0)//每对一个分组进行排序便使用了一次插入排序
{
if (tmp < a[end])
{
a[end + gap] = a[end];
end -= gap;
}
else
{
break;
}
}
a[end + gap] = tmp;
}
}
}
这样便实现了我们希尔的全部。
2.3希尔排序的特性总结
-
希尔排序是对直接插入排序的优化。
-
当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试的对比。
-
希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,因此在好些树中给出的希尔排序的时间复杂度都不固定:
《数据结构(C语言版)》— 严蔚敏
《数据结构-用面相对象方法与C++描述》— 殷人昆
因为咋们的gap是按照Knuth提出的方式取值的,而且Knuth进行了大量的试验统计,我们暂时就按照: O ( n 1.25 ) O(n^{1.25}) O(n1.25) 到 O ( 1.6 ∗ n 1.25 ) O(1.6*n^{1.25}) O(1.6∗n1.25)来算。
- 稳定性:不稳定
总结
本文介绍了直接插入排序和希尔排序。
本文章为作者的笔记和心得记录,顺便进行知识分享,有任何错误请评论指点:)。