1)插入排序
插入排序是一种非常古老的排序思想,他的过程可以被形象的模拟出来:假设一个人要摸起一副被打乱的扑克牌,我们注意到,刚开始时这个人手上并没有牌,并且他每次只从牌堆中摸出一张牌,每当摸上一张牌时,他选择从右向左(即从大到小)将这张牌与手中原有的牌进行比较,并最终将其插入到自己手牌中适合的位置,当牌堆中的牌全部被摸出时,他便完成了一次简单的插入排序。
这里面显然有一些规律:
第一,这个人手中的牌始终是有序的,因为他始终将摸上来的牌按顺序插入手牌中;
第二,在整个插入排序的过程中,我们每次只拿出一个元素来对他进行比较操作,因此,插入排序的空间效率是O(1)。
第三,每次将牌插入手牌时,牌与手牌的比较次数,实际上取决于原本牌堆中不同大小的牌的摆放顺序。因此,该算法的效率并不稳定,当牌堆中牌按照从小到大分布,这意味着每一次我们摸起一张牌,都可以直接将他插入手牌的最右边,这样我们拥有了O(n)级别的时间效率。而当牌堆中牌按照从大到小分布,意味着每一次的插入,我们都需要将该牌与手牌中所有的牌比较一遍,并将它置于手牌的最左边,这显然需要浪费大量的时间,时间效率实际上降低到了O(
n
2
n^2
n2)。
因此,我们需要对简单的插入排序进行改进。
(插入排序C++代码实现)
template<class t>
void insert_sort(t Array[], int n) {
//Array中存放待排序数组,n为数组长度
t current_insert;
for (int i = 0; i < n; i++) {
current_insert = Array[i];
int j = i - 1;
while ((j >= 0) && (current_insert < Array[j])) {
Array[j + 1] = Array[j];
j--;
}
Array[j + 1] = current_insert;
}
}
2)希尔(Shell)排序
希尔排序实际上是对插入排序的优化处理。
算法思想 :
- 现将待排序序列转换成若干个小序列,对于这些小序列内部进行插入处理。
- 在确定当前小序列都排序完毕后,逐渐增加小序列的规模,并减少小序列的个数,使得该待排序列整体逐渐处于更有序的状态
- 这时我们发现,该待排序序列逐渐接近扑克牌问题中的最优情况,这时候对排队进行一次整体的插入排序,能达到接近O(n)的算法效率。
以上就是希尔排序的全部算法思想,这里面最重要的部分很显然就在于“逐渐增加小序列的规模,并减少小序列的个数”。我们需要找到一个合适的方法来控制小序列的规模,从而能尽可能的增加排序的效率。
我们用gap来表示增量,首先选择一个增量gap=n/2,当我们完成了该规模下小序列的排序时,再按照gap=gap/2的规则来改变增量,这时我们实际上得到了一个关于增量的序列{n/2,n/2/2…,1}。注意这里最后一步的增量一定为1,这意味着按照当前增量序列进行希尔排序,我们不需要再额外进行一次整体的插入排序。
如上图所示,遵循希尔排序的第一个增量n/2,我们从乱序数组的第一个元素开始,将每个元素跳跃着与他后面第n/2个元素进行插入排序,我们实际上将原数组分为了{4,7},{2,8},{9,4},{3,11},{1,10}这五组数据,并对每一组数据分别进行插入排序。
第二步,遵循希尔排序的第二个增量n/2/2,我们将待排序数组分为{4,4,1,8,11},{2,3,7,9,10}两组,并分别对两组数据进行插入排序。
最后一步我们对整个数据整体进行希尔排序,可以看到进行到最后一步时,该数组已经非常接近于一个正序数组,这将始我们最后一次插入排序的效率非常接近于O(n)
针对gap=gap/2的增量序列的代码实现
template<class t>
void shell_sort(t Array[], int n) {
int i, dealta;
for (delta = n / 2; delta > 0; delta /= 2) {
for (int i = 0; i < delta; i++)
insert_sort(&Array[i], n - i; delta);
}
}
当然,这种增量序列仍然存在很多可以改进的地方,由于我们只是单纯的对于增量进行了不断除2的操作,这导致我们选取的增量并不互质,这导致在我们比较的过程中,可能会产生某些位置之间被重复多次比较的可能性,降低了算法的处理效率,Hibbard等各种增量序列是一个可行的解决方法。