1. 程式人生 > >二叉樹的Python實現

二叉樹的Python實現

樹的定義與基本術語

  樹型結構是一類重要的非線性資料結構,其中以樹和二叉樹最為常用,是以分支關係定義的層次結構。樹結構在客觀世界中廣泛存在,如人類社會的族譜和各種社會組織機構;在計算機領域中也有廣泛應用,如在編譯程式中,可用樹來表示源程式的語法結構;在資料庫系統中,樹型結構也是資訊的重要組織形式之一;在機器學習中,決策樹,隨機森林,GBDT等是常見的樹模型。
  樹(Tree)是\(n(n\geq 0)\)個結點的有限集。在任意一棵樹中:(1)有且僅有一個特定的稱為根(Root)的節點;(2)當\(n>1\)時,其餘節點可分為\(m(m>0)\)個互不相交的有限集\(T_1,T_2,...,T_m,\)

其中每一個集合本身又是一棵樹,並且稱為根的子樹(SubTree)。

圖1 樹型結構

  在圖1,該樹一共有13個節點,其中A是根,其餘節點分成3個互不相交的子集:\(T_1=\{B,E,F,K,L\}\),\(T_2=\{C,G\}\),\(T_3=\{D,H,I,J,M\}\);\(T_1,T_2和T_3\)都是根A的子樹,且本身也是一棵樹。例如\(T_1\),其根為B,其餘節點分為兩個互不相交的子集;\(T_{11}=\{E,K,L\}\),\(T_{12}=\{F\}\)\(T_{11}\)\(T_{12}\)都是B的子樹。而在\(T_{11}\)中E是根,\(\{K\}\)\(\{L\}\)

是E的兩棵互不相交的子樹,其本身又是隻有一個根節點的樹。
  接下來講一下樹的基本術語。
  樹的結點包含一個數據元素及若干指向其子樹的分支。節點擁有的子樹數量稱為節點的度(Degree)。在圖1中,A的度為3,B的度為2,C的度為1,F的度為0。度為0的結點稱為葉子(Leaf)結點。在圖1中,K,L,F,G,M,I,J都是該樹的葉子。度不為0的結點稱為分支結點樹的度是指樹內個結點的度的最大值。
  結點的子樹的根稱為該結點的孩子(Child),相應地,該結點稱為孩子的雙親(Parent)。在圖1,中,D是A的孩子,A是D的雙親。同一個雙親的孩子之間互稱兄弟(Sibling)。在圖1中,H,I,J互為兄弟。結點的祖先
是從根到該結點所經分支上的所有結點。在圖1中,M的祖先為A,D,H。對應地,以某結點為根的子樹中的任一結點都稱為該結點的子孫。在圖1中,B的子孫為E,F,K,L。
  樹的層次(Level)是從根開始,根為第一層,根的孩子為第二層等。雙親在同一層的結點互為同兄弟,在圖1中,K,L,M互為堂兄弟。樹中結點的最大層次稱為樹的深度(Depth)或高度,在圖1中,樹的深度為4。
  如果將樹中結點的各子樹看成從左到右是有次序的(即不能交換),則稱該樹為有序樹,否則為無序樹
  森林(Forest)\(m(m\geq 0)\)棵互不相交的樹的集合。對樹中每個結點而言,其子樹的集合即為森林。在機器學習模型中,決策樹為樹型結構,而隨機森林為森林,是由若干決策樹組成的森林。

二叉樹的定義與基本性質

  二叉樹(Binary Tree)是一種特殊的樹型結構,它的特點是每個結點至多有兩棵子樹(即二叉樹中不存在度大於2的結點),且二叉樹的子樹有左右之分,其次序不能任意顛倒(有序樹)。
  根據二叉樹的定義,其具有下列重要性質:(這裡不給出證明,證明細節可參考清華大學出版社 嚴蔚敏 吳偉民的《資料結構(C語言版)》

性質1)在二叉樹的第\(i\)層上至多有\(2^{i-1}\)個結點\((i\geq 1)\)
性質2)深度為\(k\)的二叉樹至多有\(2^{k}-1\)個結點\((k\geq 1)\)
性質3)對任何一棵二叉樹,如果其葉子節點數為\(n_{0}\),度為2的結點數為\(n_2\),則\(n_0=n_2+1\)

  一棵深度為\(k\)且有\(2^k-1\)個結點的二叉樹稱為滿二叉樹。深度為\(k\),結點數數\(n\)的二叉樹,當且僅當其每一個結點都與深度為\(k\)的滿二叉樹中編號為1至n的結點一一對應時,稱之為完全二叉樹。在下圖2中,(a)為滿二叉樹,(b)為完全二叉樹。

圖2 特殊形態的二叉樹

  下面介紹完全二叉樹的兩個特性:

性質4)具有\(n\)個結點的完全二叉樹的深度為\([log_{2}n]+1\),其中\([x]\)表示不大於x的最大整數。
性質5)如果對一棵有n個結點的完全二叉樹的結點按層序編號(從第一層到最後一層,每層從左到右),則對任一結點\(i(1\leq i\leq n)\),有:
(1)如果i=1,則結點i是二叉樹的根,無雙親;如果i>1,則其雙親結點為[1/2]。
(2)如果2i>n,則結點i無左孩子;否則其左孩子是結點2i。
(3)如果2i+1>n,則結點i無右孩子;否則其右孩子是結點2i+1。

  介紹完了二叉樹的定義及基本性質,接下來,我們需要了解二叉樹的遍歷。所謂二叉樹的遍歷,指的是如何按某種搜尋路徑巡防樹中的每個結點,使得每個結點均被訪問一次,而且僅被訪問一次。對於二叉樹,常見的遍歷方法有:先序遍歷,中序遍歷,後序遍歷,層序遍歷。這些遍歷方法一般使用遞迴演算法實現。
  先序遍歷的操作定義為:若二叉樹為空,為空操作;否則(1)訪問根節點;(2)先序遍歷左子樹;(3)先序遍歷右子樹。
  中序遍歷的操作定義為:若二叉樹為空,為空操作;否則(1)中序遍歷左子樹;(2)訪問根結點;(3)中序遍歷右子樹。
  後序遍歷的操作定義為:若二叉樹為空,為空操作;否則(1)後序遍歷左子樹;(2)後序遍歷右子樹;(3)訪問根結點。
  層序遍歷的操作定義為:若二叉樹為空,為空操作;否則從上到下、從左到右按層次進行訪問。
  如對於下圖3,

圖3 示例二叉樹

其先序遍歷、中序遍歷、後序遍歷、層序遍歷的結果為:

先序遍歷為:
18 7 3 4 11 5 1 3 6 2 4 
中序遍歷為:
3 7 4 18 1 5 3 11 2 6 4 
後序遍歷為:
3 4 7 1 3 5 2 4 6 11 18 
層序遍歷為:
[[18], [7, 11], [3, 4, 5, 6], [1, 3, 2, 4]]

  關於二叉樹的儲存結構,可以選擇鏈式儲存結構。用於表示二叉樹的連結串列中的結點至少包含3個域:資料域和左、右指標。下面會給出如何利用利用鏈式儲存結構實現二叉樹(Python實現)。

二叉樹的Python實現

  瞭解了二叉樹的基本情況後,筆者使用Python實現了二叉樹,其完整的Python程式碼(Binary_Tree.py)如下:

from graphviz import Digraph
import uuid
from random import sample

# 二叉樹類
class BTree(object):

    # 初始化
    def __init__(self, data=None, left=None, right=None):
        self.data = data    # 資料域
        self.left = left    # 左子樹
        self.right = right  # 右子樹
        self.dot = Digraph(comment='Binary Tree')

    # 前序遍歷
    def preorder(self):

        if self.data is not None:
            print(self.data, end=' ')
        if self.left is not None:
            self.left.preorder()
        if self.right is not None:
            self.right.preorder()

    # 中序遍歷
    def inorder(self):

        if self.left is not None:
            self.left.inorder()
        if self.data is not None:
            print(self.data, end=' ')
        if self.right is not None:
            self.right.inorder()

    # 後序遍歷
    def postorder(self):

        if self.left is not None:
            self.left.postorder()
        if self.right is not None:
            self.right.postorder()
        if self.data is not None:
            print(self.data, end=' ')

    # 層序遍歷
    def levelorder(self):

        # 返回某個節點的左孩子
        def LChild_Of_Node(node):
            return node.left if node.left is not None else None
        # 返回某個節點的右孩子
        def RChild_Of_Node(node):
            return node.right if node.right is not None else None

        # 層序遍歷列表
        level_order = []
        # 是否新增根節點中的資料
        if self.data is not None:
            level_order.append([self])

        # 二叉樹的高度
        height = self.height()
        if height >= 1:
            # 對第二層及其以後的層數進行操作, 在level_order中新增節點而不是資料
            for _ in range(2, height + 1):
                level = []  # 該層的節點
                for node in level_order[-1]:
                    # 如果左孩子非空,則新增左孩子
                    if LChild_Of_Node(node):
                        level.append(LChild_Of_Node(node))
                    # 如果右孩子非空,則新增右孩子
                    if RChild_Of_Node(node):
                        level.append(RChild_Of_Node(node))
                # 如果該層非空,則新增該層
                if level:
                    level_order.append(level)

            # 取出每層中的資料
            for i in range(0, height):  # 層數
                for index in range(len(level_order[i])):
                    level_order[i][index] = level_order[i][index].data

        return level_order

    # 二叉樹的高度
    def height(self):
        # 空的樹高度為0, 只有root節點的樹高度為1
        if self.data is None:
            return 0
        elif self.left is None and self.right is None:
            return 1
        elif self.left is None and self.right is not None:
            return 1 + self.right.height()
        elif self.left is not None and self.right is None:
            return 1 + self.left.height()
        else:
            return 1 + max(self.left.height(), self.right.height())

    # 二叉樹的葉子節點
    def leaves(self):

        if self.data is None:
            return None
        elif self.left is None and self.right is None:
            print(self.data, end=' ')
        elif self.left is None and self.right is not None:
            self.right.leaves()
        elif self.right is None and self.left is not None:
            self.left.leaves()
        else:
            self.left.leaves()
            self.right.leaves()

    # 利用Graphviz實現二叉樹的視覺化
    def print_tree(self, save_path='./Binary_Tree.gv', label=False):

        # colors for labels of nodes
        colors = ['skyblue', 'tomato', 'orange', 'purple', 'green', 'yellow', 'pink', 'red']

        # 繪製以某個節點為根節點的二叉樹
        def print_node(node, node_tag):
            # 節點顏色
            color = sample(colors,1)[0]
            if node.left is not None:
                left_tag = str(uuid.uuid1())            # 左節點的資料
                self.dot.node(left_tag, str(node.left.data), style='filled', color=color)    # 左節點
                label_string = 'L' if label else ''    # 是否在連線線上寫上標籤,表明為左子樹
                self.dot.edge(node_tag, left_tag, label=label_string)   # 左節點與其父節點的連線
                print_node(node.left, left_tag)

            if node.right is not None:
                right_tag = str(uuid.uuid1())
                self.dot.node(right_tag, str(node.right.data), style='filled', color=color)
                label_string = 'R' if label else ''  # 是否在連線線上寫上標籤,表明為右子樹
                self.dot.edge(node_tag, right_tag, label=label_string)
                print_node(node.right, right_tag)

        # 如果樹非空
        if self.data is not None:
            root_tag = str(uuid.uuid1())                # 根節點標籤
            self.dot.node(root_tag, str(self.data), style='filled', color=sample(colors,1)[0])     # 建立根節點
            print_node(self, root_tag)

        self.dot.render(save_path)                              # 儲存檔案為指定檔案

  在上述程式碼中,筆者建立了二叉樹類BTree,實現瞭如下方法:

  1. 初始化方法:該樹存放的資料為data,左子樹,右子樹為left和right,預設均為None;
  2. preorder()方法:遞迴實現二叉樹的先序遍歷;
  3. inorder()方法:遞迴實現二叉樹的中序遍歷;
  4. postorder()方法:遞迴實現二叉樹的後序遍歷;
  5. levelorder()方法:遞迴實現二叉樹的層序遍歷;
  6. height()方法:計算二叉樹的高度;
  7. leaves()方法:計算二叉樹的葉子結點;
  8. print_tree()方法:利用Graphviz實現二叉樹的視覺化,需要設定的引數為save_path和label,save_path為檔案儲存路徑,預設的儲存路徑為當前路徑下的Binary_Tree.gv,可以使用者自己設定;label為是否在Graphviz檔案中新增二叉樹的左右子樹的標籤,用於分清哪棵是左子樹,哪棵是右子樹,可以用使用者自己設定。

  若我們需要實現圖3的示例二叉樹,完整的Python程式碼如下:

from Binary_Tree import BTree

# 構造二叉樹, BOTTOM-UP METHOD
right_tree = BTree(6)
right_tree.left = BTree(2)
right_tree.right = BTree(4)

left_tree = BTree(5)
left_tree.left = BTree(1)
left_tree.right = BTree(3)

tree = BTree(11)
tree.left = left_tree
tree.right = right_tree

left_tree = BTree(7)
left_tree.left = BTree(3)
left_tree.right = BTree(4)

right_tree = tree # 增加新的變數
tree = BTree(18)
tree.left = left_tree
tree.right = right_tree

print('先序遍歷為:')
tree.preorder()
print()

print('中序遍歷為:')
tree.inorder()
print()

print('後序遍歷為:')
tree.postorder()
print()

print('層序遍歷為:')
level_order = tree.levelorder()
print(level_order)
print()

height = tree.height()
print('樹的高度為%s.' % height)

print('葉子節點為:')
tree.leaves()
print()

# 利用Graphviz進行二叉樹的視覺化
tree.print_tree(save_path='E://BTree.gv', label=True)

  OK,當我們執行上述程式碼時,可以得到該二叉樹的一些資訊,輸出結果如下:

先序遍歷為:
18 7 3 4 11 5 1 3 6 2 4 
中序遍歷為:
3 7 4 18 1 5 3 11 2 6 4 
後序遍歷為:
3 4 7 1 3 5 2 4 6 11 18 
層序遍歷為:
[[18], [7, 11], [3, 4, 5, 6], [1, 3, 2, 4]]

樹的高度為4.
葉子節點為:
3 4 1 3 2 4 

該Python程式碼的優勢在於利用Graphviz實現了二叉樹的視覺化,可以形象直觀地得到二叉樹的圖形。在上面的程式碼中,我們可以看到,構建二叉樹不是很方便,需要手動地一個個結點去新增。那麼,如果當我們需要根據某個列表,按列表順序去構建二叉樹時,即二叉樹的層序遍歷為該列表,那又該怎麼辦呢?有什麼好的辦法嗎?
  答案是必須有!按照某個列表去構建二叉樹的完整Python程式碼如下:

from Binary_Tree import BTree

# 利用列表構造二叉樹
# 列表中至少有一個元素
def create_BTree_By_List(array):

    i = 1
    # 將原陣列拆成層次遍歷的陣列,每一項都儲存這一層所有的節點的資料
    level_order = []
    sum = 1

    while sum < len(array):
        level_order.append(array[i-1:2*i-1])
        i *= 2
        sum += i
    level_order.append(array[i-1:])
    # print(level_order)

    # BTree_list: 這一層所有的節點組成的列表
    # forword_level: 上一層節點的資料組成的列表
    def Create_BTree_One_Step_Up(BTree_list, forword_level):

        new_BTree_list = []
        i = 0
        for elem in forword_level:
            root = BTree(elem)
            if 2*i < len(BTree_list):
                root.left = BTree_list[2*i]
            if 2*i+1 < len(BTree_list):
                root.right = BTree_list[2*i+1]
            new_BTree_list.append(root)
            i += 1

        return new_BTree_list

    # 如果只有一個節點
    if len(level_order) == 1:
        return BTree(level_order[0][0])
    else: # 二叉樹的層數大於1

        # 建立最後一層的節點列表
        BTree_list = [BTree(elem) for elem in level_order[-1]]

        # 從下往上,逐層建立二叉樹
        for i in range(len(level_order)-2, -1, -1):
            BTree_list = Create_BTree_One_Step_Up(BTree_list, level_order[i])

        return BTree_list[0]

#array = list(range(1,19))
array = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
tree = create_BTree_By_List(array)

print('先序遍歷為:')
tree.preorder()
print()

height = tree.height()
print('\n樹的高度為%s.\n'%height)

print('層序遍歷為:')
level_order = tree.levelorder()
print(level_order)
print()

print('葉子節點為:')
tree.leaves()
print()

# 利用Graphviz進行二叉樹的視覺化
tree.print_tree(save_path='E://create_btree_by_list.gv', label=True)

在上述程式中,筆者利用create_BTree_By_List()函式實現了按照某個列表去構建二叉樹,輸入的引數array為列表,要求列表中至少有一個元素。執行上述程式,我們得到的26個大寫字母列表所構建的二叉樹的影象如下:

圖4 26個大寫字母列表所構建的二叉樹

輸出的結果如下:

先序遍歷為:
A B D H P Q I R S E J T U K V W C F L X Y M Z G N O 

樹的高度為5.

層序遍歷為:
[['A'], ['B', 'C'], ['D', 'E', 'F', 'G'], ['H', 'I', 'J', 'K', 'L', 'M', 'N', 'O'], ['P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z']]

葉子節點為:
P Q R S T U V W X Y Z N O

總結

  二叉樹是很多重要演算法及模型的基礎,比如二叉搜尋樹(BST),哈夫曼樹(Huffman Tree),CART決策樹等。本文先介紹了樹的基本術語,二叉樹的定義與性質及遍歷、儲存,然後筆者自己用Python實現了二叉樹的上述方法,筆者程式碼的最大亮點在於實現了二叉樹的視覺化,這個功能是激動人心的。
  在Python中,已有別人實現好的二叉樹的模組,它是binarytree模組,其官方文件的網址為:https://pypi.org/project/binarytree/。其使用的例子如下

binarytree模組演示

關於這個模組的更多功能,可參考其官方文件。當然,筆者還是建議您親自實現一下二叉樹哦,這樣能夠加深對二叉樹的理解~
  在後面的文章中,筆者將會介紹二叉搜尋樹(BST),哈夫曼樹(Huffman Tree)等,歡迎大家關注~

注意:本人現已開通微信公眾號: Python爬蟲與演算法(微訊號為:easy_web_scrape), 歡迎大家關注哦~~