今天這篇文章源於上週在工做中解決的一個實際問題,它是個比較廣泛的問題,不管作什麼開發,估計都有遇到過。具體是這樣的,咱們有一份高校的名單(2657個),須要從海量的文章標題中找到包含這些高校的標題,其實就是模糊查詢。(關注公衆號 渡碼,回覆關鍵詞 trie 獲取完整源代碼)對應的僞代碼以下node
selected_titles = [] for 標題 in 海量標題: for 高校 in 高校名單: if 標題.contains(高校): selected_titles.add(標題) break複製代碼
若是是大數據開發,對應的SQL的僞代碼是這樣的python
select title from tb where title rlike '清華大學|北京大學|...2657個高校'複製代碼
上面這兩種作法都能實現咱們的需求,但它們的共同問題是查詢效率過低。若是咱們要匹配的高校不是2657個而是幾十萬甚至上百萬,那這種方式耗費時之久是不可想象的。
數組
優化這類問題一般須要在數據結構上作文章,這個問題中咱們能優化的數據結構也只有「高校名單」這個了,上面的僞代碼中咱們存放「高校名單」的數據結構是數組,當咱們查找某個title是否包含某個高校的時候,須要從頭至尾遍歷一遍「高校名單」,而且名單越長,遍歷耗時就越長。bash
清楚了數組這種數據結構的缺點後,接下來咱們重點要作的就是尋找一個數據結構能夠作到在不遍歷整個「高校名單」的狀況下就能夠完成模糊查詢。這個數據結構就是咱們今天要介紹的 Trie 樹,冷眼一看這個單詞有點陌生,又是一個樹型結構,感受會很複雜似的,實際上這個數據結構的設計思想很是簡單,一學就會。markdown
下面咱們就來學習一下 Trie 樹。爲了方面講解,假設「高校名單」裏只有下面5個元素數據結構
ABC、ABD、BCD、BCE、C、CAB、CDE複製代碼
對應的兩種數據結構以下:性能
拋開這兩種數據結構查找的時間複雜度,咱們先從直觀上看看爲何 Trie 樹的查找效率要比數組高。假設咱們要查找,「CDE」這個字符串,在數組結構中,咱們要遍歷一遍數組,比較7次才能找到結果,作了比較多的「無用功」。而在 Tire 樹中只須要比較3次就能夠找到,它的優點很是明顯,因爲樹型結構咱們根本不用考慮左側A、B開頭的兩個分支,這就大大減小了比較的次數,從而減小「無用功」。下面用一個動畫來演示一下如何建立 Trie 樹,以及在 Trie樹上查找字符串(若是視頻播放不了能夠看源碼目錄中的gif)學習
樹的創建過程其實就是遍歷字符串每一個元素並在樹上創建相應的節點。字符串查找過程其實就是按照字符串對樹進行遍歷。Trie 樹的創建與字符串查找仍是比較簡單的。大數據
不知道你們是否注意到上圖 Trie 樹中的節點有兩種顏色——白色和綠色。綠色節點表明從根節點到當前節點的字符串是「高校名單」中的字符串,也就是咱們創建 Trie 樹用到的字符串。以最左側的葉子結點「C」爲例,它表明「ABC」字符串是「高校名單」中的字符串。同理,字符串「AB」就不是「高校名單」裏的元素,由於「B」節點不是綠色的,所以當咱們在這棵樹上查找字符串「AB」時,是查不到的。這一點須要你們注意,下面編碼中咱們也有體現。優化
另外,有朋友可能會有疑問,咱們最開始的需求不是模糊查詢嗎,在 Trie 樹講解這部分怎麼都在說字符串全詞(精確)匹配。這是由於全詞匹配是 Tire 樹支持的最基本的查找方式,在此基礎上,咱們作一些變通就能夠很容易實現模糊匹配。
接下來,咱們就來看看代碼實現(Python版),首先建立兩個數組
colleges = utils.read_file_to_list('key_words.txt') titles = utils.read_file_to_list('titles.txt')複製代碼
colleges就是咱們一直在說的「高校名單」,titles即是「海量標題」,它們都是一維數組,數組每一個元素都是一個字符串。
再來編寫 Trie 樹相關的代碼,若是理解了 Trie 樹的設計思想,再編寫下面的代碼其實很容易。首先要定義一個類表明 Trie 樹節點
class TrieNode: def __init__(self): self.nodes = dict() # is_end=True 表明從根節點到當前節點構造Trie樹的字符串(出如今「高校名單」裏) self.is_end = False複製代碼
is_end=True就是咱們上面說的綠色節點。
再來編寫建立 Trie 樹的代碼,代碼在 TrieNode 類中
def insert_many(self, items: [str]): """ 支持輸入字符串數組,直接構造一個 Trie 樹 :param items: 字符串數組 :return: None """ for word in items: self.insert(word) def insert(self, item: str): """ 向 Trie 樹插入一個短語 :param item: 待插入的字符串 :return: None """ curr = self for word in item: if word not in curr.nodes: curr.nodes[word] = TrieNode() curr = curr.nodes[word] curr.is_end = True複製代碼
再來編寫查找 Trie 樹的代碼,代碼在 TrieNode 類中
def suffix(self, item: str) -> bool: """ 匹配前綴,也就是判斷item字符串是不是以「高校名單」中某個字符串開頭 :param item: 待匹配字符串 :return: True or False """ curr = self for word in item: if word not in curr.nodes: return False curr = curr.nodes[word] # 取得子節點 if curr.is_end: # 若是is_end=True說明當前字符串包含了「高校名單」的某個字符串 return True return False # 未匹配上複製代碼
這裏並非全詞匹配,而是前綴匹配,也就是判斷待查找的字符串item是不是以「高校名單」中某個字符串開頭。
再來編寫模糊匹配,代碼在 TrieNode 中
def infix(self, item: str) -> bool: for i in range(len(item)): sub_item = item[i:] # 將待查找的字符串分紅不一樣子串 # 若是子串的前綴在 Trie 樹中能匹配上 # 說明待查找的字符串item中包含「高校名單」中的元素, # 即實現了 tile rlike '清華大學|北京大學|...其餘大學' 的功能 if self.suffix(sub_item): return True return False複製代碼
這裏其實就是把待查找字符串item分紅不一樣子串去作前綴匹配,若是子串匹配上,那就說整個字符串item就包含了「高校名單」裏面的某個字符串。
最後,咱們運行一下上面的代碼,並記錄查找時間,與最開始數組結構那一版作個對比。代碼以下
# 數組版本 cnt = 0 start_time = int(time.time() * 1000) for title in titles: for x in colleges: if x in title: cnt += 1 break end_time = int(time.time() * 1000) print(cnt) print('spend: %.2fs' % ((end_time - start_time) / 60.0)) # Trie 樹版本 root = TrieNode() root.insert_many(colleges) cnt = 0 start_time = int(time.time() * 1000) for title in titles: if root.infix(title): cnt += 1 end_time = int(time.time() * 1000) print(cnt) print('spend: %.2fs' % ((end_time - start_time) / 60.0))複製代碼
輸出結果以下:
5314 spend: 9.13s 5314 spend: 0.23s複製代碼
能夠看到,用數組匹配用了9s,而用 Trie 樹匹配僅用0.23s!
今天介紹的這種提升海量數據模糊查詢性能的方式是經過寫代碼的方式實現的,對於常常寫 SQL 的大數據開發者來講,要把它用起來只是建個 UDF 就能夠了,須要在 UDF 的初始化代碼中用「高校名單」創建一顆 Trie 樹。
今天的內容就分享到這裏了,但願對你有幫助。公衆號回覆關鍵詞 trie 獲取完整源代碼
歡迎公衆號「渡碼」,輸出別地兒看不到的乾貨。