1. 程式人生 > >python利用Trie(字首樹)實現搜尋引擎中關鍵字輸入提示(學習Hash Trie和Double-array Trie)

python利用Trie(字首樹)實現搜尋引擎中關鍵字輸入提示(學習Hash Trie和Double-array Trie)

python利用Trie(字首樹)實現搜尋引擎中關鍵字輸入提示(學習Hash Trie和Double-array Trie)

 主要包括兩部分內容:
(1)利用python中的dict實現Trie;
(2)按照darts-java的方法做python的實現Double-array Trie

比較:
(1)的實現相對簡單,但在詞典較大時,時間複雜度較高
(2)Double-array Trie是Trie高效實現,時間複雜度達到O(n),但是實現相對較難

 

最近遇到一個問題,希望對地名檢索時,根據使用者的輸入,實時推薦使用者可能檢索的候選地名,並根據實時熱度進行排序。這可以以歸納為一個Trie(字首樹)問題。



Trie在自然語言處理中非常常用,可以實現文字的快速分詞、詞頻統計、字串查詢和模糊匹配、字串排序、關鍵輸入提示、關鍵字糾錯等場景中。

這些問題都可以在單詞樹/字首樹/Trie來解決,關於Trie的介紹看【小白詳解 Trie 樹】這篇文章就夠了

一、Hash實現Trie(python中的dict)

   github上有Trie實現關鍵字,實現Trie樹的新增、刪除、查詢,並根據熱度CACHED_THREHOLD在node節點對字尾進行快取,以便提高對高頻詞的檢索效率。本人在其程式碼上做了註解。
   並對其進行了測試,測試的資料包括了兩列,包括關鍵詞和頻次。
【code】

#!/usr/bin/env python
# encoding: utf-8
"""
@date:    20131001
@version: 0.2
@author:  [email protected]
@desc:    搜尋下拉提示,基於後臺提供資料,建立資料結構(字首樹),使用者輸入query字首時,可以提示對應query字首補全

@update:
    20131001 基本結構,新增,搜尋等基本功能
    20131005 增加快取功能,當快取開啟,使用者搜尋某個字首超過一定次數時,進行快取,減少搜尋時間
    20140309 修改程式碼,降低記憶體佔用

@TODO:
    test case
    加入拼音的話,導致記憶體佔用翻倍增長,要考慮下如何優化節點,共用記憶體

""" #這是實現cache的一種方式,也可以使用redis/memcached在外部做快取 #https://github.com/wklken/suggestion/blob/master/easymap/suggest.py #一旦開啟,search時會對每個節點做cache,當增加刪除節點時,其路徑上的cache會被清除,搜尋時間降低了一個數量級 #代價:記憶體消耗, 不需要時可以關閉,或者通過CACHED_THREHOLD調整快取數量 #開啟 #CACHED = True #關閉 CACHED = False #注意,CACHED_SIZE >= search中的limit,保證search從快取能獲取到足夠多的結果 CACHED_SIZE = 10 #被搜尋超過多少次後才加入快取 CACHED_THREHOLD = 10 ############### start ###################### class Node(dict): def __init__(self, key, is_leaf=False, weight=0, kwargs=None): """ @param key: 節點字元 @param is_leaf: 是否葉子節點 @param weight: 節點權重, 某個詞最後一個位元組點代表其權重,其餘中間節點權重為0,無意義 @param kwargs: 可傳入其他任意引數,用於某些特殊用途 """ self.key = key self.is_leaf = is_leaf self.weight = weight #快取,存的是node指標 self.cache = [] #節點字首搜尋次數,可以用於搜尋query資料分析 self.search_count = 0 #其他節點無關僅和內容相關的引數 if kwargs: for key, value in kwargs.iteritems(): setattr(self, key, value) def __str__(self): return '<Node key:%s is_leaf:%s weight:%s Subnodes: %s>' % (self.key, self.is_leaf, self.weight, self.items()) def add_subnode(self, node): """ 新增子節點 :param node: 子節點物件 """ self.update({node.key: node}) def get_subnode(self, key): """ 獲取子節點 :param key: 子節點key :return: Node物件 """ return self.get(key) def has_subnode(self): """ 判斷是否存在子節點 :return: bool """ return len(self) > 0 def get_top_node(self, prefix): """ 獲取一個字首的最後一個節點(補全所有後綴的頂部節點) :param prefix: 字元轉字首 :return: Node物件 """ top = self for k in prefix: top = top.get_subnode(k) if top is None: return None return top def depth_walk(node): """ 遞迴,深度優先遍歷一個節點,返回每個節點所代表的key以及所有關鍵位元組點(葉節點) @param node: Node物件 """ result = [] if node.is_leaf: #result.append(('', node)) if len(node) >0:#修改,避免該字首剛好是關鍵字時搜尋不到 result.append((node.key[:-1], node)) node.is_leaf=False depth_walk(node) else: return [('', node)] if node.has_subnode(): for k in node.iterkeys(): s = depth_walk(node.get(k)) #print k , s[0][0] result.extend([(k + subkey, snode) for subkey, snode in s]) return result #else: #print node.key #return [('', node)] def search(node, prefix, limit=None, is_case_sensitive=False): """ 搜尋一個字首下的所有單詞列表 遞迴 @param node: 根節點 @param prefix: 字首 @param limit: 返回提示的數量 @param is_case_sensitive: 是否大小寫敏感 @return: [(key, node)], 包含提示關鍵字和對應葉子節點的元組列表 """ if not is_case_sensitive: prefix = prefix.lower() node = node.get_top_node(prefix) #print 'len(node):' ,len(node) #如果找不到字首節點,代表匹配失敗,返回空 if node is None: return [] #搜尋次數遞增 node.search_count += 1 if CACHED and node.cache: return node.cache[:limit] if limit is not None else node.cache #print depth_walk(node) result = [(prefix + subkey, pnode) for subkey, pnode in depth_walk(node)] result.sort(key=lambda x: x[1].weight, reverse=True) if CACHED and node.search_count >= CACHED_THREHOLD: node.cache = result[:CACHED_SIZE] #print len(result) return result[:limit] if limit is not None else result #TODO: 做成可以傳遞任意引數的,不需要每次都改 2013-10-13 done def add(node, keyword, weight=0, **kwargs): """ 加入一個單詞到樹 @param node: 根節點 @param keyword: 關鍵詞,字首 @param weight: 權重 @param kwargs: 其他任意儲存屬性 """ one_node = node index = 0 last_index = len(keyword) - 1 for c in keyword: if c not in one_node: if index != last_index: one_node.add_subnode(Node(c, weight=weight)) else: one_node.add_subnode(Node(c, is_leaf=True, weight=weight, kwargs=kwargs)) one_node = one_node.get_subnode(c) else: one_node = one_node.get_subnode(c) if CACHED: one_node.cache = [] if index == last_index: one_node.is_leaf = True one_node.weight = weight for key, value in kwargs: setattr(one_node, key, value) index += 1 def delete(node, keyword, judge_leaf=False): """ 從樹中刪除一個單詞 @param node: 根節點 @param keyword: 關鍵詞,字首 @param judge_leaf: 是否判定葉節點,遞迴用,外部呼叫使用預設值 """ # 空關鍵詞,傳入引數有問題,或者遞迴呼叫到了根節點,直接返回 if not keyword: return top_node = node.get_top_node(keyword) if top_node is None: return #清理快取 if CACHED: top_node.cache = [] #遞迴往