上一遍博客,我已經給大家介紹過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