字符串的排序可使用通用排序算法。java
下面这些排序算法比通用排序算法效率更高,它们突破了NlogN的时间下界。由于基数排序不须要直接将元素进行比较和交换,只是对元素进行“分类”。算法
算法 | 是否稳定 | 原地排序 | 运行时间 | 额外空间 | 优点领域 |
低位优先的字符串排序(LSD) | 是 | 否 | O(W (N+R) ) | N | 较短的定长字符串 |
高位优先的字符串排序(MSD) | 是 | 否 | O(W(N+R)) | N+WR | 随机字符串 |
三向字符串快速排序 | 否 | 是 | O(NlogN) | W+logN | 通用排序算法,特别适用于含有较长公共前缀的字符串数组 |
注:字母表的长度为R,待排序的字符串个数为N,字符串平均长度为w,最大长度为W。性能
例如:一个公司有不少个部门,而后须要将员工按照部门排序。spa
使用int数组count[]计算每一个键出现的频率,若是键为r,则count[r+1]++; (注意为何是r+1)..net
使用count[]数组计算每一个键在排序结果中的起始位置。通常来讲,任意给定键的起始索引均为较小键所出现的频率之和,计算方法为count[r+1] += count[r]; 从左到右将count[]数组转化为一张用于排序的索引表。code
将全部元素移动到一个辅助数组aux[]中进行排序。每一个元素在aux[]中对应的位置由它的键对应的count[]决定。在移动以后将count[]中对应的元素值加1,来保证count[r]老是下一个键为r的元素在aux[]中的索引的位置。这个过程只需遍历一次便可产生排序结果,这种实现方法具备稳定性----键相同的元素排序后会被汇集到一块儿,但相对位置没有发生改变(后面两种排序算法就是基于此算法的稳定性来实现的)。blog
将将排序的结果复制回原数组中。排序
时间复杂度:O( N+R )递归
基于键索引记数法来实现。
低位优先的字符串排序可以稳定地将定长字符串进行排序。生活中不少状况须要将定长字符串排序,好比车牌号、身份证号、卡号、学号......
算法思路:低位优先的字符串排序能够经过键索引记数法来实现----从右至左以每一个位置的字符做为键,用键索引记数法将字符串排序W遍(W为字符串的长度)。稍微思考下就能够理解,由于键索引记数法是稳定的,因此该方法可以产生一个有序的数组。
public class LSD { public static void sort(String[]a,int W) { int N = a.length; int R = 256; String[] aux = new String[N]; //循环W次键索引记数法 for(int d = W-1; d>=0;d++) { int[] count = new int[R+1]; //键索引记数法第一步--频率统计 for(int i=0;i<N;i++) count[a[i].charAt(d)+1]++; //键索引记数法第二步--将频率转化为索引 for(int r=0;r<R;r++) count[r+1]+=count[r]; //键索引记数法第三步--排序 for(int i=0;i<N;i++) aux[count[a[i].charAt(d)]++] = a[i]; //键索引记数法第四步--回写 for(int i=0;i<N;i++) a[i]=aux[i]; } } }
从代码能够看出,这是一种线性时间排序算法,不管N有多大,它都只遍历W次数据。
对于基于R个字符的字母表的N个以长为W的字符串为键的元素,低位优先字符串排序的运行时间为NW,使用的额外空间与N+R成正比(大小为R的count数组和大小为N的aux数组)。
时间复杂度:O(W(N+R))
起始:
末位排序:
倒数第二位排序:
......
结束:
本算法也是基于键索引记数法来实现的。
高位优先字符串排序是一种递归算法,它从左到右遍历字符串的字符进行排序。和快速排序同样,高位优先字符串排序算法会将数组切分为可以独立进行排序的子数组进行排序,但它的切分会为每一个首字母获得一个子数组,而非像快速排序那样产生固定的两个或三个数组。
核心思想:先使用键索引记数法根据首字符划分红不一样的子数组,而后用下一个字符做为键递归地处理子数组。
由于是不一样长度的字符串,因此要关注字符串末尾的处理状况。能够将全部字符都已经被检查过的字符串所在的数组排在全部子数组的前面,这样就不须要递归地将该数组排序。
算法实现:引入直接插入排序(处理小数组),当剩余字符串长度小于某个设定的值时,切换到直接插入排序以保证算法性能。实现charAt( String s, int d) 方法实现获取目标字符串的指定位置的字符。每一层递归用键索引记数法切分子数组,而后递归每个子数组实现排序。
public class MSD { private static int R = 256; //字符串中最多可能出现的字符的数量 private static final int M = 15; //当子字符串长度小于M时,用直接插入排序 private static String[] aux; //辅助数组 //实现本身的chatAt()方法 private static int charAt(String s, int d) { if(d<s.length())return s.charAt(d); else return -1; } public static void sort(String[] a) { int N = a.length; aux = new String[N]; sort(a,0,N-1,0); } private static void sort(String[] a,int lo, int hi, int d) { if(hi<=lo+M) { //切换为直接插入排序 Insertion.sort(a,lo,hi,d); return; } int[] count = new int[R+2]; //键索引记数法第一步 for(int i=lo; i<=hi;i++) count[charAt(a[i],d)+2]++; //键索引记数法第二步 for(int r=0;r<R+1;r++) count[r+1]+=count[r]; //键索引记数法第三步 for(int i=lo;i<=hi;i++) aux[count[a[i].charAt(d)+1]++] = a[i]; //键索引记数法第四步 for(int i=lo;i<=hi;i++) a[i]=aux[i-lo]; //递归以每一个字符为键进行排序 for(int r=0;r<R;r++) sort(a,lo+count[r],lo+count[r+1]-1,d+1); } }
时间复杂度:O(W(N+R))
上面的算法很是简洁,但高位优先算法虽然简单但可能很危险:若是使用不当,它可能消耗使人没法忍受的时间和空间。咱们先来讨论任何排序算法都要回答的三个问题:
一、小型子数组
高位优先算法可以快速地将所须要排序的数组切分红较小的数组。但问题是咱们须要处理大量微型数组,并且处理必须快速。小型子数组对高位优先的字符串排序算法的性能相当重要(快速排序和归并排序也是这种状况)。这里能够采用在合适时候切换为直接插入排序来改善。
二、等值键
第二是对于含有大量等值键的子数组排序会变慢。若是相同的子字符串出现过多,切换排序方法条件将不会出现,那么递归方法就会检查全部相同键中的每个字符。另外,键索引记数法没法有效判断字符串中的字符是否所有相同:它不只须要检查每一个字符和移动每一个字符,还须要初始化全部频率统计并将它们转化为索引等。
三、额外空间
高位优先算法使用了两个辅助数组。aux[]的大小为N能够在sort()方法外建立,若是牺牲稳定性,则能够去掉aux[]数组。但count[]所须要的空间才是最须要关注的(由于它没法在sort()外建立,每次循环都要从新计算count[]值)。
该算法思路与高为优先的字符串排序算法几乎相同,只是对高位优先的字符串排序算法作了小小的改进。
算法思想:根据键的首字符进行三向切分,而后递归地选取下一位字符将三个子数组进行排序。
算法实现:三向字符串快速排序实现并不困难,只需对三向快排代码作些修改便可:
/** *a:要排序的字符串数组 *lo, hi:排序范围 *d:按照哪一位的字符排序 */ private static void sort(String[] a, int lo, int hi, int d) { //数组长度小于阈值,切换到直接插入排序 if (hi <= lo + CUTOFF) { insertion(a, lo, hi, d); return; } int lt = lo, gt = hi; int v = charAt(a[lo], d); int i = lo + 1; while (i <= gt) { //从lo下一位开始日后遍历一遍数组 int t = charAt(a[i], d); if (t < v) exch(a, lt++, i++); //小于v的所有放到左边 else if (t > v) exch(a, i, gt--); //大于v的所有放到右边 else i++; } sort(a, lo, lt-1, d); //左侧递归进行本位排序 if (v >= 0) sort(a, lt, gt, d+1); //中间进行下一位排序 sort(a, gt+1, hi, d); //右侧递归进行本位排序 }
三向字符串快排的特色: