辞書ツリーはプレフィックスツリーとも呼ばれ、辞書専用に設計されたデータ構造であり、このデータ構造は通常、文字列を処理するために使用されます。
知識のポイント
バックグラウンド
単語がn個ある場合は、それらをマップに配置し(最下層はツリーとして実装されます)、検索の時間の複雑さはo(logn)です。100万個の単語がある場合、lognは約20であり、辞書ツリーのデータ構造はnと同じ数になります。単語の数とは関係ありません。単語には数文字あり、時間の複雑さはほんのわずかです。単語よりも優れた文字は通常20以内であり、効率が大幅に向上します。
デザインの導出
1.辞書ツリー
1.辞書ツリーは多分岐ツリーであり、各単語の文字はツリー内に格納されます。
2.ルートノードから開始するように次のノードを設計できます。各ノードには26個の子ノードがあります。下の図に示すように(単語が格納されていない一部のノードは図から省略されています)
2.デザインノード
class{
char c; // 节点代表的字符
Node next[26]; //指向26个子节点
}
さまざまな言語とさまざまなシナリオに応じて、このサブノードのデザインも変更されます。上記のように、英語の単語文字列として保存された26の代表を記述し、純粋な大文字または純粋な小文字のみを考慮します。大文字と小文字が混在している場合、子ノードは次のようになります。
class{
char c; // 节点代表的字符
Node next[52]; //指向52个子节点
}
実際、URLとメールアドレスを保存できるTrieを設計すると、26文字だけでなく、さらに多くの文字を保存できるので、子ノードを書き留めるのは明らかに無理です。実際、次のように設計する必要があります。各ノードは、複数のノードへのポインターを指します。ここでは、マップを使用して次のことを実現できます。
上図に示すように、実際にはワードが格納されており、ワードを取得するときに現在のノードを格納(フェッチ)すると、次のノードのワードはすでに認識されています。したがって、変数char cはもう使用されていません。マップに配置するだけで、追加しました。isWordは、現在のノードが単語の終わりであるかどうかを示します
class{
boolean isWord;// 当前节点是否为单词结尾
Map<Character,Node> next;//此单词,指向的下一节点
}
3.基本設計を試す
/**
* Create by SunnyDay on 2020/08/24
* 默认设计为字符(Character)类型,这里不使用泛型。因为英文最为广泛。每个分隔单元就是一个字符
*/
public class Trip {
/**
* 节点的设计:每个单词的字母就是一个节点
*/
private class Node {
/**
* 当前节点是否为单词的结尾。除了叶子节点,中间的节点也可能为单词的结尾。
* 例如:panda,前三个pan 也是一个单词。
*/
private boolean isWord;
/**
* 当前节点的下一节点。
* 一般来说我们只考虑大写或者小写的话,设计一个trie时每个单词节点后面可能有26个节点,
* 此时应该设计为 Node next[26],但是这26个节点并不一定存在值,所以不用使用数组申请
* 固定的空间。可以使用映射来申请。即:
* TreeMap<Character, Node> next
* 表示当前字符对应的下一节点。
*/
private TreeMap<Character, Node> next;
public Node(boolean isWord) {
this.isWord = isWord;
this.next = new TreeMap<>();
}
public Node() {
this(false);
}
}
private Node root;
private int size;
public Trip() {
root = new Node();
size = 0;
}
/**
* trie 中存储的单词数量
*/
public int getSize() {
return size;
}
}
デザインを追加してみてください
/**
* 向trie 中添加单词(添加单词不重复)
*
* @param word 要添加的单词字符串
*/
public void add(String word) {
Node current = root;//树、链表的遍历都要从头开始。所以创建头结点的副本。
//遍历单词的每个字符,作为节点加入trie
for (int i = 0; i < word.length(); i++) {
char c = word.charAt(i);
if (current.next.get(c) == null) {
// 当前字符对应的下节点未存储在trie中时,存入。
current.next.put(c, new Node());
}
current = current.next.get(c);//节点往下遍历
//最后一个单词时,标志结束。
if (!current.isWord) {
current.isWord = true;
size++;
}
}
}
ここで語尾の扱いに注意してください
クエリを試す
/**
* 查询 word 是否在 trie中
*
* @param word 要查询的单词
*/
public boolean contains(String word) {
Node cur = root;
for (int i = 0; i < word.length(); i++) {
char c = word.charAt(i);
if (cur.next.get(c) == null) {
return false;
}
cur = cur.next.get(c);
}
/**
* 节点遍历完,cur代表最后一个节点。这时不应该返回true,应该返回此节点的isWord值。
* 因为:可能存在这种情况用户存了panda 这个单词,未存pan这个单词。若遍历到n返回true
* 而用户未存储,所以出现逻辑错误。应该使用此节点的isWord判断比较精确。
*/
return cur.isWord;
}
ここで単語の終わりの判断に注意してください
Trieのプレフィックス検索
Trieは、プレフィックス検索のため、プレフィックス検索ツリーと呼ばれることもあります。Trieは単語プレフィックス検索を提供します。
/**
* 查询trie中是否有单词前缀为 prefix
*
* @param prefix 要查询前缀
*/
public boolean isPrefix(String prefix) {
Node cur = root;
for (int i = 0; i < prefix.length(); i++) {
char c = prefix.charAt(i);
if (cur.next.get(c) == null) {
return false;
}
cur = cur.next.get(c);
}
/**
* 遍历到结尾直接返回true,前缀并不是单词。可直接返回true这里。
* */
return true;
}
総括する
ここで、このデータ構造は基本的に再び実現されます。たとえば、トライには実際にカタログに多くの知識ポイントが含まれていますが、将来的にはこれらを徐々に理解していきましょう!