排序算法介绍:
排序的功能是将数据元素的任意序列,重新排列成按照关键字有序的序列
它之所以重要是因为查找操作很重要,而有序的顺序表可以采用效率较高的二分查找(O(logN)),而无序的查找只能顺序查找(O(N)),而创建的二叉搜索树、平衡二叉树、堆的过程其实就是一个排序过程。
排序算法的稳定性:
假设待排序的序列中可能存在两个或两个以上关键字相同的数据,k[i] == k[j]并且i<j,在排序的整个过程中,i必定在j的前面,那么认为该排序算法是稳定的,反之认为是不稳定的排序
1、经典排序:
仅能够完成排序操作,但没有任何的优化动作,没有进行数据比较次数减少的优化,数据交换的次数也没有优化,没有任何优点,不能称得上是算法。
// 经典排序 void classic_sort(TYPE* arr,size_t len) { for(int i=0; i<len-1; i++) { for(int j=i+1; j<len; j++) { if(arr[j] < arr[i]) { SWAP(arr[i],arr[j]); } } } }
2、冒泡排序
排序的过程类似水中气泡上升,越大的气泡上升越快。
从第一个数据开始,让前后相邻的数据两两比较,如果k[i]>k[i+1]则交换它们,每一趟冒泡排序完成一个数据的排序,反复以上过程,直到待排序的数据为1,结束排序
与其他排序相比,冒泡排序对数据的有序性敏感,如果在一趟的排序过程中,没有发生一次交换,则说所有数据前后两两有序,则可以立即停止冒泡排序,提前结束。
注意:如果待排序的序列基本有序,则使用冒泡排序速度最快,因为可以提前结束
// 冒泡排序 // 最优 O(N) 最差\平均 O(N^2) 稳定的 void bubble_sort(TYPE* arr,size_t len) { // 保证有序性敏感 bool flag = true; for(int i=len-1; i>0 && flag; 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更小的数据,则先更新min下标,当后面所有数据都比较完后,如果min!=i,则交换min和i的值,继续重复以上步骤。
和经典排序相比,数据的比较次数没有减少,但是数据交换次数大大降低O(N-1),节约了很多的数据交换时间,所以虽然时间复杂度没变化O(N^2),但是它的排序效率比经典排序提高很多
注意:选择排序最突出的特点是数据交换次数最少的,如果待排序的元素的字节数较多,比如:结构、类对象时,则此时使用选择排序速度最快
// 选择排序 // O(N^2) 不稳定 void select_sort(TYPE* 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[j] < arr[min]) min = j; } if(min != i) SWAP(arr[min],arr[i]); } }
4、插入排序
往有序的序列中添加新的数据,使得序列保持有序,步骤:假定新的数据位置是i,值是val,让该数据与前面的数据从后往前逐一比较,如果val<arr[i-1],则把前面的数据往后一位拷贝,然后i--,然后重复以上操作,直到val不再小于前面的值,或者i走完所有数据,则该位置就是val应该存放的位置,把数据val放入则完成本次插入操作
使用以上步骤,对无序的序列,看成左右两部分,左边是已经有序的,和右边带插入的无序序列,逐一往左边部分完成插入操作,当无序部分插入完成即完成排序操作。
注意:插入排序适合往已经有序的序列中添加新的数据,优点是排序过程中没有数据交换,节约了大量的无效的时间
// 插入排序 // 时间复杂度 最优O(N) 最差\平均O(N^2) // 稳定的 void insert_sort(TYPE* arr,size_t len) { for(int i=1; i<len; i++) { // j是要插入的位置 int j = i; TYPE val = arr[i]; // 待插入数据 // 与前一个位置相比 for(j=i; j>0 && arr[j-1] > val; j--) { arr[j] = arr[j-1]; } if(j!=i) arr[j] = val; } }
5、希尔排序
设计该算法的作者叫希尔,所以叫希尔排序,在插入排序的基础上引入增量的概念(数据在插入的过程中,每次移动的距离),原来的插入排序每次只移动数据一个位置,当数据量比较大,或者数据距离最终正确位置较远时,原来的插入排序速度较慢,在希尔排序中最开始以序列长度的一半作为移动增量,进行第一次插入排序,然后再对半减少移动增量,直到增量为1时,每个数据已经很接近最终位置,最后再完成一次插入排序,就完成排序。
注意:希尔排序适合在数据量大的时候,或者非常无序的序列中添加新的数据。
// 希尔排序 // 时间复杂度 O(NlogN) 不稳定 void shell_sort(TYPE* arr,size_t len) { // k是移动增量 for(int k=len/2; k>0; k/=2) { for(int i=k; i<len; i++) { int val = arr[i],j = i; for(j=i; j-k>=0 && arr[j-k] > val; j-=k) { arr[j] = arr[j-k]; } if(j!=i) arr[j] = val; } } }
6、快速排序
先找到一个标杆位置p,一般是待排序序列的最左边(也可以任意,中间、最右边),然后备份标杆的值val,然后找到待排序序列的最坐标标杆l,和最右标杆r,然后先让r往左走,找比val小的数,找到了把该值赋值给p位置,然后更新p到r;当p和r重合后,换成l从左往右找比val更大数,找到了把该值赋值给p位置,然后更新p到l,重复以上过程,最终l和r相遇时,结束这一次快排,把val的值赋值到相遇点p,结束后,val的左边都比它小,右边都比它大,达到局部有序,然后继续对左右两边分别进行同样快排,最后全部达到有序停止。
注意:快速排序之所以成为快排,综合各种情况下它的表现是最好、速度最快的,所以如果对于待排序的数据不了解时,建议优先选择快排
// 从arr的left下标到right下标位置进行快排 // 时间复杂度:O(NlogN) // 不稳定 void _quick_sort(TYPE* arr,size_t len,int left,int right) { if(left >= right) return; // 记录标杆下标 int p = left; // 备份记录标杆位置的值 int val = arr[p]; // 准备左右标杆 int l = left, r = right; // 只要l和r还没相遇 一直继续 while(l < r) { // 让右标杆 从右往左找 比val小的数值 while(l < r && arr[r]>=val) r--; // 如果找到了更小 if(l < r) { // 把更小的值付给标杆 arr[p] = arr[r]; // 更新标杆 p = r; } // 让左标杆 从左往右找比val大数值 while(l < r && arr[l] <= val) l++; // 如果找到了 if(l < r) { arr[p] = arr[l]; p = l; } } // l和r相遇,该位置就是原来val应该在位置 arr[p] = val; // 对左右两边继续快排 _quick_sort(arr,len,left,p-1); _quick_sort(arr,len,p+1,right); } // 快速排序 void quick_sort(TYPE* arr,size_t len) { _quick_sort(arr,len,0,len-1); }
7、堆排序
堆是一种特殊的二叉树,有两种堆,大顶堆(根节点的值大于左右子树,并且所有子树都满足),小顶堆(根节点的值小于左右子树,并且所有子树都满足)
所谓的堆排序就是把一个数据当做大顶堆\小顶堆处理,逐步把堆顶的最值交换到序列的末尾,然后重新调整剩下的数据变回堆结构,重复最终有序,之前写过,不再赘述。
注意:理论上堆排序的速度并不比快排慢,但是对于无序的序列需要先构建成堆结构,时间复杂度已经需要O(N),然后再逐一堆顶出堆完成堆排序O(NlogN),因此无序序列的堆排序不见得很快,所以实际应用中不会使用堆排序,是不稳定的
8、归并排序
先把待排序的序列以k=2为单位进行分组拆分,每组有左右两部分,按照从小到大的顺序合并到另一个内存空间的对应位置,然后让k*=2,继续两组两组合并,最终当k/2>=len则排序完成,在合并的过程,合并的左右部分,都是有序的
归并排序需要一块额外的内存空间,用于存储每次合并的结果,因此节约了大量的数据交换的时间,但是也耗费了额外的空间,也是以空间换时间的策略
注意:如果用户对排序的速度有高要求,但是又不在意内存的消耗,适合使用归并排序
// 时间复杂度:O(NlogN) // 稳定的 // 把l~p p+1~r两有序部分 进行合并 // l左部分最左 p左部分最右 // p+1 右部分最左 r右部分最右 void __merge(TYPE* arr,size_t len,TYPE* temp,int l,int p,int r) { // 已经都有序了 if(arr[p] <= arr[p+1]) return; int i = l, j = p+1,k = l; // 左右部分还没比完 while(i<=p && j<=r) { if(arr[i] <= arr[j]) // 让其稳定 temp[k++] = arr[i++]; else temp[k++] = arr[j++]; } // 把左边剩余的放入后面 while(i<=p) temp[k++] = arr[i++]; // 把右边剩余的放入后面 while(j<=r) temp[k++] = arr[j++]; // 把数据拷贝回arr中对应的位置 memcpy(arr+l,temp+l,sizeof(TYPE)*(r-l+1)); } // 拆分l~r成两个部分,并形成有序,然后有序合并这两部分 void _merge_sort(TYPE* arr,size_t len,TYPE* temp,int l,int r) { if(l >= r) return; int p = (l+r)/2; // 拆分成两部分,并各自形成有序 _merge_sort(arr,len,temp,l,p); _merge_sort(arr,len,temp,p+1,r); // 合并有序的左右两部分 __merge(arr,len,temp,l,p,r); } // 归并排序 void merge_sort(TYPE* arr,size_t len) { // 准备一段同样大小的额外空间 TYPE* temp = malloc(sizeof(TYPE)*len); // 拆分 归并 _merge_sort(arr,len,temp,0,len-1); // 释放额外空间 free(temp); }
9、计数排序
找出数据中的最大值和最小值,创建哈希表,把 数据-最小值 当做访问哈希表的下标去给对应位置计数+1,然后遍历所有数据对哈希表进行计数
然后遍历哈希表,当表中的数据大于0时,再通过该位置的下标+最小值得到原数据,并依次放回原数组中,得到有序。
注意:理论上该算法速度非常快,它不是基于比较的算法,在一定范围的正整数数据的排序中,要快于任何一种比较的排序算法,但是有很大的局限性,只适合整形数据,并且数据的差值不宜过大,否则非常浪费内存,因此数据越平均,重复数越多,性价比越高
// 时间复杂度:O(N+k) // 稳定的 // 计数排序 void count_sort(TYPE* arr,size_t len) { // 找出最大、最小值 TYPE min = arr[0],max = arr[0]; for(int i=1; i<len; i++) { if(arr[i] < min) min = arr[i]; if(arr[i] > max) max = arr[i]; } // 申请哈希表内存 TYPE* hash = calloc(sizeof(TYPE),max-min+1); // 标记哈希表 for(int i=0; i<len; i++) { hash[arr[i]-min]++; } // 遍历哈希表,还原数据 for(int i=0,j=0; i<max-min+1; i++) { while(hash[i]--) arr[j++] = i+min; } // 释放哈希表 free(hash); }
10、基数排序
先创建10个队列并编号0~9,然后逆序计算出每个数据的个、十、百...的数,然后入队到对应编号的队列中,然后依次把每个队列的值出队,再计算下一位的值并入队,依次循环,直到最大值的所有位数都处理过后,最后一次出队的顺序就是排序成功后的序列
使用该排序也需要额外的内存空间(队列),也是以空间换时间策略
注意:数据的位数不多,差别不大的整型数据才适合
// 基数排序 void radix_sort(TYPE* arr,size_t len) { // 创建10个队列 ListQueue* queue[10] = {}; for(int i=0; i<10; i++) { queue[i] = create_list_queue(); } // 循环次数由最大值的位数决定 TYPE max = arr[0]; for(int i=1; i<len; i++) { if(arr[i] > max) max = arr[i]; } // i表示处理哪位 1处理个位,2处理十位,... for(int i=1,k=1; max/k >0;k*=10,i++) { int mod = pow(10,i); int div = mod/10; // 把每个数据按照规则入到对应的队列中 for(int j=0; j<len; j++) { // 获取到每个数的某位 int index = arr[j]%mod/div; push_list_queue(queue[index],arr[j]); } for(int j=0,l=0; j<10; j++) { // 把队列中的数据依次放回arr中 while(!empty_list_queue(queue[j])) { arr[l++] = front_list_queue(queue[j]); pop_list_queue(queue[j]); } } } // 销毁队列 for(int i=0; i<10; i++) { destroy_list_queue(queue[i]); } }
11、桶排序
桶排序是把待排序的数据,一般根据值的不同范围,划分到不同的“桶”中,然后再根据桶中数据的特点,选择合适的其他的排序算法对各个桶中的数据分别进行排序,最终合并桶中的数据达到了排序的目的
之所以使用桶排序的思想,是因为待排序的数据量非常多的时候,会影响排序算法的性能,桶排序的目的是对数据分类后,降低了数据的规模,并且可能按照特征分类,从而提高排序的效率,和选择更合适的排序算法
// 分桶排序 cnt桶数 range桶中数据的范围 void _bucket(TYPE* arr,size_t len,int cnt,TYPE range) { // 申请桶内存 // bucket指向桶的开头 end指向桶的末尾 数据加入到end位置 TYPE* bucket[cnt],*end[cnt]; for(int i=0; i<cnt; i++) { // 万一全部数据都在一个桶 bucket[i] = malloc(sizeof(TYPE)*len); end[i] = bucket[i]; } // 遍历数据,按照范围放入对应桶中 for(int i=0; i<len; i++) { for(int j=0; j<cnt; j++) { if(j*range <= arr[i] && arr[i] < range*(j+1)) { *(end[j]) = arr[i]; end[j]++; } } } // 对每个桶分别使用其他排序算法来排序 for(int i=0; i<cnt; i++) { // 计算出每个桶中元素个数 int size = end[i] - bucket[i]; if(size > 1) bubble_sort(bucket[i],size); // 把桶中的排序后的数据 拷贝回arr对应 memcpy(arr,bucket[i],sizeof(TYPE)*size); arr += size; free(bucket[i]); } } // 桶排序 只对正整数 void bucket_sort(TYPE* arr,size_t len) { // 分4个桶排序 _bucket(arr,len,4,25); }