資料結構裡面每個元素叫做節點, 都是葉子節點
二叉樹基礎
樹(Tree)
樹是一種非線性表結構,比線性表的資料結構要複雜的多:
樹的種類 |
---|
樹、二叉樹 |
二叉查詢樹 |
平衡二叉查詢樹、紅黑樹 |
遞迴樹 |
“樹”的特徵
“樹”這種資料結構裡面每個元素叫做“節點”;用來連線相鄰節點之間的關係叫做“父子關係”。
比如下面這幅圖,A節點就是B結點的父節點,B節點是A結點的子節點。B、C、D這三個結點的父節點是同一個節點,所以他們之間互稱為兄弟節點。沒有父節點的節點叫根節點,也就是圖中的節點E。沒有子節點的節點叫做葉子節點
G、H、I、J、K、L 都是葉子節點。
高度(Height)、深度(depth)、層(Level)的定義:
節點的高度 = 節點到葉子節點的最長路徑(邊數)
節點的深度 = 根節點到這個節點所經歷的邊的個數
節點的層數 = 節點的深度 + 1
樹的高度 = 根節點的高度
“高度”是從下往上度量,從最底層開始計數,計量的起點是0。
“深度”是從上往下度量,從根節點開始度量計數起點也是0。
“層數”跟深度的計算類似,不過計數起點是1。
二叉樹(Binary Tree)
二叉樹的每個節點最多有兩個子節點,分別是左子節點和右子節點。二叉樹中,有兩種比較特殊的樹,分別是滿二叉樹和完全二叉樹。滿二叉樹又是完全二叉樹的一種特殊情況。
二叉樹可以用鏈式儲存,也可以用陣列順序儲存。陣列順序儲存的方式比較適合完全二叉樹,其他型別的二叉樹用陣列儲存會比較浪費儲存空間。除此之外,二叉樹裡非常重要的操作就是前、中、後序遍歷操作,遍歷的時間複雜度是O(n),需要用遞迴程式碼來實現。
二叉樹是樹的一種,特點是每個節點最多有兩個子節點,分別是左子節點和右子節點。
不過,二叉樹有的節點只有左子節點,有的節點只有右子節點:
上圖編號2的二叉樹中,葉子節點全都在最底層,除了葉子節點之外,每個節點都有左右兩個子節點,這種二叉樹就叫做滿二叉樹
編號3的二叉樹中,葉子節點都在最底下兩層,最後一層的葉子節點都靠左排列,並且除了最後一層,其他層的節點個數都要達到最大,這種二叉樹叫做完全二叉樹。
完全二叉樹
完全二叉樹和非完全二叉樹的區別:
最後一層的葉子節點靠左排列的才叫完全二叉樹,如果靠右排列就不能叫完全二叉樹了。
儲存一棵樹有兩種方式,一種是基於指標或者引用的二叉鏈式儲存法,一種是基於陣列的順序儲存法。
鏈式儲存法中每個節點有三個欄位,其中一個儲存資料,另外兩個是指向左右子節點的指標。從根節點開始可以通過左右子節點的指標,把整棵樹都串起來。這種儲存方式我們比較常用。大部分二叉樹都是通過這種結構來實現的。
基於陣列的順序儲存法:把根節點儲存在下標i = 1的位置,那左子節點儲存在下標2 * i的位置,右子節點儲存在2 * i + 1 = 3的位置。以此類推,B節點的左子節點儲存在2 * i = 2 * 2 = 4的位置,右子節點儲存在2 * i + 1 = 2 * 2 + 1 = 5的位置。
如果節點X儲存在陣列中下標為i的位置,左子節點的下標為2 * i,右子節點為2 * i + 1.反過來,下標為 i / 2的位置儲存就是它的父節點。通過這種方式,只要知道根節點儲存的位置(一般情況下,為了方便計運算元節點,根節點會儲存在下標為1的位置),就可以通過下標計算,把整棵樹都串起來。
一顆完全二叉樹僅僅“浪費”了一個下標為0的儲存位置,如果是非完全二叉樹,會浪費比較多的陣列儲存空間。
如果某棵二叉樹是一棵完全二叉樹,用陣列儲存無疑是最節省記憶體的一種方式。因為陣列儲存方式並不需要儲存額外的左右子節點的指標。
堆其實就是一種完全二叉樹,最常用的儲存方式是陣列。
二叉樹的遍歷
將二叉樹所有節點遍歷打印出來有三種方式,前序遍歷、中序遍歷和後序遍歷。其中前、中、後序,表示的是節點與它的左右子樹節點遍歷列印的先後順序。
- 前序遍歷是指,對於樹中的任意節點來說,先列印這個節點,然後再列印它的左子樹,最後列印它的右子樹。
- 中序遍歷是指,對於書中的任意節點,先列印它的左子樹,然後再列印它本身,最後列印它的右子樹。
- 後序遍歷是指,對於樹中的任意節點來說,先列印它的左子樹,然後再列印它的右子樹,最後列印這個節點本身。
實際上,二叉樹的前、中、後序遍歷就是一個遞迴的過程。比如,前序遍歷,其實就是先列印根節點,然後再遞迴列印左子樹,最後遞迴地列印右子樹。
寫遞推公式的關鍵就是,如果要解決問題A,就假設子問題B、C已經解決,然後再來看如何利用B、C來解決A。前、中、後序遍歷的遞推公式:
前序遍歷的遞推公式
preOrder(r) = print r -> preOrder(r -> left) -> preOrder(r -> right)
中序遍歷的遞推公式
inOrder(r) = inOrder(r -> left) -> print r -> inOrder(r -> right)
後序遍歷的遞推公式
postOrder(r) = postOrder(p -> left) -> postOrder(r -> right) -> r
java虛擬碼
void preOrder(Node* root){
if (root == null) return;
print root
preOrder(root -> left);
preOrder(root -> right)
}
void inOrder(Node* root){
if (root == null) return;
inOrder(root -> left);
print root;
inOrder(root -> right);
}
void postOrder(Node* root){
if (root == null) return;
postOrder(root -> left);
psotOrder(root -> right);
print root;
}
二叉樹遍歷的時間複雜度
遍歷過程中每個節點最多會被訪問兩次,所以遍歷操作的時間複雜度跟節點的個數n成正比,二叉樹遍歷的時間複雜度是O(n)。
按層次遍歷二叉樹
除了前、中、後序三種二叉樹遍歷方式外還有按層遍歷這種遍歷方式。
實現思路:
按照廣度優先的遍歷演算法的思路,引入一個佇列,根節點現如佇列,然後開始從佇列頭部取元素,每取一個元素則先列印當前元素,然後依次將左右子節點加入佇列,若左子節點或右子節點為空則跳過此步。
python實現程式碼:
from collections import deque
# 層級遍歷
def layer_order(root: TreeNode):
if not root: return
queue = deque([root])
while queue:
e: TreeNode = queue.popleft()
yield e.val
if e.left: queue.append(e.left)
if e.right: queue.append(e.right)
給定一個二叉樹,返回其按層次遍歷的節點值。(即逐層地,從左到右訪問所有節點)
例如:給定二叉樹:[3, 20, null, null, 15, 7]
3
/ \
9 20
/ \
15 7
返回其層次遍歷結果:
[
[3],
[9, 20]
[15, 7]
]
python程式碼實現
:
def level_order(root: TreeNode):
levels = []
if not root:
return levels
level = 0
queue = deque([root])
while queue:
levels.append([])
for i in range(len(queue)):
node = queue.popleft()
levels[level].append(node.val)
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
level += 1
return levels
二叉查詢樹(Binary Search Tree)
二叉查詢樹是二叉樹中最常用的一種型別,也叫二叉搜尋樹。二叉查詢樹支援動態資料集合的快速插入、刪除、查詢操作。
二叉查詢樹要求,在樹中任意一個節點,其左子樹中的每個節點的值都小於這個節點的值,而右子樹每個節點的值都大於這個節點的值:
1.二叉查詢樹的查詢操作
先取根節點,如果它等於要查詢的資料就返回。如果要查詢的資料比根節點的值小,那就在左子樹中遞迴查詢,如果要查詢的資料比根節點的值大,那就在右子樹中遞迴查詢。
java程式碼實現:
public class BinarySearchTree{
private Node tree;
public Node find(int data){
Node p = tree;
while(p != null){
if (data < p.data){
p = p.left;
} else if (data > p.data){
p = p.right;
} else {
return p;
}
}
return null;
}
public static class Node{
private int data;
private Node left;
private Node right
public Node(int data){
this.data data;
}
}
}
2.二叉查詢樹的插入操作
二叉查詢樹的插入過程需要從根節點開始,依次比較要插入的資料和節點的大小關係。
如果要插入的資料比節點的資料大,並且節點的右子樹為空,就將資料直接插到右子節點的位置;如果不為空,就再遞迴遍歷右子樹,查詢插入位置。同理,如果要插入的資料比節點數值小,並且節點的左子樹為空,就將新資料插入到左子節點的位置;如果不為空,就再遞迴遍歷左子樹,查詢插入位置。
java程式碼實現:
public void insert(int data){
if (tree == null){
tree = new Node(data);
return;
}
Node p = tree;
while(p != null){
if (data > p.data){
if (p.right == null){
p.right = new Node(data);
return;
}
p = p.right;
} else {
if (p.left == null){
p.left = new Node(data);
return;
}
p = p.left;
}
}
}
3.二叉查詢樹的刪除操作
針對要刪除節點的子節點個數的不同需要分2中情況來處理。
如果要刪除的節點只有一個子節點(只有左子節點或者右子節點)或沒有子節點(左右子節點均為null),只需要將要刪除的父節點的指標指向要刪除節點的子節點。比如下圖中刪除節點55、13。
如果要刪除的節點有兩個子節點。需要找到這個節點的右子樹的最小節點,把它替換到要刪除的節點上。然後再按照上面方法刪除掉這個最小節點。比如下圖中的刪除節點18。(用左子樹的最大節點進行替換也可以)
java實現程式碼:
public void delete(int data){
// p指向要刪除的節點,初始化指向根節點
Node p = tree;
// pp記錄的是p的父節點
Node pp = null;
while (p != null && p.data != data){
pp = p;
if (data > p.data) {
p = p.right;
} else {
p = p.left;
}
}
// 沒有找到
if (p == null){
return;
}
//要刪除的節點有兩個子節點
if (p.left != null && p.right != null){
//查詢右子樹中最小的節點
Node minP = p.right;
// minPP表示minP的父節點
Node minPP = p;
while (minP.left != null){
minPP = minP;
minP = p.left;
}
//將minP的資料替換到p中
p.data = minP.data;
//下面就變成了刪除minP了
p = minP;
pp = minPP;
}
// 刪除節點是葉子節點或者僅有一個子節點
// p的子節點
Node child;
if (p.left != null){
child = p.left;
} else if (p.right != null){
child = p.right;
} else {
child = null;
}
//刪除的是根節點
if (pp == null) {
tree = child;
} else if (pp.left = p){
pp.left = child;
} else {
pp.right = child;
}
}
關於二叉查詢樹的刪除操作,最簡單的方法是單純將要刪除的節點標記為“已刪除”並不真正從樹中將這個節點去掉。這樣原本刪除的節點還需要儲存在記憶體中,缺點是比較浪費記憶體空間。
4.二叉查詢樹的其他操作
二叉查詢樹中還可以支援快速地查詢最大節點和最小節點、前驅結點和後繼結點。
二叉查詢樹也叫做二叉排序樹,中序遍歷二叉查詢樹,可以輸出有序的資料序列,時間複雜度是O(n)。
Python程式碼實現:
def find_min(self) -> Optional[TreeNode]:
if self.tree is Node: return None
p = self.tree
while p.left:
p = p.left
return p
def find_max(self) -> Optional[TreeNode]:
if self.tree is None: return None
p = self.tree
while p.right:
p = p .right
return p
def _in_order(self, root: Optional[TreeNode]):
if root:
yield from self._in_order(root.left)
yield root.data
yield from self._in_order(root.right)
def in_order(self) -> list:
if self.tree is Node:
return []
return list(self._in_order(self.tree))
二叉查詢樹的Python實現程式碼
from collections import deque
from typing import Optional, List
class TreeNode:
def __init__(self, data=None):
self.data = data
self.left = None
self.right = None
class BinarySearchTree:
def __init__(self, val_list=None):
if val_list is None:
val_list = []
self.tree: Optional[TreeNode] = None
for n in val_list:
self.insert(n)
def find(self, data):
p = self.tree
while p and p.data != data:
p = p.left if data < p.data else p.right
return p
def insert(self, data):
if not self.tree:
self.tree = TreeNode(data)
return
pp = None
p = self.tree
while p:
pp = p
p = p.left if p.data > data else p.right
if pp.data > data:
pp.left = TreeNode(data)
else:
pp.right = TreeNode(data)
def delete(self, data):
p: Optional[TreeNode] = self.tree # p指向要刪除的節點,初始化指向根節點
pp: Optional[TreeNode] = None # pp記錄的是p的父節點
while p and p.data != data:
pp = p
p = p.right if p.data < data else p.left
if not p: return # 沒有找到
if p.left and p.right: # 查詢右子樹中最小節點
min_p = p.right # 記錄右子樹的最小節點
min_pp = p # minPP表示minP的父節點
while min_p.left:
min_pp = min_p
min_p = min_p.left
p.data = min_p.data # 替換資料
p, pp = min_p, min_pp # p指向要刪除的min_p
# 刪除節點是葉子節點或者僅有一個子節點
child: Optional[TreeNode] = p.left if p.left else p.right
if not pp:
self.tree = child # 刪除的是根節點
elif pp.left == p:
pp.left = child
else:
pp.right = child
def find_min(self) -> Optional[TreeNode]:
if self.tree is None: return None
p = self.tree
while p.left:
p = p.left
return p
def find_max(self) -> Optional[TreeNode]:
if self.tree is None: return None
p