Radix Sorts
基数排序
Strings In Java
Char Data Type
C 语言中的字符数据类型占一个字节(8 比特),最多只能表示 256 个字符。支持 7 位的标准 ASCII(American Standard Code for Information Interchange,美国标准信息交换编码),最高位用于奇偶校验。或是拓展的 ASCII,最高位用来确定附加的 128 个特殊的字符。
标准 ASCII 的十六进制转换表

Java 中的字符数据类型占两个字节,支持 16 位的 Unicode 编码。
String Data Type
字符串就是一些字符组成的序列,不是原生的数据类型,在 Java 中是不可变(immutable)的对象,实现了一些方便的方法:

表示字符串的字符数组是不可变的,获取的子字符串其实也保存在这个字符数组里,所以在常数时间内就能完成。但是连接两个字符串的时候,就要新建字符串对象,需要正比于字符串长度的时间。
StringBuilder Data Type
StringBuilder 的字符序列是可变的(mutable),内部实现用的是可变长的字符数组,所以连接字符串的操作几乎能在常数时间完成。但是相应的,获取子字符串的操作需要正比于字符串长度的时间。
所以在实际的使用中,我们要根据不同的需求,选择合适的对象来表示字符串。
Key-indexed counting
基于比较的排序至少需要 \(NlgN\) 次的比较,先介绍一种不是基于比较的,适用于排序的键是小整数的情况,它也是下面其它字符串排序算法的基础,称作:键索引计数法。
设想有个数组 a = {d, a, c, f, f, b, d, b, f, b, e, a} 要排序,知道总共有 6 个不同的字母,要先统计它们出现的频数。
int R = 6;
int N = a.length;
int[] count = new int[R + 1];
for (int i = 0; i < N; i++)
    count[a[i] - 'a' + 1]++;
字母 a 的键为 0 (a[i] - 'a'),出现的次数在 count 数组中的索引为键值加一。

遍历一遍 count 数组:
for (int i = 0; i < R; i++)
    count[i + 1] += count[i];
现在 count 数组中保存的即对应字母在排好序的数组中开始的索引值,像两个 d 应该放在 a[6] 和 a[7]。
char[] aux = new char[N];
for (int i = 0; i < N; i++)
    aux[count[a[i] - 'a']++] = a[i];
辅助数组 aux 借助 count 数组找到了每个 a[i] 的位置。注意 count 数组在这一步中还会改变,每次要加一,下次相同的 a[i] 就会放在下一个位置。所以这个算法也是稳定的(stable),相同元素间的相对顺序不会改变。

最后把 aux 数组一个个赋值回原数组,即完成了排序。
键索引计数法排序只需要几个一重循环,不需要比较,只要 R 在 N 的一个常数因子范围内,它就是一个线性时间级别的排序方法。
LSD Radix Sort
低位优先(Least Significant digit first)的基数排序,可以对等长的字符串进行排序。这样的应用挺常见,像电话号码,车牌号等等。你需要做的是从右到左,分别对每个位置的字符进行基数排序,像下面的例图那样:

因为基数排序是稳定的,相同键之间的相对顺序不会改变,所以第 i + 1 次排完之后,i + 1 位有相同键的字符串的前面 i 位间的相对顺序是不会变的,仍然有序。
LSD: Java Implementation
public class LSD {
    // fixed-length W strings
    public static void sort(String[] a, int W) {
        int R = 256;    // radix R
        int N = a.length;
        String[] aux = new String[N];
        // do key-indexed couting
        // for each digit from right to left
        for (int d = W - 1; d >= 0; d--) {
            int[] count = new int[R + 1];
            // key-indexed counting
            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];
        }
    }
}
对于典型的应用,R(基数)远小于 N(总数),对定长(W)的字符串排序的时间是 \(MN\) 级别。
MSD Radix Sort
高位优先(Most Significant Digit First)的基数排序,能对长度不同的字符串进行排序。对最高位的字符使用基数排序,然后再递归地对子字符串们使用基数排序:

这些字符串可以不是定长的,我们要约定一下的字符串的末尾。这里用私有方法 charAt() 在字符串的末尾加个 -1,这样下一轮的基数排序中它也不会改变位置。C 语言中字符串以 ‘\0’ 结尾,需要注意对代码做些调整。
private static int charAt(String s, int d) {
    if (d < s.length) return s.charAt(d);
    else return -1;
}
MSD: Java Implementation
于是相当于现在多了个键 -1,所以 conut 数组的大小为 R + 2。
public static void sort(String[] a) {
    aux = new String[a.length];
    sort(a, aux, 0, a.length - 1, 0);
}
private static void sort(String[] a, String[] aux, int lo, int hi, int d) {
    if (hi <= lo) 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 <= h; i++)
        aux[count[charAt(a[i], d) + 1]++] = a[i];
    for (int i = lo; i <= hi; i++)
        a[i] = aux[i -lo];
    // sort R subarrays recursively
    for (int r = 0; r < R; r++)
        sort(a, aux, lo + count[r], lo + count[r + 1] - 1, d + 1);
}
用于回写的辅助数组 aux 可以重复使用,但是每次基数排序都需要新的 count[R + 2],不仅需要空间,而且默认的初始化也需要时间。于是,类似的,对于小型子数组,我们使用插入排序来改进算法。
public static void sort(String[] a, int lo, int hi, int d) {
    for (int i = lo; i <= hi; i++)
        for (int j = i; j > lo && less(a[j], a[j - 1], d); j--)
            exch(a, j, j -1);
}
private static boolean less(String v, String w, int d) {
    return v.substring(d).compareTo(w.substring(d)) < 0;
}
MSD 算法的性能取决于输入的数据,最坏的情况的下需要检查所有的字符,和 LSD 一样是线性时间级别。

3-way Radix Quicksort
三向字符串快速排序算法结合了 MSD 和快排,改进了快速排序处理字符串时的性能。当两个字符串有很长的相同前缀时,不需要重复扫描很多字符,也不像 MSD 因为辅助数组而需要很多额外空间。
具体来说,算法每次根据第一个字符串的首字母将数组三向切分成小于,等于和大于首字母的三个子数组,然后再递归地对这些子数组三向切分。其中,首字母相等的子数组用第一个字符串的第二个字母来三向切分,因为首字母都一样,同时也避免了原来快排的重复扫描。例图:

3-way String Quicksort: Java Implementation
private 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;
    int v = charAt(a[lo], d);    // 约定字符串末尾返回 -1
    int i = lo + 1;
    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++;
    }
    sort(a, lo, lt - 1, d);
    if (v >=0) sort(a, lt, gt, d + 1);    // 用第二个字母三向切分
    sort(a, gt + 1, hi, d);
}
Suffix Arrays
后缀数组包括字符串的所有后缀,有很多应用,比如说可以用于关键字查找,最长重复子字符串等。
因为 Java 中子字符串共享原来的字符数组,所以构建输入字符串的后缀数组只需要线性级别的时间和空间。
public static String[] suffixes(String s) {
    int N = s.length();
    String[] suffixes = new String[N];
    for (int i = 0; i < N; i++)
        suffixes[i] = s.substring(i, N);
    return suffixes;
}
对后缀数组进行排序,就能把相同的字符串安排在一起,查找关键字也就快了很多。

最长重复子字符串也差不多。

LRS: Java Implementation
public String lrs(String s) {
    int N = s.length();
    String[] suffixes = new String[N];
    for (int i = 0; i < N; i++)
        suffixes[i] =s.substring(i, N);
    Arrays.sort(suffixes);
    tring lrs = "";
    for (int i = 0; i < N - 1; i++) {
        // compute longest common prefix
        // between adjacent suffixes insorted order
        int len = lcp(suffixes[i], suffixes[i + 1]);
        if (len > lrs.length())
           lrs = suffixes[i].substring(0, len);
    }
    return lrs;
}
不过,最坏情况下,它可能达到平方级别的复杂度。

输入的字符串重复时,需要进行很多次比较才能完成排序。于是乎我们来了解一下 Manber-Myer MSD 算法。

它和普通的 MSD 从左到右一位位排序不同,每次排好的位数是倍增的,像上面排好了前四位,下一轮就能按前八位排好后缀数组。大概的原理,举例来说看字符串 0 和字符串 9,它们有相同的前四位,前者的后四位和字符串 4 的前四位一样,后者的后四位和字符串 13 的前四位一样。而按前四位排序,字符串 13 在字符串 4前面,所以下一次按八位排,字符串 0 应该排在字符串 9 的前面。

大概是这么个过程吧,了解一下,具体实现的细节没提。
Radix Sorts的更多相关文章
- 基数排序(radix sort)
		#include<iostream> #include<ctime> #include <stdio.h> #include<cstring> #inc ... 
- [Algorithms] Radix Sort
		Radix sort is another linear time sorting algorithm. It sorts (using another sorting subroutine) the ... 
- Java字节数组转按radix进制输出
		代码如下: public class Main_bytesToStr { public static void main(String[] args) throws IOException { // ... 
- 1010. Radix (25)(未完成)
		Given a pair of positive integers, for example, 6 and 110, can this equation 6 = 110 be true? The an ... 
- Trie / Radix Tree / Suffix Tree
		Trie (字典树) "A", "to", "tea", "ted", "ten", "i ... 
- Uva 110 - Meta-Loopless Sorts(!循环,回溯!)
		题目来源:https://uva.onlinejudge.org/index.php?option=com_onlinejudge&Itemid=8&category=3&pa ... 
- PAT 解题报告 1010. Radix (25)
		1010. Radix (25) Given a pair of positive integers, for example, 6 and 110, can this equation 6 = 11 ... 
- Uva110 Meta-Loopless Sorts
		Meta-Loopless Sorts Background Sorting holds an important place in computer science. Analyzing and i ... 
- 基数树(radix tree)
		原文 基数(radix)树 Linux基数树(radix tree)是将指针与long整数键值相关联的机制,它存储有效率,并且可快速查询,用于指针与整数值的映射(如:IDR机制).内存管理等.ID ... 
随机推荐
- %notfound的理解——oracle存储过程 .
			文档中的解释:It returns TRUE if an INSERT, UPDATE, or DELETE statement affected no rows, or a SELECT INTO ... 
- iOS 性能优化套路
			*** 一级套路 *** 使用ARC管理内存- 防止内存泄露- 保证释放掉不再需要的内存,提高性能 在正确的地方使用 reuseIdentifier平时接触的需要考虑重用的视图有UICollect ... 
- [转]C# 6.0 的新特性
			本文的内容包括引入C#6.0中的新的语言特性有哪些. 还有已经被引入的代码名称为 “Roslyn”新编译器. 编译器是开放源码的,并且可以从 codeplex 网站的这个地址下载到源代码: https ... 
- js 千分位符号 正则方法
			function toThousands(num) { return (num || 0).toString().replace(/(\d)(?=(?:\d{3})+$)/g, '$1,');} 
- 04-Tomcat体系结构与插件配置
			一.发布程序详解 Context docBase:web应用的文件路径 path:URL入口 reloadable:字节码变化服务器是否重新加载web应用 二.tomcat服务器体系结构 1.Serv ... 
- js延迟加载优化页面响应速度
			网页打开速度是衡量网站性能的一个极为重要的指标,今天就来说说如何通过JS延迟加载的方式提高页面响应速度: JS延迟加载的 含义:即等页面加载完成之后再加载 JavaScript 文件.作用:JS延迟加 ... 
- Excel Metadata
			using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; usin ... 
- 洛谷P5057 [CQOI2006]简单题(线段树)
			题意 题目链接 Sol 紫色的线段树板子题??... #include<iostream> #include<cstdio> #include<cmath> usi ... 
- 爬虫必备—requests
			Requests 是使用 Apache2 Licensed 许可证的 基于Python开发的HTTP 库,其在Python内置模块的基础上进行了高度的封装,从而使得Pythoner进行网络请求时,变得 ... 
- slice()方法 和splice 方法的区别
			定义 splice() 方法 用于插入.删除或替换数组的元素. slice() 方法 可提取字符串的某个部分,并以新的字符串返回被提取的部分. 更多的可查看: http://www.cnblogs.c ... 
