01.计数排序、桶排序与基数排序
并不是所有的排序 都是基于比较的,计数排序和基数排序就不是。基于比较排序的排序方法,其复杂度无法突破\(n\log{n}\) 的下限,但是 计数排序 桶排序 和基数排序是分布排序,他们是可以突破这个下限达到O(n)的的复杂度的。
1. 计数排序
概念
计数排序是一种稳定的线性时间排序算法。计数排序使用一个额外的数组C
,使用 C[i]
来计算 i
出现的次数。然后根据数C
来将原数组A中的元素排到正确的位置。
复杂度
计数排序的最坏时间复杂度、最好时间复杂度、平均时间复杂度、最坏空间复杂度都是O(n+k)
。n为元素个数,k为待排序数的最大值。
优缺点
计数排序不是比较排序,排序的速度优于任何比较排序算法。由于用来计数的数组C的长度取决于待排序数组中数据的范围(等于待排序数组的最大值与最小值的差加1),这使得对于数组中数据范围很大的数组,需要大量的时间和内存。(简言之,不适于大范围数组)
通俗地理解,例如有10个年龄不同的人,统计出有8个人的年龄比A小,那A的年龄就排在第9位,用这个方法可以得到其他每个人的位置,也就排好了序。当然,年龄有重复时需要特殊处理(保证稳定性),这就是为什么最后要反向填充目标数组,以及将每个数字的统计减去1的原因。算法的步骤如下:
- 找出待排序数组中的最大元素和最小元素。
- 统计数组中值为 i 的元素出现的次数,存入数组C的第 i 项。
- 对所有的计数累加(从C中第一个元素开始,每一项和前一项累加)。
- 反向填充目标数组:将每个元素 i 放在新数组的第 C[i] 项, 每放一个元素就将 C[i] 减去1.
C语言实现
#include <stdio.h> #include <stdlib.h> #include <time.h> void print_arr(int *arr, int n) { int i; printf("%d", arr[0]); for (i = 1; i < n; i++) printf("%d", arr[i]); priintf("\n"); } void counting_sort(int *ini_arr, int *sortrd_arr, int n) { int *count_arr = (int *)malloc(sizeof(int) * 100); int i, j, k; // 初始计数化数组 for (k = 0; k < 100; k++) count_arr[k] = 0; // 步骤二, 计数 for (i = 0; i < 100; i++) count_arr[i]++; // 步骤三, 对所有的计数累加(计算每个数的实际顺序) for (k = 1; k < 100; k++) count_arr[k] += count_arr[k-1]; // 步骤4, 反向填充数组 for (j = n; j > 0; j--) { int elem = ini_arr[j - 1]; // 取待排序元素 int index = count_arr[elem] - 1; // 取待排序元素在有序数组中的序号 sortrd_arr[index] = elem; // 将待排序数组存入结果数组中 count_arr[elem]--; // 修正排序结果,保证sorted_arr数组中元素的稳定性 } /* * 上述句子也可以写为: * sortrd_arr[--count_arr[ini_arr[j -1]]] = ini_arr[j -1]; * */ free(count_arr); } int main(int argc, char **argv) { int n =10; int i; int *arr = (int *)malloc(sizeof(int) * n); int *sorted_arr = (int *)malloc(sizeof(int) * n); srand(time(0)); for (i = 0; i< n; i++) arr[i] = rand()%100; printf("Init array:"); print_arr(arr, n); counting_sort(arr, sorted_arr, n); printf("Sorted_arr;"); free(arr); free(sorted_arr); return 0; }
2. 桶排序
概念
桶排序(Bucket sort)或所谓的箱排序,其工作原理是将阵列分到有限数量的桶里。每个桶再分别排序。桶排序是分布排序,不是比较排序,因而不受到比较排序 O(\(n\log{n}\))下限的影响。
复杂度
最坏时间复杂度是O(n^2), 平均复杂度是O(n+k),最坏空间复杂度是O(n*k)。
关键步骤
- 设置一个定量的阵列当作空桶子。
- 寻访序列,并且把项目一个一个放到对应的桶子去。
- 对每个不是空的桶进行排序。可以在放入元素的时候进行插入排序,也可以在写回的时候进行快速排序。
- 从不是空的桶子里把项目放回到原来的序列中。
算法实现
假设数据分布在[0, 100]之间,每个桶内部用链表表示,在数据入桶的同时插入排序。然后把各个桶中的数据合并。
#include <iterator> #include <iostream> #include <vector> using namespace std; const int BUCKET_NUM = 10; // 数据结构的定义,explicit表示不允许构造函数发生隐式转换 struct ListNode { explicit ListNode(int i = 0): mData(i), mNext(NULL){}; ListNode *mNext; int mData; }; ListNode *insert(ListNode *head, int val) { ListNode dummyNode; // 节点指针 ListNode *newNode = new ListNode(val); //待插入的新节点 ListNode *pre , *curr; dummyNode.mNext = head; // 将指针指向链表头部 pre = &dummyNode; // 设置临时指针。pre是当前检查元素的上一个元素 curr = head; while (NULL != curr && curr->mData <= val) // 末尾检测 { pre = curr; // 不断向前循环,直到末尾或者找到不小于val的元素 curr = curr -> mNext; } newNode->mNext = curr; // 改变指针指向 pre ->mNext = newNode; return dummyNode.mNext; // 返回链表头节点 } ListNode *Merge(ListNode *head1, ListNode *head2) // 将head2合并到head1 { ListNode dummyNode; ListNode *dummy = &dummyNode; // 临时指针 while(NULL != head1 && NULL != head2) // 循环直到末尾 { if (head1->mData <= head2 ->mData) // 类似于归并排序 { dummy->mNext = head1; head1 = head1 -> mNext; }else{ dummy->mNext = head2; head2 = head2->mNext; } dummy = dummy->mNext; } if (NULL != head1) dummy->mNext = head1; if(NULL!=head2) dummy->mNext = head2; return dummyNode.mNext; } void BucketSort(int n,int arr[]){ vector<ListNode*> buckets(BUCKET_NUM,(ListNode*)(0)); // 将元素分配到桶 for(int i=0;i<n;++i){ int index = arr[i]/BUCKET_NUM; ListNode *head = buckets.at(index); buckets.at(index) = insert(head,arr[i]); } // 逐一合并各个桶 ListNode *head = buckets.at(0); for(int i=1;i<BUCKET_NUM;++i){ head = Merge(head,buckets.at(i)); } //将排好序的元素写回原数组 for(int i=0;i<n;++i){ arr[i] = head->mData; head = head->mNext; } }
3. 基数排序
定义
基数排序是桶排序的扩充,也是一种分布排序。其原理是将整数按位数切割为不同的数字,然后按每个数分别比较。根据比较的方向,基数排序又可以分为MSD(从左到右)和LSD(从右向左)
LSD原理 将所有带比较数值统一为同样的数位长度,数位较短的前面补0,。然后,从最低位开始,进行一次排序,一直到最高位排序完成以后,数列就变成一个有序数列。其思想就是,将待排序数据中的每组关键字依次进行桶分配。
MSD原理
msd算法从左向右遍历字符。其核心思想是分治,我们采用递归的方法来实现。原理如下:
- 首先,使用键索引排序的方法对首字母进行排序,此时排好序的数组已经是首字母有序的数组,并且已经按照首字字母分好了组。
- 按照分好的组,递归的对每个首字母对应的子数组进行排序。
- 重复步骤二。
复杂度
最坏时间复杂度是O(kN),最坏空间复杂度是O(k+N);
LSD实现
/** * 基数排序:C 语言 * * */ #include <stdio.h> // 数组长度 #define LENGTH(array) ( (sizeof(array)) / (sizeof(array[0])) ) /* * 获取数组a中最大值 * * 参数说明: * a -- 数组 * n -- 数组长度 */ int get_max(int a[], int n) { int i, max; max = a[0]; for (i = 1; i < n; i++) if (a[i] > max) max = a[i]; return max; } /* * 对数组按照"某个位数"进行排序(桶排序) * * 参数说明: * a -- 数组 * n -- 数组长度 x* exp -- 指数。对数组a按照该指数进行排序。 * * 例如,对于数组a={50, 3, 542, 745, 2014, 154, 63, 616}; * (01) 当exp=1表示按照"个位"对数组a进行排序 * (02) 当exp=10表示按照"十位"对数组a进行排序 * (03) 当exp=100表示按照"百位"对数组a进行排序 * ... */ void count_sort(int a[], int n, int exp) { // 存储"被排序数据"的临时数组 int *output = (int *)malloc(sizeof(int)*n); int i, buckets[10] = {0}; // 将数据出现的次数存储在buckets[]中 for (i = 0; i < n; i++) buckets[ (a[i]/exp)%10 ]++; // 更改buckets[i]。目的是让更改后的buckets[i]的值,是该数据在output[]中的位置。 for (i = 1; i < 10; i++) buckets[i] += buckets[i - 1]; // 将数据存储到临时数组output[]中 for (i = n - 1; i >= 0; i--) { output[buckets[ (a[i]/exp)%10 ] - 1] = a[i]; buckets[ (a[i]/exp)%10 ]--; } // 将排序好的数据赋值给a[] for (i = 0; i < n; i++) a[i] = output[i]; } /* * 基数排序 * * 参数说明: * a -- 数组 * n -- 数组长度 */ void radix_sort(int a[], int n) { int exp; // 指数。当对数组按各位进行排序时,exp=1;按十位进行排序时,exp=10;... int max = get_max(a, n); // 数组a中的最大值 // 从个位开始,对数组a按"指数"进行排序 for (exp = 1; max/exp > 0; exp *= 10) count_sort(a, n, exp); } void main() { int i; int a[] = {53, 3, 542, 748, 14, 214, 154, 63, 616}; int ilen = LENGTH(a); printf("before sort:"); for (i=0; i<ilen; i++) printf("%d ", a[i]); printf("\n"); radix_sort(a, ilen); printf("after sort:"); for (i=0; i<ilen; i++) printf("%d ", a[i]); printf("\n"); }
msd实现
/* MSD(Most Significant Digit First) 高位优先的字符串排序 该算法基于键索引计数法的思想,进行了扩展,使得该算法可以 处理不等长的字符串排序,其中涉及两个关键点。 1、采用分治法,从高位向低位的方向,依次选取关键字做为排序 的键字进行排序,每一轮排序后,将字符串组进行拆分,对拆 分后的每个子组分别进行排序,这里子组拆分的依据就是本轮 键索引计数法排序之后的分类组。 如下示例,最初只有一个字符串组,其中组成员数为5个字符串 首先,选择第0列做为排序的链字进行键索引计数法排序,排序 完成后,按分类组划分,此时分为了两组(见第一次后的情况), 这时候对这两组分别进行链索引计数法排序,注意这时每组第 0列已经为相同字符,所以此时选择第1做为排序的链字进行键 索引计数法排序,在第二次排序后,此时已经分为了4组lp字符串 组,依次类推,直到所有子组仅含有一个成员,所有子组排序 处理完后,即整个字符串排序算法完成。 原始: 第一次后: 第二次后: abcd abcd abcd ddba acca daca acca acca ddba daab daca daca daab daab ddba 2、刚才提到了该算法可以处理不等长的字符串排序,该算法采用一 种比较巧妙的方法,将短字符串长度已经满足不了排序的处理也 做为键值比较处理了,同时如果短字符串长度满足不了排序处理 时,该键值优先级最高,所以就会出现排在最上方。 如下示例,当第2列(字符c的位置)处理完后,开始进行第3列比较 处理,此时第一个条目abcd的第3列键值为d、第二个条目abc的第 3列键值已经不存在,长度已经满足不了排序,但此时键值优先级 为最高,第三个条目abcde的第3列键值为d,所以本轮最终将第二 个条目abc排在了最上面,相同原理,abcd条目就会比abcde条目的 优先级高。 原始: 排序后: abcd abc abc abcd abcde abcde */ #include <IOSTREAM> #include <FSTREAM> #include <STRING> #include <VECTOR> const int R = 256; // 基数 const int M = 15; // 小数组使用插入排序的阈值 using namespace std; int charAt(const string& str, int d) { if ( d < str.size() ) return str[d]; else return -1; } //参数分别表示字符串容器,排序字符串起始位置,排序字符串结束位置,键的位数,辅助存储容器 void MSD_sort(vector<string>& sVec, int lo, int hi, int d, vector<string>& aux) { int i,r; /* * 这里存在一个优化:当数组数量较少的时候,我们可以使用插入排序优化算法 * * 故而下列return语句可以改写为: * if (hi <= low + M) * { * insert_sort(svec, low, hi, d, aux ); * return; * } */ if (hi <= lo) return; /* * R+2 的原因是: * 在charAt函数中,但索引值大于字符串本身的大小时,我们返回了-1,在下面,我么们将会看到, * 我们在所有的返回值上都加了1,然后将它作为count数组的索引,这意味着对于每个字符,都有 * 可能是R+1种分组结果,因为键索引排序的方法本身就需要多一个额外的位置,故而是R+2. * * 返回-1的原因: * 返回-1是为了处理具有相同首字母的字符串中长度稍短的那个字符。举例来说, * 对于三个顺序字符abcfg,abc,abcds来说,abc是三者中最短的,我们很容易知道, * abc应该排在最前面,那么我如何让计算机知道这件事呢。我们是这样处理的,在charAt * 方法中可以看到,对于索引超出长度的字符串,我们返回的是-1.在count中我们对返回值加了1, * 从而让count的索引都是非负整数。这样,我们就将所有字符都被检查过的字符串所在的子数组 * 排在其他子数组的前面,这样就不需要递归的将该子数组排序。 * */ int count[R+2]={0}; //计算频率, 注意此处的 R+2 for (i=lo; i<=hi; i++) count[charAt(sVec[i], d) + 2]++; //频率转化为索引 for (r=0; r<R+1; r++) count[r+1] += count[r]; //分类 for (i=lo; i<=hi; i++) aux[count[charAt(sVec[i], d) + 1]++] = sVec[i]; //回写 for (i=lo; i<=hi; i++) sVec[i] = aux[i-lo];//注意aux下标 //以从左到右的每个字符为键进行排序 for (r=0; r<R; r++)//count[R+1]为0,不对应任何字符 MSD_sort(sVec, lo+count[r], lo+count[r+1]-1, d+1, aux); } int main(int argc, char* argv[]) { string str; vector<string> sVec; ifstream infile("data.txt"); cout<<"------Before sort:"<<endl; while (infile>>str) { cout<<str<<endl; sVec.push_back(str); } int n = sVec.size(); vector<string> aux(n); MSD_sort(sVec, 0, n-1, 0, aux); cout<<"------After sort:"<<endl; for (int i=0; i<n; i++) cout<<sVec[i]<<endl; return 0; }
msd算法的性能及其改进
我们从三个方面衡量算法的性能:需要检查的字符数量,统计字符出现频率时所需的时间和空间,将频率转化为索引所需的时间和空间。
msd算法的性能取决于数据,因为他并非是比较排序,键的顺序并不重要,所需关注的只是键所对应的值:
对于随机输入,msd只检查足以区分字符串的(字符),因为其运行时间是亚线性的。
对于非随机输入,他的运行时间仍然可能是亚线性的。特别是当存在大量等值键的情况下,msd会检查比随机输入更多的键,因而可能耗费更多的时间,其时间接近线性。
最坏情况下,即为所有字符都相同并且所有字符长度都一致,那么其运行时间是线性的。
用msd算法对基于大型字母表的字符串排序时,msd算法可能消耗大量的时间和空间,特别是在有大量重复字符串的情况下。
msd算法可以注意的地方:
- 小数组时可以使用改进后的插入排序。
- 对于含有大量等值键的子数组排序会比较慢。msd的最坏情况就是所有的键均相同。
- 额外空间的占用。为了进行切分,msd算法使用了两个辅助数组,一个是aux, 一个是count。本质上,使用aux只是为了保证稳定性。aux当然可以舍弃,但是这样就不会有稳定性了。
4.参考链接
- https://segmentfault.com/a/1190000012923917
- https://zh.wikipedia.org/wiki/%E5%9F%BA%E6%95%B0%E6%8E%92%E5%BA%8F
- https://zh.wikipedia.org/wiki/%E8%AE%A1%E6%95%B0%E6%8E%92%E5%BA%8F
- https://blog.csdn.net/liujianfeng1984/article/details/48488597
- https://blog.csdn.net/xuelabizp/article/details/50781616
来源:https://www.cnblogs.com/imilano/p/9655754.html