前缀树及计数排序、基数排序【十大经典排序】
1 前缀树(prefix tree/trie)
- 单个字符串中,字符从前到后的加到一棵多叉树上
- 字符放在路上,节点上有专属的数据项(常见的就是pass和end值)
- 所有样本都这样添加,如果没有路就新建,如果有路就复用
- 沿途节点的pass值增加1,每个字符串结束时来到的节点end值增加1
1.1 前缀树设计思路
例子
设计一种结构,用户可以:
- void insert(String str) 添加某个字符串,可以重复添加,每次算1个
- int search(String str) 查询某个字符串在结构中还有几个
- void delete(String str) 删除某个字符串,可以重复删除,每次算1个
- int prefixNumber(String str) 查询有多少个字符串,是以str作前缀的
前缀树的实现方式:
1)固定数组实现
2)哈希表实现
1.2 代码实现
1.2.1 前缀树节点Node(组成元素)
//前缀树节点
public static class Node1{
//经过次数
private int pass;
//结尾次数
private int end;
//横向数组【路径选择】
private Node1[] nexts;
public Node1(){
this.pass = 0;
this.end = 0;
//字符串,一共26种字母组成【26种路径】
nexts = new Node1[26];
}
}
1.2.2 添加元素insert(String word)
//添加字符串
public void insert(String word){
if(word == null){
return;
}
char[] chs = word.toCharArray();
Node1 node = root;
//添加元素,经过root的pass++
node.pass++;
int path = 0;
for(int i = 0; i < chs.length; i++){
//找到是哪条路径
path = chs[i] - 'a';
//该路径上没有字母,新建【有:pass++】
if(node.nexts[path] == null){
node.nexts[path] = new Node1();
}
//继续向下走
node = node.nexts[path];
node.pass++;
}
node.end++;
}
1.2.3 删除单词/路径 delete(String word)
//删除前缀树的单词【删除一条路径】
public void delete(String word){
//先判断该单词是否存在于树中
if(search(word) != 0){
char[] chs = word.toCharArray();
Node1 node = root;
//路径从根开始
node.pass--;
int path = 0;
for(int i = 0; i < chs.length; i++){
path = chs[i] - 'a';
//先--,如果pass为0,就将其设为null
if(--node.nexts[path].pass == 0){
node.nexts[path] = null;
return;
}
node = node.nexts[path];
}
node.end--;
}
}
1.2.4 搜寻某个字符串出现次数/几条路径 search(String word)
//搜寻某个单词在树中出现了几次
public int search(String word){
if(word == null){
return 0;
}
char[] chs = word.toCharArray();
//从根节点开始向下找
Node1 node = root;
int index = 0;
for(int i = 0; i < chs.length; i++){
//走哪条路径
index = chs[i] - 'a';
if(node.nexts[index] == null){
return 0;
}
node = node.nexts[index];
}
return node.end;
}
1.2.5 以pre为前缀的单词个数prefixNumber(String pre)
//所有加入的字符串中,有多少以pre作为前缀
public int prefixNumber(String pre){
if(pre == null){
return 0;
}
char[] chs = pre.toCharArray();
Node1 node = root;
int index = 0;
for(int i = 0; i < chs.length; i++){
index = chs[i] - 'a';
//该路径下面为null,直接返回0
if(node.nexts[index] == null){
return 0;
}
node = node.nexts[index];
}
return node.pass;
}
1.2.6 全部代码
//前缀树节点
public static class Node1{
//经过次数
private int pass;
//结尾次数
private int end;
//横向数组【路径选择】
private Node1[] nexts;
public Node1(){
this.pass = 0;
this.end = 0;
//字符串,一共26种字母组成【26种路径】
nexts = new Node1[26];
}
}
public static class Trie1{
//根节点
private Node1 root;
public Trie1(){
root = new Node1();
}
//添加字符串
public void insert(String word){
if(word == null){
return;
}
char[] chs = word.toCharArray();
Node1 node = root;
//添加元素,经过root的pass++
node.pass++;
int path = 0;
for(int i = 0; i < chs.length; i++){
//找到是哪条路径
path = chs[i] - 'a';
//该路径上没有字母,新建【有:pass++】
if(node.nexts[path] == null){
node.nexts[path] = new Node1();
}
//继续向下走
node = node.nexts[path];
node.pass++;
}
node.end++;
}
//删除前缀树的单词【删除一条路径】
public void delete(String word){
//先判断该单词是否存在于树中
if(search(word) != 0){
char[] chs = word.toCharArray();
Node1 node = root;
//路径从根开始
node.pass--;
int path = 0;
for(int i = 0; i < chs.length; i++){
path = chs[i] - 'a';
//先--,如果pass为0,就将其设为null
if(--node.nexts[path].pass == 0){
node.nexts[path] = null;
return;
}
node = node.nexts[path];
}
node.end--;
}
}
//搜寻某个单词在树中出现了几次
public int search(String word){
if(word == null){
return 0;
}
char[] chs = word.toCharArray();
//从根节点开始向下找
Node1 node = root;
int index = 0;
for(int i = 0; i < chs.length; i++){
//走哪条路径
index = chs[i] - 'a';
if(node.nexts[index] == null){
return 0;
}
node = node.nexts[index];
}
return node.end;
}
//所有加入的字符串中,有多少以pre作为前缀
public int prefixNumber(String pre){
if(pre == null){
return 0;
}
char[] chs = pre.toCharArray();
Node1 node = root;
int index = 0;
for(int i = 0; i < chs.length; i++){
index = chs[i] - 'a';
//该路径下面为null,直接返回0
if(node.nexts[index] == null){
return 0;
}
node = node.nexts[index];
}
return node.pass;
}
}
2 不基于比较的排序【桶排序】
桶排序思想下的排序:计数排序 & 基数排序
1)桶排序思想下的排序都是不基于比较的排序
2)时间复杂度O(N),空间复杂度O(M)
3)应用范围有限,需要样本的数据状况来满足桶的划分
计数排序和基数排序
- 一般来讲,计数排序,要求样本是整数,且范围窄小【每一个都列出来】
- 一般来讲,基数排序,要求样本是10进制的正整数(可以为负,需要改写,同时加上负数的绝对值【越界问题】)
2.1 计数排序
//计数排序
public static void countSort(int[] arr){
if(arr == null || arr.length < 2){
return;
}
int max = Integer.MIN_VALUE;
for(int i : arr){
max = Math.max(i, max);
}
//创建桶【假设最大值是14】
// 【0 1 2 3 4 .....13 14】
int[] buckets = new int[max+1];
//入桶
for(int i = 0; i < arr.length; i++){
buckets[arr[i]]++;
}
//出桶【从小的出】
int index = 0;
for(int j = 0; j < buckets.length; j++){
while(buckets[j]-- > 0){
arr[index++] = j;
}
}
}
2.2 基数排序
进阶写法:不用创建多个桶,直接在help数组上修改
//基数排序:arr中只能为正值【如果有负值,需要做特殊处理】
public static void radixSort(int[] arr){
if(arr == null || arr.length < 2){
return;
}
radixSort(arr, 0, arr.length - 1, maxbits(arr));
}
//获取arr中的最大位数【循环几次】
public static int maxbits(int[] arr){
int max = Integer.MIN_VALUE;
for(int i : arr){
max = Math.max(max, i);
}
int res = 0;
while (max != 0){
max /= 10;
res++;
}
return res;
}
//arr[L, R]排序 digit:最大的十进制位数
public static void radixSort(int[] arr, int L, int R, int digit){
final int radix = 10;
int i = 0, j = 0;
//有多少个数,准备多少个辅助空间
int[] help = new int[R - L + 1];
//有多少位,就进出多少次
for(int d = 1; d <= digit; d++){
//count -> count' [前缀和]
//10个空间
//count[0] 当前位【d位】是0的的数字有多少个【个、十、百、...】
//count[1] 当前位【d位】是0和1的数字有多少个
//count[2] 当前位【d位】是0、1、2的数字有多少个(<=2)
//count[i] 当前位【d位】是0~i的数字有多少个
int[] count = new int[radix]; //count[0...9]
for(i = L; i <= R; i++){
j = getDigit(arr[i], d);
count[j]++;
}
for(i = 1; i < radix; i++){
//变为前缀和count[1]表明<=1的数有几个
count[i] = count[i] + count[i-1];
}
for(i = R; i >= L; i--){
j = getDigit(arr[i], d);
help[count[j] - 1] = arr[i];
count[j]--;
}
for(i = L, j = 0; i <= R; i++, j++){
arr[i] = help[j];
}
}
}
//获取x每一位的数值
public static int getDigit(int x, int d){
return ((x / (int) Math.pow(10, d - 1)) % 10);
}
3 排序算法拓展
3.1 排序算法的稳定性
- 稳定性是指同样大小得样本再次排序之后不会改变相对次序
比如:2 1 3 1 5
排序完成之后 1 1 2 3 5,第一个1还是在第二个1后面,那么该排序算法就是稳定的,反之不稳定 - 对基础类型来说,稳定性毫无意义【因为没有实际应用场景】
- 对非基础类型来说,稳定性有意义
比如:一个Student数组,原本按照年龄age顺序排列好了,现在要求按照排序排序
那么,具有稳定性的排序算法排序完成之后,该数组中便是Student按照班级排序,且内部按照年龄排序 - 有些排序算法可以实现成稳定的,而有些排序算法无论如何都实现不成稳定的
3.2 排序算法比较
注:希尔排序也是不稳定的,时间复杂度O(N*logN)
- 基于比较类的排序,时间复杂度极限:O(N*logN)
- 时间复杂度O(N*logN)、额外空间复杂度低于O(N)且稳定的基于比较的排序是不存在的
- 为了绝对的速度选快排、为了省空间选堆排、为了稳定性选归并
工程上对于排序的改进:
1)稳定性考虑
Java内部的Arrays.sort()内部是改良后的快排,会根据传入的参数是否是基础类型,如果是基础类型,对稳定性无要求,直接快排,如果非基础类型,有稳定性要求,走归并
2)充分利用O(N*logN)和O(N^2)排序各自的优势
快排如果L + 60 > R 内部直接走插入(常数项小)