前缀树及计数排序、基数排序、排序算法拓展【十大经典排序】

前缀树及计数排序、基数排序【十大经典排序】

1 前缀树(prefix tree/trie)

  1. 单个字符串中,字符从前到后的加到一棵多叉树上
  2. 字符放在路上,节点上有专属的数据项(常见的就是pass和end值)
  3. 所有样本都这样添加,如果没有路就新建,如果有路就复用
  4. 沿途节点的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)

  1. 基于比较类的排序,时间复杂度极限:O(N*logN)
  2. 时间复杂度O(N*logN)、额外空间复杂度低于O(N)且稳定的基于比较的排序是不存在的
  3. 为了绝对的速度选快排、为了省空间选堆排、为了稳定性选归并

工程上对于排序的改进:
1)稳定性考虑

Java内部的Arrays.sort()内部是改良后的快排,会根据传入的参数是否是基础类型,如果是基础类型,对稳定性无要求,直接快排,如果非基础类型,有稳定性要求,走归并

2)充分利用O(N*logN)和O(N^2)排序各自的优势

快排如果L + 60 > R 内部直接走插入(常数项小)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值