Overview:
不同于对整数排序,字符串的排序的特点在于,两个字符串比较时需要对字符串中的每个字符进行比较。
如果使用一般的排序方法,每次进行比较的时候都要对两个字符串的字符进行比较。例如用冒泡。但是可能在前边的排序过程中,这两个字符串已经比较过了。
为了规避这种重复的字符比较,我们需要针对字符串来开发排序方法。
键索引计数法(不基于比较)
概述:设数据类型为“字符串:键”,通过键来对数据进行分类。
以此为基础,给字符创排序的时候,可以以字符串中的每个字符为键.
分类过程:1,计算每个键的出现频率。 2,根据计算的频率结计算出每个键的类在最终排序数组中的开始索引位置。
3,将元素按照计算好的索引位置(让开始索引+元素的偏移量即可)存入排序数组中。
public void sort(){
int[] count = new int[R+2];
Element[] aux = new Element[len];
//计算频率
for(int i=0;i<len;i++)
count[eles[i].key+1]++;
//将频率转化为索引
for(int i=1;i<R;i++)
count[i+1] += count[i];
//将元素分类
for(int i=0;i<len;i++)
aux[count[eles[i].key]++] = eles[i];
//写回
eles = aux;
}
class Element{
String val;
int key;
public Element(String val,int key){
this.val = val;
this.key = key;
}
}
低位优先的字符串排序
概述:就是如果字符创的长度为W,那从左向右以每个位置的字符为键,用键索引计数法将字符串排序W遍。
排序的字符串要等长,不等长要特殊处理。
是一种适用于一般应用的线性时间排序算法。无论N有多大,它都只遍历W次数据。
public clss LSD{
public static void sort(String[] a,int W){
int N = a.length;
int R = 256;
String[] aux = new String[N];
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];
}
}
}
高位优先的字符串排序
概述:先以首字母键索引排序,然后在递归的将每个首字母多对应的子数组排序。可以对小型子数组采用插入排序以提升效率。
排序的字符串可以不等长。如果等长,其实可以按照低位优先的方式用迭代来实现也是可以的。
1:高位优先的字符串排序的成本与字母表中的字符量有很大关系。
2:最坏情况是多有的字符串都相同,这样就需要检查每一个字符。
3:因为如果可以在前几个字符就将一个字符串区别开来,那么后边的字符就不需要检查。所以如果字符串有相当长的共同前缀,那么这些前缀的字符都要一一检查。这就是性能瓶颈所在。
//高位优先的字符串拍序
public class MSD {
private static int R =256;
private static final int M = 15;
private static String[] aux;
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) return;
//count[1]用来保存已经到末尾的字符串数量
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[charAt(a[i], 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);
}
}
三向字符快速排序
1:由于高位优先的字符串排序在有较长的相同前缀或者有大量相同的字符串时的时效率不好,会产生大量的额外空间用于计算频率。
2:所以采用三向字符快速排序。因为只将数组切分为3个部分(高位优先相当于切分为多个子数组)。不会浪费大量空间。而且这种方法能够适应键的不同部分的不同结构。
3:就是根据键的首字母进行切分,得到三个切分数组,大于等于小于。然后再递归对这三个数组排序。
4:可以对小型子数组进行特殊处理来提高效率。
5:性能不直接取决于字母表的大小。因为不需要计算频率,转换索引。
comment: 1:说白了,就是三向切分不会在排序过程中使用大量辅助空间用于计算频率,减少了数组访问。提高了效率。但是注意这种切基于比较,高位优先可没有比较的成本
2:另一点。如果利用一般不是针对字符串的排序算法如标准快速排序来对字符串排序,其实相当于使用了高位优先排序。因为String.compareTo()的比较就是从首字依次开始比较的。
但是这样每次两个字符串的比较都要从首字母开始,浪费了前边比较的结果。但是由于三向切分从下一个字母开始切分,所以克服了这个缺点。
//三向切分排序
public class ThreeParti {
public static void sort(String[] strs){
if(strs.length < 2) return;
sort(strs,0,strs.length-1,0);
}
//取得该字符在字母表中的索引值
private static int charAt(String string, int d) {
if(d < string.length())
return string.charAt(d)-97;
else return -1;
}
private static void swap(String[] strs, int i, int j) {
String tem = strs[i];
strs[i] = strs[j];
strs[j] = tem;
}
//排序,不需要辅助数组。
private static void sort(String[] strs, int lo, int hi, int d) {
if(lo >= hi) return;
int gt = hi - 1;
int i = lo+1;
int lt = lo;
int v = charAt(strs[lo],d);//用于切分的元素
//进行切分,切分为三个子数组
//不需要计算频率。但是要进行比较。
while(i <= gt){
if(charAt(strs[i], d) < v) swap(strs,lt++,i++);
else if(charAt(strs[i], d)>=v) swap(strs,i,gt--);
else i++;
}
sort(strs,lo,lt-1,d);//对小于的数组排序
//对等于的数组排序,切分元素为下一个
if(v>=0) sort(strs,lt,gt,d+1);
sort(strs,gt+1,hi,d);//对大于的数组排序
}
public static void main(String[] args) {
String[] strs = new String[]{"she", "sells" ,"seashells", "by" ,"the",
"seashore","the","shells","she","sells","are","surely","seashells"};
sort(strs);
for(String e: strs)
System.out.println(e);
}
}