🏝用Python來實現 Binary Tree 二元樹

🏝用Python來實現 Binary Tree 二元樹

更新於 發佈於 閱讀時間約 1 分鐘

接著來進入圖論的重點之一,Tree與Binary Tree。

二元樹(Binary Tree)是一種樹狀數據結構,其中每個節點最多有兩個子節點,通常稱為左子節點右子節點。這些子節點可以是實體節點 或 空節點。

二元樹是其他進階樹的基礎,可延伸推廣到Binary Search Tree二元搜索樹、Heap堆、Huffman Tree霍夫曼樹...等等。


定義


二元樹一種樹狀數據結構,其中每個節點最多有兩個子節點,通常稱為左子節點右子節點。這些子節點可以是實體節點 或 空節點。


  1. 根節點(Root Node):樹的頂部節點
  2. 葉節點(Leaf Node)沒有子節點的節點。
  3. 內部節點(Internal Node)至少有一個子節點的節點。
  4. 深度(Depth):從根節點到葉節點的最長路徑上的節點數


一開始,初始化時是一顆空樹,隨著資料的增加而成為一顆擁有實體節點的二元樹。

raw-image


兩種特殊形式

  1. Complete Binary Tree

除了最後一層外,每一層的節點都是滿的,且最後一層的節點從左到右依次排列


  1. Full Binary Tree

所有內部節點都有兩個子節點,最後一層皆為葉子節點。


優點

1.可以根據需求動態延伸或縮短,有效利用記憶體空間

缺點

1.不支援random acess,不支援BinaryTree[ index ] 這種操作。

2.如果要搜尋或者訪問某個元素,一定要從根節點開始拜訪(或搜尋)整顆樹。

run-time成本相當昂貴。

3.只能單向訪問,相當於單行道,只能從上到下(從根節點到葉子節點)訪問。


節點Node的Python實作

class Node:

def __init__(self, data):

# 資料欄位
self.data = data

# 左子節點
self.left = None

# 右子節點
self.right = None

Binary Tree的class定義 與 建構子(初始化的函式)

class BinaryTree:

def __init__(self):
# 初始化時是一顆空樹
self.root = None

Binary Tree常見的操作

1.插入節點 insert(data)


如果當下節點為空,則放入當下的位置。

如果左子節點為空,則放入左子節點。

如果右子節點為空,則放入右子節點。

如果左子節點、右子節點都已經使用,則往下一層尋找空位


(補充: Binary Tree並無硬性規定插入方式,讀者可自行定義)


時間複雜度: O(n) 線性時間

  def insert(self, data):
if self.root == None:
self.root = Node(data)
else:
self._insert(self.root, data)
return


def _insert(self, cur_node, data):

if cur_node == None:
# 如果當下節點為空,則放入當下的位置。
cur_node = Node(data)

elif cur_node.left == None:
# 如果左子節點為空,則放入左子節點。
cur_node.left = Node(data)

elif cur_node.right == None:
# 如果右子節點為空,則放入右子節點。
cur_node.right = Node(data)

# 如果左子節點、右子節點都已經使用,則往下一層尋找空位。
elif cur_node.left.left is None or cur_node.left.right is None:
self._insert(cur_node.left, data)

elif cur_node.right.left is None or cur_node.right.right is None:
self._insert(cur_node.right, data)

else:
self._insert(cur_node.left, data)

return

2.搜尋某個節點值 search(data)


從根節點開始往下搜尋查看某個節點值是否存在

如果有,返回該節點。

如果沒有,返回None。


時間複雜度: O(n) 線性時間

  def search(self, data):
return self._search(self.root, data)

def _search(self, cur_node, data):

if not cur_node:
return None

if cur_node.data == data:
return cur_node
else:
return self._search(cur_node.left, data) or self._search(cur_node.right, data)

3.刪除指定的節點 delete( data )


從根節點開始往下搜尋,查看某個節點值是否存在。

如果有,刪除該節點,填補空缺,返回被刪除的節點

如果沒有,返回None。


(補充: Binary Tree並無硬性規定填補空缺的方式,讀者可自行定義)


時間複雜度: O(n) 線性時間

  def delete(self, data):

return self._deleteNode(self.root, data)

def _deleteNode(self, cur_node, data):


if not cur_node:
return None

if cur_node.data != data:

return self._deleteNode( cur_node.left, data ) or self._deleteNode( cur_node.right, data )

else:
# 當下就是要被刪除的節點

if (not cur_node.left) or (not cur_node.right):

# 至少有一個子節點是空的
# 由存在的那個子節點 或者 None 填補刪除後的空缺
cur_node = cur_node.left if cur_node.left else cur_node.right

else:
# 兩個子節點都存在
# 由右子樹最左下角的節點 填補刪除後的空缺
replace = cur_node.right

while replace.left:
replace = replace.left

cur_node.data = replace.data
cur_node.right = self._deleteNode( cur_node.right, replace.data )

return Node(data)

4.取得樹的深度 depth()


從根節點開始往葉子節點拜訪,取得最長路徑上的節點數 = 樹的深度。


時間複雜度: O(log n) 對數時間

  def depth(self):
return self._depth(self.root)

def _depth(self, cur_node):
if not cur_node:
return 0

return 1 + max( self._depth(cur_node.left), self._depth(cur_node.right) )

5.樹的中序拜訪 inorder traversal


先拜訪左子樹、接著拜訪當下節點、最後拜訪右子樹。

依此定義,遞迴拜訪整顆樹。


時間複雜度: O(n) 線性時間

  def inorder(self):
self._inorder(self.root)
return

def _inorder(self, cur_node):

if cur_node:
self._inorder(cur_node.left)
print(str(cur_node.data) + "->", end='')
self._inorder(cur_node.right)
return

6.樹的逐層拜訪 level-order traversal


從樹根開始,逐層由上往下拜訪每一層,每一層由左往右拜訪每個節點。


時間複雜度: O(n) 線性時間

  def levelorder(self):

bfs_q = [self.root] if self.root else []

while bfs_q:

next_level = []

for cur_node in bfs_q:

print(str(cur_node.data) + "->", end='')

if cur_node.left:
next_level.append(cur_node.left)
if cur_node.right:
next_level.append(cur_node.right)

bfs_q = next_level

return

測試範例

raw-image


完整的Binary Tree實作和程式碼

class Node:

def __init__(self, data):
self.data = data
self.left = None
self.right = None


class BinaryTree:

def __init__(self):
self.root = None

def insert(self, data):
if self.root == None:
self.root = Node(data)
else:
self._insert(self.root, data)
return


def _insert(self, cur_node, data):

if cur_node == None:
cur_node = Node(data)

elif cur_node.left == None:
cur_node.left = Node(data)

elif cur_node.right == None:
cur_node.right = Node(data)

elif cur_node.left.left is None or cur_node.left.right is None:
self._insert(cur_node.left, data)

elif cur_node.right.left is None or cur_node.right.right is None:
self._insert(cur_node.right, data)

else:
self._insert(cur_node.left, data)
return

def inorder(self):
self._inorder(self.root)
return

def _inorder(self, cur_node):
if cur_node:
self._inorder(cur_node.left)
print(str(cur_node.data) + "->", end='')
self._inorder(cur_node.right)
return


def levelorder(self):

bfs_q = [self.root] if self.root else []

while bfs_q:

next_level = []

for cur_node in bfs_q:

print(str(cur_node.data) + "->", end='')

if cur_node.left:
next_level.append(cur_node.left)
if cur_node.right:
next_level.append(cur_node.right)

bfs_q = next_level

return


def search(self, data):
return self._search(self.root, data)


def _search(self, cur_node, data):

if not cur_node:
return None

if cur_node.data == data:
return cur_node
else:
return self._search(cur_node.left, data) or self._search(cur_node.right, data)


def delete(self, data):

return self._deleteNode(self.root, data)


def _deleteNode(self, cur_node, data):


if not cur_node:
return None

if cur_node.data != data:

return self._deleteNode( cur_node.left, data ) or self._deleteNode( cur_node.right, data )

else:
# 當下就是要被刪除的節點

if (not cur_node.left) or (not cur_node.right):

# 至少有一個子節點是空的
# 由存在的那個子節點 或者 None 填補刪除後的空缺
cur_node = cur_node.left if cur_node.left else cur_node.right

else:
# 兩個子節點都存在
# 由右子樹最左下角的節點 填補刪除後的空缺
replace = cur_node.right

while replace.left:
replace = replace.left

cur_node.data = replace.data
cur_node.right = self._deleteNode( cur_node.right, replace.data )

return Node(data)


def depth(self):
return self._depth(self.root)


def _depth(self, cur_node):
if not cur_node:
return 0

return 1 + max( self._depth(cur_node.left), self._depth(cur_node.right) )


def test():

tree = BinaryTree()
for i in range(1, 11):
print(f"insert {i}")
tree.insert(i)

print("\nSearch 5:")
res = tree.search(5)
if res:
print(res.data)
else:
print("None")

print("\nInorder traversal:")
tree.inorder()
print()

print("\nLevel-order traversal:")
tree.levelorder()
print()

print("\nDelete 8:")
res = tree.delete(8)
if res:
print(res.data)
else:
print("None")

print("\nDelete 11:")
res = tree.delete(11)
if res:
print(res.data)
else:
print("None")


print(f"\nTree depth = {tree.depth()}" )

if __name__ == "__main__":
test()

測試輸出

insert 1
insert 2
insert 3
insert 4
insert 5
insert 6
insert 7
insert 8
insert 9
insert 10

Search 5:
5

Inorder traversal:
8->4->9->2->10->5->1->6->3->7->

Level-order traversal:
1->2->3->4->5->6->7->8->9->10->

Delete 8:
8

Delete 11:
None

Tree depth = 4

結語


其實Tree就是Node的組合與推廣,每個節點最多可以有兩個子節點的樹狀資料結構

讀者可以透過紙筆追蹤演算法和程式執行邏輯,測試幾個簡單的小範例,

會有更深刻的了解和體會!


Binary Tree相關的演算法練習題與詳解


一題多解 二元樹的右側視角Binary Tree Right Side View_Leetcode #199_精選75題


🎄圖論應用: 二元樹的後序拜訪 Binary Tree Postorder Traversal_LC #145


圖論應用題 驗證二元樹節點 Validate Binary Tree Nodes_Leetcode #1361


BFS 經典入門題 Binary Tree Level Order Traversal II Leetcode #107


BFS 經典入門題 Binary Tree Level Order Traversal_Leetcode #102


經典圖論面試題 Invert Binary Tree_Leetcode #226


使用DFS 模板 + 基礎圖論題目 Binary Tree Paths Leetcode #257


一題多解 二元樹的最大深度 Maximum Depth of Binary Tree_Leetcode 104精選75

avatar-img
小松鼠的演算法樂園
95會員
426內容數
由有業界實戰經驗的演算法工程師, 手把手教你建立解題的框架, 一步步寫出高效、清晰易懂的解題答案。 著重在讓讀者啟發思考、理解演算法,熟悉常見的演算法模板。 深入淺出地介紹題目背後所使用的演算法意義,融會貫通演算法與資料結構的應用。 在幾個經典的題目融入一道題目的多種解法,或者同一招解不同的題目,擴展廣度,並加深印象。
留言
avatar-img
留言分享你的想法!
從Python 內建deque資料結構的角度切入, 同時了解deque 與 FIFO Queue相關的function用法。 collections.deque是一種兩端點皆可進出的雙端佇列 在兩端點高效地在O(1)常數時間內添加和刪除元素。 這使得deque非常適合實現FIFO Queue
井字遊戲(OOXX)的遊戲描述 Tic Tac Toe(井字遊戲)是經典的雙人棋盤遊戲,在一個3x3的方格中進行。 每回合兩個玩家輪流選一個位置,先讓自己的符號(是 X 或 O)在 水平線、垂直線或對角線上連成一線的玩家宣告獲勝。
深入探討圖(Graph)的基本概念 及 最短路徑Shortest Path的尋找。 我們專注於廣度優先搜尋(BFS)演算法,以等權圖的最短路徑為例, 詳細說明如何利用BFS從起點擴散到終點,並且提供詳細的程式碼範例。 透過實作,讀者能夠更清楚理解圖論及BFS的應用,並體會水波紋擴散模型的重要性。
從Python 內建deque資料結構的角度切入, 同時了解deque 與 FIFO Queue相關的function用法。 collections.deque是一種兩端點皆可進出的雙端佇列 在兩端點高效地在O(1)常數時間內添加和刪除元素。 這使得deque非常適合實現FIFO Queue
井字遊戲(OOXX)的遊戲描述 Tic Tac Toe(井字遊戲)是經典的雙人棋盤遊戲,在一個3x3的方格中進行。 每回合兩個玩家輪流選一個位置,先讓自己的符號(是 X 或 O)在 水平線、垂直線或對角線上連成一線的玩家宣告獲勝。
深入探討圖(Graph)的基本概念 及 最短路徑Shortest Path的尋找。 我們專注於廣度優先搜尋(BFS)演算法,以等權圖的最短路徑為例, 詳細說明如何利用BFS從起點擴散到終點,並且提供詳細的程式碼範例。 透過實作,讀者能夠更清楚理解圖論及BFS的應用,並體會水波紋擴散模型的重要性。