zoukankan      html  css  js  c++  java
  • 字符串算法—字典树

    本文将介绍字符串的查找算法:R-way tries和ternary search tries(TST)。

    1. 前文回顾

      在字符串算法—字符串排序(上篇)字符串算法—字符串排序(下篇)中,我们介绍了字符串的排序方法。

      但如果我们只想进行字符串的查找工作而不想排序呢?

      提到查找,我们自然而然地就想起了高效的两种查找算法:搜索算法—红黑树搜索算法—哈希表

      它们的效率如下图:

      

      注释:图中N为元素的总个数。

      先看红黑树,我们可以把每个字符串当成一个节点,然后进行搜索。但是,搜索过程中需要进行数次比较(log2N),每次比较都需两个字符串将所有字符逐一对比,这里相对来说,比较慢。

      在看哈希表,因为只需进行很少次数的比较,所以红黑树的比较问题在哈希表中并不严重。但是,想用哈希表算法,我们是需要计算哈希值的,并且消耗一定量的空间。

      有没有比红黑树和哈希表更快、更省空间的算法呢?

      有!请看下文介绍。

     2. R-way tries

      这个算法名字在网络上并没找到正统的翻译,我就不翻译过来了。

      先从例子中直观地感受一下这棵树:

      

      这里有一堆字符串,每个字符串对应一个数字,数字大小不必在意,反正不重复就行。接下来,把这堆字符串建成树:

      

      这个是R-way tries。应注意到:

      1. 根节点为空;

      2. 每个节点只有一个字符;

      3. 有些节点会有一些数字,而有一些没有。

      为了容易理解,我们先介绍如何查找某个字符串,再介绍如何建这棵树。

    寻找字符串“she”

      首先,she的第一个字符为s,从根节点出发,去找s:

      

      找到s了,然后,she的第二个字符为h,从s出发,去找h:

      

      找到h了,然后,she的第三个字符为e,从h出发,去找e:

      

      找到e了,然后,she没有第四个字符。看我们找到的e,它有个数字0,因此she在这堆字符串里,且对应的数字为0。

    寻找字符串“shell”

      首先,shell的第一个字符为s,从根节点出发,去找s:

      

      找到s了,然后,shell的第二个字符为h,从s出发,去找h:

      

      找到h了,然后,shell的第三个字符为e,从h出发,去找e:

      

      找到e了,然后,shell的第四个字符为l,从e出发,去找l:

      

      找到l了,然后,shell的第五个字符为l,从l出发,去找l:

      

      找到l了,然后,shell没有第六个字符。看我们找到的l,它没有数字,说明shell不在这堆字符串里。

    查找字符串"are"

      首先,are的第一个字符为a,从根节点出发,去找a:
      找不到!说明are不在这堆字符串里。

      通过上述3个字符串的寻找,相信大家已经会看这棵树了,接下来介绍建树的方法

      首先建个空节点作为根,然后逐一输入字符串:(输入顺序随意)

    输入字符串“she”,对应数字为0:

      首先,she的第一个字符为s,从根节点出发,去找s;

      结果s不存在,在根节点下面建一个空节点,并把s放进去:

      

      she的第二个字符为h,从s出发,去找h;

      结果h不存在,在s下面建一个空节点,并把h放进去:

      

      she的第三个字符为e,从h出发,去找e;

      结果e不存在,在h下面建一个空节点,并把e放进去:

      

      she没第四个字符,把对应的数字填入e里:

      

    输入字符串“shells”,对应数字为3:

      首先,shells的第一个字符为s,从根节点出发,去找s;

      

      shells的第二个字符为h,从s出发,去找h;

      

      shells的第三个字符为e,从h出发,去找e;

      

      shells的第四个字符为l,从e出发,去找l;

      结果l不存在,在e下面建一个空节点,并把l放进去:

      

      shells的第四个字符为l,从e出发,去找l;

      结果l不存在,在e下面建一个空节点,并把l放进去:

      

      shells的第五个字符为l,从l出发,去找l;

      结果l不存在,在l下面建一个空节点,并把l放进去:

      

      shells的第六个字符为s,从l出发,去找s;

      结果s不存在,在l下面建一个空节点,并把s放进去:

       

      shells没第七个字符,把对应的数字填入s里:

      

    输入字符串“sea”,对应数字为6:

      首先,sea的第一个字符为s,从根节点出发,去找s;

      

      sea的第二个字符为e,从s出发,去找e;

      结果e不存在,在s下面建一个空节点,并把e放进去:

      

      sea的第三个字符为a,从e出发,去找a;

      结果a不存在,在e下面建一个空节点,并把a放进去;

      并且sea没第四个字符,把对应的数字放进a里:

      

      注意,在R-way tries里,新建的节点在已有的节点左边还是右边都无所谓。

      如此类推,把所有字符串全部输入进去后,树建成:

      

      到现在为止,我们知道了如何去建树和用树去找字符串,还缺什么操作?删除!

      删除也很简单,例如

    删除shells:

      首先,按照查找方法,从根节点开始,逐个字符地找到shells的最后一个字符:

      

      然后把这个s的数字删掉:

      

      然后检查s是否有非空子节点,结果没有,把s删掉:

      

      然后检查s的上一个节点l是否有非空子节点或者数字,结果都没有,把l删掉:

      

      然后检查l的上一个节点l是否有非空子节点或者数字,结果都没有,把l删掉:

      

      然后检查l的上一个节点e是否有非空子节点或者数字,结果有数字,删除操作结束。

      原则上,R-way tries里不允许出现即没非空子节点又没数字的节点。

      从原理上来看,一切都是多么的美好!但用代码实现时,就会遇到一个严重的问题。

      每个节点都有一个节点数组,用来存储此节点的子节点。那么,这个数组建立的时候,应该建多大?

      每个节点会有多少个子节点?这要看输入的字符串里的字符总共有多少种字符。

      如果我们能保证输入的字符串全都是字母,那么每个节点最多有26个子节点(因为只有26个字母),即每个节点的数组应该能容纳26个元素。

      

      根据具体情况来选择R值吧,每个节点最多有R个子节点,即每个节点的数组应该能容纳R个元素。

      此时,再看回我们的这个例子:

      

      假设我们输入的字符串的字符串全都是字母,那么每个节点都有26个子节点,但实际用到的只有几个,其余全都是空节点,这是对内存的极大浪费!

      放心,下面会介绍优化方法。

    先看回R-way tries的实现代码:

      

      

      

    再看R-way tries的效率:

      

      注释:

      1. N为所有字符串的总个数。

      2. L为字符串的长度

      3. R为我们选择的R值(上文提及的R)

      

      4. moby.txt和actors.txt是测试文件。

      从图中可看出,R-way tries比红黑树快,比哈希表慢,且如果字符串过多,则内存有可能原地爆炸。

    3. ternary search tries

      这个名字直译过来就是三叉搜索树,但是网络上没看到正统的翻译,所以这里也保留了英文原名。

      ternary search tries简称TST。它是R-way tries的进化版。

      每个节点都有三个子节点,左边的子节点比此节点小,中间的子节点等于此节点,右边的子节点比此节点大。

      从一个例子直观的感受一下:(有些空节点标出来是为了避免歧义)

      

      与R-way tries有点像,但根节点不再为空,且找字符串的方法要些许不同。TST的节点最多有3个节点。

      先来看查找字符串:

    寻找字符串“by”

      by的第一个字符为b,与根节点对比,b<s,故去s的左节点比较,然后发现s的左节点为b,相等:

      

      by的第二字符为y,从目前所处的b节点出发,与此节点的中间节点相比较,相等:

      

      by没第三个字符,我们找到的节点y有数字4,故查找成功,by对应的数字为4。

    寻找字符串“shor”

      shor的第一个字符为s,与根节点对比,s=s,:

      

      shor的第二个字符为h,从目前所处的s节点出发,与此节点的中间节点相比较,相等:

      

     shor的第三个字符为e,从目前所处的h节点出发,与此节点的中间节点相比较,o>e,故去此节点的右节点中进行比较,相等:

     

      shor的第四个字符为r,从目前所处的o节点出发,与此节点的中间节点相比较,相等:

      

      shor没第五个字符,我们找到的节点r没有数字,故查找失败,shor不在这堆字符串里。

      总结一下:

      1. 假设要查找的字符串为X;整数变量int d=1; 节点Node=根节点;

      2. 把X的第d个字符与Node节点进行比较,如果这个字符大,则去根Node的右节点A,且Node=A; 如果这个字符小,则去Node的左节点B,且Node=B;如果相等,去第4步;

      3. 重复第2步,直到找到与X的第d个字符相等的节点为止,如果找不到,则说明要查找的字符串X不存在。

      4. d+=1; Node=Node的中间节点C;如果C不存在,且d<=X的长度,则说明要查找的字符串X不存在。重复第二步,直到d>X的长度为止,此时,去第5步;

      5. 检查X的最后一个字符所在的节点(即现在的Node)是否有数字,如果有,则说明找到这个字符串了;如果没有,则找不到。

      看懂了查找字符串,添加字符串也差不多了:

    添加字符串“share”,对应数字为16:

      share的第一个字符为s,与根节点进行比较,相等:

      

      share的第二个字符为h,与s节点的中间节点进行比较,相等:

      

      share的第三个字符为a,与h节点的中间节点进行比较,a小;

      去与h的左节点e进行比较,a小;

      e节点左节点为空节点,把a填进去:

      

      share的第四个字符为r,与a节点的中间节点进行比较,发现中间节点为空节点,把r填进去:

      

      share的第五个字符为e,与r节点的中间节点进行比较,发现中间节点为空节点,把e填进去,且由于share没第六个字符,把数字也进去:

      

      添加完毕。

      删除操作与R-way tries的一模一样,这里不做累述

     实现代码:

      

      

      

    TST的效率:

      

      

     注释:

      1. N为所有字符串的总个数。

      2. L为字符串的长度

      3. R为我们选择的R值(上文提及的R)

      

      4. moby.txt和actors.txt是测试文件。

      TST速度上比哈希表还要快,占用的空间相对来说也不多!

    4. 算法应用

    字符串排序:

      在TST的基础上是可以进行字符串排序的,只需从最左边一直读到最右边即可。

      

    代码实现:

      

    搜索引擎:

      

      像google这类的引擎,我们可以用TST来实现。

      例如在我们上述的例子中:

      

      当我们输入了"she",我们先搜索she:

      

      经过3次键索后,找到了she,然后我们发现e下面有非空节点,继续往下走,就可以得到共用“she”的字符串:she,shells,sheore。

    实现代码:

      

      TST的应用还有很多,这里不一一列举。

  • 相关阅读:
    Mac下搭建SVN服务器
    iOS的扩展类,扩展属性
    关于TableViewCell高度自适应问题的整理
    关于适配的一点考虑
    Visual format language
    css命名定义
    定位之初解
    定位以及relative和absolute的结合
    float的一点想法
    javascript的学习路子
  • 原文地址:https://www.cnblogs.com/mcomco/p/10416436.html
Copyright © 2011-2022 走看看