trie树简介

本博客(http://blog.youkuaiyun.com/livelylittlefish )贴出作者(阿波)相关研究、学习内容所做的笔记,欢迎广大朋友指正!

Content

1. trie基础

(1) 是什么?

(2) 性质

(3) 应用

(4) 优点

2. 一个例子

(1) 功能

(2) 代码

(3) 运行结果

(4) 分析

3. 一点点修改

(1) 分析

(2) 修改

(3) 代码

(4) 运行结果

(5) 分析

(6) 一个稍微复杂点的结果

4. 小结

 

 

1. trie基础

 

(1) 是什么?

Trie,又称单词查找树或键树,是一种树形结构,是一种哈希树的变种。

 

(2) 性质

  • 根节点不包含字符,除根节点外每一个节点都只包含一个字符
  • 从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串
  • 每个节点的所有子节点包含的字符都不相同

 

例如,单词序列a, to, tea, ted, ten, i, in, inn,对应的trie

(3) 应用

用于统计和排序大量的字符串,但不仅限于字符串,所以经常被搜索引擎系统用于文本词频统计。

 

(4) 优点

  • 最大限度地减少无谓的字符串比较
  • 查询效率比哈希表高

 

2. 一个例子

 

该列子从中文wiki百科而来。

 

(1) 功能

 

从控制台输入字符串,每个字符串以回车结束,所有字符串的输入以'#'结束。程序统计输入的所有字符串中每种字符串的出现次数。

 

(2) 代码

 

一个实现了该功能的例子代码如下。

[cpp]  view plain copy
  1. /** 
  2.  * trie tree test 
  3.  * descriptioin: make statistics on every word for its frequency 
  4.  * usage: input some strings, each followed by a 'enter' character, and end with '#' 
  5.  */  
  6. #include <stdio.h>  
  7. #include <stdlib.h>  
  8. #include <string.h>  
  9. #define Max_Char_Number 256  
  10. #define Max_Word_Len      128  
  11. struct Trie_Node  
  12. {  
  13.     int count_;  
  14.     struct Trie_Node *next_[Max_Char_Number];  
  15. };  
  16. static struct Trie_Node root= {0, {NULL}};  
  17. static char *spaces=" /t/n/./"/'()";  
  18. static int insert(const char *word)  
  19. {  
  20.     int loop;  
  21.     struct Trie_Node *cur, *newnode;  
  22.     if (word[0] == '/0')  
  23.         return 0;  
  24.     cur = &root;  
  25.     for (loop=0; ; ++loop)  
  26.     {  
  27.         if (cur->next_[word[loop]] == NULL)  
  28.         {  
  29.             newnode=(struct Trie_Node*)malloc(sizeof(struct Trie_Node));  
  30.             memset(newnode, 0, sizeof(struct Trie_Node));  
  31.             cur->next_[word[loop]] = newnode;  
  32.         }  
  33.         if (word[loop] == '/0')  
  34.             break;  
  35.         cur = cur->next_[word[loop]];  
  36.     }  
  37.     cur->count_++;  
  38.     return 0;  
  39. }  
  40. void input()  
  41. {  
  42.     char *linebuf = NULL, *line = NULL, *word = NULL;  
  43.     size_t bufsize=0;  
  44.     int ret;  
  45.     while (1)  
  46.     {  
  47.         ret=getline(&linebuf, &bufsize, stdin);  
  48.         if (ret==-1)  
  49.             break;  
  50.         line=linebuf;  
  51.         word = strsep(&line, spaces);  
  52.         /* input '#' will terminate this input */  
  53.         if (strcmp(word, "#") == 0)  
  54.             break;  
  55.         if (word[0]=='/0')  
  56.             continue;  
  57.         insert(word);  
  58.     }  
  59. }  
  60. static void printword(const char *str, int n)  
  61. {  
  62.     printf("%s/t%d/n", str, n);  
  63. }  
  64. static int traverse(struct Trie_Node *rootp)  
  65. {  
  66.     static char worddump[Max_Word_Len+1];  
  67.     static int pos=0;  
  68.     int i;  
  69.     if (rootp == NULL)  
  70.         return 0;  
  71.     if (rootp->count_)  
  72.     {  
  73.         worddump[pos]='/0';  
  74.         printword(worddump, rootp->count_);  
  75.     }  
  76.     for (i=0; i<Max_Char_Number; ++i)  
  77.     {  
  78.         worddump[pos++]=i;  
  79.         traverse(rootp->next_[i]);  /* recursive call */  
  80.         pos--;  
  81.     }  
  82.     return 0;  
  83. }  
  84. void dump(struct Trie_Node* node)  
  85. {  
  86.     static int depth = 0;  
  87.     static const char prefix[] = "    ";  
  88.     int loop = 0;  
  89.     if (node == NULL)  
  90.         return;  
  91.     for (loop = 0; loop < Max_Char_Number; loop++)  
  92.     {  
  93.         if (node->next_[loop])  
  94.         {  
  95.             printf ("%.*s", (int) depth++, prefix);  
  96.             printf("next['%c'] = 0x%x, count = %d/n", loop, (unsigned int)(node->next_[loop]), node->next_[loop]->count_);  
  97.             dump(node->next_[loop]);  /* recursive call */  
  98.             depth--;  
  99.         }  
  100.     }  
  101. }  
  102. int main(void)  
  103. {  
  104.     input();  
  105.     printf("/n");  
  106.     traverse(&root);  
  107.     printf("/n");  
  108.     dump(&root);  
  109.     return 0;  
  110. }  

(3) 运行结果

# ./trie

a

to

tea

ted

ten

i

in

inn

#

 

a       1

i       1

in      1

inn     1

tea     1

ted     1

ten     1

to      1

 

next['a'] =0x88c1088, count = 1

 next[''] = 0x88c1490, count = 0

next['i'] =0x88c40e8, count = 1

 next[''] = 0x88c44f0, count = 0

 next['n'] = 0x88c48f8, count = 1

  next[''] = 0x88c4d00, count = 0

  next['n'] = 0x88c5108, count = 1

   next[''] = 0x88c5510, count = 0

next['t'] =0x88c1898, count = 0

 next['e'] = 0x88c24b0, count = 0

  next['a'] = 0x88c28b8, count = 1

   next[''] = 0x88c2cc0, count = 0

  next['d'] = 0x88c30c8, count = 1

   next[''] = 0x88c34d0, count = 0

  next['n'] = 0x88c38d8, count = 1

   next[''] = 0x88c3ce0, count = 0

 next['o'] = 0x88c1ca0, count = 1

  next[''] = 0x88c20a8, count = 0

 

(4) 分析

 

在该程序中,笔者加入了dump函数,递归地打印出next数组中不为空的值,如下图所示。

 

加上次数,如下图。其中边(包括实边和虚边)上的数字即为next数组的下标,该数字即为该边所到节点字符的ASCII码。程序中使用字符本身,实际上是该字符的ASCII码,作为next数组下标,这样能够获得o(1)的访问效率,无需查找,直接访问。

 

 

由图直接看出,根节点的

next[97]出现的次数为1,即字符串"a"的次数;

next[105]的次数也为1,即字符串"i"的次数;

next[105]->next[110]出现的次数为1,即字符串"in"的次数;

next[105]->next[110]->next[110]出现的次数为1,即字符串"inn"的次数;

...

 

感觉怎样?是不是很直观呢?

——of course!

 

3. 一点点修改

 

(1) 分析

 

如上述例子,程序已经统计出next[105]->next[110]出现的次数为1,即字符串"in"的次数为1,为什么next[105]->next[110]->next[0]还有值呢?即next[0]这个指针实际上还指向一个节点,虽然该节点的值全为0

 

如果被统计的字符串数量庞大,叶子节点数量庞大,或者如上图'i'这样的节点数量庞大,那么,空间的浪费也是很显然的,因为从程序中,我们知道,每个节点的大小sizeof(struct Trie_Node)=1028字节。如果,这样的节点有10000个,则浪费1028*10000字节,大约10M

 

(2) 修改

 

调试上面的代码,发现insert函数,即向tire树中插入一个字符串时,for循环的退出条件可以修改,如下。

 

static int insert(const char *word)

{

    int loop;

    struct Trie_Node*cur*newnode;

 

 

    if (word[0== '/0')

        return 0;

 

    cur = &root;

    for (loop=0; ; ++loop)

    {

        if (word[loop== '/0')  /* the break condition should be here */

            break;

 

        if (cur- >next_[word[loop]] == NULL)

        {

            newnode=(struct Trie_Node*)malloc(sizeof(struct Trie_Node));

            memset(newnode0sizeof(struct Trie_Node));

            cur- >next_[word[loop]] newnode;

        }

 

        cur cur- >next_[word[loop]];

    }

    cur- >count_++;

 

    return 0;

}

 

(3) 代码

 

省略。

 

(4) 运行结果

 

# ./trie

a

to

tea

ted

ten

i

in

inn

#

 

a       1

i       1

in      1

inn     1

tea     1

ted     1

ten     1

to      1

 

next['a'] =0x8bb8088, count = 1

next['i'] =0x8bb9cc0, count = 1

 next['n'] = 0x8bba0c8, count = 1

  next['n'] = 0x8bba4d0, count = 1

next['t'] =0x8bb8490, count = 0

 next['e'] = 0x8bb8ca0, count = 0

  next['a'] = 0x8bb90a8, count = 1

  next['d'] = 0x8bb94b0, count = 1

  next['n'] = 0x8bb98b8, count = 1

 next['o'] = 0x8bb8898, count = 1

 

(5) 分析

 

对比修改前后的运行结果发现,修改后的程序中不再含有next[0]的节点。如下图。

由上图也可直接看出,根节点的

next[97]出现的次数为1,即字符串"a"的次数;

next[105]的次数也为1,即字符串"i"的次数;

next[105]->next[110]出现的次数为1,即字符串"in"的次数;

next[105]->next[110]->next[110]出现的次数为1,即字符串"inn"的次数;

...

 

相比修改前的图,该图没有next[0]的垃圾数据,更直观。

 

(6) 一个稍微复杂点的结果

 

# ./trie

a

to

tea

ted

ten

i

in

inn

a

to

tea

ted

ten

i

in

inn

a

i

a

a

#

 

a       5

i       3

in      2

inn     2

tea     2

ted     2

ten     2

to      2

 

next['a'] =0x9604088, count = 5

next['i'] =0x9605cc0, count = 3

 next['n'] = 0x96060c8, count = 2

  next['n'] = 0x96064d0, count = 2

next['t'] =0x9604490, count = 0

 next['e'] = 0x9604ca0, count = 0

  next['a'] = 0x96050a8, count = 2

  next['d'] = 0x96054b0, count = 2

  next['n'] = 0x96058b8, count = 2

 next['o'] = 0x9604898, count = 2

 

4. 小结

 

本文简单介绍了tire树的基本概念,并用一个例子说明其应用。

在设计程序时,可以使用一些小的技巧来提高程序效率,例如本文采用字符本身,即其ASCII码值,作为next数组下标,获得o(1)的访问效率。也要注意使用(循环)边界条件,消除垃圾数据。

 

 

reference

http://zh.wikipedia.org/wiki/Trie

http://en.wikipedia.org/wiki/Trie
http://en.wikipedia.org/wiki/Hash_trie

http://en.wikipedia.org/wiki/Hash_array_mapped_trie

http://www.topcoder.com/tc?module=Static&d1=tutorials&d2=usingTries

<think>好的,用户想了解Trie的构建方法、简介以及应用场景,还有数据结构和算法相关内容。首先我需要回忆Trie的基本概念。根据引用[4],Trie又叫前缀,是一种形数据结构,用于高效存储和检索字符串集合中的键。公共前缀的节点路径相同,节省空间。 接下来,构建方法部分。引用[1]提到构造过程是逐步插入字符串,每个插入操作对应的一个节点扩展。可能需要详细描述插入步骤,比如从根节点开始,逐个字符检查子节点是否存在,不存在则创建新节点,直到字符串结束标记。引用[3]也提到类似哈夫曼编码的压缩特性,公共前缀只存储一次,所以构建时要注意共享前缀的结构。 应用场景方面,引用[2]和[5]提到中文分词、数据压缩、单词检测等。比如单词语法检查,利用Trie快速查找是否存在。引用[4]还提到自动补全和拼写检查,这些需要前缀匹配的场景很适合。 数据结构和算法部分,需要说明Trie的节点结构,通常是每个节点包含子节点的指针数组和一个结束标志。插入、查询的时间复杂度,比如O(L),L是字符串长度。还要比较优缺点,如查询效率高但空间消耗可能大,引用[4]中的优缺点部分需要整合。 用户可能的问题包括构建步骤的具体例子,如何优化空间,与哈希表的比较,以及实际应用案例。需要确保回答中涵盖这些点,并引用合适的参考资料。例如,在构建时用示例分解步骤,可能像引用[1]中的图示过程。优化部分可以提到压缩Trie,如引用[2]中的方法,但准确性可能受影响。 需要注意的是,用户要求用中文回答,数学公式用$...$,独立公式用$$...$$,但Trie可能涉及较少数学公式,更多是结构描述。需要检查是否有变量或表达式需要格式处理,比如节点数量或时间复杂度的大O表示法,可能需要用行内公式。 最后,生成相关问题时要围绕Trie的构建、优化、应用差异等,比如空间优化方法,与哈希表的对比,前缀匹配实现方式等。确保问题覆盖用户可能感兴趣的扩展方向。</think>### Trie简介及构建方法 #### 1. Trie是什么? Trie(字典、前缀)是一种形数据结构,主要用于**高效存储和检索字符串集合**。其核心特点是: - 节点不直接存储字符,而是通过**路径表示字符**; - 公共前缀的字符串共享中的路径,例如"apple"和"app"在中共享前三个字符的路径[^4]; - 典型结构包含**根节点、子节点指针数组、结束标志**(标记字符串末尾)。 #### 2. 如何构建Trie? **构建步骤**: 1. **初始化根节点**:根节点为空,不包含字符。 2. **插入字符串**: - 从根节点开始,逐字符遍历字符串; - 若当前字符对应的子节点存在,则移动到该子节点; - 若不存在,则创建新节点并链接到父节点; - 重复直至字符串末尾,标记结束标志[^1]。 3. **重复插入**:对所有字符串执行上述操作。 **示例**(插入"app"和"apple"): ``` 根 → a → p → p(标记结束) ↘ p → l → e(标记结束) ``` #### 3. 时间复杂度与空间复杂度 - **插入/查询**:$O(L)$($L$为字符串长度) - **空间复杂度**:最坏情况$O(N \cdot L)$($N$为字符串数量) #### 4. 优缺点对比 | 优点 | 缺点 | |------|------| | 前缀匹配高效 | 空间占用较高 | | 适合动态字符串集合 | 非字符串场景适用性低 | | 避免哈希冲突 | 实现复杂度较高 | #### 5. 应用场景 1. **自动补全**:输入前缀时快速匹配候选词(如搜索引擎); 2. **拼写检查**:检测单词是否存在词典中[^5]; 3. **中文分词**:利用公共前缀压缩存储分词结果[^2]; 4. **IP路由表**:最长前缀匹配优化路由查找; 5. **数据压缩**:通过共享前缀减少存储冗余[^3]。 --- ### 代码示例(Python实现) ```python class TrieNode: def __init__(self): self.children = {} self.is_end = False class Trie: def __init__(self): self.root = TrieNode() def insert(self, word): node = self.root for char in word: if char not in node.children: node.children[char] = TrieNode() node = node.children[char] node.is_end = True def search(self, word): node = self.root for char in word: if char not in node.children: return False node = node.children[char] return node.is_end ``` ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值