前言
说起排序我们都不会陌生,日常生活中处处藏着排序的影子。比如班级排名、网络购物时物品的排列等等…
我们知道排序,但是排序的方法也是多种多样的,有的排序效率低:比较排序、插入排序等,有的排序效率高:比如快速排序等,博主将带着你了解一些排序有关的算法,你准备好了吗!
排序
排序概念
排序:使一串记录按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
稳定性:假定在待排序的记录序列中,存在多个具有相同关键字的记录,若经过排序这些记录的相对次序保持不变,即在原序列中r[i] = r[j],且r[i] 在 r[j]之前,在排序后的序列中,r[i] 仍在 r[j]之前,称这种排序算法是稳定的,否则就是不稳定的。
内部排序:数据元素全部放在内存中的排序。
外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能再内外存之间移动数据的排序。
常见排序算法
1. 直接插入排序
首先登场的是插入排序,让我们来认识一下它吧!
插入排序思想
把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,便得到一个新的有序序列。
把待插入的元素依次与有序序列元素比较(从前往后或从后往前),直到满足一定条件(大于或小于)就把该元素插入到某一位置。
插入排序要求向有序序列中插。
对于一个数组中的元素进行排序(默认排升序),想要借助插入排序思想,一般把第一个元素当作有序序列,然后从第二个元素开始依次插入有序序列中进行插入排序。
假设一个数组arr[ ],有n个随机的元素。
把数组第一个元素当作起始的有序序列,并从第二个元素依次向有序序列插入并组成新的有序序列,直到剩余n-1个元素都插入到有序序列中,就可以得到整个数组都是有序的。
假设数组有序序列下标范围为[0, end],那么非有序下标范围为[end+1, n-1],显然end从0开始,即初始有序序列只有一个元素。
对于每一次元素的插入:是以下标为[end+1]的元素进行的。过程是:待插入元素arr[end+1]从有序序列的end下标位置开始,从后往前依次与升序序列元素比较:如果待插入元素大于该升序序列元素就停止比较,待插入元素的位置就应该在该升序元素的下一个位置;如果待插入元素小于该升序序列元素,就把该升序序列元素后移一个位置,然后再与前一个升序序列元素比较,直到待插入元素大于该升序元素为止或待插入元素与升序序列元素比较完为止。
在待插入元素与有序序列元素比较之前,应该先把待插入元素用一个临时变量记录下来,以防止有序序列元素后移时把待插入元素位置覆盖。
一种代码实现
//插入排序
//最坏时间复杂度O(N^2) - 逆序或接近逆序
//最好时间复杂度O(N) - 顺序或接近顺序
void InsertSort(int* a, int n) {
//最后一个插入的元素是数组下标为n-1的元素,该元素前面的有序数组下标为[0,n-2]
for (int i = 0; i < n-1; ++i) {
//每次有序数组的范围[0, end];
int end = i;
//tmp是将要插入有序数组的元素
int tmp = a[end + 1];
while (end >= 0) {
if (a[end] > tmp) {
a[end + 1] = a[end];
--end;
}
else {
break;
}
}
a[end + 1] = tmp;
}
}
直接插入排序特性
时间复杂度
O(N^2)
空间复杂度O(1)
元素越接近有序,直接插入排序算法的时间效率越高
直接插入排序算法稳定性:稳定
2. 希尔排序
希尔排序又叫缩小增量排序,
基本思想
以待排序元素排成升序为例
分为预排序和直接插入排序两部分来看。(其实可以合并,也是一部分)
预排序
先选定一个整数gap,把待排序文件中所有元素分成gap组,要求是所有距离为gap的元素分在同一组内,并对每一组的元素进行直接插入排序。
gap组都排好序后,gap减1,重复分组和排序,直到gap为1时停止。
直接插入排序
在预排序完成后,gap为1,此时所有的元素已经接近有序,可以对预排序后的所有元素进行直接进行插入排序,在直接插入排序完成后,所有元素就是有序的。
对于预排序
先考虑一个确定的gap时,预排序的情况
数组arr[ ] 有n个元素,首先可以把整个数组分成gap组。对于每一组来说,相邻元素之间距离相差
gap
首先对第一组进行直接插入排序:
初始假设该组第一个元素有序的,end
表示有序序列右区间,则有序序列下标范围[0 ,end]
,该组第二个元素开始依次与已经有序序列倒着比较。在比较之前需要先借助创建临时变量tmp
保存待插入元素arr[end+gap]
。
如果待插入元素小于有序序列元素arr[end]
,该位置元素就往后移一位,然后继续比较;
如果带插入元素大于有序序列元素,说明end+gap
位置就是待插入元素的位置,比较结束;
如果有序序列所有元素都小于待插入元素,此时下标end
的值就会是-gap
,带插入元素的位置就在下标end+gap
处。
第er组排好序后,接着对第三组进行排序:
初始假设该组第一个元素有序的,end
表示有序序列右区间,则有序序列下标范围[1 ,end]
,该组第二个元素开始依次与已经有序序列元素倒着比较。在比较之前需要先借助创建临时变量tmp
保存待插入元素arr[end+gap]
。
如果待插入元素小于有序序列元素arr[end]
,该位置元素就往后移一位,然后继续比较;
如果带插入元素大于有序序列元素,说明end+gap
位置就是待插入元素的位置,比较结束;
如果有序序列所有元素都小于待插入元素,此时下标end
的值就会是-gap
,带插入元素的位置就在下标end+gap
处。
最后对第三组进行排序:
初始假设该组第一个元素有序的,end
表示有序序列右区间,则有序序列下标范围[2 ,end]
,该组第二个元素开始依次与已经有序序列倒着比较。在比较之前需要先借助创建临时变量tmp
保存待插入元素arr[end+gap]
。
如果待插入元素小于有序序列元素arr[end]
,该位置元素就往后移一位,然后继续比较;
如果带插入元素大于有序序列元素,说明end+gap
位置就是待插入元素的位置,比较结束;
如果有序序列所有元素都小于待插入元素,此时下标end
的值就会是-gap
,带插入元素的位置就在下标end+gap
处。
gap为3
的情况下进行了预排序,数组元素相比为排序之前是比较有序的。
我们先在来看看gap
的取值问题:
gap
是是分成的gap组
的每一组元素的距离。取多少合适呢?
gap越大
,大的数据就越快的跳到后面,小的数据越快跳到前面,排序完结果越不接近有序。
gap越小
,大的数据越慢跳到后面,小的数据越慢跳到前面,排序完结果越接近有序。
初始时gap
在整个数组中应该是一个适中的值,不应过大,也不应过小。
我们需要进行多次预排序。每次预排序gap
相比上一次是减小的,并且最后一次gap
要是1
,以此达到最后一次是直接插入排序的目的。
gap = gap/2
gap = gap/3 + 1
这两种gap
的变化都是可取的。
gap
变化时排序结果:每一次预排完都会更加有序
时间复杂度
简记希尔排序的时间复杂度为
O(N^1.3)
,略逊于O(N*logN)
。
其实希尔排序的时间复杂度并不好算,可以说是非常难算的,我们需要知道希尔排序的思路,知道怎样一步一步从一组排序到多组排序的。
因为希尔排序在预排序过程中,每次排序的结果都会使整个序列更加倾向于有序,这样会有利于以后的排序。
每次排序都会使序列更加有序所以不能直接按最坏情况计算时间复杂度。
特性分析
- 希尔排序是对直接插入排序的优化,
- 在
gap>1
时都是预排序,目的是让序列更接近有序,那么在gap==1
时,序列就是基本有序的,这时在采用直接插入排序就非常快接近O(N)
,综合来说是优化明显的。- 希尔排序算法不稳定。
代码实现
完整代码
//希尔排序
//最坏时间复杂度O(N^(1.3)),略差于O(N*logN)
//最好时间复杂度O()
void ShellSort(int* a, int n) {
//把数组元素分为多组,对于每一组中的元素来说都相差一个相等的距离gap
//即gap是每一组的元素的距离
//gap越大,大的数据可以越快的跳到后面;小的数据可以越快的调到前面
//预排结果不是很接近有序
//gap越小,大的数据与小的数据跳的也越小,越接近有序
int gap = n;
//进行多次预排序,每次gap都在变化,直到变为1,就是直接插入排序
while (gap > 1) {
//gap = gap / 2;
gap = gap / 3 + 1;
//分别对每一组进行插入排序
//j是每一组的起始下标
//j=0,是第一组
//j=1,是第二组
//......
for (int j = 0; j < gap; ++j) {
//[0,end]有序,end+gap 排序到-> [0,end+gap]有序
//总共分为gap组,先对每一组进行插入排序
for (int i = j; i < n - gap; i += gap) {
int end = i;
int tmp = a[end + gap];
while (end >= 0) {
if (a[end] > tmp) {
a[end + gap] = a[end];
end -= gap;
}
else {
break;
}
}
a[end + gap] = tmp;
}
}
}
}
3. 性能比较测试
实现了直接插入排序
O(N^2)
和希尔排序O(N^1.3)
,让我们来比较一下二者的时间效率,验证以下时间复杂度。
前面实现了堆排序,其时间复杂度是O(N*logN)
,用来做一下基本的对比。
在此之前先补充一个小知识:
clock()//返回一个长整型long,作为程序开始运行到当前语句经历的时间,单位是ms
#include <stdio.h>
#include <stdl.b.h>
#include <time.h>
void TestSpeed() {
srand(time(0));
int size = 100000;
int* a1 = (int*)malloc(sizeof(int) * size);
assert(a1);
int* a2 = (int*)malloc(sizeof(int) * size);
assert(a2);
int* a3 = (int*)malloc(sizeof(int) * size);
assert(a3);
for (int k = 0; k < size; ++k) {
a1[k] = rand();
a2[k] = a1[k];
a3[k] = a1[k];
//...
}
int begin1 = clock();
InsertSort(a1, size);
int end1 = clock();
int begin2 = clock();
HeapSort(a2, size);
int end2 = clock();
int begin3 = clock();
ShellSort(a3, size);
int end3 = clock();
printf("InsertSort: %d\n", end1 - begin1);
printf("HeapSort: %d\n", end2 - begin2);
printf("shellSort: %d\n", end3 - begin3);
free(a1);
free(a2);
free(a3);
}
int main() {
//TestSpeed();
TestSort();
return 0;
}
注释掉直接插入排序后时间效率比较
结语
本节介绍排序算法中的直接插入排序和优化的希尔排序,重要的是要很好的理解希尔排序的思路,怎样对序列进行分组,对于
gap
的选择与及gap
之后的变化。
END