排序算法介绍:
排序是计算机程序设计过程中的重要操作,它的功能是将一个数据元素的任意序列,重新排列成一个按关键字有序的序列。
它之所以重要是因为查找操作非常重要,而有序的顺序表可以采用效率较高二分查找(时间复杂度为O(logn)),而无序的顺序表只能进行顺序查找(时间复杂度为O(N) ),而创建二叉排序树、有序二叉树、堆的过程本身就是一个排序过程。
排序算法的稳定性:
待排序的数据序列中可能存储两个或两个以上的关键字相等的数据,假设ki==kj且 i!=j,如果排序前i<j,排序后也肯定i<j,这种排序算法是稳定的排序,反之是不稳定的排序。
1、经典排序:(不是算法)
能够完成排序操作,但没有任何优化,数据的比较次数一次没少,数据的交换次数一次没少,没有任何优点,不能称得上是算法。
void classics_sort(int* arr,size_t len)
{
for(int i=0; i<len-1; i++)
{
for(int j=i+1; j<len; j++)
{
if(arr[i] > arr[j])
{
swap(arr[i],arr[j]);
}
}
}
}
2、冒泡排序:
排序过程像是水中的气泡上升,越大的气泡升的越快,所以叫冒泡排序,从第一个数据开始,让前后相邻的数据进行比较,如果Ki>Ki+1则交换它们,每一趟排序完成一个数据,反复这个过程,直接待排序的数据为1,则结束排序。
和经典排序相比,冒泡排序对数据的有序性敏感,如果一趟排序没有发生交换,则说明所前面的数据都比它的后继数据要小,则可以立即停止,提前结束排序。
注意:如果待排序的数据基本有序,则使用冒泡排序速度最快,因为它数据的有序性敏感可以提前结束。
// 最优时间复杂度:O(n) 最差时间复杂度:O(n^2) 平均时间复杂度:O(n^2) 稳定
void bubble_sort(int* arr,size_t len)
{
bool flag = true;
for(int i=len-1; flag && i>0; i--)
{
flag = false;
for(int j=0; j<i; j++)
{
if(arr[j] > arr[j+1])
{
swap(arr[j],arr[j+1]);
flag = true;
}
}
}
}
3、选择排序:
从第i个待排序的数据开始,假定它是最小值,并用min记录它的下标,然后与用arr[min]与它后面的数据进行比较,如果有它还小的数据,则更新min的值,排序一趟后,如果min==i,则说明第一个数据是最小值,不需要交换,如果min!=i则交接第一个数据与arr[min],然后i++,重复这个步骤直到待排序的数据为1(i=len-1),排序结束。
选择排序是对经典排序的一种优化,和经典排序相比,它的数据比较次数没有减少,但是它的数据交换数次大大降低(O(N-1)),节约了很多数据交换的时间,虽然时间复杂度没有变化,排序效率比经典排序提高了很多。
注意:选择排序最突出的特点是数据的交换次数最少,如果待排序的数据字节数较多,如:结构、类对象,则使用选择排序速度最快。
// 时间复杂度:O(n^2) 不稳定
void select_sort(int* arr,size_t len)
{
for(int i=0; i<len-1; i++)
{
int min = i;
for(int j=i+1; j<len; j++)
{
if(arr[min] > arr[j])
min = j;
}
if(min != i)
swap(arr[min],arr[i]);
}
}
4、插入排序:
往有序的序列中添加新的数据,使序列继续保持有序,具体步骤是:假定新的数据存放在i=len位置,val与它前面的数据逐一进行比较val<arr[i-1],如果前面的数据大于tmp大,则把前面数据的向后拷贝一下,然后i自减1重复以比较,直到arr[i-1]<=tmp或0==i,则位置i就是tmp应该存放的位置,新插入的数据就排序完成。
可以使用以上方法对无序序列进行排序,把序列看作两个总分,已经有序的部分(数量为1)和待插入的部分(len-1),把待插入部分的数据逐个向前面有序部分插入,就完成了对无序序列进行排序。
注意:顾名思义,插入排序算法适合往有序的序列中添加新的数据,优点是排序过程中没有进行数据交换(与经典排序相比),节约了大量时间。
// 最优时间复杂度:O(n) 最差时间复杂度:O(n^2) 平均时间复杂度:O(n^2) 稳定
void _insert_sort(int* arr,size_t len,int data)
{
int i=len;
while(i-1>=0 && arr[i-1] > data)
{
arr[i] = arr[i-1];
i--;
}
arr[i] = data;
}
void insert_sort(int* arr,size_t len)
{
for(int i=1; i<len; i++)
{
_insert_sort(arr,i,arr[i]);
}
}
//直接插入排序
void insert_sort(int* arr,size_t len)
{
for(int i=1; i<len; i++)
{
int tmp = arr[i], j = i;
while(j-1>=0 && arr[j-1] > tmp)
{
arr[j] = arr[j-1];
j--;
}
arr[j] = tmp;
}
}
5、希尔排序:
设计该算法的作者叫希尔,所以叫希尔排序,它在插入排序的基础上引入了增量概念(数据在插入时,每次移动的距离),插入排序默认一次只移动一个位置,当数据量比较大时,移动的速度比较慢,希尔排序先以数量的一半为移动增量,进行插入排序,对数据进行大致排序,然后再减小增量对数据进行微调,进而完成插入排序。
注意:希尔排序适合在数量比较大的时候,向有序的序列中添加新的数据。
// 时间复杂度:O(nlog2n) 不稳定
void shell_sort(int* arr,size_t len)
{
for(int k=len/2; k>0; k/=2)
{
for(int i=k; i<len; i++)
{
int tmp = arr[i], j = i;
while(j-k>=0 && arr[j-k] > tmp)
{
arr[j] = arr[j-k];
j -= k;
}
arr[j] = tmp;
}
}
}
6、快速排序:
先在待排序的序列中找出一个标杆,然后与剩余的数据进行比较,比标杆小的数据放在标杆的左边,比标杆大的数据放在它右边,这样就做到以标杆为准的大致有序,然后再次同样的方法对标杆左边的数据进行排序、标杆右边的数据进行排序,直到整个序列完全有序。
注意:快速排序之所以叫快速排序,综合各种情况它的表示最好,速度最快,如果对待排序的数据不了解,建议优先选择快速排序。
标杆:83 83 86 77 15 93 35 86 92 49 21 l r 第一趟排序完成 都比标杆小 < 83 < 都比标杆大 21 49 77 15 35 83 86 92 93 86 第二趟排序左边 标杆:21 15 21 77 49 35 第二趟排序右边 86 92 93 86
/ / 时间复杂度:O(nlogn) 不稳定
void _quick_sort(int* arr,int left,int right)
{
// 当待排序的数据量 <= 1 则结束
if(left >= right)
return;
// 把标杆挖起来,并记录坑的位置
int pi = left, pv = arr[pi];
// 备份右边界,备份右边界
int l = left, r = right;
while(l<r)
{
// 从右向左寻找比标杆小的值,如果比标杆大就继续循环
while(l<r && arr[r] >= pv) r--;
// 当循环结束,且l<r,说明找到了比标杆小的数据
if(l<r)
{
// 把数据放入坑里,数据的原位置就变成了新的坑
arr[pi] = arr[r];
// 记录新坑的位置
pi = r;
}
// 从左向右寻找比标杆大的值,如果比标杆小就继续循环
while(l<r && arr[l] <= pv) l++;
// 当循环结束,且l<r,说明找到了比标杆大的数据
if(l<r)
{
// 把数据放入坑里,数据的原位置就变成了新的坑
arr[pi] = arr[l];
// 记录新坑的位置
pi = l;
}
}
// 当l与r相遇,循环结束,把标杆放入坑里,标杆左边的数据都比它小,标杆右边的数据都比它大,以标杆为衡量标准,序列已经大致有序
arr[pi] = pv;
// 对标杆左边的数据进行快速排序
_quick_sort(arr,left,pi-1);
// 对标杆右边的数据进行快速排序
_quick_sort(arr,pi+1,right);
}
void quick_sort(int* arr,int len)
{
_quick_sort(arr,0,len-1);
}
7、堆排序:
堆一种特殊的二叉树,有两种堆,一种叫大根堆(根结点比它的左子树、右子树都大,并且所有子树都满足这个特点),另一种叫小根堆(根结点比它的左子树、右子树都小,并且所有子树都满足这个特点)。
所谓堆排序就是把待排序的数据当作一个大根堆(完全二叉树),然后逐步把堆顶的最大值弹出存储在序列的末尾,也就是借助大根堆这一数据结构完成的排序。
注意:理论上来说堆排序的速度不比快排序慢,但是对无序的序列排序需要先创建堆,时间复杂度是O(N),然后再逐一出堆完成排序时间复杂度是O(NlogN),所以对无序的序列排序快速排序比堆的速度要快,所以一般在实际应用中不使用堆排序,只活在教课书中。
// 时间复杂度:O(nlogn) 不稳定
void _head_sort(int* arr,int root,size_t len)
{
// 计算出左子树的下标:(root+1)*2-1,如果root没有左子树就肯定没有右子树,不需要再往下探索
while(root*2+1 < len)
{
// 假定左子树是左右子树中的最大值
int max = root*2+1;
// 判断是否有右子树,再判断右子树是否大于左子树
if(max+1 < len && arr[max+1] > arr[max])
max++;
if(arr[max] > arr[root])
swap(arr[max],arr[root]);
// 左右子树,谁与根交换,谁就需要重新调整
root = max;
}
}
void heap_sort(int* arr,size_t len)
{
// 构建大根堆
for(int i=len/2-1; i>=0; i--)
_head_sort(arr,i,len);
for(int i=len-1; i>0; i--)
{
swap(arr[0],arr[i]);
_head_sort(arr,0,i);
}
}
8、归并排序:
把待排序的数据以k=2为单位进行分组,每组分为左右两部分,然后按从小到大的顺序合并到另一块空间,然后k*=2重复该过程,直到k/2>=len则排序完成。
归并排序需要一块额外的空间,用于存储合并的结果,它的时间复杂度与快速、堆相同,但是在排序过程中没有进行数据交换,而直接数据拷贝,因此节约了大量的数据交换的时间,但也耗费了额外的内存,所以它是一各典型的用空间换取时间的排序算法。
注意:如果用户对排序速度有很高的要求,但不在意内存的耗费,适合使用归并排序。
循环语句实现归并排序:
// 时间复杂充:O(nlogn) 稳定
void _merge(int* dest,int* src,int ls,int le,int rs,int re)
{
int i = ls;
while(ls < le && rs < re)
{
if(src[ls] < src[rs])
dest[i++] = src[ls++];
else
dest[i++] = src[rs++];
}
while(ls < le)
dest[i++] = src[ls++];
while(rs < re)
dest[i++] = src[rs++];
}
// 循环归并
void merge_sort(int* arr,size_t len)
{
int* src = arr;
int* dest = malloc(sizeof(arr[0])*len);
for(int k=1; k<len; k*=2)
{
for(int i=0; i<len; i+=2*k)
{
int ls = i, le = i+k>len ? len : i+k;
int rs = le, re = rs+k>len ? len : rs+k;
int j = ls;
while(ls < le && rs < re)
{
dest[j++] = src[ls] < src[rs] ? src[ls++] : src[rs++];
}
while(ls < le)
dest[j++] = src[ls++];
while(rs < re)
dest[j++] = src[rs++];
}
swap(src,dest);
}
if(arr != src)
{
memcpy(arr,src,sizeof(arr[0])*len);
dest = src;
}
free(dest);
}
函数递归实现归并排序:
void _merge(int* dest,int* src,int ls,int le,int rs,int re)
{
int i = ls;
while(ls < le && rs < re)
{
if(src[ls] < src[rs])
dest[i++] = src[ls++];
else
dest[i++] = src[rs++];
}
while(ls < le)
dest[i++] = src[ls++];
while(rs < re)
dest[i++] = src[rs++];
}
// 递归归并
void _merge_sort(int* dest,int* src,int left,int right)
{
if(left+1 >= right)
return;
int pi = (left+right)/2;
_merge_sort(dest,src,left,pi);
_merge_sort(dest,src,pi,right);
_merge(dest,src,left,pi,pi,right);
memcpy(src+left,dest+left,sizeof(*src)*(right-left));
}
void merge_sort(int* arr,size_t len)
{
int dest[len];
_merge_sort(dest,arr,0,len);
}
9、计数排序:
首先定义一个计算数据出现的次数的数组cnts,并所有成员初始化0,然后使用数据的值作为数组的下标,然后统计每个数据出现的次数。
以i=[0,max]遍历cnts数组,当cnst[i]>0说明i出现过,然后把i往待排序的数组中存储cnts[i]个,然后排序完成。
注意:计算排序的局限比较大,它只能对整型数据进行排序,无法对浮点型、字符串型数据进行排序,待排序的数据重复性越高,差值越小,速度就越快,反之虽然也可以排,但得不尝失。
知道待排序的数据范围:
void count_sort(int* arr,size_t len)
{
int count[100] = {};
for(int i=0; i<len; i++)
{
count[arr[i]]++;
}
int index = 0;
for(int i=0; i<100; i++)
{
for(int j=0; j<count[i]; j++)
{
arr[index++] = i;
}
}
}
不知道待排序的数据范围:
void count_sort(int* arr,size_t len)
{
int max = arr[0], min = arr[0];
for(int i=1; i<len; i++)
{
if(arr[i] > max)
max = arr[i];
else if(arr[i] < min)
min = arr[i];
}
int* count = calloc(max-min+1,sizeof(count[0]));
for(int i=0; i<len; i++)
{
count[arr[i]-min]++;
}
int index = 0;
for(int i=min; i<=max; i++)
{
for(int j=0; j<count[i-min]; j++)
{
arr[index++] = i;
}
}
}
10、基数排序
先根据数据个位的大小对数据进行排序,然后再对排序结果的十位进行排序,然后百位、千位...,直到排序完成。
使用这种方式排序的优点是不需要对待排序的数据进行比较、交换,所以它的排序速度要比普通排序快的多,但局限性很大,只能对整型数据排序,并且对正负数还有要求,还需要额外的内存空间。
注意:当数据的位数不多,并且差别不大,的整形数据适合使用基数排序。
void count_sort(int* arr,size_t len)
{
int max = arr[0], min = arr[0];
for(int i=1; i<len; i++)
{
if(arr[i] > max)
max = arr[i];
else if(arr[i] < min)
min = arr[i];
}
int* count = calloc(max-min+1,sizeof(count[0]));
for(int i=0; i<len; i++)
{
count[arr[i]-min]++;
}
int index = 0;
for(int i=min; i<=max; i++)
{
for(int j=0; j<count[i-min]; j++)
{
arr[index++] = i;
}
}
}
11、桶排序
桶排序就是把待排序的数据,根据值划分不同的范围存储到不同的"桶"中,然后再使用其它排序算法对桶中的数据进行排序,最终再合并桶中的数据达到排序的目的。
之所以这样,是因为待排序的数据比较多时,会影响排序算法的性能,桶排序就是通过数据进行分类,降低数据的规模,从而提高排序算法的性能。
注意:该算法代码简单,但实现难度很大,需要对数据有足够的了解,每个桶中数量要够均匀,才能达到预期效果。
void bucket_sort(int* arr,size_t len)
{
int bucket[3][len];
int cnt[3] = {};
for(int i=0; i<len; i++)
{
if(arr[i] < 10000)
bucket[0][cnt[0]++] = arr[i];
else if(arr[i] < 50000)
bucket[1][cnt[1]++] = arr[i];
else
bucket[2][cnt[2]++] = arr[i];
}
int index = 0;
quick_sort(bucket[0],cnt[0]);
for(int i=0; i<cnt[0]; i++)
arr[index++] = bucket[0][i];
quick_sort(bucket[1],cnt[1]);
for(int i=0; i<cnt[1]; i++)
arr[index++] = bucket[1][i];
quick_sort(bucket[2],cnt[2]);
for(int i=0; i<cnt[2]; i++)
arr[index++] = bucket[2][i];
}