前言:程序代码的高效运行离不开数据结构,在数据结构中排序显得格外重要,一个好的排序算法能够大幅度提高排序的效率,节约内存资源、提高我们算法的可用性。
1、什么是排序
简单来说,所谓排序就是将杂乱无章的数据整理成有序的序列的过程,例如
//排序前
5、7、8、3、1、4、9、6、2
//排序后
1、2、3、4、5、6、7、8、9
看起来是不是舒服多了呢,没错就是排序,是不是 so easy
2、排序的分类
当然,我们说的排序不可能只有一种,排序算法大致分为以下几种:
- 1、插入类排序
- 2、交换类排序
- 3、选择类排序
- 4、归并类排序
- 5、基数类排序
- 6、…
大致分为以上几种,当然肯定还有其他的,这里简单介绍几种常用的排序
3、直接插入排序(插入类排序)
算法思路:每趟执行将一个待排序的关键字按照其值的大小插入到已经排好的序列上。
可能读完比较蒙,我找个图
声明:图是复制网络上的。
看完这个相比读者肯定知道怎么回事了,不多说了,直接上代码干:
算法实现:
void directInsertSort(int R[], int n){
int temp;
for(int i = 1;i<n;i++){
temp = R[i];//默认第一个有序,从第二个开始取
j = i-1;//
while(j>=0 && temp<R[j]){//如果待排序的值比前面的一个小,则将前面那个关键字后移
R[j+1]=R[i]
--j;
}
//执行到这,说明一趟排序完毕,将待排序的值赋值到指定的未知
R[j+1] = temp;
}
}
算法分析:
1、时间复杂度:
最坏的情况下,序列是逆序的,内循环中的R[j+1]<R[j一直成立,此时最里面的代码执行次数为 n(n-1)/2,因此其时间复杂度为O(n2)
2、最好的情况,也就是走一遍就完事了
那就是O(n)咯
综合分析:算法时间复杂度 O(n2)
3、空间复杂度:没什么辅助变量的变化,因此空间复杂度为O(1)
4、折半插入排序(插入类排序)
听这名字就能理解是把排序序列分成两半来排序,哈哈哈,这是我的理解。说正经的,折半排序是通过折半查找法来查找未知进行插入的。
算法思路:
//设已经排序的序列为
13 38 49 65 76 97
/**插入27 49
折半查找法 low = 0 , high = 5 ,mid = (0+5)/2 = 2
所以27应该插入下标为2的位置,但是下标为2的位置关键字为49,因此27因该插入49的前半部分
因此: low=0 high = 2-1 =1, mid = 1
最后可以得到27插入到13后得到
*/
13 27 38 49 65 76 97
上面的分析是否能看懂呢,我也觉得刚开始看比较复杂,熟悉也就那么回事
算法实现:
void BInsertSort(int a[],int size){
int i,j,low = 0,high = 0,mid;
int temp = 0;
for (i=1; i<size; i++) {
low=0;
high=i-1;
temp=a[i];
//采用折半查找法判断插入位置,最终变量 low 表示插入位置
while (low<=high) {
mid=(low+high)/2;
if (a[mid]>temp) {
high=mid-1;
}else{
low=mid+1;
}
}
//有序表中插入位置后的元素统一后移
for (j=i; j>low; j--) {
a[j]=a[j-1];
}
a[low]=temp;//插入元素
print(a, 8, i);
}
}
算法分析:
1、时间复杂度:
折半插入排序适合关键字比较多的场景,与直接插入排序相比,在查找位置上,大大减少了时间,但是其移动关键字的次数是一样的,所以其时间复杂度跟直接插入排序一样,折半插入排序关键字比较的次数和初始序列无关,因为每次折半查找都是固定的,因此次数是一定的。
因此:最好的情况为O(nlog2n)、最差的情况O(n2),平均时间复杂度O(n2)
2、空间复杂度:
通直接复杂度一样,没有创建更多的辅助变量,因此未O(1);
5、希尔排序(插入类排序)
算法分析:
希尔排序是用人名命名的排序,牛逼啊。其本质还是插入排序,有时候也叫缩小增量排序。通俗的来讲就是将序列先分为很多个小序列,每个序列自己排序完成后,再汇总排序
算法思路:
//将设原始数列为
49 38 65 97 76 13 27 49 55 04
//增量为5,就是每隔5个数取一个数组成一个序列
子序列1: 49 13
子序列2: 38 27
子序列3: 65 49
子序列4: 97 55
子序列5: 76 04
//先对这五个序列自己排完序后得到
子序列1: 13 49
子序列2: 27 38
子序列3: 49 65
子序列4: 55 97
子序列5: 04 76
一趟希尔排序后得到
13 27 49 55 04 49 38 65
再将增量缩小为3,也就是每隔三个进行收集为一个子序列进行排序
//增量为3,就是每隔3个数取一个数组成一个序列
子序列1: 13 55 38 76
子序列2: 27 04 65
子序列3: 49 49 97
//先对这三个序列自己排完序后得到
子序列1: 13 38 55 76
子序列2: 04 27 65
子序列3: 49 65 97
第二趟趟希尔排序后得到
13 04 49 38 27 49 55 65 97 76
一眼望去好像基本有序了,
最后再来一趟直接插入排序就OK了
04 13 27 38 49 49 55 65 76 97
**算法实现**
```c
void shell_sort(int *arr){
register int i, j, k, tmp;
int incre; //选择一个增量,这里我们用简单的二分法
for(incre = N/20; incre > 0;incre /= 2)
{
for(i = incre; i < N/10; i++)
{
tmp = arr[i];
// 很明显和插排的不同就是插排这里是j = i - 1
j = i - incre;
while( j >= 0 && tmp < arr[j])
{
arr[j + incre] = arr[j];
j -= incre;
}
arr[j + incre] = tmp;
}
}
}
希尔排序的最后一次其实是一次直接插入排序,这个算法理解很好理解,但是代码却不好看,找张图给大家刺激下
算法分析
1、时间复杂度,希尔排序的时间复杂度与选取的增量有关,太复杂了,一般不讨论他的时间复杂度,一般都说O(n1.5)
2、空间复杂度
通直接插入排序一样,为O(1)
6、冒泡排序(交换类排序)
关于冒泡就比较好理解了,就是每次最大或者最小的一个数被选出来,这个很形象,鱼儿在水里吐泡泡,到达水平面的那个泡泡是最大的。还是找张形象的图吧
算法分析:
由于冒泡排序比较简单,这里就简单描述下他的思路,从上面的gif图我们可以看到,每次都是选了一最大的数。
初试序列: 4 1 3 7 9 2 6 5 8
第一趟:从第一个两辆两比较选择最大,最后得到最大的数是9,移动到末尾
第二趟:还是两两比较选择最大的8,移动到倒数第二位,因为最后一位是9
...
...
...
最后一趟可以得到: 1 2 3 4 5 6 7 8 9
代码实现:
/*
* array是待排列的数组
* n 是数组的大小
*/
void BubbleSort(int array[],int n){
int i;
int j;
int tag;
int temp;
for(i = n-1,i>=1;i--){ //思考这里为什么是 n-1,而不直接是两个for循环完事?
tag = 0;
for(j=1;j<=i;++j){
if(array[j-1]>arrar[j]]){//如果前一个数比后一个数要大,就交换交换两个数的位置
temp = array[j];
array[j] = array[j-1];
array[j-1] = temp;
tag = 1;//有交换操作
}
}
//当某一趟没有发生交换时,说明前面的序列已经有序了
if(tag==0)return 0;//排序结束
}
}
冒泡排序虽然简单,但是还是强调两点
- for循环改进
一般大家都会直接两个for循环,条件直接是 i<n,j<n如下:
for(int i = 0; i<n;i++){
for(int j=0;j<n;j++){
......
}
}
当然这样也能达到效果,但是似乎比较的次数就多了很多,序列的最后几个数都是已经排序过的,我们就没有必要去比较,因为上面的 n-1 就是实现这样的效果
- 循环条件
从上面我们可以看到,代码中多了一个 tag 这个tag 是干什么用的呢?
实际上,当我们序列在某种特殊情况下,只比较一部分的时候,发现序列已经有序,但是计算机不知道,还是会去每个比较,因此我们可以设置一个tag ,当一趟循环下来,没有交换操作,说明序列本身已经有序了,后面的比较已经没有必要再进行下去了
算法的性能分析
1、时间复杂度:由冒泡排序代码可知。最内层的循环中的关键字是最基本的操作,从两个for循环我们可以计算出。基本操作的总的执行次数为(n-1+1)*(n-1)/ 2 可知时间复杂度为O(n2),最好的情况就是序列本身有序,只用比较一趟,时间复杂度为O(n)
2、空间复杂度,没有河外的辅助空间只有一个temp,因为空间复杂度为O(1)
7、快速排序(交换类排序)
快速排序,从名字上看,就是很明显,他的排序效果是最好的。实际上他的排序效果也是最好的。
算法分析
快速排序可能不是很好理解,这里简单介绍下,快速排序是选择一个数作为分割,大与这个分割数的就移动到后面,小于这个数就移动到这个数的前面,举个例子
原序列为: 2 5 9 6 4 8 2 7
选择分割数为 7
移动序列,是的小于7的在7的左边,大于7的在7的右边
得到 2 5 6 4 7 8 9
然后再分别把 2 5 6 4 和 8 9 看成两个序列分别做处理
这就是快速排序
代码实现
/**
* array是待排列的数组
* low是低位
* height 是高位
*/
void FastSort(int array,int low,int height){
int temp;
int i = low;
int j = high;
if(low<high){
temp = array[low];//取数组中的第一个元素作为分割
while(i<j){
//开始从右边往左边扫面,找到小于temp的值
while(i<j&&array[j]>=temp)--j;
if(i<j){
//找到了这样的数,小于temp,将它和low位置上的数交换
array[i] = array[j];
++i;//交换后i要+1
}
//上面的执行完后,要开始从左往右查找了大于temp的值了
while(i<j&&array[i]<temp)++i;
//找到了一个数大于temp
if(i<j){//交换
array[j] = array[i];
--j;
}
}
//一趟执行完后 将temp的值赋值到指定的位置
array[i] = temp;
//下面就要开始递归了
FastSort(array,low,i-1)
FastSort(array,i+1,high)
}
}
由于快速排序不好理解,我这里手动操作一下
原始序列为:49 38 65 97 76 13 27 25
1、选取49进行分割,从右边往左找到比49小的,找到25 (low = 0,high = 7, temp = 49)
2、将25和49进行交换得到以下序列
25 38 65 97 76 13 27 25 (此时 low = 1 ,high = 7,temp = 49)
3、从左往右找比49大的,注意low为1,从38开始比较,找到65比49大
此时交换数据,将65于array[high]也就是25进行交换
得到序列为 49 38 65 97 76 13 27 65 (此时 low = 2 ,high = 6,temp = 49)
4、继续,从右往左找到比49小的元素也就是27,将其于array[low]也就是65进行交换
得到序列 49 38 27 97 76 13 27 65 (此时low = 2,high = 6)
5、从左往右找到比49大的元素97,于array[high]交换得到
序列 49 38 27 97 76 13 97 65 (此时low = 3,high = 5,temp = 49)
6、从右往左找到比49小的元素13,将其于array[low]也就是97交换得到序列
49 38 27 13 76 13 97 65 (此时low = 3,high = 4,temp = 49)
7、在从左往右找比较49小的元素,没有满足条件的
8、再从右往左找到比49大的元素76,于是交换array[high]也就是76得到序列
49 38 27 97 13 76 97 65 (此时low = 4,high = 4,temp = 49)
本趟执行完毕,将49赋值到array[low]的位置得到一趟后的排序序列
49 38 27 97 49 76 97 65 (此时low = 3,high = 5,temp = 49)
接下来进行递归,49两边分别为一个序列进行排序
算法性能分析
1、时间复杂度:由于快速的特点,代排序的序列越是无序此算法的效率越高,越是有序,算法的效率越低最坏的时间复杂度为O(2),平均时间复杂度为O(nlog2n)
2、空间复杂度:由于使用了辅助栈,空间复杂度为O(log2n)