对于许多应用,决定顺序的键都是字符串。本篇讲述如何利用字符串的特殊性质来对其进行高效的排序。
第一类方法会从右到左检查键中的字符。这种方法一般被称为低位优先(Least-Significant-DigitFirst
,LSD
)的字符串排序。如果将一个字符串看做一个256进制的数字,那么从右向左检查字符串就等价于先检查数字的最低位。这种方法最适合用于键的长度都相同的字符串排序应用。
第二类方法会从左到右检查键中的字符,首先查看的是最高位的字符。这种方法通常称为高位优先(MSD
)的字符串排序。高位优先的字符串排序和快速排序类似,因为它们都会将需要排序的数组切分为独立的部分并递归地用相同的方法处理子数组来完成排序。它们的区别之处在于高位优先的字符串排序算法在切分时仅使用键的第一个字符,而快速排序的比较则会涉及键的全部。
第三种方法是高位优先的字符串排序算法的改进快速排序
,根据键的首字母进行三向切分,仅在中间子数组中的下一个字符(因为键的首字母都与切分字符相等)继续递归排序。
作为热身,我们先学习一种适用于小整数键的简单排序方法。这种叫做键索引计数
的方法本身就很实用,同时也是要学习的三种排序算法中前两种的基础。它其实就桶计数。
现在来情景引入,老师在统计学生的分数时可能会遇到以下数据处理问题。学生被分为若干组,标号为1、2、3、4等。在某些情况下,我们希望将全班同学按组分类。因为组的编号是较小的整数,使用键索引计数法来排序时很合适的。假设数组a[]
中的每个元素都保存了一个名字和一个组号,其中组号在0到R-1
之间,代码a[i].key()
会返回指定学生的组号。四个步骤见代码
int N = a.length;
int R = 256; //R为字符基数
String[] aux = new String[N];
int[] count = new int[R + 1];
//计算出现频率
for (int i = 0; i < N; i++)
count[a[i].key() + 1]++;
//将频率转换为索引
for (int r = 0; r < R; r++)
count[r + 1] += count[r];
//将元素分类
for (int i = 0; i < N; i++)
aux[count[a[i].key()]++] = a[i];
//回写
for (int i = 0; i < N; i++)
a[i] = aux[i];
命题A
:键索引计数法排序N
个键为0
到R-1
之间的整数的元素需要访问数组11N+4R+1
次
如果字符串的长度均为W
,那就从右向左以每个位置的字符作为键,用键索引计数法将字符串排序W
遍。
命题B
:低位优先的字符串排序算法能够稳定地将定长字符串排序
class LSD{
// Least-Significant-Digit First
//低位优先的字符串排序(基数排序)
public static void sort(String[] a, int W) {
//通过前W个字符将a[]排序
int N = a.length;
int R = 256; //基数
String[] aux = new String[N]; //辅助数组\
for(int d = W - 1; d >= 0; d--) {
//根据第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];
}
}
}
在许多字符串排序的应用中,键的长度可能互不相同。改进后的低位优先的字符串排序是可以适应这些情况的。下来讲解两种处理变长键排序的算法
首先用键索引计数法将所有字符串按照首字母排序,然后(递归地)再将每个首字母所对应的子数组排序(忽略首字母,因为每一类中的所有首字母都是相同的)。和快速排序一样,高位优先的字符串排序会将数组切分为能够独立排序的子数组来完成排序任务,但它的切分会为每个首字母得到一个子数组,而不是像快速排序中那样产生固定的两个或者三个切分。
在高位优先的字符串排序算法中,要特别注意到达字符串末尾的情况。在排序中,合理的做法是将所有字符都已被检查过的字符串所在的子数组排在所有子数组的前面,这样就不需要递归地将该子数组排序。为了简化这两步计算,我们使用了一个接受两个参数的私有方法charAt()
来将字符串中字符索引转化为数组索引,当指定的位置超过了字符串末尾时该方法返回-1
,。然后将所有返回值加1,得到一个非负的int值并用它作为count[]
的索引。这种转换意味着字符串中的每个字符都可能产生R+1
种不同的值:0表示字符串的结尾,1表示字符串的第一个字符,2表示字符串的第二个字符,等等。因为建索引计数法本来就需要一个额外的位置,所以使用代码int count[] = new int[R + 2]
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 sortInsert(String[] a, int lo, int hi) {
//小型数组进行插入排序
for (int i = lo + 1; i <= hi; i++) {
for(int j = i; j > lo && a[j].compareTo(a[j - 1]) < 0; j--) {
String tmp = a[j];
a[j] = a[j - 1];
a[j - 1] = tmp;
}
}
}
private static void sort(String[] a, int lo, int hi, int d) {
//以第d个字符为键将a[lo]至a[hi]排序
if(hi <= lo + M) {
sortInsert(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 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);
}
}
}
我们也可以根据高位优先的字符串排序算法改进快速排序,根据键的首字母进行三向切分,仅在中间子数组的下一个字符(因为键得出首字母都与切分字母相同)继续递归排序。这个算法的实现并不困难,参考往期排序算法中的三向切分快排即可。
尽管排序的方式有所不同,但三向字符串快速排序根据的仍然是键的首字母并使用递归的方法将其余部分排序。对于字符串的排序,这个方法比普通的快速排序和高位优先的字符串排序更友好。实际上,它就是两种算法的结合。
三向字符串快速排序只将数组切分为三部分,因此当相应的高位优先的字符串排序产生的非空切分较多时,它需要移动的数据量就会变大,因此它需要进行一系列的三向切分才能够取得多向切分的效果。但是,高位优先的字符串排序可能会创建大量(空)子数组,而三向字符串快速排序的切分总是只有三个。因此三向字符串快速排序能够很好地处理等值键、有较长公共前缀的键、取值范围较小的键和小数组
-----所有高位优先的字符串排序算法不擅长的各种情况。
class Quick3string{
//三向字符串快速排序
private static int charAt(String s, int d) {
if(d < s.length()) {
return s.charAt(d);
}
return -1;
}
public static void sort(String[] a) {
sort(a, 0, a.length - 1, 0);
}
private static void sort(String[] a, int lo, int hi, int d) {
if(hi <= lo) {
return;
}
int lt = lo, gt = hi, i = lo + 1;
int v = charAt(a[lo], d);
while(i <= gt) {
int t = charAt(a[i], d);
if(t < v) {
exch(a, lt++, i++);
}else if(t > v) {
exch(a, i, gt--);
}else {
i++;
}
}
//a[lo..lt-1] < v = a[lt..gt] < a[gt+1..hi]
sort(a, lo, lt - 1, d);
if(v >= 0) {
sort(a, lt, gt, d + 1);
}
sort(a, gt + 1, hi, d);
}
private static void exch(String[] a, int i, int j) {
String t = new String(a[i]);
a[i] = a[j];
a[j] = t;
}
}
在将字符串数组a[]
排序时,根据它们的首字母进行三向切分,然后(递归地)将得到的三个子数组排序:一个含有所以首字母小于切分字符的字符串子数组,一个含有所以首字母等于切分字符串的子数组(排序时忽略它们的首字母),一个含有所有首字母大于切分字符的字符串的子数组。
参考资料:《算法》第四版
文章浏览阅读3.3k次,点赞2次,收藏8次。最近做压测中发现一个应用中cpu过高,导致接口超时rt情况有些不大稳定,jstack打印线程一直在parallelStream相关的代码出进行计算。故对parallelStream相关做一下研究,找一下优化方法。java8并行流parallelStream,相信很多人都喜欢用,特别方便简单。但是有多少人真正知道里面采用的共享线程池对密集型任务,高并发下的性能影响呢可能你的一个应用里面..._jdk8 parallelstream 性能
文章浏览阅读292次。https://www.cnblogs.com/DswCnblog/p/5628195.htmlunique_ptr 不共享它的指针。它无法复制到其他 unique_ptr,无法通过值传递到函数,也无法用于需要副本的任何标准模板库 (STL) 算法。只能移动unique_ptr。这意味着,内存资源所有权将转移到另一 unique_ptr,并且原始 unique_ptr 不再拥有此资源。我们建议..._unique_ptr创建空节点
文章浏览阅读853次。1:新增类:RouteConvention,继承自IApplicationModelConvention/// <summary> /// 全局路由前缀配置 /// </summary> public class RouteConvention : IApplicationModelConvention { /// <summary> /// 定义一个路由前缀变量 /// </su_selector.attributeroutemodel.template
文章浏览阅读64次。从woody那里copy一段最简的fib代码[code="ruby"]x,y = 0,1 Array.new(10) {|i| [0,1].include?(i) ? 1 : (x,y = y,x+y)&&(x+y) } [/code]生成了这么多,太多了,中途终止了,不知道多少条。[code="ruby"] 1, 1, 2, ..._359579325206583560961765665172189099052367214309267232255589801
文章浏览阅读280次。本文原题“从实践角度重新理解BIO和NIO”,原文由Object分享,为了更好的内容表现力,收录时有改动。1、引言这段时间自己在看一些Java中BIO和NIO之类的东西,也看了很多博客,发现各种关于NIO的理论概念说的天花乱坠头头是道,可以说是非常的完整,但是整个看下来之后,发现自己对NIO还是一知半解、一脸蒙逼的状态(请原谅我太笨)。基于以上原因,..._java bio粘包处理
文章浏览阅读9k次,点赞2次,收藏12次。Robot Framework是一个基于Python的,可扩展的关键字驱动的测试自动化框架,用于端到端验收测试和验收测试驱动开发(ATDD)。_python-3.9.9-amd64用那个版本ride
文章浏览阅读2.4k次。1.进入shellhbase(main):003:0>hbase shell2.查看所有表hbase(main):003:0> list3.根据rowKey查询某个记录hbase(main):003:0>get '表名','rowKey'4.常用过滤器过滤方式是通过value过滤,匹配出value含7259的数据。scan 'buss_surface', FILTER=>"ValueFilter(=,'substring:7259')"过滤方式是通_hbase 查询
文章浏览阅读2k次,点赞4次,收藏16次。Practical Poissonian-Gaussian noise modeling and fitting for single-image raw-data文章目录Practical Poissonian-Gaussian noise modeling and fitting for single-image raw-dataPoissonian-Gaussian ModelingThe Noise Profile AlgorithmWavelet domain analysisSegmentat_practical poissonian-gaussian noise modeling and fitting for single-image ra
文章浏览阅读4k次。由于win7电脑使用时间过长或者存放时间久了,难免会出现硬件各方面的老化或者堆积了大量的垃圾,因此就会导致电脑开机时的速度有所降低,对此有些用户就想,在不更换硬件的条件下,有没有方法能够提高一下开机速度,那么win7电脑提高开机速度如何操作呢?这里小编就来告诉大家win7电脑开机更快操作步骤。具体方法:1、在任意界面按下:windows键+R,然后在框内输入msconfig,点确定2、然后选择“启..._如何提高w7系统的开机速度
文章浏览阅读672次。今天分享的是1688平台API,item_search - 按关键字搜索商品接口1688的API开发接口,我们需要做下面几件事情。1)开放平台注册开发者账号;2)然后为每个1688应用注册一个应用程序键(App Key) ;3)下载1688API的SDK并掌握基本的API基础知识和调用;4)利用SDK接口和对象,传入AppKey或者必要的时候获取并传入SessionKey来进行程序开发;5)利用1688平台的文档中心和API测试工具,对接口进行测试。从而了解返回信息,方便程序获取1688_1688 一件代发 api
文章浏览阅读3.1k次,点赞2次,收藏12次。在Vue中使用TypeScript时,非常好用的一个库,使用装饰器来简化书写。一、安装npmi-Svue-property-decorator@Prop @PropSync @Provide @Model @Watch @Inject @Provide @Emit @Component(provided byvue-class-component) Mixins(the helper function namedmixinsprovided byvue-cla..._vue-property-decorator emit update
文章浏览阅读467次。本示例演示如何用Web图表控件 ChartDirector 绘制一个配置有刷新率的实时图表。在本例中,由一个计时器驱动的随机数生成器生成新的数据值,新产生的值会转换到数据数组中,然后显示在图表上。图表由一个秒表进行更新,这样图表的刷新率可独立于数据率。此外,这个图表支持暂停以方便用户查看,但是后台的数据仍然在继续更新。图表刷新计时器调用CChartViewer.update..._c++ chartdirect updateviewport