一 键索引计数法
首先针对小数组的排序方法,我们将数组中不同的字符串看做一个键r,对应键有个值r,如果需要按键值排序,那么键索引计数法就十分高效
例如,我们将学生分为若干组,要求按照组号进行排序。此处组好就是对应的键值,我们分一下四个步骤进行排序:
1 频率统计
创建一个int数组count,并计算每个键出现的频率。对于每一个字符串,使用对应的键访问count数组并将其加1。如果键为r,则将count[r + 1]加1。上述例子中统计频率后的count数组如下:
index | 0 | 1 | 2 | 3 | 4 | 5 |
value | 0 | 0 | 3 | 5 | 6 | 6 |
2 将频率转化为索引
对count数组进行叠加:count[r + 1] += count[r] 便可以将频率转化为索引。上述例子中转化为索引后的count数组如下:
index | 0 | 1 | 2 | 3 | 4 | 5 |
value | 0 | 0 | 3 | 8 | 14 | 20 |
3 数据分类
新建一个辅助数组aux,将所有字符串移动到aux中。aux的索引是字符串对应的键的count值决定的,在移动之后将count[r]加1,保证count[r]总是下一个键为r的元素在aux中的索引位置。
4 回写
将aux数组回写到a中
整个过程码如下:
/**
* Created by HP on 2017/5/19.
*/
public class Main {
private static class Data {
K key;
V value;
public Data(K key, V value) {
this.key = key;
this.value = value;
}
public K getKey() {
return key;
}
public V getValue() {
return value;
}
@Override
public String toString() {
return String.valueOf(key);
}
}
public static void indexBasedSort(Data[] a) {
int size = a.length;
int R = 5;
Data[] aux = new Data[size];
int[] count = new int[R + 1];
// 1 频率统计
for (int i = 0; i < size; i++) {
count[a[i].getValue() + 1]++;
}
// 2 将频率转化为索引
for (int i = 0; i < R; i++) {
count[i + 1] += count[i];
}
// 3 数据分类
for (int i = 0; i < size; i++) {
aux[count[a[i].getValue()]++] = a[i];
}
// 4 回写
for (int i = 0; i < size; i++) {
a[i] = aux[i];
}
}
public static void main(String[] args) {
Data[] a = new Data[20];
a[0] = new Data<>("anderson", 2);
a[1] = new Data<>("brown", 3);
a[2] = new Data<>("davis", 3);
a[3] = new Data<>("carcia", 4);
a[4] = new Data<>("harris", 1);
a[5] = new Data<>("jackson", 3);
a[6] = new Data<>("johnson", 4);
a[7] = new Data<>("jones", 3);
a[8] = new Data<>("martin", 1);
a[9] = new Data<>("martinez", 2);
a[10] = new Data<>("miller", 2);
a[11] = new Data<>("moore", 1);
a[12] = new Data<>("robinson", 2);
a[13] = new Data<>("smith", 4);
a[14] = new Data<>("taylor", 3);
a[15] = new Data<>("thomas", 4);
a[16] = new Data<>("thompson", 4);
a[17] = new Data<>("white", 2);
a[18] = new Data<>("williams", 3);
a[19] = new Data<>("wilson", 4);
indexBasedSort(a);
for (int i = 0; i < a.length; i++) {
System.out.println(a[i].getKey() + " " + a[i].getValue());
}
}
}
二 低位优先的字符串排序
该方法从右向左检查字符串中字符并进行排序,适用于字符串长度都相等的排序应用中。如果字符串长度为W,那么就从右向左将每个字符都看成键,用键索引计数法排序。
/**
* Created by HP on 2017/5/19.
*/
public class LSD {
/**
* 低位优先排序
*
* @param a
* @param W
*/
public static void sort(String[] a, int W) {
int size = a.length;
int R = 256;
String[] aux = new String[size];
for (int j = W - 1; j >= 0; j--) {
int[] count = new int[R + 1];
for (int i = 0; i < size; i++) {
count[a[i].charAt(j) + 1]++;
}
for (int i = 0; i < R; i++) {
count[i + 1] += count[i];
}
for (int i = 0; i < size; i++) {
aux[count[a[i].charAt(j)]++] = a[i];
}
System.out.println("j = " + j + ", result:");
for (int i = 0; i < size; i++) {
a[i] = aux[i];
System.out.println(a[i]);
}
}
}
public static void main(String[] args) {
String[] a = new String[13];
a[0] = "4PGC938";
a[1] = "2IYE230";
a[2] = "3CIO720";
a[3] = "1ICK750";
a[4] = "1OHV845";
a[5] = "4JZY524";
a[6] = "1ICK750";
a[7] = "3CIO720";
a[8] = "1OHV845";
a[9] = "1OHV845";
a[10] = "2RLA629";
a[11] = "2RLA629";
a[12] = "3ATW723";
sort(a, 7);
}
}
如果字符串长度都不相同,那么应该从左向右遍历字符排序。
/**
* Created by HP on 2017/5/19.
*
* 高位优先字符串排序
*/
public class MSD {
private static final int R = 256;
private static final int M = 15;
private static String[] aux;
public static int charAt(String a, int index) {
if (index < a.length()) {
return a.charAt(index);
}
return -1;
}
public static void sort(String[] a) {
int length = a.length;
aux = new String[length];
sort(a, 0, length - 1, 0);
}
private static void sort(String[] a, int lo, int hi, int d) {
if (lo > hi) {
return;
}
if (lo + M >= hi) {
insertSort(a, lo, hi);
return;
}
int[] count = new int[R + 2];
for (int i = lo; i <= hi; i++) {
count[charAt(a[i], d) + 2]++;
}
for (int i = 0; i <= R; i++) {
count[i + 1] += count[i];
}
for (int i = lo; i <= hi; i++) {
aux[count[charAt(a[i], d) + 1]++] = a[i];
}
for (int i = lo; i <= hi; i++) {
a[i] = aux[i - lo];
}
for (int i = 0; i < R; i++) {
sort(a, lo + count[i], lo + count[i + 1] - 1, d + 1);
}
}
private static void insertSort(String[] a, int lo, int hi) {
for (int i = lo; i <= hi; i++) {
for (int j = i; j > 0; j--) {
if (a[j].compareTo(a[j - 1]) < 0) {
exange(a, j, j - 1);
}
}
}
}
private static void exange(String[] a, int i, int j) {
String temp = a[i];
a[i] = a[j];
a[j] = temp;
}
public static void main(String[] args) {
String[] a = new String[13];
a[0] = "she";
a[1] = "sells";
a[2] = "seashells";
a[3] = "by";
a[4] = "the";
a[5] = "seashore";
a[6] = "the";
a[7] = "shells";
a[8] = "she";
a[9] = "sells";
a[10] = "are";
a[11] = "surely";
a[12] = "seashells";
MSD.sort(a);
for (String s: a) {
System.out.println(s);
}
}
}
性能分析:
该算法采用递归来实现,性能方面有三个要注意的点
1 小型子数组
假设需要将数百万个字符串(R=256)进行排序而不处理小数组,那么每个字符串最终会产生只含有它自己的子数组,因此你要对数百万个大小为1的子数组进行排序,每次排序都要将大小为R=256 + 2 个元素进行初始化并将其转化为索引,这十分消耗时间。
2 相等的字符串
如果待排序数组中含有大量相等的字符串,那么MSD的性能将会下降,最坏情况就是所有的键都相同,这样会退化到基于低位优先的排序。
3 额外空间
由于MSD采用递归实现,每次递归都要初始化一个count数组,所以count占有的空间才是主要问题。
时间复杂度:
基于大小为R的字母表的N个字符串排序,时间复杂度为N~Nw之间,w是字符串平均长度
空间复杂度:
count数组必须在sort()方法中创建,因此控件需求总量与R和递归深度成正比(再加上辅助数组N)。而递归深度就是最长字符串的长度,故空间复杂度为 RW+N。
四 三向字符串快速排序
三向字符串快速排序可以应对含有较长公共前缀的字符串的情况。例如分析网站日志便会应用此算法。
/**
* Created by HP on 2017/5/22.
*
* 三项切分的字符串排序
*/
public class Quick3String {
public static void sort(String[] a) {
if (a == null || a.length == 0) {
return;
}
sort(a, 0, a.length - 1, 0);
}
private static void sort(String[] a, int lo, int hi, int d) {
if (lo >= hi) {
return;
}
int lt = lo;
int gt = hi;
int i = lo + 1;
int index = charAt(a[lo], d);
while (i <= gt) {
int t = charAt(a[i], d);
if (index < t) {
exange(a, i, gt--);
} else if (index > t) {
exange(a, i++, lt++);
} else {
i++;
}
}
sort(a, lo, lt, d);
if (index > 0) {
sort(a, lt + 1, gt, d + 1);
}
sort(a, gt + 1, hi, d);
}
public static int charAt(String a, int index) {
if (index < a.length()) {
return a.charAt(index);
}
return -1;
}
private static void exange(String[] a, int i, int j) {
String temp = a[i];
a[i] = a[j];
a[j] = temp;
}
public static void main(String[] args) {
String[] a = new String[13];
a[0] = "she";
a[1] = "sells";
a[2] = "seashells";
a[3] = "by";
a[4] = "the";
a[5] = "seashore";
a[6] = "the";
a[7] = "shells";
a[8] = "she";
a[9] = "sells";
a[10] = "are";
a[11] = "surely";
a[12] = "seashells";
Quick3String.sort(a);
for (String s: a) {
System.out.println(s);
}
}
}
五 总结
算法 | 是否稳定 | 原地排序 | 时间复杂度 | 空间复杂度 | 优势领域 |
插入排序 | 是 | 是 | N~N^2 | 1 | 小数组或部分有序数组 |
低位优先排序 | 是 | 否 | NW | N+R | 较短的定长字符串 |
高位优先排序 | 是 | 否 | N~Nw | RW+N | 随机字符串 |
三向字符串排序 | 否 | 是 | N~Nw | logN | 含有较长公共前缀的字符串 |
六 参考资料
算法(第四版)