算法:用Java实现有序trie树,并用于实现单词字典排序的功能

上一遍博客,我已经给大家介绍过trie树。trie树算是一种前缀树,用来储存单词再合适不过了,而且很多单词都有相同的前缀,如果用这个树来存单词,那是相当省空间的。今天我要实现的这个有序trie树,和上一篇博客的trie树的最大不同,就是今天的有序trie树,字母的下级结点集合是用的TreeSet实现的。这就使得每个字母结点的下级结点都是按照字典序列来排布的。

这样设计有什么好处呢?如果我们仿照二叉树的先序遍历方式来遍历有序trie树,那么结果会是相当有趣的:有序trie树储存的单词会被按照字典序,在遍历过程中有序生成出来。这里当然不是先序遍历,但是类似先序遍历:先遍历根结点,然后再遍历从左起第一个结点,之后就依次遍历右边的节点了。由于是递归遍历,所以遍历过程中会先走遍trie树左边的分支,左边的分支都走完了,才开始走右边的分支。你也可以理解为一种贪婪算法,见到左边的元素就先遍历,直到没有左边的元素了,才开始尝试右边的元素。

来个图,比如我构建以下几个单词的有序trie树:he、hell、hop、boil、base、bass、bee,那么构建的有序trie树就会是这样(有序trie树跟加入单词的顺序无关,也就是你无论以什么顺序加入这些单词,最终生成的有序trie树都是一样的):
在这里插入图片描述
上面的图,哪里体现了有序呢?每一个字母结点的所有子节点,从左到右都是按字母顺序来排列的。下图的蓝色框,就标出了有序的地方:
在这里插入图片描述
而前面说到的类似二叉树先序遍历的遍历反式,大概是如下图所示(画得不好,请见谅):
在这里插入图片描述
接下来上代码,有序trie树的字母结点的实现与添加单词的实现和上一篇博客的trie树实现一样,只不过是字母结点的下一字母集合变成了有序的TreeSet,也就是上面第二张图蓝色长方形框住的地方。然后我在这个 TrieTreeSort 类里面实现了将有序trie树的所有单词排序后封装为List的遍历获取方法。下面是源码:

import java.util.ArrayList;
import java.util.List;
import java.util.TreeSet;

/**
 * @Author: LiYang
 * @Date: 2020/2/27 22:23
 * @Description: 有序trie树(有序字典树)做单词的字典排序
 */
public class TrieTreeSort {

    /**
     * 有序trie树的结点类(TrieTreeSort的静态内部类)
     */
    private static class TrieNode implements Comparable<TrieNode> {

        //当前结点的字母
        private char letter;

        //当前结点字母是否可以作为结束字母
        private boolean isEndLetter = false;

        //当前结点的下一结点集合
        //注意,这里用TreeSet,使得下一结点的letter在树枝上自然有序
        private TreeSet<TrieNode> next;

        /**
         * 空构造方法
         */
        public TrieNode() {

        }

        /**
         * 入参letter的构造方法
         * @param letter
         */
        public TrieNode(char letter) {
            this.letter = letter;

            //为每一个结点创建一个空的next集合
            this.next = new TreeSet<>();
        }

        /**
         * 重写equals方法,结点字母相等,结点即相等
         * @param obj
         * @return
         */
        @Override
        public boolean equals(Object obj) {
            //如果不是同一类,则绝对不等
            if (!(obj instanceof TrieNode)) {

                //返回不等
                return false;
            }

            //字母相等,则结点相等,否则不等
            return this.letter == ((TrieNode)obj).letter;
        }

        /**
         * 重写了equals方法,就得跟着重写hashCode方法
         * @return
         */
        @Override
        public int hashCode() {
            //以letter的数值代表hashcode
            return this.letter;
        }

        /**
         * 重写compareTo方法,实现字符串排序
         * @param o
         * @return
         */
        @Override
        public int compareTo(TrieNode o) {
            //按照char的原始顺序排序
            return this.letter - o.letter;
        }

        /**
         * 查找某字母的下级字母结点
         * @param letter 查找的下一个字母
         * @return 下一个字母的结点
         */
        public TrieNode findNext(char letter) {
            //遍历查找
            for (TrieNode node : next) {

                //如果找到了
                if (node.letter == letter) {

                    //返回该结点
                    return node;
                }
            }

            //没找到,返回null
            return null;
        }

    }


    /************** 接下来是TrieTreeSort类的属性和方法 **************/

    //有序trie树的根结点,默认为'/'
    private TrieNode root = new TrieNode('/');


    /**
     * 空构造方法
     */
    public TrieTreeSort() {

    }

    /**
     * 带初始单词数组的构造方法
     * @param words
     */
    public TrieTreeSort(String[] words) {
        //遍历单词数组
        for (int i = 0; i < words.length; i++) {

            //加入初始单词
            addWord(words[i]);
        }
    }

    /**
     * 加入需要排序的单词
     * @param word 待加入的单词
     */
    public void addWord(String word) {
        //将单词转化为字符数组
        char[] charArray = word.toCharArray();

        //当前有序trie树结点为根结点
        TrieNode currentNode = root;

        //遍历单词每一个字母
        for (int i = 0; i < charArray.length; i++) {

            //在当前有序trie树结点的下一个字母结点集合里,寻找当前字母结点
            TrieNode nextNode = currentNode.findNext(charArray[i]);

            //如果当前字母存在
            if (nextNode != null) {

                //找到的当前字母结点作为当前有序trie树结点
                //接下来在当前字母结点的下一个字母集合里找下一个字母
                currentNode = nextNode;

            //如果当前字母不存在
            } else {

                //为当前字母创造有序trie树结点
                TrieNode trieNode = new TrieNode(charArray[i]);

                //将当前字母创造的有序trie树结点,加入到当前结点的下一个结点集合里
                currentNode.next.add(trieNode);

                //以当前字母创造的有序trie树结点作为当前结点,继续找下一个字母
                //有就继续看下一个字母,没有就创建,直到创建完整单词树枝
                currentNode = trieNode;
            }
        }

        //将最后一个字母,设置为结束字母
        currentNode.isEndLetter = true;
    }

    /**
     * 将有序trie树里的单词做排序的方法
     * @return 有序的trie树单词列表
     */
    public List<String> wordSort() {
        //接收最终排序的单词的列表
        List<String> sortedWords = new ArrayList<>();

        //将有序trie树的根结点的下级所有结点按序遍历
        //这里遍历的,就是所有单词的首字母
        for (TrieNode trieNode : this.root.next) {

            //调用同名的重载方法,类似二叉树先序遍历来遍历有序trie树
            wordSort(trieNode, new ArrayList<>(), sortedWords);
        }

        //最后,返回排好序的单词列表
        return sortedWords;
    }

    /**
     * 将有序trie树里面的单词树,以类似先序遍历的方式进行遍历
     * 并一层层递归下去,直到叶结点,然后即完成了单词排序
     * @param current 当前即将遍历的字母结点
     * @param sequence 当前形成的单词前缀序列
     * @param sortedWords 收集已经排好序了的单词
     */
    public void wordSort(TrieNode current, List<Character> sequence, List<String> sortedWords) {
        //先将当前字母加入到前缀序列中
        sequence.add(current.letter);

        //如果当前字母是结束字母
        if (current.isEndLetter) {

            //完成当前字母结束的单词的排序,将当前字母序列
            //转化为单词字符串,并加入到收集列表中
            sortedWords.add(charSequenceToString(sequence));
        }

        //如果当前的字母结点还有下级结点(同样是TreeSet,有序)
        if (current.next.size() > 0) {

            //遍历下级有序TreeSet
            for (TrieNode trieNode : current.next) {

                //递归调用本方法,接着往下进行类先序遍历式排序
                wordSort(trieNode, copyList(sequence), sortedWords);
            }
        }
    }

    /**
     * List<Character>的深度拷贝方法
     * @param source 拷贝源
     * @return 拷贝好的副本List<Character>
     */
    public List<Character> copyList(List<Character> source) {
        //新建拷贝副本列表
        List<Character> copy = new ArrayList<>();

        //将源字符全部加入
        copy.addAll(source);

        //返回拷贝副本
        return copy;
    }

    /**
     * 将List<Character>转化为String
     * @param sequence 待转化的List<Character>
     * @return 转化好的String,也就是单词
     */
    public String charSequenceToString(List<Character> sequence) {
        //用StringBuffer来拼接
        StringBuffer sbuf = new StringBuffer();

        //遍历字符,拼接
        for (Character character : sequence) {
            sbuf.append(character);
        }

        //返回拼接好的单词
        return sbuf.toString();
    }


    /**
     * 测试有序trie树
     * @param args
     */
    public static void main(String[] args) {
        //新建有序trie树的实例
        TrieTreeSort trieTreeSort = new TrieTreeSort();

        //乱序加入各种单词
        trieTreeSort.addWord("he");
        trieTreeSort.addWord("zip");
        trieTreeSort.addWord("kill");
        trieTreeSort.addWord("i");
        trieTreeSort.addWord("content");
        trieTreeSort.addWord("hallo");
        trieTreeSort.addWord("area");
        trieTreeSort.addWord("kiss");
        trieTreeSort.addWord("context");
        trieTreeSort.addWord("run");
        trieTreeSort.addWord("zoo");
        trieTreeSort.addWord("contact");
        trieTreeSort.addWord("hello");
        trieTreeSort.addWord("fire");
        trieTreeSort.addWord("hell");
        trieTreeSort.addWord("basic");

        //调用有序trie树的单词排序方法,返回排序结果
        List<String> sortedWordList = trieTreeSort.wordSort();

        //遍历排序结果
        for (String word : sortedWordList) {

            //按排好的顺序依次打印单词
            System.out.println(word);
        }
    }

}

执行TrieTreeSort类的main方法,控制台依次输出排好序的所有单词,测试通过:

area
basic
contact
content
context
fire
hallo
he
hell
hello
i
kill
kiss
run
zip
zoo
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章