排序算法总结:
字符串的排序可以使用通用排序算法。
下面这些排序算法比通用排序算法效率更高,它们突破了NlogN的时间下界。因为基数排序不需要直接将元素进行比较和交换,只是对元素进行“分类”。
算法 | 是否稳定 | 原地排序 | 运行时间 | 额外空间 | 优势领域 |
低位优先的字符串排序(LSD) | 是 | 否 | O(W (N+R) ) | N | 较短的定长字符串 |
高位优先的字符串排序(MSD) | 是 | 否 | O(W(N+R)) | N+WR | 随机字符串 |
三向字符串快速排序 | 否 | 是 | O(NlogN) | W+logN | 通用排序算法,特别适用于含有较长公共前缀的字符串 |
注:字母表的长度为R,待排序的字符串个数为N,字符串平均长度为w,最大长度为W。
基础:建索引计数法:
例如:一个公司有很多个部门,然后需要将员工按照部门排序。
第一步:频率统计
使用int数组count[]计算每个键出现的频率,如果键为r,则count[r+1]++; (注意为什么是r+1).
第二步:将频率转化为索引
使用count[]数组计算每个键在排序结果中的起始位置。一般来说,任意给定键的起始索引均为较小键所出现的频率之和,计算方法为count[r+1] += count[r]; 从左到右将count[]数组转化为一张用于排序的索引表。
第三步:排序
将所有元素移动到一个辅助数组aux[]中进行排序。每个元素在aux[]中对应的位置由它的键对应的count[]决定。在移动之后将count[]中对应的元素值加1,来保证count[r]总是下一个键为r的元素在aux[]中的索引的位置。这个过程只需遍历一次即可产生排序结果,这种实现方法具有稳定性----键相同的元素排序后会被聚集到一起,但相对位置没有发生改变(后面两种排序算法就是基于此算法的稳定性来实现的)。
第四步:回写
将将排序的结果复制回原数组中。
时间复杂度:O( N+R )
算法1 低位优先的字符串排序:
基于键索引记数法来实现。
低位优先的字符串排序能够稳定地将定长字符串进行排序。生活中很多情况需要将定长字符串排序,比如车牌号、身份证号、卡号、学号......
算法思路:低位优先的字符串排序可以通过键索引记数法来实现----从右至左以每个位置的字符作为键,用键索引记数法将字符串排序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有多大,它都只遍历W次数据。
对于基于R个字符的字母表的N个以长为W的字符串为键的元素,低位优先字符串排序的运行时间为NW,使用的额外空间与N+R成正比(大小为R的count数组和大小为N的aux数组)。
时间复杂度:O(W(N+R))
Excel模拟LSD:
起始:
末位排序:
倒数第二位排序:
......
结束:
算法2 高位优先的字符串排序:
本算法也是基于键索引记数法来实现的。
高位优先字符串排序是一种递归算法,它从左到右遍历字符串的字符进行排序。和快速排序一样,高位优先字符串排序算法会将数组切分为能够独立进行排序的子数组进行排序,但它的切分会为每个首字母得到一个子数组,而非像快速排序那样产生固定的两个或三个数组。
核心思想:先使用键索引记数法根据首字符划分成不同的子数组,然后用下一个字符作为键递归地处理子数组。
因为是不同长度的字符串,所以要关注字符串末尾的处理情况。可以将所有字符都已经被检查过的字符串所在的数组排在所有子数组的前面,这样就不需要递归地将该数组排序。
算法实现:引入直接插入排序(处理小数组),当剩余字符串长度小于某个设定的值时,切换到直接插入排序以保证算法性能。实现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
时间复杂度:O(W(N+R))
上面的算法非常简洁,但高位优先算法虽然简单但可能很危险:如果使用不当,它可能消耗令人无法忍受的时间和空间。我们先来讨论任何排序算法都要回答的三个问题:
1、小型子数组
高位优先算法能够快速地将所需要排序的数组切分成较小的数组。但问题是我们需要处理大量微型数组,而且处理必须快速。小型子数组对高位优先的字符串排序算法的性能至关重要(快速排序和归并排序也是这种情况)。这里可以采用在合适时候切换为直接插入排序来改善。
2、等值键
第二是对于含有大量等值键的子数组排序会变慢。如果相同的子字符串出现过多,切换排序方法条件将不会出现,那么递归方法就会检查所有相同键中的每一个字符。另外,键索引记数法无法有效判断字符串中的字符是否全部相同:它不仅需要检查每个字符和移动每个字符,还需要初始化所有频率统计并将它们转化为索引等。
3、额外空间
高位优先算法使用了两个辅助数组。aux[]的大小为N可以在sort()方法外创建,如果牺牲稳定性,则可以去掉aux[]数组。但count[]所需要的空间才是最需要关注的(因为它无法在sort()外创建,每次循环都要重新计算count[]值)。
算法3 三向字符串快速排序:
该算法思路与高为优先的字符串排序算法几乎相同,只是对高位优先的字符串排序算法做了小小的改进。
算法思想:根据键的首字符进行三向切分,然后递归地选取下一位字符将三个子数组进行排序。
算法实现:三向字符串快速排序实现并不困难,只需对三向快排代码做些修改即可:
/**
*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); //右侧递归进行本位排序
}
图解三向字符串快排:
三向字符串快排的特点:
- 高位优先字符串算法可能会创建许多的空数组(前缀相同的情况下),但本算法总是只有三个
- 本算法不需要额外的空间
- 三向字符串快速排序是不稳定排序
- 三向字符串快速排序时间复杂度为O(N)~O(NlogN)
- 三向字符串快速排序特别适合大量重复键的字符排序,如域名