软件水平 > 初级资格 > 程序员 > 文章内容

计算机软考程序员常考基础必知必会(58)

2016-5-4编辑:ljnbset

后缀树

  后缀树(Suffix tree)是一种数据结构,能快速解决很多关于字符串的问题。后缀树提出的目的是用来支持有效的字符串匹配和查询。

  学习后缀树之前,先了解一下Trie这个数据结构Trie是一种搜索树,可用于存储并查找字符串。Trie每一条边都对应一个字符。在Trie中查找字符串S时,只要按顺序枚举S的各个字符,从Trie的根节点开始选择相应的边走,如果枚举完的同时恰好走到Trie树的叶子节点,说明S存在于Trie中。如果未到达叶子节点,或者枚举中未发现相应的边,则S没有被包含在Trie中。

  后缀树就是一种压缩后的Trie树。

  比如 S:banana,对S建立后缀树。

  首先给出S的后缀们

  0:banana

  1:anana

  2:nana

  3:ana

  4:na

  5:a

  6:空

  为了更清楚的表示后缀,我们在后缀的后面加上$

  0:banana$

  1:anana$

  2:nana$

  3:ana$

  4:na$

  5:a$

  6:$

  然后对其进行分类:

  5:a$

  3:ana$

  1:anana$

  0:banana$

  4:na$

  2:nana$

  6: $

  后缀树的应用:

  example 1:在树中查找an(查找子字符串)

  example 2:统计S中出现字符串T的个数

  每出现一次T,都对应着一个不同的后缀,而这些后缀们又对应着同一个前缀T,因此这些后缀必定都属于同一棵子树,这棵子树的分支数就是T在S中出现的次数。

  example 3:找出S中最长的重复子串,所谓重复子串,是指出现了两次以上。首先定义节点的“字符深度” = 从后缀树根节点到每个节点所经过的字符串总长。找出有最大字符深度的非叶节点。则从根节点到该非叶节点所经过的字符串即为所求。

后缀树的用途,总结起来大概有如下几种 :

  1. 查找字符串o是否在字符串S中

  方案:用S构造后缀树,按在trie中搜索字串的方法搜索o即可。

  原理:若o在S中,则o必然是S的某个后缀的前缀。

  听起来有点拗口,举个例子。例如S: leconte,查找o: con是否在S中,则o(con)必然是S(leconte)的后缀之一conte的前缀。有了这个前提,采用trie搜索的方法就不难理解了。

  2. 指定字符串T在字符串S中的重复次数

  方案:用S+'$'构造后缀树,搜索T节点下的叶节点数目即为重复次数 。

  原理:如果T在S中重复了两次,则S应有两个后缀以T为前缀,重复次数就自然统计出来了。

  3. 字符串S中的最长重复子串

  方案:原理同2,具体做法就是找到最深的非叶节点。

  这个深是指从root所经历过的字符个数,最深非叶节点所经历的字符串起来就是最长重复子串。为什么要非叶节点呢?因为既然是要重复,当然叶节点个数要>=2。

  4. 两个字符串S1,S2的最长公共部分

  方案:将S1#S2$作为字符串压入后缀树,找到最深的非叶节点,且该节点的叶节点既有#也有$(无#)。大体原理同3。

  //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

  后缀树的存储:为了节省空间,我们不在边上存储字符串,而是存储该字符串在原串中的起止位置,空间复杂度O(n)。

  后缀树的构造:最简单的方法,使用Trie的构造方法,时间复杂度为O(n^2);

  后缀树也可以在O(n)的时间复杂度内构造,但比较复杂。

  如,基本思路:先向后缀树中插入最长的后缀串(S本身),其次插入次长的后缀串,以此类推,最后插入空串。定义后缀链接(Suffix Link)=从节点A指向节点B的指针,B所表示的子串是A所表示的子串的最长后缀。既,根节点到A所经过的字符串s=aw,则从根节点到B所经过的字符串为w。

  算法所用符号描述:

  后缀树的构造,算法流程:

  1)定义SL(root)=root,首先插入S,此时后缀树仅有两个节点。

  2)设已经插入了S(i),现在要插入S(i+1),分两种情况讨论:

  1:P(S(i))在插入之前已经存在,(如,na,ana,a是na的parent),则P(S(i))有后缀链接,令u=SL(P(S(i))),从u开始沿着树往下查找,在合适的地方插入。

  2:P(S(i))是插入S(i)过程中产生的,此时G(S(i))必定存在并有后缀链接,比如(na,ana,bana),令u=SL(G(S(i))),w=W(G(S(i)),P(S(i))).从u开始,对w进行快速定位, 在合适的地方插入新的节点。

  不断重复以上步骤,即可完成后缀树的构造。

后缀树代码如下:

  //SuffixTree.h

  typedef struct node //声明节点的结构

  {

  string strdata; //存储节点上的字符串

  vector Child; //存储该节点的子节点的地址

  int flag; //辅助标志位,用0和1表示该节点是否有子节点

  int breakpoint; //辅助变量,当该节点需要分裂时,用于记录分裂点的位置

  }*mynode;

  classCSuffixTree

  {

  public:

  mynode ST; //ST生成的后缀树的根节点

  mynode point; //point节点指针,搜索时指向搜索节点的父节点,搜索结束时根据搜索

  //结果指向要操作的节点

  CSuffixTree(string str);

  ~CSuffixTree(void);

  int Search(string str);

  void CreatTree();

  void Show(mynode ST);

  void PrintNode(mynode p, int c, vector& isend);

  private:

  string data; //data源字符串变量,在构造函数中初始化

  string left; //left用于记录每次搜索结束后,目标字符串中的剩余字符串

  };

  //SuffixTree.cpp

  //构造函数,初始化data变量和ST,point指针并产个根节点的第一个子节点,ST的flag置1

  CSuffixTree::CSuffixTree(stringstr)

  {

  data = str;

  ST = (mynode) new node;

  point = (mynode) new node;

  point->strdata = data[0];

  point->flag = 0;

  ST->Child.push_back(point);

  ST->flag = 1;

  }

  //析构函数

  CSuffixTree::~CSuffixTree(void)

  {

  }

  voidCSuffixTree::CreatTree()

  {

  int i, j, n, h, ic, jc;

  string temp;

  string tempuse;

  mynode cnode;

  for (i = 1; i <= (data.length()-1); i++)//调用两层循环,产生目标字符串每一个前缀的所 有后缀

  {

  for (j = 0; j <= i; j++)

  {

  temp.erase(temp.begin(),temp.end());

  ic = i;

  jc = j;

  for (; jc <= ic; jc++)

  {

  temp.insert(temp.end(), data[jc]);

  }

  n = Search(temp); //调用Search函数搜索生成的字符串

switch (n)

  {

  case (-1)://分裂节点,父分裂点为point->breakpoint

  tempuse =point->strdata;

  cnode=(mynode) new node;

  if (point->Child.size() !=0)

  {

  cnode->Child =point->Child;

  }

  cnode->flag =point->flag;

  point->Child.erase(point->Child.begin(),point->Child.end());

  point->strdata.erase(point->strdata.begin(),point->strdata.end());

  for (h = 0; h<(point->breakpoint); h++)

  {

  point->strdata.insert(point->strdata.end(),tempuse[h]);

  }

  for (h =(point->breakpoint); h < p>

  {

  cnode->strdata.insert(cnode->strdata.end(), tempuse[h]);

  }

  point->Child.push_back(cnode);

  cnode = (mynode) new node;

  cnode->strdata = left;

  cnode->flag = 0;

  point->Child.push_back(cnode);

  point->flag = 1;

  break;

  case (0)://do nothing

  break;

  case (1)://在叶节点point处追加left字符串

  point->strdata =point->strdata+left;

  break;

  case (2)://在父节点point下添加子节点cnode

  cnode = (mynode) new node;

  cnode->strdata = left;

  cnode->flag = 0;

  point->Child.push_back(cnode);

  point->flag = 1;

  break;

  }

  }

  }

  }

  //返回1: 则在指针point所指向的节点的strdata后追加 left字符串

  //返回2: 则生成point所指向的节点的子节点,子节点的strdata值为left字符串

  //返回0: 则donothing

  //返回-1:则分裂节点将分裂点写入point指针所指向的 节点的breakpoint,并将目标字符串的剩余字符串写入left

  intCSuffixTree::Search(string str)

  {

  stack s;

  int i, n = 0;

  mynode child;

  char target;

  point = ST; //初始搜索位置为根

  child = NULL;

  for (i = (str.length()-1); i >= 0; i--)//将目标字符串str压栈

  {

  s.push(str[i]);

  }

  while(!s.empty())//直到搜索完str串

  {

  //寻找point所指向的节点下与str首字母相同的子节点

  for (i = 0; i <=(point->Child.size()-1); i++)

  {

  if(point->Child[i]->strdata[0] == s.top())

  {

  child =point->Child[i]; //child指针指向与str具有相同首字母的节点

  break;

  }

  }

if (child == NULL)//父节点point下没有首字母相同的子节点

  { //将str中剩余的字符串保存在left中

  left.erase(left.begin(),left.end());

  while(!s.empty())

  {

  left.insert(left.end(),s.top());

  s.pop();

  }

  return (2);

  }

  target = s.top(); //

  s.pop();

  if (1)

  {

  //child节点下的每个字符与str中的字符顺序比较

  for (i = 0; i <=(child->strdata.length()-1); i++)

  {

  if (child->strdata[i] ==target)

  {

  if (!s.empty())

  {

  target = s.top();

  s.pop();

  continue;

  }

  else return(0); //若str中的字符串先于strdata中的字符串完成比较,则说明该字符 //串已经包含在树中

  }

  else //比较中出现不同,需要分裂节点

  {

  point = child; //point指向要分裂的节点

  left.erase(left.begin(),left.end()); //将str中剩余的字符串写入left

  s.push(target);

  while(!s.empty())

  {

  left.insert(left.end(),s.top());

  s.pop();

  }

  point->breakpoint =i; //写入父节点point的分裂点

  return (-1); //分裂节点

  }

  }

  point = child; //走出循环则进行下一个节点的搜索

  if (!point->flag)//判断刚刚搜索的是否为叶节点,若是则停止搜索

  {

  left.erase(left.begin(),left.end());

  s.push(target);

  while(!s.empty())

  {

  left.insert(left.end(),s.top());

  s.pop();

  }

  return (1);

  }

  s.push(target);

  child = NULL;

  }

  }

  //字符串str搜索完成,仍没有到达叶节点,返回0

  return(0);

  }

  voidCSuffixTree::Show(mynode m_pSTreeRoot)

  {

  vector bIsEnd;

  bIsEnd.push_back(0);

  cout << "Root" << endl;

for(int i = 0; i <(int)m_pSTreeRoot->Child.size(); i++)

  {

  if(i ==(int)m_pSTreeRoot->Child.size() - 1)

  {

  bIsEnd[0] = 1;

  }

  PrintNode(m_pSTreeRoot->Child[i], 1,bIsEnd);

  }

  cout << endl;

  }

  voidCSuffixTree::PrintNode(mynode p, int c, vector& isend)

  {

  for(int j=0; j<>

  {

  if(isend[j] == 0)

  {

  if(j != c-1)

  cout << "│";

  else

  cout << "├";

  }

  else

  {

  if(j != c-1)

  cout << " ";

  else

  cout << "└";

  if(j != c-1)

  cout << " ";

  else

  cout <<""; //最后一个

  }

  }

  cout << " "

  //if(p->Child.size() == 0)cout <<" (" << p->nIndex << ")";

  cout << endl;

  for(int i=0; i<(int)p->Child.size();i++)

  {

  if(isend.size() == c)

  {

  isend.push_back(0);

  }

  else

  {

  isend[c]=0;

  }

  if(i == (int)p->Child.size() - 1)

  {

  if(isend.size() == c)

  isend.push_back(1);

  else

  isend[c]=1;

  }

  PrintNode(p->Child[i], c + 1,isend);

  }

  }

  //mian.cpp

  intmain(int argc, char* argv[])

  {

  string a;

  cout << "Please input astring:";

  cin >> a; //通过输入获得目标字符串a

  CSuffixTree mytree(a); //产生CsuffixTree类的实例mytree

  mytree.CreatTree(); //调用CreatTree方法创建一棵后缀树

  mytree.Show(mytree.ST); //调用Show方法输出生成的后缀树

  }

计算机软考程序员常考基础必知必会(57)

热点推荐

登录注册
触屏版电脑版网站地图