目录
树的概念及其结构
要了解二叉树,那么首要的就是要知道树的概念。
在计算机科学中,树是一种广泛使用的抽象数据类型,用于模拟层次结构,树由节点构成。在这种层级系统中,存在一个最顶层的节点,称为根节点,它没有父节点。树中的其他节点可以有一个(在非二叉树中)或多个子节点,但只有一个父节点,除了根节点外。
树结构的基本术语:
- 节点(Node): 树的基本部分,可能包含一个值或数据,并可能有子节点。
- 根节点(Root): 树的顶端节点,没有父节点。
- 叶子节点(Leaf nodes): 没有子节点的节点。
- 子节点(Child nodes): 某个节点的直接后继节点。
- 父节点(Parent node): 某个节点的直接前驱节点。
- 兄弟节点(Sibling nodes): 具有相同父节点的节点。
- 边(Edge): 连接节点的线,它定义了节点之间的父子关系。
- 路径(Path): 从一个节点到另一个节点的边的序列。
- 深度(Depth): 从根节点到某个节点的边的数目。
- 高度(Height): 一个节点到最远叶子节点的最长路径的边的数目。树的高度是其所有节点高度的最大值。
下图是树的结构示意图
树结构没有循环或闭环,这是它和图结构的主要区别,如下就不能称作是树结构,因为子树之间有相交。
树的构造——代码表示
class TreeNode:
def __init__(self, value):
self.value = value # 当前节点的值
self.children = [] # 子节点的列表
def add_child(self, child_node):
"""添加子节点"""
self.children.append(child_node)
def remove_child(self, child_node):
"""移除子节点"""
self.children = [child for child in self.children if child is not child_node]
def traverse(self):
"""遍历树节点"""
nodes_to_visit = [self]
while len(nodes_to_visit) > 0:
current_node = nodes_to_visit.pop()
print(current_node.value)
nodes_to_visit.extend(current_node.children)
# 示例构造树
root = TreeNode('Root')
child1 = TreeNode('Child1')
child2 = TreeNode('Child2')
child3 = TreeNode('Child3')
root.add_child(child1)
root.add_child(child2)
child1.add_child(child3)
# 遍历和打印树
root.traverse()
上述代码定义了一个树节点,它包含一个值和一个子节点列表。并且可以添加或移除子节点,并有一个用于遍历树的方法,它采用深度优先遍历(Depth-First Search, DFS)的方式来访问每个节点。
这个例子中构建的树结构如下所示:
当运行 root.traverse() 方法时,它会打印出:
Root
Child1
Child3
Child2
这不是二叉树,因为每个节点可以有任意数量的子节点。 若要创建一个二叉树的结构,你需要为每个节点定义两个指定的子节点属性,通常是 left 和 right。
二叉树概念及介绍
二叉树是一种重要的数据结构,在计算机科学中广泛应用于各类算法中。二叉树是由节点组成的,每个节点最多有两个子节点,分别称为左子节点和右子节点。
二叉树的类别:
1、完全二叉树(Complete Binary Tree): 除了最后一层外,每一层都被完全填满,最后一层的所有节点都尽可能地靠左排列。
2、满二叉树(Full Binary Tree): 每个节点要么没有子节点,要么有两个子节点。
3、平衡二叉树(Balanced Binary Tree): 任意两个叶子节点之间的高度差最多为一,即两个子树的高度差都可以是 -1,0 或 1。最常见的平衡二叉树是 AVL 树。
所有的平衡二叉树都满足二叉搜索树的条件。平衡条件确保了树的深度最大只能是 O(log n)
(n为节点数量),这有助于防止树的一侧异常过长,导致操作效率下降。
一个平衡二叉树的结构示例(AVL 树):
在这个 AVL 树中,任何一个节点的左右子树高度差都不超过 1,这意味着它是平衡的。
在此,我们展示一个不是平衡二叉树的结构:
平衡二叉树是为了解决普通二叉搜索树在极端情况下会退化成链表,从而导致效率下降的问题。二叉搜索树如果不平衡,它的操作(查找、插入、删除等)的时间复杂度在最坏的情况下可以变成O(n)
。而平衡二叉树通过旋转操作来保持平衡,确保操作的时间复杂度稳定在 O(log n)
。
4、二叉搜索树(Binary Search Tree, BST): 满足以下性质的二叉树:
- 每个节点的左子树只包含小于当前节点的数。
- 每个节点的右子树只包含大于当前节点的数。
- 所有左右子树也必须是二叉搜索树。
二叉树的存储结构
二叉树一般可以使用两种存储结构,一种顺序结构,一种链式结构。
二叉树的顺序结构
普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结构存储。我们通常把这种完全二叉树使用顺序结构的数组来存储。
而这种存储又称作堆,这里的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。
二叉树的链式结构
二叉树的链式存储结构是指,用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。 通常的方法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址 。链式结构又分为二叉链和三叉链,遇到的一般都是二叉链。
链表的代码展示
二叉链表的定义:
class BinaryTreeNode:
def __init__(self, value):
self.value = value
self.left = None
self.right = None
# 示例创建二叉链表节点
root = BinaryTreeNode('A')
root.left = BinaryTreeNode('B')
root.right = BinaryTreeNode('C')
三叉链表的定义:
class TernaryTreeNode:
def __init__(self, value):
self.value = value
self.left = None
self.right = None
self.parent = None
# 生成三叉链表节点并建立连接
root = TernaryTreeNode('A')
node_b = TernaryTreeNode('B')
node_c = TernaryTreeNode('C')
root.left = node_b
root.right = node_c
node_b.parent = root
node_c.parent = root
堆的基本概念和结构
将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
堆的性质:
1、堆中某个节点的值总是不大于或不小于其父节点的值;
2、堆总是一棵完全二叉树。
堆的代码体现
小根堆展示:
import heapq
# 创建小根堆
min_heap = []
heapq.heappush(min_heap, 3)
heapq.heappush(min_heap, 1)
heapq.heappush(min_heap, 2)
# 从堆中弹出最小元素
print(heapq.heappop(min_heap)) # 输出 1
print(heapq.heappop(min_heap)) # 输出 2
print(heapq.heappop(min_heap)) # 输出 3
大根堆展示:
import heapq
# 创建大根堆的辅助函数
def heappush_max(heap, item):
heapq.heappush(heap, -item)
def heappop_max(heap):
return -heapq.heappop(heap)
# 创建大根堆
max_heap = []
heappush_max(max_heap, 3)
heappush_max(max_heap, 1)
heappush_max(max_heap, 2)
# 从堆中弹出最大元素
print(heappop_max(max_heap)) # 输出 3
print(heappop_max(max_heap)) # 输出 2
print(heappop_max(max_heap)) # 输出 1
在这两个例子中,小根堆可以直接通过 heapq 提供的函数 heappush 和 heappop 来操作,而大根堆则通过取反的方式使用了相同的函数。
当想将元素添加到大根堆时,可以使用 heappush_max 函数;并且当想从大根堆中弹出最大元素时,可以使用 heappop_max 函数。通过取反操作,能够保持堆的性质而适用于 heapq的小根堆实现。
二叉树生成
生成二叉树通常是指创建二叉树的过程。二叉树可以通过多种方式生成,例如:递归地构建、从数组构建、通过用户输入构建等。二叉树的生成通常包括初始化根节点以及递归或迭代地添加子节点至适当位置。
二叉树遍历
遍历二叉树是指按照某种顺序访问二叉树中的每一个节点,确保每个节点被访问一次。通常有以下几种遍历方法:
-
前序遍历(Pre-order Traversal):
- 访问根节点
- 遍历左子树
- 遍历右子树
- 可用于打印树结构,复制树结构等。
-
中序遍历(In-order Traversal):
- 遍历左子树
- 访问根节点
- 遍历右子树
- 对于二叉搜索树(BST),这种遍历方式会按照节点的升序排列访问它们。
-
后序遍历(Post-order Traversal):
- 遍历左子树
- 遍历右子树
- 访问根节点
- 常用于删除或释放树中的节点,因为它确保节点在其子节点被访问后才被访问。
-
层序遍历(Level-order Traversal):
- 按照树的层级从上至下访问节点
- 通常使用队列来辅助实现
- 也被称为广度优先搜索(BFS, Breadth-First Search)
每种遍历方式都有其特定的用途,并且可以递归或迭代地实现。访问节点的具体操作可以根据需要进行变化,例如打印节点值、计算树的深度,或者累加节点值等。
四种不同遍历方式——代码展示
我们通过0-9这10个数字进行展示说明:
# 二叉树的节点类
class btree_node():
# 初始化节点,每个节点有三个属性:节点值、节点左子树、节点右子树
def __init__(self,val):
#节点幅值
self.data=val
# 节点左子树
self.left=None
# 节点右子树
self.right=None
# 定义二叉树类
class binary_Tree():
def __init__(self):
self.tree_struct=None
# 增加节点,这个函数也是建树的过程
def add_node(self,new_node):
# 将树的结构放到列表中
list_nodes=[self.tree_struct]
# 如果是孔数,直接在书上加上这个节点
if self.tree_struct==None:
# 在空树上加上节点
self.tree_struct= new_node
return
#在循环前list_nodes只有一棵树,在循环中间list_nodes中追加了这棵树的子树。
while len(list_nodes)>0:
# 从列表中弹出提排在最前面的树
cur_node=list_nodes.pop(0)
# 如果树的左子树为空,把该节点当作该树的左子树
if cur_node.left==None:
# 让新结点成为树的左子树
cur_node.left=new_node
return
else:
# 如果树的左子树不为空,则把这棵树的左子树追加到列表后面
list_nodes.append(cur_node.left)
# 如果树的右子树为空,把新结点当作该数的右子树
if cur_node.right==None:
# 让新结点成为树的右子树
cur_node.right=new_node
return
else:
# 如果树的右子树不为空,把这个树的右子树加到列表后面
list_nodes.append(cur_node.right)
# 广度遍历,从树的顶层,一层层遍历,每一层从左到右取各结点数值
def breadth_travel(self):
# 如果是空树直接返回
if self.tree_struct == None:
return
# 将树的结构放到列表中
list_nodes = [self.tree_struct]
# 以下循环主要流程是:
# 每次从列表中弹出最前面的一个树(第一次弹出的是整个树,以后是弹出的是子树),
# 打印这个树根结点的值,
# 先看一下这个树的左孩子(左子树)是否为空,不为空,
# 把该树的左孩子(左子树)追加到列表后面,
# 再看一下这个树的右孩子(右子树)是否为空,
# 不为空,把该树的右孩子(右子树)追加到列表后面,
# 进入下一次循环,再次从列表最前面弹出一个树(子树)
while len(list_nodes) > 0:
cur_node = list_nodes.pop(0)
print(cur_node.data, end='')
if cur_node.left != None:
list_nodes.append(cur_node.left)
if cur_node.right != None:
list_nodes.append(cur_node.right)
# 先序遍历,按照根节点、左子树、右子树的顺序访问二叉树
# 第1步:访问根节点(或子树根节点);
# 第2步:递归调用本函数遍历左子树;
# 第3步:递归调用本函数遍历右子树。
def pre_travel(self, tree_node):
# 如果树为空直接返回
if tree_node == None:
return
# 打印出当前树根结点的值
print(tree_node.data, end='')
# 对左子树递归调用
self.pre_travel(tree_node.left)
# 对右子树递归调用
self.pre_travel(tree_node.right)
# 中序遍历:按照左子树、根节点、右子树的顺序访问
# 第1步:递归调用本函数遍历左子树;
# 第2步:访问根节点(或子树根节点);
# 第3步:递归调用本函数遍历右子树。
def midd_travle(self, tree_node):
# 如果树为空直接返回
if tree_node == None:
return
# 对左子树递归调用
self.midd_travle(tree_node.left)
# 打印出当前树根结点的值
print(tree_node.data, end='')
# 对右子树递归调用
self.midd_travle(tree_node.right)
# 后序遍历:按照左子树、右子树、根节点的顺序访问
# 第1步:递归调用本函数遍历左子树;
# 第2步:递归调用本函数遍历右子树;
# 第3步:访问根节点(或子树根节点)。
def back_travel(self, tree_node):
# 如果树为空直接返回
if tree_node == None:
return
# 对左子树递归调用
self.back_travel(tree_node.left)
# 对右子树递归调用
self.back_travel(tree_node.right)
print(tree_node.data, end='')
# 主程序main
if __name__ == "__main__":
tree = binary_Tree()
for i in range(10):
new_node = btree_node(i)
tree.add_node(new_node)
print('广度遍历结果:', end='')
tree.breadth_travel()
print('')
print('前序遍历结果:', end='')
tree.pre_travel(tree.tree_struct)
print('')
print('中序遍历结果:', end='')
tree.midd_travle(tree.tree_struct)
print('')
print('后序遍历结果:', end='')
tree.back_travel(tree.tree_struct)
运行结果展示: