這篇簡單的談談后綴樹原理及實現。
如前綴樹原理一般,后綴trie樹是將字符串的每個后綴使用trie樹的算法來構造。例如banana的所有后綴:
0: banana 1: anana 2: nana 3: ana 4: na 5: a
按字典序排列后:
5: a 3: ana 1: anana 0: banana 4: na 2: nana
形成一個樹形結構。
代碼:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// banana中不重復的字符有:a b n
/*
* a b n
* n $ a a
* a n n $
* n $ a a
* a n $
* $ a
$*/
#define SIZE 27
#define Index(c) ((c) - 'a')
#define rep(i, a, b) for(i = a; i < b; i++)
typedef struct BaseNode {
struct BaseNode*next[SIZE];
char c;
int num;
} suffix_tree, *strie;
void initialize(strie* root)
{
int i;
*root = (strie)malloc(sizeof(suffix_tree));
(*root)->c = 0;
(*root)->num = -1;
rep(i, 0, SIZE) (*root)->next[i] = NULL;
}
void insert(strie*root, const char*str, int k)
{
suffix_tree*node = *root, *tail;
int i, j;
for (i = 0; str[i] != '\0'; i++)
{
if (node->next[Index(str[i])] == NULL)
{
tail = (strie)malloc(sizeof(suffix_tree));
tail->c = str[i];
tail->num = -1;
rep(j, 0, SIZE) tail->next[j] = NULL;
node->next[Index(str[i])] = tail;
}
node = node->next[Index(str[i])];
}
tail = (strie)malloc(sizeof(suffix_tree));
tail->c = '$';
tail->num = k;
rep(i, 0, SIZE) tail->next[i] = NULL;
node->next[SIZE - 1] = tail;
}
void show(suffix_tree*root)
{
if (root)
{
int i;
rep(i, 0, SIZE) show(root->next[i]);
printf("%c\n", root->c);
if (root->num > -1)
{
printf("%d\n", root->num);
}
}
}
void destory(strie*root)
{
if (*root)
{
int i;
rep(i, 0, SIZE) destory(&(*root)->next[i]);
free(*root);
*root = NULL;
}
}
int main()
{
suffix_tree*root;
initialize(&root);
char str[] = "banana", *p = str;
int i = 0;
while(*p)
{
insert(&root, p, i);
p++;
i++;
}
show(root);
destory(&root);
return 0;
}
時間復雜度分析:算法中對於建立一串長m的字符串,需要一個外層的m次循環 + 一個內層m次循環 + 一些常數,於是建立一顆后綴字典樹所需的時間為O(m2),27的循環在這里可看作常數;
空間復雜度分析:一個字符的字符串長度為1,需要消耗的1個該字符 + 1個根節點 + 1個\$字符的空間,兩個字符的字符串長度為2,需要消耗3個字符空間+ 1個根節點 + 2個\$空間...以此類推,發現總是含有1個根節點和m個\$字符,\$的個數等於字符串長度m,而存儲的源字符串后綴所需的空間有如下規律:
$$ \begin{aligned} O(s_1) &= 1 \\ O(s_2) &= 1+2 \\ O(s_3) &= 1+2+3 \\ \cdot \cdot \cdot \\ O(s_m) &= 1+2+ \cdot \cdot \cdot + m \end{aligned} $$
設以長為m的字符串s建立后綴樹T,於是有:
$$ O(T) = O(\frac{(1 + m)m}{2} + 1 + m) = O(m^2) $$
由於上面算法對於無重復的字符串來說空間復雜度比較大,所以使用路徑壓縮以節省空間,這樣的樹就稱為后綴樹,也可以通過下標來存儲,如圖:

p.s.寫壓縮路徑的后綴樹時,腦子犯傻了...錯了,改天再把正確的補上。。。
路徑壓縮版后綴樹:
#include <iostream>
using namespace std;
#define rep(i, a, b) for(int i = a; i < b; i++)
#define trans(c) (c - 'a')
#define SIZE 26
#define MAX (100010 << 2)
struct BaseNode {
int len;
const char*s;
int pos[MAX];
BaseNode*next[SIZE];
BaseNode()
{
len = 0;
rep(i, 0, MAX) pos[i] = 0;
rep(i, 0, SIZE) next[i] = nullptr;
}
BaseNode(const char*s, int p)
{
this->s = s, this->len = p;
rep(i, 0, MAX) pos[i] = 0;
rep(i, 0, SIZE) next[i] = nullptr;
}
};
class SuffixTree {
private:
BaseNode*root;
/**/
void add(const char*s, int p);
void print(BaseNode*r);
void destory(BaseNode*&r);
public:
SuffixTree()
{
root = nullptr;
}
void insert(const char*s);
void insert(string s)
{
insert(s.c_str());
}
void remove(const char*s)
{
}
void visual()
{
print(root);
}
bool match(const char*s);
bool match(string s)
{
match(s.c_str());
}
~SuffixTree()
{
destory(root);
}
};
void SuffixTree::add(const char*s, int p)
{
int i = 0; while (s[i]) i++;
if (!root->next[p]) root->next[p] = new BaseNode(s, i);
root->next[p]->pos[i] = i;
}
void SuffixTree::insert(const char*s)
{
root = new BaseNode();
while (*s)
{
add(s, trans(*s));
s++;
}
}
bool SuffixTree::match(const char*s)
{
const char* ps = root->next[trans(*s)]->s;
while (*s) if (*ps++ != *s++) return false;
return true;
}
void SuffixTree::print(BaseNode*r)
{
if (r)
{
rep(i, 0, SIZE)
if (r->next[i])
{
cout << i << ':' << endl;
rep(j, 0, r->next[i]->len + 1)
if (r->next[i]->pos[j])
{
rep(k, 0, r->next[i]->pos[j])
cout << r->next[i]->s[k];
cout << '$' << endl;
}
}
}
}
void SuffixTree::destory(BaseNode*&r)
{
if (r)
{
rep(i, 0, SIZE) destory(r->next[i]);
delete r;
}
}
int main()
{
SuffixTree st;
st.insert("banana");
st.visual();
if (st.match("na")) cout << "Yes" << endl;
else cout << "No" << endl;
return 0;
}
上面的后綴樹都是對於一個字符串的處理方法,而廣義后綴樹將算法推廣到了不同的字符串上,但我還沒寫過,改天補上。。。
參考:https://en.wikipedia.org/wiki/Suffix_tree
