上一遍博客,我已经给大家介绍过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