1. 程式人生 > >[從頭學數學] 第255節 Python實現資料結構:字典樹(Trie)

[從頭學數學] 第255節 Python實現資料結構:字典樹(Trie)

劇情提要:
阿偉看到了一本比較有趣的書,是關於《計算幾何》的,2008年由北清派出版。很好奇
它裡面講了些什麼,就來看看啦。


正劇開始:
星曆2016年08月03日 09:35:13, 銀河系厄爾斯星球中華帝國江南行省。

[工程師阿偉]正在和[機器小偉]一起研究[計算幾何]]。


關於字典樹的實現,已經有非常多的博文涉及到了。但基本都是用內嵌陣列/列表實現的。

本博文是它的遞迴實現。


###
# @usage   字典樹
# @author  mw
# @date    2016年08月02日  星期二  08:56:34 
# @param
# @return
#
###
class Trie:
    class TrieNode:
        def __init__(self,item,next = None, follows = None):
            self.item = item
            self.next = next
            self.follows = follows

        def __str__(self):
            return str(self.item);

        def getnext(self):
            return self.next;

        def setnext(self, next):
            self.next = next;            

        def getfollows(self):
            return self.follows;

        def setfollows(self, follows):
            self.follows = follows;

        def getitem(self):
            return self.item;

        def setitem(self, item):
            self.item = item;

        def __iter__(self):
            yield self;

            if (self.follows != None):
                for x in self.follows:
                    yield x;

        def iternext(self):
            yield self;

            if (self.next != None):
                for x in self.next.iternext():
                    yield x;

        #從兩個方向觀察節點資訊,一是從它的後續看,這是看一個單詞
        #另一個是從它的兄弟看,這是在這個點的各分支
        def info(self, direction = 0):            
            s = '';

            if direction == 0:
                for x in self:
                    s += str(x)+'-->';
            else:
                for x in self.iternext():
                    s += str(x)+'-->';
            
            print(s);

    def __init__(self):
        self.start = Trie.TrieNode('^', None, None);

    def insert(self,item):
        self.start = Trie.__insert(self.start,item)

    def __contains__(self,item):
        return Trie.__contains(self.start,item)

    #生成詡根所在的結點
    def __genNode(item):
        if (len(item) == 1):
            return Trie.TrieNode(item[0], None, None);
        elif (len(item) > 1):
            return Trie.TrieNode(item[0], None, Trie.__genNode(item[1:]));
        else:
            return None;        

    def __insert(node,item):
        # This is the recursive insert function.
        if (item == None or item == ''):
            return None;
        elif (node == None):
            return Trie.__genNode(str(item));
        elif (node.item == item[0]):
            node.setfollows(Trie.__insert(node.getfollows(), item[1:])); 
        else:                
            node.setnext(Trie.__insert(node.getnext(), item));

        return node;

    def __contains(node, item):
        # This is the recursive membership test.
        #單詞結尾用'$'分隔,當然,如果用其它分隔符,此處必須更改item+'$'
        if Trie.__getNode(node, item) != None:
            return True;
        else:
            return False;

    #一般都是從字典的根結點開始遍歷才有意義
    def getNode(self, item):                
        return Trie.__getNode(self.start, item);
    
    #找到某一節點
    def __getNode(node, item):
        if item == None or item == '':
            return None;
        elif node == None:
            return None;
        elif node.item != item[0]:
            return Trie.__getNode(node.next, item);
        else:
            if (len(item) > 1):
                return Trie.__getNode(node.follows, item[1:]);
            else:                
                return node;

    #取某一結點的所有子結點
    def getChild(self, item):
        node = self.getNode(item);
        
        child = [];
        if (node != None):
            if (node.follows != None):
                child.append(node.follows);

                p = node.follows;

                while (p.next != None):
                    child.append(p.next);
                    p = p.next;

        return child;

    #取某一結點的兄弟結點
    def getBrother(self, item):
        if item not in self:
            return [];
        else:
            if (len(item) <= 1):
                root = self.start;
                brothers = [];
 
                for x in root.iternext():
                    brothers.append(x);
                return brothers;
            else:
                return self.getChild(item[:-1]);
        

    #取得某一結點的前一結點
    def getPrev(self, item):
        if item not in self:
            return None;
        
        len_ = len(item);
        
        if (len_ <= 0):
            return None;
        elif (len_ == 1):            
            root = self.start;

            for x in root.iternext():
                if x.next.item == item:
                    return x;

            return None;
        else:
            root = self.getNode(item[:-1]);
            root = root.follows;
            if (root.item == item[-1]):
                return None;
            else:
                for x in root.iternext():
                    if x.next.item == item[-1]:
                        return x;

                return None;

    #取得父結點
    def getParent(self, item):
        if item not in self:
            return None;
        
        len_ = len(item);
        
        if (len_ <= 1):
            return None;
        else:
            return self.getNode(item[:-1]);       
                

    #判斷是否第一個孩子
    def isFirstChild(self, item):
        if item not in self:
            return False;
        
        len_ = len(item);
        
        if (len_ <= 1):
            return False;
        else:
            parent = self.getNode(item[:-1]);  
            if parent.follows.item == item[-1]:
                return True;

            return False;       
    

    #從字典樹裡刪除某一單詞
    def delete(self, item):
        node = self.getNode(item);
        
        #詞不存在或只是部分,就不操作
        if node == None or node.follows != None:
            return [];
        
        len_ = len(item);

        if (len_ < 1): #一般這是不可能的,但有時詞典中也有空詞的位置
            return [];
        else:
            for i in range(len_, 0, -1):
                s = item[0:i]
                brothers = self.getBrother(s);
                count = len(brothers);

                print(s);
                print(count);

                if (count > 1):
                    break;

            if (len(s) <= 1):
                if count <= 2:
                    #樹根要佔去一個位置,所以如果根的兄弟不多於兩個,這棵樹就只有一個詞
                    #刪掉後就只剩下一個樹根了
                    self.start.setnext(None);
                else:
                    prev = self.getPrev(s);
                    prev.setnext(prev.next.next);
            else:
                if (self.isFirstChild(s)):
                    parent = self.getParent(s);
                    parent.setfollows(parent.follows.next);
                else:
                    prev = self.getPrev(s);
                    prev.setnext(prev.next.next);

            print('在節點{0}處刪除'.format(s));
            return item;

    #遍歷檢視字典
    def dict(self):
        #單詞結束的末尾標記
        endChar = '$';

        count = 0;
        
        if (self.start != None):            
            cursor = self.start;

            #具有後續節點的詞段
            dict_1 = [];
            #最終版
            dict_2 = [];
            
            while cursor != None:
                #if (cursor.follows == None):
                if (cursor.follows == None):
                    #過濾掉詞尾結束標記
                    dict_2.append(str(cursor.item)[:-1]);
                else:
                    dict_1.append(str(cursor.item));

                cursor = cursor.next;

            while (len(dict_1) > 0):
                a = dict_1.pop(0);
                
                cursor = Trie.__getNode(self.start, a);
                count+=1;
                if (cursor != None):
                    cursor = cursor.follows;
                    
                    while cursor != None:
                        if (cursor.follows == None):
                            dict_2.append((a+str(cursor.item))[:-1]);
                        else:
                            dict_1.append(a+str(cursor.item));

                        cursor = cursor.next;

            print('找結點{0}次'.format(count));
            print('字典:', dict_2);
        else:
            print('字典為空');

    #從某個結點開始遍歷,以這個結點為根的子樹所有成員
    def dictFromItem(self, item):
        #單詞結束的末尾標記
        endChar = '$';
        count = 0;

        if (item == '^'):
            return self.dict();
        
        root = self.getNode(item);

        if root == None:
            return [];
        else:     
            #具有後續節點的詞段
            dict_1 = [];
            #最終版
            dict_2 = [];
            
            dict_1.append(item);                    

            while (len(dict_1) > 0):
                a = dict_1.pop(0);
                
                cursor = Trie.__getNode(self.start, a);
                count+=1;
                if (cursor != None):
                    cursor = cursor.follows;
                    
                    while cursor != None:
                        if (cursor.follows == None):
                            dict_2.append((a+str(cursor.item))[:-1]);
                        else:
                            dict_1.append(a+str(cursor.item));

                        cursor = cursor.next;

            return dict_2;





用例:
</pre><p><span style="font-size:18px"></span><pre name="code" class="python">def main():
    #計時開始
    startTime = time.clock();

    t = Trie();
    
    a = ['I', 'love', 'lot', 'lance', 'you', 'you', 'love', 'me',
         'i', 'have', 'a', 'book', 'you', 'have', 'a', 'pencil',
         'pen', 'paper', 'lottol', 'banana'];

    #插入單詞,詞尾加結束符
    for i in range(len(a)):
        t.insert(a[i]+'$');

    
    #測試結點資訊
    t.start.next.next.follows.next.info();
    t.start.info(1);     
    
    n1 = t.getNode('lottol');
    if (n1 != None):
        n1.info();
        n1.info(1);
    else:
        print('無該節點');
    

    t.dict();

    '''
    #測試存在
    print('banana'+'$' in t);
    print('how'+'$' in t);

    #測試關聯
    print(t.getChild('lo'));
    print(t.isFirstChild('lo'));
    print(t.getPrev('la'));
    print(t.getParent('banana'));
    print(t.getPrev('i'));

    #測試兄弟
    brothers = t.getBrother('i'); 
    for i in range(len(brothers)):
        print(brothers[i], end = ', ');
    '''

    #測試刪除
    print(t.delete('have'+'$'));
    t.dict();

    print(t.delete('pen'+'$'));
    t.dict();

    t.insert('pen'+'$');
    t.dict();


短短數行程式碼,寫得非常頭痛。用遞迴是不是比用列表有更大好處,阿偉也不清楚。

但是有一點可以肯定,用遞迴可以把整個字典連成一棵樹,而列表做不到。

即將奧運會了,阿偉決定好好地去研究一下奧運會,另外,最近也出了不少好看的電視劇,

也要抽點時間去看看。

本節到此結束,欲知後事如何,請看下回分解。