二叉樹的Python實現
樹的定義與基本術語
樹型結構是一類重要的非線性資料結構,其中以樹和二叉樹最為常用,是以分支關係定義的層次結構。樹結構在客觀世界中廣泛存在,如人類社會的族譜和各種社會組織機構;在計算機領域中也有廣泛應用,如在編譯程式中,可用樹來表示源程式的語法結構;在資料庫系統中,樹型結構也是資訊的重要組織形式之一;在機器學習中,決策樹,隨機森林,GBDT等是常見的樹模型。
樹(Tree)是\(n(n\geq 0)\)個結點的有限集。在任意一棵樹中:(1)有且僅有一個特定的稱為根(Root)的節點;(2)當\(n>1\)時,其餘節點可分為\(m(m>0)\)個互不相交的有限集\(T_1,T_2,...,T_m,\)
在圖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\}\)
接下來講一下樹的基本術語。
樹的結點包含一個數據元素及若干指向其子樹的分支。節點擁有的子樹數量稱為節點的度(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互為兄弟。結點的祖先
樹的層次(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)為完全二叉樹。
下面介紹完全二叉樹的兩個特性:
性質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,
其先序遍歷、中序遍歷、後序遍歷、層序遍歷的結果為:
先序遍歷為:
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,實現瞭如下方法:
- 初始化方法:該樹存放的資料為data,左子樹,右子樹為left和right,預設均為None;
- preorder()方法:遞迴實現二叉樹的先序遍歷;
- inorder()方法:遞迴實現二叉樹的中序遍歷;
- postorder()方法:遞迴實現二叉樹的後序遍歷;
- levelorder()方法:遞迴實現二叉樹的層序遍歷;
- height()方法:計算二叉樹的高度;
- leaves()方法:計算二叉樹的葉子結點;
- 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個大寫字母列表所構建的二叉樹的影象如下:
輸出的結果如下:
先序遍歷為:
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/。其使用的例子如下:
關於這個模組的更多功能,可參考其官方文件。當然,筆者還是建議您親自實現一下二叉樹哦,這樣能夠加深對二叉樹的理解~
在後面的文章中,筆者將會介紹二叉搜尋樹(BST),哈夫曼樹(Huffman Tree)等,歡迎大家關注~
注意:本人現已開通微信公眾號: Python爬蟲與演算法(微訊號為:easy_web_scrape), 歡迎大家關注哦~~