查找算法 | 鍵樹詳細分析

鍵樹,又稱數字查找樹(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樹+二分查找搞定。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章