資料結構實現 8.1:字典樹(C++版)
資料結構實現 8.1:字典樹(C++版)
1. 概念及基本框架
字典樹 顧名思義,就是用來存放單詞的樹,也稱 Trie樹 ,其本質上是一種多叉樹,我們可以通過圖示說明。
這裡有一個灰色的根結點,不儲存資訊,然後下面的每個結點儲存一個字母,利用結點顏色區分從根結點到當前結點構成的字串是否是一個單詞。這樣,就可以構成一部字典。很顯然,從字典中查詢一個單詞,與字典 的容量並無關係,而只與單詞長度有關。這樣只要建立好了字典,那麼可以大大減小查詢操作的時間複雜度。
當然,我們也可以把字母儲存在路徑上,只用結點顏色表示當前結點構成的字串是否是一個單詞。
我們先來定義一個字典樹的結點,程式碼如下:
class TrieNode{
public:
TrieNode(bool isWord = false){
this->isWord = isWord;
BSTMap<char, TrieNode*> *p = new BSTMap<char, TrieNode*>;
next = p;
}
TrieNode operator= (TrieNode& node){
this->isWord = node.isWord;
this->next = node. next;
return *this;
}
public:
bool isWord;
BSTMap<char, TrieNode*> *next;
};
這裡使用了我們之前實現的 map 作為結點,其中 key 作為字母,value 用來儲存子樹。(很顯然,每個結點都可以看成一棵多叉樹的根)isWord 表示是否是一個單詞,初始化預設為 false 。這裡對 = 運算子進行了過載,方便我們後面的操作。
有了結點,字典樹的大體框架如下:
class Trie{
public:
Trie(){
m_size = 0;
}
...
private:
TrieNode root;
int m_size;
};
root 表示字典樹的根,不儲存資訊。
m_size 表示字典樹的大小。
同樣,為了保護資料,這些變數都放在 private 區。
接下來我們就對字典樹的增加、查詢以及一些其他基本操作用程式碼去實現。
2. 基本操作程式實現
2.1 增加操作
class Trie{
public:
...
void add(std::string word){
TrieNode* cur = &root;
for (int i = 0; i < word.size(); ++i){
char c = word[i];
if (!cur->next->contains(c)){
TrieNode* node = new TrieNode;
cur->next->add(c, node);
}
cur = cur->next->get(c);
}
if (!cur->isWord){
cur->isWord = true;
m_size++;
}
}
...
};
這裡新增操作有兩個:
第一個是新增字母結點,只有結點不存在才會執行此操作;
第二個是新增新單詞,而只有字典中沒有當前單詞才會記錄增加了新單詞。
2.2 查詢操作
這裡我們提供兩個查詢函式,contains 和 isPrefix ,函式實現程式碼如下。
class Trie{
public:
...
bool contains(std::string word){
TrieNode* cur = &root;
for (int i = 0; i < word.size(); ++i){
char c = word[i];
if (!cur->next->contains(c)){
return false;
}
cur = cur->next->get(c);
}
return cur->isWord;
}
bool isPrefix(string prefix){
TrieNode *cur = &root;
for (int i = 0; i < prefix.size(); ++i){
char c = prefix[i];
if (!cur->next->contains(c)){
return false;
}
cur = cur->next->get(c);
}
return true;
}
...
};
contains 用於查詢字典中是否包含該單詞。
isPrefix 用於查詢字典中是否包含該字首串。
2.3 其他操作
字典樹還有一些其他的操作,包括 字典樹大小 等的查詢操作。
class Trie{
public:
...
int size()const{
return m_size;
}
bool isEmpty()const{
return m_size == 0;
}
...
};
3. 演算法複雜度分析
因為字典樹的增加、查詢操作只與單詞長度有關,所以時間複雜度均是 O(1) 級別的。
3.1 增加操作
函式 | 最壞複雜度 | 平均複雜度 |
---|---|---|
add | O(1) | O(1) |
3.2 查詢操作
函式 | 最壞複雜度 | 平均複雜度 |
---|---|---|
contains | O(1) | O(1) |
isPrefix | O(1) | O(1) |
總體情況:
操作 | 時間複雜度 |
---|---|
增 | O(1) |
查 | O(1) |
4. 完整程式碼
程式完整程式碼(這裡使用了標頭檔案的形式來實現類)如下:
這裡不再給出 BSTMap 實現程式碼,如有需要可檢視 5.1 。
字典樹 類程式碼:
#ifndef __TRIE_H__
#define __TRIE_H__
#include <string>
#include "BSTMap.h"
class TrieNode{
public:
TrieNode(bool isWord = false){
this->isWord = isWord;
BSTMap<char, TrieNode*> *p = new BSTMap<char, TrieNode*>;
next = p;
}
TrieNode operator= (TrieNode& node){
this->isWord = node.isWord;
this->next = node.next;
return *this;
}
public:
bool isWord;
BSTMap<char, TrieNode*> *next;
};
class Trie{
public:
Trie(){
m_size = 0;
}
int size()const{
return m_size;
}
bool isEmpty()const{
return m_size == 0;
}
void add(std::string word){
TrieNode* cur = &root;
for (int i = 0; i < word.size(); ++i){
char c = word[i];
if (!cur->next->contains(c)){
TrieNode* node = new TrieNode;
cur->next->add(c, node);
}
cur = cur->next->get(c);
}
if (!cur->isWord){
cur->isWord = true;
m_size++;
}
}
bool contains(std::string word){
TrieNode* cur = &root;
for (int i = 0; i < word.size(); ++i){
char c = word[i];
if (!cur->next->contains(c)){
return false;
}
cur = cur->next->get(c);
}
return cur->isWord;
}
bool isPrefix(string prefix){
TrieNode *cur = &root;
for (int i = 0; i < prefix.size(); ++i){
char c = prefix[i];
if (!cur->next->contains(c)){
return false;
}
cur = cur->next->get(c);
}
return true;
}
private:
TrieNode root;
int m_size;
};
#endif