1 什麼是Trie樹
Trie樹,即前綴樹,又稱單詞查找樹,字典樹,是一種樹形結構,是一種哈希樹的變種。典型應用是用於統計和排序大量的字符串(但不僅限於字符串),所以經常被搜索引擎系統用於文本詞頻統計。
Trie樹的核心思想是空間換時間,利用字符串的公共前綴來降低查詢時間的開銷以達到提高效率的目的。 它的優點是:最大限度地減少無謂的字符串比較,查詢效率比哈希表高。
它有3個基本性質:
- 根節點不包含字符,除根節點外每一個節點都只包含一個字符。
- 從根節點到某一節點,路徑上經過的字符連接起來,爲該節點對應的字符串。
- 每個節點的所有子節點包含的字符都不相同。
2 樹的構建
一個字符串加的過程中總是從頭結點開始,依次看有沒有沿途的路,如果有則複用,如果沒有則新建。
對於字符串“abc”、“bce"、"bef"、"abd"
這裏的思想主要是將字母放在邊上,在節點中並不存放字母。
擴存功能:
- 實現判斷某個前綴的字符串有多少個
- 判斷某個字符串出現了多少次
這樣就需要對節點進行填寫成員,記錄在劃過該節點的次數,記錄字符串在該節點結束的次數。
3 Trie樹的應用
除了本文引言處所述的問題能應用Trie樹解決之外,Trie樹還能解決下述問題:
1、有一個1G大小的一個文件,裏面每一行是一個詞,詞的大小不超過16字節,內存限制大小是1M。返回頻數最高的100個詞。
2、1000萬字符串,其中有些是重複的,需要把重複的全部去掉,保留沒有重複的字符串。請怎麼設計和實現?
3、 一個文本文件,大約有一萬行,每行一個詞,要求統計出其中最頻繁出現的前10個詞,請給出思想,給出時間複雜度分析。
4、尋找熱門查詢:搜索引擎會通過日誌文件把用戶每次檢索使用的所有檢索串都記錄下來,每個查詢串的長度爲1-255字節。假設目前有一千萬個記錄,這些查詢串的重複讀比較高,雖然總數是1千萬,但是如果去除重複和,不超過3百萬個。一個查詢串的重複度越高,說明查詢它的用戶越多,也就越熱門。請你統計最熱門的10個查詢串,要求使用的內存不能超過1G。
4 C++實現Trie樹
主要注意,在刪除字符串的時候,若該字符串只出現了一次,對一次的節點進行刪除。將TrieNode節點進行刪除。運用遞歸循環
向上刪除。若直接刪除當前節點會丟失下一個節點。
#pragma once
#include<vector>
#include<string>
using std::string;
using std::vector;
struct TrieNode
{
int path;
int end;
TrieNode* nexts[26];//將字符串限定在26個字母中,可以用vector爲了避免也可以用map或者hashmap
TrieNode() {
path = 0;
end = 0;
for (int i = 0; i < 26; i++)
{
nexts[i] = NULL;
}
};
};
class Tried
{
public:
Tried();
~Tried();
//在前綴樹中插入這個字符串
void insert(string str);
//在前綴樹中刪除這個字符串
bool Delete(string str);
//刪除某個節點開始的所有節點
void destory(TrieNode* root,const char* chs);
//在前綴樹中查找以該字符串爲前綴的字符串有幾個
int search_pre(string str);
//這個字符出現過幾次
int search_end(string str);
private:
TrieNode* root;
};
Tried::Tried()
{
root = new TrieNode();
}
Tried::~Tried()
{
delete root;
}
void Tried::insert(string str) {
if (str.empty())
{
return;
}
const char *chs=str.c_str();//將string轉換爲C字符串
int index;
TrieNode* node = root;
while (*chs!=NULL)//這裏是c字符串。c字符串結尾是一個以null結尾的字符串。不能判斷是chs!=NULL
{
index = *chs - 'a';
if (node->nexts[index] == NULL) {
node->nexts[index] = new TrieNode();
}
node = node->nexts[index];
node->path++;
chs++;
}
node->end++;
}
inline bool Tried::Delete(string str)
{
if (search_end(str) != 0)
{
TrieNode* node = root;//每次都需要回到根節點
int index = 0;
const char* chs = str.c_str();
while (*chs != NULL) {
index = *chs - 'a';
if (--node->nexts[index]->path == 0) {//若該節點的path爲0那麼後面的都需要刪除
destory(node->nexts[index],++chs);//遞歸刪除節點
return true;
}
//判斷的時候已經相減,不需要再次相減
node = node->nexts[index];
chs++;
}
}
return false;
}
inline int Tried::search_pre(string str)
{
if (str.empty())
{
return 0;
}
const char* chs = str.c_str();
int index;
TrieNode* node = root;
while (*chs != NULL) {
index = *chs - 'a';
if (node->nexts[index] == NULL) {
return 0;
}
node = node->nexts[index];
chs++;
}
return node->path;
}
inline int Tried::search_end(string str)
{
if (str.empty())
{
return 0;
}
const char* chs = str.c_str();
int index;
TrieNode* node = root;
while (*chs != NULL) {
index = *chs - 'a';
if (node->nexts[index]==NULL) {
return 0;
}
node = node->nexts[index];
chs++;
}
return node->end;
}
//使用遞歸來實現先刪除後面的節點,再刪除前面的節點
void Tried::destory(TrieNode* root,const char* chs) {
if (root == NULL||chs==NULL) {
return;
}
destory(root->nexts[*chs-'a'],++chs);
delete root;
}