日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

数据结构-----基于双数组的Trie树

發布時間:2024/4/19 编程问答 32 豆豆
生活随笔 收集整理的這篇文章主要介紹了 数据结构-----基于双数组的Trie树 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

Trie樹簡介

Trie樹也稱字典樹,在字符串的查找中優勢比較明顯,適用于在海量數據中查找某個數據。因為Trie樹的查找時間和數據總量沒有關系,只和要查找的數據長度有關。比如搜索引擎中熱度詞語的統計。除此之外也可用于將數據按字典序排序。
另外Trie樹是典型的空間換時間的數據結構,構建一顆Trie樹需要花費比較大的內存空間。

簡單的Trie樹的實現有兩種方式,每個節點存儲一個子節點數組,或者用鏈表將每個節點的子節點連接起來。
采用數組浪費了大量的空間,因為在Trie樹使用的過程中不可能整個樹是滿的,所以數組中絕大多數的位置都是空閑的,空間得不到有效利用,但是因為數組支持隨機訪問,所以查找時效率很高
采用鏈表雖然節省了不少空間,但是在查找的過程中難以高效定位。

為了將兩者的優點有效地結合起來,出現了一種僅用兩個線性數組描述的Trie樹,稱為雙數組Trie樹(DAT)。兩個數組分別是base和check,理解它們的含義是學習DAT的關鍵。

base數組和check數組

base和check數組用于記錄節點和節點之間的關系,而自身的數據是通過在數組中的索引表示的。上文提到的Trie樹為每個節點都開辟一定大小的數組來存儲孩子節點,但是開辟的大小是事先規定好的,只能處理比如說英文單詞這種簡單的數據,而對于中文的處理不是很理想,雙數組Trie樹將每個數據都映射成一個整型數,既是數據的編碼又可用于在base和check數組中定位索引。

雙數組Trie樹中只有樹結構意義上的節點,并無實際的表述,而節點與節點之間的關系僅是通過base和check記錄的。

編碼
在理解base和check數組之前,先了解一下有關編碼的事情。計算機在存儲數據時都是以二進制的形式存儲的,
比如想要存儲字符’a’,并不是直接將’a’放入內存,而是將其轉換成對應的編碼(一個整型數97),隨后進行進制轉換存儲在內存中。想要存儲中文”一”,也是需要要將其轉換成對應的編碼(19968),然后再進行存儲。
所以可以理解成,任何數據都有唯一的整數值與其對應,這就是數據的編碼。目前比較流行的就是Unicode碼,可以有效處理中文字符。

起始索引begin
有了編碼的知識,首先想到的是將每個數據轉換成對應的Code,然后這個Code就是這個數據在base和check中的下標。但是沒辦法維護節點之間的關系,不過也不能說這種想法是錯的,至少對了一部分。原因是在base和check中的下標不是只由Code組成,還需要一個起始索引begin,這個begin是在程序中需要計算的整數(目前先假設begin以求出)。
這個起始索引begin很像HashTable中的hash地址,在哈希表中,對于每個數據,都有一個確定的哈希散列函數將這個數據轉換成一個整數,這個整數就是數據在哈希表中的索引。
而對于雙數組而言,begin + Code組成了某個數據在base和check中的下標。

base數組
但是你可能會問,這樣也沒有父節點子節點的關系?
這就是base要解決的事情,考慮一下,每個數據都有一個唯一的begin作為它的起始索引,這個begin就好比于是數據的歸屬地。父節點有,孩子節點也同樣有,那么就可以利用base數組來存儲數據的孩子節點的起始索引child_begin。也就是說,base[begin + code] = child_begin,這樣,在知道父節點的編碼,和父節點的起始索引begin后,就能找到它的孩子節點的起始索引child_begin,然后child_begin + child_code就是孩子節點在base和check數組中的下標。

你可能又會問,父節點會有多個孩子節點,而base中只存儲了一個begin?
具有相同父節點的節點之間互為兄弟關系,只需要保證兄弟節點具有相同的起始索引begin就可以了,對嗎。

check數組
而對于check數組,它存儲的值是每個數據的起始索引begin,也就是說
check[begin + code] = begin。它的作用在于判斷某個數據是否存在,比如說現在知道了父節點的begin和code,想要判斷父節點有沒有表示某個數據的孩子節點,這個數據的編碼為child_code,那么就可以先求出父節點的孩子們的起始索引child_begin(利用base數組就可以了,child_begin = base[begin + code])。求出之后考慮,如果那個數據存在,那么它就是父節點的孩子,它的起始索引就是child_begin,那么它的check數組中存儲的就應該是child_begin。所以可以根據
check[child_begin + child_code]是否等于child_begin來判斷是否存在這個數據。

初始化操作

以下面的數據為例,任務是將這些數據放入Trie樹中。

//如下數據 一舉 一舉一動 一舉成名 一舉成名天下知 萬能 萬能膠

每個字的編碼如下:

膠 名 動 知 下 成 舉 一 能 天 萬 33014 21517 21160 30693 19979 25104 20030 19968 33021 22825 19975

中文的編碼一般都比較大(小于65536),所以在創建一棵雙數組Trie樹時,需要為base和check開辟很大的內存。又因為begin的值也有可能很大,所以僅僅開辟65536是遠遠不夠的。
先考慮上面的問題,應該采用什么方法將這些數據添加到樹中,換言之就是放入base和check數組中呢。

根據base的含義,兄弟節點之間具有相同的起始索引begin,所以在添加的過程中,每次添加的節點們是互為兄弟的關系。

起始索引begin的選擇
對于互為兄弟的幾個數據,假設他們的編碼是a1,a2,a3,…,an。選擇begin的依據是需要滿足:

check[begin + a1] = 0; check[begin + a2] = 0; check[begin + a3] = 0;... check[begin + an] = 0

check數組的賦值
初始化時check中的元素都為0,表示沒有位置被占用,check[i]不為0表示i這個索引位置已經被其他的數據占用了,需要重新為這些兄弟數據找begin。而找到滿足上述條件的begin之后,需要將這些數據的check賦值為他們的起始索引begin,表示對應索引位置已經被占用。

check[begin + a1] = begin; check[begin + a2] = begin; check[begin + a3] = begin;... check[begin + an] = begin;

base數組的賦值
而base數組在什么時候賦值呢,base數組存儲的是孩子們的起始索引child_begin,也就是說需要在為孩子們找到child_begin之后才能將父節點的base設置成child_begin。即

base[begin + code] = child_begin;

又因為初始化操作是從根節點開始的,所以很顯然需要利用遞歸的思想。

舉例
在上面的例子中,互為兄弟的字如下:

"一","萬" 父節點為根節點 "舉" 父節點為"一" "null", "一", "成" 父節點為"舉" "動" 父節點為"一" "名" 父節點為"成" ... "能" 父節點為"萬" ...

注意在計算兄弟節點時會多計算一個”null”,表示葉子節點,標識從根節點到它的父節點為止表示的數據是一個完整的詞,也就是”一舉”是一個詞。
不同于其他的數據節點,子結點的編碼為0,在base中的值需要設為負數,在判斷是否存在某個詞時,需要利用這個負值判斷。
比如說,想要判斷”一舉”是否在詞典中,只需要找到”舉”的孩子們的起始索引child_begin后,判斷base[child_begin + 0]是否是負數即可。
那在插入數據的過程中怎么判斷哪個節點是葉子結點呢??紤]一下,在插入數據的時候,每次都需要為某個數據生成它的孩子數據(像為”舉”找到”null”,”一”,”成”一樣),而葉子結點沒有孩子數據,所以可以根據找到的孩子是否為空進行判斷。

初始時,為根節點的base和check賦值。根節點的下標為0,base[0]表示孩子們的起始索引,初始化時設置為1.

base[0] = 1; check[1] = 0;

初始化操作流程:

  • 先選取第一批互為兄弟的節點,”一”和”萬”。
  • 為它們尋找一個滿足條件的起始索引begin1,并改變二者的check.
  • 找到”一”的孩子”舉”。
  • 為”舉”尋找滿足條件的起始索引begin2,并改變check。
  • 找”舉”的孩子們”null”,”一”,”成”。
  • 為孩子們找到滿足條件的起始索引begin3。
  • 找”null”的孩子,沒有找到,將”null”的base賦值為負數
  • 找”一”的孩子”動”。
  • ….
  • 當找到最后的,遞歸到最后一層時,向上返回每次尋找的begin。
  • ….
  • 將”舉”的base設置為begin3,將”一”的base設置為begin2,將”0”的base設置為begin1。
  • 然后再為”萬”字進行同樣的操作。
  • 查詢操作

    整個流程下來,所有的數據都在base和check中存儲。對于查詢操作,比如說給定一個詞語,需要判斷這個詞語是否在Trie樹表示的詞典中,步驟如下:
    1. 計算begin1(base[0])
    2. 計算第一個字的編碼code1
    3. 判斷第一個字是否存在,check[begin1 + code1] == begin1表示存在
    4. 若存在,將begin賦值為第二個字的起始索引,begin2 = base[begin1 + code1]
    5. 計算第二個字的編碼code2
    6. 判斷第二個字是否存在,check[begin2 + code2] == begin2表示存在
    7. 若存在,將begin賦值為第三個字的起始索引,begin3 = base[begin2 + code2]
    8. …
    9. 當全部遍歷后,beginN表示最后一個字的孩子們的起始索引。判斷base[beginN + 0] <
    0是否成立,成立就表示存在,否則表示不存在。

    注:只要有一處check[begin + code] != begin就表示不存在,返回fasle

    流程示例

    下面以

    一舉 一舉一動 一舉成名 一舉成名天下知 萬能 萬能膠

    為例,進行初始化操作的說明。

    每個字的編碼如下:膠 名 動 知 下 成 舉 一 能 天 萬 33014 21517 21160 30693 19979 25104 20030 19968 33021 22825 19975












    在處理完以”一”字開頭的詞語后,繼續處理以”萬”字開頭的詞語。如果詞典中詞語有很多,則依次處理。
    需要考慮的函數有

  • 找到以某個字作為前綴字的字,比如說處理完”舉”字,需要尋找”舉”的孩子”null”,”一”,”成”。
  • 為互為兄弟關系的幾個字尋找滿足條件的起始索引begin。然后遞歸地進行步驟1。當遇到葉子節點時,將葉子節點的base設置成負數。每層遞歸返回本層的begin作為父節點的base值。
  • 參考的博客
    http://www.hankcs.com/program/java/%E5%8F%8C%E6%95%B0%E7%BB%84trie%E6%A0%91doublearraytriejava%E5%AE%9E%E7%8E%B0.html

    總結

    以上是生活随笔為你收集整理的数据结构-----基于双数组的Trie树的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。