Trie树简介
Trie树也称字典树,在字符串的查找中优势比较明显,适用于在海量数据中查找某个数据。因为Trie树的查找时间和数据总量没有关系,只和要查找的数据长度有关。比如搜索引擎中热度词语的统计。除此之外也可用于将数据按字典序排序。
另外Trie树是典型的空间换时间的数据结构,构建一颗Trie树需要花费比较大的内存空间。
简单的Trie树的实现有两种方式,每个节点存储一个子节点数组,或者用链表将每个节点的子节点连接起来。
采用数组浪费了大量的空间,因为在Trie树使用的过程中不可能整个树是满的,所以数组中绝大多数的位置都是空闲的,空间得不到有效利用,但是因为数组支持随机访问,所以查找时效率很高
采用链表虽然节省了不少空间,但是在查找的过程中难以高效定位。
为了将两者的优点有效地结合起来,出现了一种仅用两个线性数组描述的Trie树,称为双数组Trie树(DAT)。两个数组分别是base和check,理解它们的含义是学习DAT的关键。
base数组和check数组
base和check数组用于记录节点和节点之间的关系,而自身的数据是通过在数组中的索引表示的。上文提到的Trie树为每个节点都开辟一定大小的数组来存储孩子节点,但是开辟的大小是事先规定好的,只能处理比如说英文单词这种简单的数据,而对于中文的处理不是很理想,双数组Trie树将每个数据都映射成一个整型数,既是数据的编码又可用于在base和check数组中定位索引。
双数组Trie树中只有树结构意义上的节点,并无实际的表述,而节点与节点之间的关系仅是通过base和check记录的。
编码
在理解base和check数组之前,先了解一下有关编码的事情。计算机在存储数据时都是以二进制的形式存储的,
比如想要存储字符’a’,并不是直接将’a’放入内存,而是将其转换成对应的编码(一个整型数97),随后进行进制转换存储在内存中。想要存储中文”一”,也是需要要将其转换成对应的编码(19968),然后再进行存储。
所以可以理解成,任何数据都有唯一的整数值与其对应,这就是数据的编码。目前比较流行的就是Unicode码,可以有效处理中文字符。
起始索引begin
有了编码的知识,首先想到的是将每个数据转换成对应的Code,然后这个Code就是这个数据在base和check中的下标。但是没办法维护节点之间的关系,不过也不能说这种想法是错的,至少对了一部分。原因是在base和check中的下标不是只由Code组成,还需要一个起始索引begin,这个begin是在程序中需要计算的整数(目前先假设begin以求出)。
这个起始索引begin很像HashTable中的hash地址,在哈希表中,对于每个数据,都有一个确定的哈希散列函数将这个数据转换成一个整数,这个整数就是数据在哈希表中的索引。
而对于双数组而言,begin + Code组成了某个数据在base和check中的下标。
base数组
但是你可能会问,这样也没有父节点子节点的关系?
这就是base要解决的事情,考虑一下,每个数据都有一个唯一的begin作为它的起始索引,这个begin就好比于是数据的归属地。父节点有,孩子节点也同样有,那么就可以利用base数组来存储数据的孩子节点的起始索引child_begin。也就是说,base[begin + code] = child_begin,这样,在知道父节点的编码,和父节点的起始索引begin后,就能找到它的孩子节点的起始索引child_begin,然后child_begin + child_code就是孩子节点在base和check数组中的下标。
你可能又会问,父节点会有多个孩子节点,而base中只存储了一个begin?
具有相同父节点的节点之间互为兄弟关系,只需要保证兄弟节点具有相同的起始索引begin就可以了,对吗。
check数组
而对于check数组,它存储的值