1. 程式人生 > >查詢演算法 | 鍵樹詳細分析

查詢演算法 | 鍵樹詳細分析

鍵樹,又稱數字查詢樹(Digital Search Trees),是一棵度>=2的樹,它的某個節點不是包含一個或多個關鍵字,而是隻包含組成關鍵字的一部分(字元或數字)。

如果關鍵字本身是字串,則鍵樹中的一個結點只包含有一個字元;如果關鍵字本身是數字,則鍵樹中的一個結點只包含一個數位。每個關鍵字都是從鍵樹的根結點到葉子結點中經過的所有結點中儲存的組合。

根結點不代表任何字元,根以下第一層的結點對應於字串的第一個字元,第二層的結點對應於字串的第二個字元……每個字串可由一個特殊的字元如“$”等作為字串的結束符,用一個葉子結點來表示該特殊字元。

把從根到葉子的路徑上,所有結點(除根以外)對應的字元連線起來,就得到一個字串。因此,每個葉子結點對應一個關鍵字。在葉子結點還可以包含一個指標,指向該關鍵字所對應的元素

。整個字串集合中的字串的數目等於葉子結點的數目。如果一個集合中的關鍵字都具有這樣的字串特性,那麼,該關鍵字集合就可採用這樣一棵鍵樹來表示。事實上,還可以賦予“字串”更廣泛的含義,它可以是任何型別的物件組成的串。常見鍵樹如圖所示:

注意:鍵樹中葉子結點的特殊符號 $ 為結束符,表示字串的結束。使用鍵樹表示查詢表時,為了方便後期的查詢和插入操作,約定鍵樹是有序樹(兄弟結點之間自左至右有序),同時約定結束符 ‘$’ 小於任何字元。

鍵樹的儲存結構


鍵樹的儲存結構有兩種:

  • 一種是通過使用樹的孩子兄弟表示法來表示鍵樹,即雙鏈樹
  • 一種是以樹的多重連結串列表示鍵樹,即 Trie 樹
    ,又稱字典樹

雙鏈樹


當使用孩子兄弟表示法來表示鍵樹時,樹的結點構成分為3部分:

  • symbol域:儲存關鍵字的一個字元;
  • first域:儲存指向第一棵子樹的根的指標;
  • next域:儲存指向右兄弟結點的指標。

注意:對於葉子結點來說,由於其沒有孩子結點,在構建葉子結點時,將 first 指標換成 info 指標(可選的,記錄附加資料),用於指向該關鍵字。當葉子結點(結束符 ‘$’ 所在的結點)中使用 info 域指向各自的關鍵字時,此時的鍵樹被稱為雙鏈樹

如下圖:

提示:每個關鍵字的葉子結點 $ 的 info 指標指向的是各自的關鍵字,通過該指標就可以找到各自的關鍵字的首地址。

雙鏈樹查詢功能的具體實現


查詢過程是:從根結點出發,順著first查詢,如果相等,繼續下一個first;否則沿著next(first 結點的兄弟結點)查詢。直到到了空指標為止。此時若仍未完成key的匹配,查詢不成功。

具體實現的程式碼(來自百度):

#include <stdio.h>

typedef enum{LEFT,BRANCH}NodeKind;//定義結點的型別,是葉子結點還是其他型別的結點
typedef  struct {
    char a[20];//儲存關鍵字的陣列
    int num;//關鍵字長度
}KeysType;

//定義結點結構
typedef struct DLTNode{
    char symbol;//結點中儲存的資料
    struct DLTNode *next;//指向兄弟結點的指標
    NodeKind *kind;//結點型別
    union{//其中兩種指標型別每個結點二選一
        struct DLTNode* first;//孩子結點
        struct DLTNode* info;//葉子結點特有的指標
    };
}*DLTree;

//查詢函式,如果查詢成功,返回該關鍵字的首地址,反則返回NULL。T 為用孩子兄弟表示法表示的鍵樹,K為被查詢的關鍵字。
DLTree SearchChar(DLTree T, KeysType k){
    int i = 0;
    DLTree p = T->first;//首先令指標 P 指向根結點下的含有資料的孩子結點
    //如果 p 指標存在,且關鍵字中比對的位數小於總位數時,就繼續比對
    while (p && i < k.num){
        //如果比對成功,開始下一位的比對
        if (k.a[i] == p->symbol){
            i++;
            p = p->first;
        }
        //如果該位比對失敗,則找該結點的兄弟結點繼續比對
        else{
            p = p->next;
        }
    }
    //比對完成後,如果比對成功,最終 p 指標會指向該關鍵字的葉子結點 $,通過其自有的 info 指標找到該關鍵字。
    if ( i == k.num){
        return p->info;
    }
    else{
        return NULL;
    }
}

Trie樹(字典樹)


對於Trie樹更詳細的介紹:小白詳解 Trie 樹。這篇文章寫得很細緻,如果沒有耐心看的話,最好也要瀏覽一遍,做個瞭解

若以樹的多重連結串列表示鍵樹,則樹中如同雙鏈樹一樣,會含有兩種結點:

  • 分支結點:含有 d 個指標域和一個整數域(記錄非空指標域的個數(可選));
  • 葉子結點:含有關鍵字域(完整的關鍵字、可選)和指向該關鍵字的指標域(可選);

d 表示每個結點中儲存的關鍵字的所有可能情況,如果儲存的關鍵字為數字,則 d= 11(0—9,以及 $),同理,如果儲存的關鍵字為字母,則 d=27(26個字母加上結束符 $)。

實際實現的時候,一般都偷懶,只包含那d個指標域。

如下圖:

在標準Trie樹的基礎上,可以壓縮:若從鍵樹中某個結點到葉子結點的路徑上每個結點都只有一個孩子,則可將該路徑上的所有結點壓縮成一個葉子結點。如下圖所示:

Trie樹查詢功能的具體實現


使用 Trie 樹進行查詢時,從根結點出發,沿和對應關鍵字中的值相對應的指標逐層向下走,一直到葉子結點,如果全部對應相等,則查詢成功;反之,則查詢失敗。

具體實現的程式碼(來自百度):

typedef enum{LEFT,BRANCH}NodeKind;//定義結點型別
typedef struct {//定義儲存關鍵字的陣列
    char a[20];
    int num;
}KeysType;

//定義結點結構
typedef struct TrieNode{
    NodeKind kind;//結點型別
    union{
        struct { KeysType k; struct TrieNode *infoptr; }lf;//葉子結點
        struct{ struct TrieNode *ptr[27]; int num; }bh;//分支結點
    };
}*TrieTree;

//求字元 a 在字母表中的位置
int ord(char  a){
    int b = a - 'A'+1;
    return b;
}

//查詢函式
TrieTree SearchTrie(TrieTree T, KeysType K){
    int i=0;
    TrieTree p = T;
    while (i < K.num){
        if (p && p->kind==BRANCH && p->bh.ptr[ord(K.a[i])]){
            i++;
            p = p->bh.ptr[ord(K.a[i])];
        }
        else{
            break;
        }
    }
    if (p){
        return p->lf.infoptr;
    }
    return p;
}

延伸閱讀:中文Trie樹


摘抄自:http://hxraid.iteye.com/blog/618962

由於中文的字遠比英文的26個字母多的多。因此對於trie樹的內部結點,不可能用一個26的陣列來儲存指標。如果每個結點都開闢幾萬個中國字的指標空間。估計記憶體要爆了,就連磁碟也消耗很大。

一般我們採取這樣種措施:

  1. 以詞語中相同的第一個字為根組成一棵樹。這樣的話,一箇中文詞彙的集合就可以構成一片Trie森林。這篇森林都儲存在磁碟上。森林的root中的字和root所在磁碟的位置都記錄在一張以Unicode碼值排序的有序字表中。字表可以存放在記憶體裡。
  2. 內部結點的指標用可變長陣列儲存。

特點:由於中文詞語很少操作4個字的,因此Trie樹的高度不長。查詢的時間主要耗費在內部結點指標的查詢。因此將這項指向字的指標按照字的Unicode碼值排序,然後載入進記憶體以後通過二分查詢能夠提高效率。

補充:我覺得對於字典這種應用,改動會很小的,真的可以記憶體中Trie樹+二分查詢搞定。