數據結構の學習記錄(進階篇1):什麼是二叉查找樹

對算法類的問題,最大的忌諱就是,想都不想直接寫代碼

如果你的這樣的程序猿,那麼狠抱歉,要麼就是你會花上數十倍的時間修改你的簡單STUPID錯誤,要麼就是你很短時間就能得到正確的結果,如果是這樣那麼恭喜你,你進化了!

說上述言論,筆者的區分點是你的目標究竟是一個碼農還是一個算法工程師。兩者的區別從工資上看不說你應該也懂:-)

(等不及的你可以迅速下拉到分界線以下尋找乾貨)

下面附上筆者的coding習慣。首先將思路以母語(中文可以但推薦英語)的形式寫在草稿紙上,覺得沒有問題之後,再將其用鉛筆轉換爲代碼,之後將其寫在你青睞的有調試功能的開發平臺上(對於筆者是Spyder,PyCharm)。如果你發現的開發平臺沒有調試功能,那麼你需要立刻更換!對於剛寫好的代碼,第一步永遠是調試,而不是直接運行。最後一步是不斷優化,考慮所有的情況,並且優化時間複雜度和空間複雜度,永遠不是喊着“哈利路亞”,然後發郵件告訴你老闆任務完成了。

                                                                              第一步:將算法寫在草稿紙上 

                                                                            第二步:將其轉換爲鉛字版 代碼

 

                                                               第三步:轉化爲實際的代碼,這一步基本上是調試


不幸的是,筆者的博客將不會有完整代碼(示範代碼除外),所有代碼均已上傳到碼雲上。如果時間和精力允許的話,強烈建議你根據思路,手動寫一遍,相信你會感覺到全身毛孔舒張而不是想砸電腦的快感。

本文學習的前提是假設你已經初步瞭解了二叉樹,並能靈活的使用它。如果你還不是很瞭解的話,歡迎查看我的博客相關教程。

和hash表一樣,二叉查找樹設計的初衷是在儘可能小的時間複雜度下小完成查找過程,尤其是令人驚豔的log(n),說實話,筆者在學習過程中就被驚訝到了。首先我們簡單定義二叉查找樹(Binary Search Tree,BST)。

所謂BST,就是把原來樹的val變爲 一個鍵(val)和一個負載(payload):

class BSTNode:
    def __init__(self,val,payload,left=None,right=None,parent=None):
        self.val = val
        self.payload = payload
        self.left_child = left
        self.right_child = right
        self.parent = parent

我們在BST節點類裏,加入一些很實用的功能。

(1)判斷當前節點是左子節點還是右子節點isLeftChild,isRightChild。

(2)判斷是否爲根節點,也就是node.parent == None:

(3)判斷是否爲葉子節點isLeaf(左右子節點均爲空);

(4)判斷至少有一個孩子 ;

(5)判斷有兩個孩子;

(6)刪除葉節點;

 不得不說的是刪除葉節點的操作非常需要注意。很多初學者是這樣寫的 Node = None。

這種寫法具有很大問題。如果該節點是左子節點,需要考慮其父節點的左子節點,也就是設置node.parent.left_child = None。右子節點同理。

(7)設置一個節點的val和payload,左孩子和右孩子;

(8)節點迭代;

你可能會問,神馬是節點迭代。比如說 for i in range(10),類似地,我們想實現for node in BSTree: 其實這裏的range就是一個迭代器。由於python是一個面向對象的語言,我們只需要在BSTNode裏面實現__iter__()內置函數。同時你需要了解函數yield: 推薦博客https://blog.csdn.net/poyue8754/article/details/84680609 說白yield返回的不是一個值而是一個迭代器。如果我們要對1000個數進行迭代,使用for i in range(1000)太佔用內存了,我們應該使用for i in xrange(1000):

節點遍歷我們採用中序遍歷的方式,也就是先遍歷左孩子,再遍歷根節點,再遍歷右子樹,都採用yield。所以代碼應該爲:

def __iter__(self):
        #implement the iteration mechanism utilising inorder transverse
        if self:
            if self.left_child!=None:
                for item in self.left_child:
                    yield item
            yield self
            if self.right_child!=None:
                for item in self.right_child:
                    yield item

你大概已經等不及了,好了讓我們正式介紹二叉查找樹把!而值得注意的是,這也是AVL樹和紅黑樹的學習基礎(驚喜:我後續博客會詳細講解它們)!我們希望在樹中兩種重要的操作——插入和刪除的時間複雜度都是log(n)。

首先讓我們來實現一些有趣的內部方法:

(1)__setitem__():當我們在類中實現這個函數,就能像數組一樣用下標進行賦值。

(2)__getitem__():當我們在類中實現這個函數,就能像數組一樣用下標進行訪問。

(3)__contains__():允許我們像 if i in Tree一樣去查詢某個val是否存在。

(4)__delitem__():允許我們像del Tree[val]一樣去刪除某個節點。

(5)__iter__():樹的迭代,直接對根節點調用__iter__()

我們先考慮如何添加一個值val到樹中。如果你能拿出筆和紙,記錄的話,那是再好不過的了。

首先如果樹爲空,那麼根節點的值設爲val。

否則,我們需要按照一定的規則,當前節點從根節點開始,其值currVal和val進行比較:

1. 如果currVal>val: 創建一個左子節點,將當前節點變爲左子節點,繼續比較;

2.如果currVal<val: 創建一個右子節點,將當前節點變爲右子節點,繼續比較;

3.如果currVal==val: 我們修改當前節點的payload爲插入的payload。

可以藉助上述圖片進行理解(左子樹<根節點<右子樹)。請注意,插入操作也是一個遞歸的過程。

我們現在來考慮刪除一個節點的操作,它有三種情況:

(1)葉子節點

葉子節點的刪除是非常簡單的。首先用isLeaf() 判斷其爲葉子節點,之後我們直接刪除即可。

(2)一個子節點

其次是一個子節點的情況,我們只需要將左(右)孩子,替換父親即可(有點大義滅親的感覺哈哈:-^o)。

node.left_child.parent = node.parent
node.parent.left_child = node .left_child 

(4)兩個子節點 

這種情況最爲複雜。還好我們有很優秀的插畫師幫我們理解:

我們把節點投影到數軸上,以P爲例,和它相鄰最近的右節點R,稱爲後繼。和它相鄰最近的左節點M,稱爲前驅。我們要做的就是找到刪除節點的後繼節點或者前驅節點替換它,以保持BST性質,實際上相當於刪除的是替換節點!

我們需要寫一個函數find_successor來尋找我們的後繼。這該如何操作呢?

你可以自己畫幾棵樹,尋找規律:。。。。(仔細作圖中)


很棒,你已經發現:後繼節點是當前節點的右子樹的最左子節點。我們可以用一個遞歸來實現,遍歷右子樹,直到左子樹爲空爲止。找到後繼節點後,再像前三種情況一樣刪除後繼節點即可,查找到的節點的val和payload用後繼節點代替即可。

你可能問,我如果要刪除根節點怎麼辦?Nice Question. 實際上,根節點並沒有被刪除,它只是被替換了,除非樹沒有右子樹,這時候需要把根節點變爲相應的左子節點。

很棒,你認真閱讀完了上述內容,並且有了自己的理解,下一期,我們正式介紹AVL樹!

希望本文對你有幫助!

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