一文帶你徹底搞懂二叉樹(Python實現)——真香

前言

終於到了數據結構中的關鍵部分了——二叉樹。說起二叉樹啊,簡直是我當初學習C語言數據結構課程的噩夢,爲什麼這麼說呢?記得老師當初在講啥子先序遍歷、中序遍歷、後序遍歷的時候,我腦殼裏簡直是一團漿糊,碰到一些複雜的二叉樹根本就整不清楚到底要咋個遍歷,並且遍歷的代碼到現在我還不會。害,以至於我在期末考試之前猛補數據結構的知識點,本以爲自己能夠得個高分(題目都會做),結果差強人意。現在我重新撿起這個,用一種全新的思維方式來解釋二叉樹、實現二叉樹,猛地發現Python實現是真香。

這裏的樹不是樹,是一種二維層次的數據結構類型,只是長的像樹,準確來說是長的像一個倒着的樹,不過我覺得說是長的像樹根最爲貼切。
樹(英語:tree)是一種抽象數據類型(ADT)或是實作這種抽象數據類型的數據結構,用來模擬具有樹狀結構性質的數據集合。它是由n(n>=1)個有限節點組成一個具有層次關係的集合。樹具有如下特點:

  • 每個節點有零個或多個子節點;
  • 沒有父節點的節點稱爲根節點;
  • 每一個非根節點有且只有一個父節點;
  • 除了根節點外,每個子節點可以分爲多個不相交的子樹;

在這裏插入圖片描述在這裏插入圖片描述
以上就是一些樹的大致結構圖。

樹中的一些術語

  • 節點的度:一個節點含有的子樹的個數稱爲該節點的度;
  • 樹的度:一棵樹中,最大的節點的度稱爲樹的度;
  • 葉節點或終端節點:度爲零的節點;
  • 父親節點或父節點:若一個節點含有子節點,則這個節點稱爲其子節點的父節點;
  • 孩子節點或子節點:一個節點含有的子樹的根節點稱爲該節點的子節點;
  • 兄弟節點:具有相同父節點的節點互稱爲兄弟節點;
  • 節點的層次:從根開始定義起,根爲第1層,根的子節點爲第2層,以此類推;
  • 樹的高度或深度:樹中節點的最大層次;
  • 堂兄弟節點:父節點在同一層的節點互爲堂兄弟;
  • 節點的祖先:從根到該節點所經分支上的所有節點;
  • 子孫:以某節點爲根的子樹中任一節點都稱爲該節點的子孫。
  • 森林:由m(m>=0)棵互不相交的樹的集合稱爲森林;

這些我們大致瞭解一下就可以了,知道有這麼個東西,然後會求解一些問題,如果不是做題的話,沒必要了解的特別的清楚。

樹的分類

  • 無序樹:樹中任意節點的子節點之間沒有順序關係,這種樹稱爲無序樹,也稱爲自由樹;
  • 有序樹:樹中任意節點的子節點之間有順序關係,這種樹稱爲有序樹;
  • 二叉樹:每個節點最多含有兩個子樹的樹稱爲二叉樹;
    • 完全二叉樹:對於一顆二叉樹,假設其深度爲d(d>1)。除了第d層外,其它各層的節點數目均已 達最大值,且第d層所有節點從左向右連續地緊密排列,這樣的二叉樹被稱爲完全二叉樹,其中滿二叉樹的定義是所有葉節點都在最底層的完全二叉樹;
    • 平衡二叉樹(AVL樹):當且僅當任何節點的兩棵子樹的高度差不大於1的二叉樹;
    • 排序二叉樹(二叉查找樹(英語:Binary Search Tree),也稱二叉搜索樹、有序二叉樹);
  • 霍夫曼樹(用於信息編碼):帶權路徑最短的二叉樹稱爲哈夫曼樹或最優二叉樹;
  • B樹:一種對讀寫操作進行優化的自平衡的二叉查找樹,能夠保持數據有序,擁有多餘兩個子樹。

樹的存儲表示

樹的存儲一般是順序存儲或者鏈式存儲,即採用順序表或者鏈表。
順序存儲:將數據結構存儲在固定的數組中,在遍歷速度上有一定的優勢,但因所佔空間比較大,是非主流二叉樹。二叉樹通常以鏈式存儲。
在這裏插入圖片描述
鏈式存儲:採用鏈表的方式來存儲二叉樹的各個結點。
在這裏插入圖片描述

二叉樹

二叉樹是每個節點最多有兩個子樹的樹結構。通常子樹被稱作“左子樹”(left subtree)和“右子樹”(right subtree)。

二叉樹的性質

  • 性質1: 在二叉樹的第i層上至多有2^(i-1)個結點(i>0)
  • 性質2: 深度爲k的二叉樹至多有2^k - 1個結點(k>0)
  • 性質3: 對於任意一棵二叉樹,如果其葉結點數爲N0,而度數爲2的結點總數爲N2,則N0=N2+1;
  • 性質4:具有n個結點的完全二叉樹的深度必爲 log2(n+1)
  • 性質5:對完全二叉樹,若從上至下、從左至右編號,則編號爲i 的結點,其左孩子編號必爲2i,其右孩子編號必爲2i+1;其雙親的編號必爲i/2(i=1 時爲根,除外)

在這裏插入圖片描述
在這裏插入圖片描述

二叉樹的實現

前面吧啦吧啦半天感覺都沒啥用,現在到了真正用Python代碼實現二叉樹的關鍵時刻了。我前面實現鏈表的時候就講到了,Python中沒有指針,採用一種類似指針的方式來實現結點,再有結點鏈接而成構成想要的數據結構,二叉樹也不列外,每一個結點封裝有數據,還有指向其左右子樹的“指針”,然後其左右子樹又有各自指向的左右子樹,就這樣構成了一個完整的二叉樹結構。

二叉樹結點的實現

是不是跟鏈表結點實現有那麼一點異曲同工之妙啊,完全像是一個模子裏面刻出來的一樣。其實你知道了鏈表怎麼實現,二叉樹這些應該就不在話下了。So easy!

class Node(object):
    """二叉樹結點"""
    def __init__(self, item):
        self.elem = item
        self.lchild = None
        self.rchild = None

二叉樹的創建

這是一個空的二叉樹,root是根節點。

class Tree(object):
    """二叉樹"""
    def __init__(self):
        self.root = None

再給出具體添加節點元素代碼之前,我們來大致復現一下添加結點元素的過程。在一個二叉樹中添加結點,首先你要判斷應該添加在哪裏添加,二叉樹是嚴格定義了結構的,一個根節點有且僅有一個左子樹和一個右子樹,要是根節點這裏這裏滿了的話,我就不能在根節點的下面添加了,我要換到一個空的地方。
在這裏插入圖片描述
我拿這個二叉樹做個例子:我們遍歷的順序定義爲從上到下,從左到右(廣度優先遍歷),那麼應該就是A B C D E F了,那麼細節是怎麼樣的呢?
首先是根節點A,然後A的左結點B,A的右結點C (A B C)
A的左右都遍歷完了,下面就是B的左右結點了,B作爲根節點,B的左結點D,B的右結點E (B C D E)
B的左右都遍歷完了,下面就是C的左右結點了,C作爲根節點,C的左結點F,C沒有右結點,那麼添加的結點就應該放在這裏。
然後你仔細看會發現,我每次從左邊取出一個根節點,然後在右邊添加他的左右結點,這種操作是不是很像一個隊列的操作啊,只能從一邊添加,然後另一邊刪除。我們就用隊列的結構來實現二叉樹。代碼如下:

    def add(self, item):
    	# 將要添加的數據封裝成一個node結點
        node = Node(item)
        # 判斷是不是一個空樹
        if self.root is None:
            self.root = node
            return
        #將根節點添加到隊列中 
        queue = [self.root]
        # 如果隊列不爲空就一直遍歷下去
        while queue:
        	# 取出隊列裏的第一個元素也就是根節點
            cur_node = queue.pop(0)
            # 判斷左結點
            if cur_node.lchild is None:
                cur_node.lchild = node
                return
            else:
                queue.append(cur_node.lchild)
             # 判斷右結點
            if cur_node.rchild is None:
                cur_node.rchild = node
                return
            else:
                queue.append(cur_node.rchild)

廣度優先遍歷

二叉樹的創建時,遍歷添加元素就是採用的廣度優先遍歷,又叫做層次遍歷,一層一層的遍歷二叉樹。

    def breadth_travel(self):
        """廣度遍歷"""
        if self.root is None:
            return
        queue = [self.root]
        while queue:
            cur_node = queue.pop(0)
            print(cur_node.elem, end=" ")
            if cur_node.lchild is not None:
                queue.append(cur_node.lchild)
            if cur_node.rchild is not None:
                queue.append(cur_node.rchild)

先序、中序、後序遍歷

在這裏插入圖片描述
先序遍歷就是先遍歷根節點,然後左子樹、右子樹。
如圖所示:首先是根節點0,然後我們將他左邊的那一部分當成一個子二叉樹也就是左子樹,接着左子樹的根節點是1;1的左子樹是他左邊下面的那一坨,根節點是3;3的左子樹是7,7後面就沒有了,然後右子樹是8;我們以1爲根節點的左邊部分就遍歷完了,接下來是其右子樹,根節點是4;4的左子樹是9,沒有右子樹,他們後面也啥也沒有,以0爲根節點的左邊的部分就都遍歷完了。然後遍歷右子樹,根節點是2;2的左右子樹分別是56。
所以我們先序遍歷的結果就是0 1 3 7 8 4 9 2 5 6
中序遍歷是先遍歷左子樹,然後根節點,最後右子樹。
如圖所示:0作爲根節點,左子樹是左邊那一大坨,然後根節點變成了1;1的左子樹是以3爲根節點,這是最小的完整二叉樹分支了,那麼按照先左後根再右的思路就應該是7 3 8;738作爲一個完整的左子樹,下一步就應該遍歷他的根節點1;再就是右子樹94,右子樹的順序是先9後4;整個大的左子樹遍歷完了,根節點是0;然後右子樹256,遍歷順序是5 2 6。
所以我們中序遍歷順序是:7 3 8 1 9 4 0 6 2 5
後序遍歷是先遍歷左子樹,然後右子樹,最後根節點。
跟上面的方法一樣,我們先抽絲剝繭,找到最小的左子樹378,其遍歷順序是7 8 3;然後以1爲根節點的右子樹49,其遍歷順序是9 4;然後以0爲根節點的右子樹是256,其遍歷順序是5 6 2;最後就是根節點0.
所以我們中序遍歷順序是:7 8 3 9 4 1 5 6 2 0

接下來就應該是具體的代碼實現了。我們前面講到的這樣一種抽絲剝繭的方式是不是有一點像函數裏面的遞歸?根節點、左子樹、左子樹的根節點、左子樹根節點的左子樹…完全就像是一種遞歸的方式,一步步重複我們的方法。大家可以自己仔細想想,具體代碼如下:

    def preorder(self, node):
        """先序遍歷"""
        if node is None:
            return
        print(node.elem, end=" ")
        self.preorder(node.lchild)
        self.preorder(node.rchild)
    def inorder(self, node):
        """中序遍歷"""
        if node is None:
            return
        self.inorder(node.lchild)
        print(node.elem, end=" ")
        self.inorder(node.rchild)
    def postorder(self, node):
        """後序遍歷"""
        if node is None:
            return
        self.postorder(node.lchild)
        self.postorder(node.rchild)
        print(node.elem, end=" ")

測試代碼:

if __name__ == "__main__":
    tree = Tree()
    tree.add(0)
    tree.add(1)
    tree.add(2)
    tree.add(3)
    tree.add(4)
    tree.add(5)
    tree.add(6)
    tree.add(7)
    tree.add(8)
    tree.add(9)
    tree.breadth_travel()
    print(" ")
    print("先序遍歷:")
    tree.preorder(tree.root)
    print(" ")
    print("中序遍歷:")
    tree.inorder(tree.root)
    print(" ")
    print("後序遍歷:")
    tree.postorder(tree.root)
    print(" ")

在這裏插入圖片描述

後記

部分代碼和圖片來源於我學習的資料
Python實現二叉樹是真的簡單啊,感覺也沒有花費很大的力氣、也有可能是我太菜了叭!
有問題的地方歡迎大家指出!

新手上路,技術有限,不喜勿噴!

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