1. 程式人生 > >ACM-字串-字典樹

ACM-字串-字典樹

字典樹,也叫trie樹,是一種比較實用的資料結構,無論是在ACM競賽的題目中,還是字串相關的某些實際應用領域內,它都能發揮巨大的作用。

首先來看看字典樹的本質是什麼。它其實是一棵儲存了很多字串的樹,這棵樹上的每一條邊就是某個或某些字串中的一個字元,而從根節點到某一個特定節點所經過的一條路徑上的所有邊組成的就是字典樹所儲存的某一個字串。不難看出,字典樹就是一顆多叉樹,它利用字串的字首來建立了這棵樹,從而達到了節省儲存空間(所有相同字首都只儲存一次),優化查詢速度(查詢單個單詞是否存在的時間複雜度僅為該單詞的長度)的目的。

再來看看字典樹的應用場景。在ACM競賽中的作用不言而喻,是查詢單詞、統計字首等操作,在時間苛刻條件下的首選資料結構。而在現實生活中的應用,字典樹就可以大顯身手了,比如最常見的對字串進行字典序排序,當字串的數量巨大、長度較長時,普通的排序方式可能就顯得力不從心,但是如果使用字典樹就只需要在建好樹後進行先序遍歷即可,比如上圖中的abc、abcd、abd、.......;再比如同樣很常見的輸入提示功能,例如百度的搜尋框,雖然不知道百度是怎麼做的,但是用字典樹也可以實現相同的效果,並且效率也不錯,只需要事先以各種關鍵詞建立字典樹,在輸入關鍵詞時動態的搜尋字典樹,即可做到高效的提示功能,當然也可以將一次完整查詢的關鍵詞插入到字典樹,這樣下次就可以提示搜尋歷史記錄了。

最後需要具體實現字典樹這一經典的資料結構了。其實想要實現字典樹,只需要解決兩個問題即可:1、如何表達字典樹的多叉屬性 2、如何標記字串的結束。其實這兩個問題就是對應字典樹的插入和查詢操作。

首先解決字典樹的多叉性,這主要體現在其節點的結構上。考慮到每一個節點可能不止延伸出一條邊,不難想到用一個指標陣列去儲存當前節點可以到達的所有下一個節點。

最後至於如何標記字串的結束,其實也並不複雜,只需在每一個節點上儲存一個標記,記錄該節點是否是儲存資訊的結尾即可。

所以,綜合這些需要儲存的資訊,可以使用一個結構體對它們進行封裝,以此來表示字典樹的一個節點。

const int MAXN = 26;
struct Trie
{
    // 代表當前節點可以延伸出的邊,數量可變
    Trie *Next[MAXN];
    // 標記當前節點是否是儲存資訊的結尾,也可以代表字首個數
    int Flag;
    Trie()
    {
        // 初始化以該資訊為字首的資訊個數
        Flag = 1;
        memset(Next, NULL, sizeof(Next));
    }
}*root;

接下來就需要實現字典樹的插入和查詢操作,以及最後的釋放佔用空間操作了。

1、插入操作

將某一個資訊插入字典樹,其實就是依次將該資訊的字首資訊儲存在對應節點中,並修改相應標記的值即可。

void Insert(char *str)
{
    int len = strlen(str);
    Trie *p = root, *q;
    // 將str的每一個字元插入trie樹
    for(int i=0; i<len; ++i)
    {
        int id = str[i] - 'a';
        // 如果沒有邊,則新建一個trie節點,產生一條邊,用以代表該字元
        if(p->Next[id] == NULL)
        {
            q = new Trie();
            p->Next[id] = q;
            p = p->Next[id];
        }
        // 如果存在邊,則沿著該邊走
        else
        {
            p = p->Next[id];
            // 累加以該資訊為字首的資訊個數
            ++(p->Flag);
        }
    }
}

2、查詢操作

查詢某個資訊是否存在於字典樹中,實質上也是將該資訊的字首資訊與字典樹上儲存的對應位置的資訊進行匹配,然後判斷標記的值即可。

int Query(char *str)
{
    int len = strlen(str);
    Trie *p = root;
    // 在trie樹上順序搜尋str的每一個字元
    for(int i=0; i<len; ++i)
    {
        int id = str[i] - 'a';
        p = p->Next[id];
        // 若為空集,表示不存以此為字首的資訊
        if(p == NULL) return 0;
    }
    // 返回以該資訊為字首的資訊個數
    return p->Flag;
}

3、刪除操作

遞迴釋放字典樹的每一個節點佔用的空間即可。

void Free(Trie* T)
{
    if(T == NULL) return;
    // 釋放trie樹的每一個節點佔用的記憶體
    for(int i=0; i<MAXN; ++i)
    {
        if(T->Next[i]) Free(T->Next[i]);
    }
    delete(T);
}

以一道例題,演示字典樹的用法,HDOJ:1251,時空轉移(點選開啟連結),題目如下:

統計難題

Time Limit: 4000/2000 MS (Java/Others)    Memory Limit: 131070/65535 K (Java/Others)
Total Submission(s): 23388    Accepted Submission(s): 9761


Problem Description Ignatius最近遇到一個難題,老師交給他很多單詞(只有小寫字母組成,不會有重複的單詞出現),現在老師要他統計出以某個字串為字首的單詞數量(單詞本身也是自己的字首).

Input 輸入資料的第一部分是一張單詞表,每行一個單詞,單詞的長度不超過10,它們代表的是老師交給Ignatius統計的單詞,一個空行代表單詞表的結束.第二部分是一連串的提問,每行一個提問,每個提問都是一個字串.

注意:本題只有一組測試資料,處理到檔案結束.

Output 對於每個提問,給出以該字串為字首的單詞的數量.

Sample Input banana band bee absolute acm ba b band abc
Sample Output 2 3 1 0
Author Ignatius.L
Recommend Ignatius.L   |   We have carefully selected several similar problems for you:  1075 1247 1671 1298 1800  題意:

題目說的很明白了,就是給出一個字串,統計在已有的字串中以該字串為字首的字串的個數。

分析:

涉及到字串字首,如果不能暴力解決的話,方法幾乎只有字典樹了,再說了統計字首本身也是字典樹的功能之一。需要注意的是,可能動態分配記憶體在不同的平臺下的處理方式有些差別,這題如果使用G++交的話可能會超時,換成使用C++提交即可。

原始碼:

#include <cstdio>
#include <cstdlib>
#include <cstring>

const int MAXN = 26;
struct Trie
{
    // 代表當前節點可以延伸出的邊,數量可變
    Trie *Next[MAXN];
    // 標記當前節點是否是儲存資訊的結尾,也可以代表字首個數
    int Flag;
    Trie()
    {
        // 初始化以該資訊為字首的資訊個數
        Flag = 1;
        memset(Next, NULL, sizeof(Next));
    }
}*root;

void Insert(char *str)
{
    int len = strlen(str);
    Trie *p = root, *q;
    // 將str的每一個字元插入trie樹
    for(int i=0; i<len; ++i)
    {
        int id = str[i] - 'a';
        // 如果沒有邊,則新建一個trie節點,產生一條邊,用以代表該字元
        if(p->Next[id] == NULL)
        {
            q = new Trie();
            p->Next[id] = q;
            p = p->Next[id];
        }
        // 如果存在邊,則沿著該邊走
        else
        {
            p = p->Next[id];
            // 累加以該資訊為字首的資訊個數
            ++(p->Flag);
        }
    }
}

int Query(char *str)
{
    int len = strlen(str);
    Trie *p = root;
    // 在trie樹上順序搜尋str的每一個字元
    for(int i=0; i<len; ++i)
    {
        int id = str[i] - 'a';
        p = p->Next[id];
        // 若為空集,表示不存以此為字首的資訊
        if(p == NULL) return 0;
    }
    // 返回以該資訊為字首的資訊個數
    return p->Flag;
}

void Free(Trie* T)
{
    if(T == NULL) return;
    // 釋放trie樹的每一個節點佔用的記憶體
    for(int i=0; i<MAXN; ++i)
    {
        if(T->Next[i]) Free(T->Next[i]);
    }
    delete(T);
}

int main()
{//freopen("sample.txt", "r", stdin);
    char str[15];
    // 建立trie樹的根節點
    root = new Trie();
    while(gets(str) && str[0]!='\0')
    {
        Insert(str);
    }
    while(~scanf("%s", str))
    {
        printf("%d\n", Query(str));
    }
    Free(root);
    return 0;
}

其餘類似的題目還有,HDOJ:1004、1671,UESTC:1060。