描述
关于字符串的排序有很多种方式,像《算法》一书中列举的低位优先、高位优先等,其中最先提到的是键索引计数法,它也是其他排序方式的基础,我们先来了解下。
适用性
关于键索引计数法进行字符串排序,并不是全部都适用,因为它的排序算法核心就是通过统计元素出现频次、构建排序因子的索引边界来递进式完成所有元素排序,它主要适合于以下场景:
- 用于小整数键的算法
比如数组arr ={1,2,3,4,2,3,4,2,1,3,4,2,3,4}
,它里面重复的数字比较多,不重复的只有1,2,3,4,这时就可以用此方法。 - 用于小整数键的分组排序
还可以应用在基于小整数键的分组排序,比如有字母{a,b,c,d,e,f,g,h}
,各字母不重复、但是分组重复,其分组序号格式{字母,分组号}
描述为{a,1}、{b,2}、{c,1}、{d,2}、{e,3}、{f,4}、{g,4}、{h,1}
,最终按照分组号进行排序实现的元素排列为a、c、h、b、d、e、f、g
。
网络异常,图片无法展示|
综上,我们可以看到按照分组号可以将元素归纳如下:
分组号 |
元素 |
1 |
a、c、h |
2 |
b、d |
3 |
e |
4 |
f、g |
步骤
1、频率统计
我们可以看到,每个分组出现频次如下:
分组号 |
元素 |
元素出现频次 |
1 |
a、c、h |
3 |
2 |
b、d |
2 |
3 |
e |
1 |
4 |
f、g |
2 |
统计频次的目的是为了给构建索引提供条件,我们知道所谓键索引计数就是通过索引 + 频次
来完成的。索引可以理解为每个分组的排序边界,每个分组的起始索引即较小一侧边界,而每个分组的结束索引即较大一侧边界,如果索引边界为startIndex
,endIndex
,分组内元素出现频次为t
,那么每个分组中的索引边界和分组内元素出现频次的关系是endIndex = startIndex + t
,每个分组都按照逻辑序号紧紧相邻,较大的分组起始索引与它相邻的第一个较小分组的结束索引一致。
如果将这个场景延展到一个直线上,也可以将索引边界理解为坐标点,将频次理解为步长,或者是坐标点之间的间距。
2、构建索引
当已知了 间距 ,下面根据间距从0开始计算和整理 索引边界 ,可以参考上图,方法很简单,是从0开始递增每个分组的频次即可。
3、数据分类
当建立好索引后,下面开始进行 数据分类 ,说白了这一步就是将元素排序填充的过程,因为已经知道了所有排序分组的起止边界了,也知道每个分组的容量了,接下来就是挨个映射填充就可以完成排序工作了。
分组元素填充后,起始索引递增
这里面的填充技巧的细节点在于,每填充一个分组内元素后,将对应的该分组的起始索引递增1,代表完成了一个分组内地排序,若后续还有同样分组内的元素填充则理所当然填充到该分组的下一个临近位即可。
4、回写数组
这一步是将已排序好的元素重新回写到原数组中即可。
代码实现
- 纯小整数版
/**
* @author: <a href=mailto:keycasiter@qq.com>guanjian</a>
* @date: 2021/4/8 13:15
* @description:
*/
public class IndexCountSortDemo {
public static void main(String[] args) {
int[] nums = {2, 3, 4, 1, 2, 4, 3, 1, 2, 2, 1};
indexCountIndex(nums);
//打印结果 1 1 1 2 2 2 2 3 3 4 4
}
public static void indexCountIndex(int[] nums) {
int[] count = new int[6];
//计算频率
for (int i = 0; i < nums.length; i++) {
count[nums[i] + 1]++;
}
//将频率转化为索引
for (int i = 1; i < count.length; i++) {
count[i] = count[i] + count[i - 1];
}
//数据分类
int[] aux = new int[nums.length];
for (int i = 0; i < nums.length; i++) {
aux[count[nums[i]]++] = nums[i];
//aux[count[nums[i]]] = nums[i];
//count[nums[i]]++;
}
//回写数据(这里是打印)
for (int i = 0; i < nums.length; i++) {
System.out.print(aux[i] + " ");
}
}
}
- 小整数分组排序元素
/**
* @author: <a href=mailto:keycasiter@qq.com>guanjian</a>
* @date: 2021/4/8 13:15
* @description:
*/
public class IndexCountSortDemo {
public static class Item<T> {
private int group;
private T data;
public Item(int group, T data) {
this.group = group;
this.data = data;
}
public int getGroup() {
return group;
}
public void setGroup(int group) {
this.group = group;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
public static void main(String[] args) {
Item<String>[] arr = new Item[]{
new Item(1, "a"),
new Item(2, "b"),
new Item(1, "c"),
new Item(2, "d"),
new Item(3, "e"),
new Item(4, "f"),
new Item(4, "g"),
new Item(1, "h")
};
indexCountIndexByItem(arr);
//打印结果 a c h b d e f g
}
public static void indexCountIndexByItem(Item[] arr) {
int[] count = new int[6];
//计算频率
for (int i = 0; i < arr.length; i++) {
count[arr[i].group + 1]++;
}
//将频率转化为索引
for (int i = 1; i < count.length; i++) {
count[i] = count[i] + count[i - 1];
}
//数据分类
Item[] aux = new Item[arr.length];
for (int i = 0; i < arr.length; i++) {
aux[count[arr[i].group]++] = arr[i];
// aux[count[arr[i].group]] = arr[i];
// count[arr[i].group]++;
}
//回写数据(这里是打印)
for (int i = 0; i < arr.length; i++) {
System.out.print(aux[i].data + " ");
}
}
}
总结
键索引计数法是一种对于小整数键排序非常有效却常常被忽略的排序方法。理解它的工作原理是理解字符串排序的第一步,它是一个线性时间级别的排序方法。
参考
字符串排序:键索引计数法
《算法》第4版