树是一种重要的非线性数据结构,它模拟了树这种自然结构,由结点(Node)和边(Edge)组成,常用于表示分层关系(如文件系统、组织结构等)。以下是树的一些基本概念和特性:
1.树的基本概念
1.1 结点(Node)
- 树由多个结点组成。
- 每个结点包含一个值或数据。
- 结点可以有零个或多个子结点。
1.2 根结点(Root Node)
- 树的最顶层的结点称为根结点。
- 每棵树只有一个唯一的根结点。
1.3 叶结点(Leaf Node)
- 也称为终端结点,没有子结点的结点。
- 叶结点是树的最底层结点。
1.4 内部结点(Internal Node)
- 具有至少一个子结点的结点。
1.5 父结点(Parent Node)
- 具有下一级子结点的结点称为父结点。
- 除了根结点之外,每个结点都有一个父结点。
1.6 子结点(Child Node)
- 父结点的直接下一级结点称为子结点。
1.7 兄弟结点(Sibling Node)
- 具有相同父结点的结点称为兄弟结点。
1.8 路径(Path)
- 从一个结点到另一个结点的顺序访问称为路径。
1.9 深度(Depth)
- 从根结点到某个结点的路径长度称为该结点的深度。
1.10 高度(Height)
- 从某个结点到叶结点的最长路径的长度称为该结点的高度。
2.树的类型
2.1 二叉树(Binary Tree)
- 每个结点最多有两个子结点(左子结点和右子结点)。
2.2 完全二叉树(Complete Binary Tree)
2.2.1完全二叉树的定义
完全二叉树(Complete Binary Tree)是一种特殊的二叉树,满足以下两个条件:
所有层(除最后一层外)都是满的:即每层包含的结点数都是最大可能的。
最后一层的结点尽量靠左排列:即最后一层上的结点都是从左到右填充的,没有空隙。
2.2.2完全二叉树的性质
完全二叉树具有一些独特的性质,使其在实践中非常有用:
-
结点数量和层次关系:
- 对于一棵高度为 (h) 的完全二叉树,结点总数 (n) 满足
。 - 如果完全二叉树有 (n) 个结点,那么它的高度 (h) 为
。
- 对于一棵高度为 (h) 的完全二叉树,结点总数 (n) 满足
-
结点的编号:
- 完全二叉树的结点可以按层次从上到下、从左到右依次编号。
- 编号为 (i) 的结点的父结点编号为 (i/2),左子结点编号为 (2i),右子结点编号为 (2i+1)。
-
满二叉树的关系:
- 完全二叉树是一种特定类型的不完全的满二叉树,即所有结点要么有两个子结点,要么没有子结点。
-
树的层数:
- 所有结点按层从上到下编号后,编号为 (i) 的结点所在层数为
。
- 所有结点按层从上到下编号后,编号为 (i) 的结点所在层数为
2.2.3完全二叉树的实现
由于完全二叉树的这些性质,可以非常方便地使用数组来表示它。这种表示法使得父、子结点间的关系易于计算。
2.2.4数组表示法
假设数组从下标 1 开始存储完全二叉树的结点:
- 若根结点在数组的第 1 位(
arr[1]
):- 下标为 (i) 的结点的左子结点下标为 (2i)。
- 下标为 (i) 的右子结点下标为 (2i+1)。
- 下标为 (i) 的结点的父结点下标为 (i/2)。
2.2.5完全二叉树的应用
-
堆(Heap):
- 堆是一种完全二叉树,可以是最大堆(根结点是最大值)或最小堆(根结点是最小值)。
- 应用于优先队列等数据结构。
-
树状数组(Fenwick Tree):
- 用作高效动态处理前缀和问题。
-
缓存机制:
- 完全二叉树的性质使其在某些存储结构(如B树、B+树)中得以应用,优化存储访问效率。
2.2.6完全二叉树的例子
考虑一个完全二叉树,有如下性质:
1
/ \
2 3
/ \ /
4 5 6
- 这种结构中,除了第3层,其他层都是满的,最后一层的结点(4, 5, 6)从左到右依次排列。
2.2.7总结
完全二叉树因其紧凑的结构和优良的性质,在实际应用中广泛存在。它的层次性表示使得在某些情况下,它可以非常高效地被存储和操作。只需简单的数组操作,即可轻松完成许多复杂的数据结构操作,从而提升算法的性能。
2.3 满二叉树(Full Binary Tree)
- 每个结点要么没有子结点,要么有两个子结点。
2.3.1 满二叉树定义
**满二叉树(Full Binary Tree 或 Perfect Binary Tree)**是一种特殊的二叉树,它有以下特点:
- 每一个结点要么是叶子结点,要么有两个子结点:即没有只有一个子结点的结点。
- 所有叶子结点都在同一层:这一点区分了满二叉树与其他类型的二叉树,如完全二叉树。
2.3.2 满二叉树的性质
满二叉树具有一些独特且重要的性质,使其在理论和实际应用中广泛使用。
-
结点数量与高度关系:
- 若高度为 ( h ) 的满二叉树,结点总数 ( n ) 满足 ( n = 2^h - 1 )。
- 若结点总数为 ( n ),则高度 ( h ) 满足 ( h = \log_2(n + 1) )。
-
叶子结点数量:
- 满二叉树中,叶子结点数量为 ( 2^{h-1} ),其中 ( h ) 是树的高度。
-
内部结点数量:
- 满二叉树中,内部结点的数量为 ( 2^{h-1} - 1 )。
-
平衡性:
- 满二叉树是完全平衡的,即所有叶子结点到根结点的路径长度相同。
-
最优性:
- 对于给定高度 ( h ) 的树结构来说,满二叉树拥有最多的结点数,是结点高度分布最优的二叉树。
2.3.3 满二叉树的例子
考虑一个高度为 3 的满二叉树,看起来如下:
1
/ \
2 3
/ \ / \
4 5 6 7
在这个例子中:
- 树的高度 ( h = 3 )。
- 结点总数 ( n = 2^3 - 1 = 7 )。
- 叶子结点数量 ( = 2^{3-1} = 4 ) 个(4, 5, 6, 7)。
2.3.4 满二叉树在数组中的表示
满二叉树可以使用数组方便地表示,有如下规则:
- 根结点在数组的第 1 位(
arr[1]
)。 - 下标为 ( i ) 的结点的左子结点下标为 ( 2i )。
- 下标为 ( i ) 的结点的右子结点下标为 ( 2i + 1 )。
- 下标为 ( i ) 的结点的父结点下标为 ( i/2 )。
2.3.5 应用场景
满二叉树在计算机科学中有很多应用场景,特别是在需要平衡和对称性的场景中:
- 计算机网络中层次结构设计:例如IP地址的分配等。
- 数据库和文件系统的设计:特别是在设计完全平衡树类型的索引结构时。
- 递归算法分析:例如在深度优先搜索中的递归树。
- 数据压缩:一些数据压缩算法(如Huffman编码)中,会用到树状结构。
2.3.6 结论
满二叉树因其高度平衡和结点分布均匀的特性,在很多理论研究和实际应用中都是一个重要的概念。它的数学性质使得我们可以轻松地计算和推导出各种相关参数,这对于分析和设计高效的算法和数据结构至关重要。
2.4 平衡二叉树(Balanced Binary Tree)
- 任何结点的左右子树的高度差不超过1。
- 如:AVL树、红黑树。
2.4.1 平衡二叉树定义
平衡二叉树(Balanced Binary Tree) 又称 AVL树(得名于其发明者 Adelson-Velsky 和 Landis),是一种特殊的二叉搜索树(BST)。它具有以下特点:
- 二叉搜索树的性质:对于任何一个结点,其左子树所有结点的值均小于该结点,右子树所有结点的值均大于该结点。
- 高度平衡:任何结点的左右子树高度差不超过 1。
2.4.2 平衡因子
平衡二叉树通过维护平衡因子来实现其高度平衡:
- 平衡因子:某个结点的平衡因子等于其左子树高度减去右子树高度。AVL 树中的任意结点的平衡因子的绝对值不超过 1。
2.4.3 AVL树的旋转操作
当插入或删除结点导致某个结点的平衡因子不再满足条件(绝对值超过 1)时,需要通过旋转操作来重新平衡树。常见的旋转操作有:
-
右旋(LL旋转):
- 当某个结点的左子树的左子树插入导致不平衡。
- 旋转示意:
y x / \ / \ x C --> A y / \ / \ A B B C
-
左旋(RR旋转):
- 当某个结点的右子树的右子树插入导致不平衡。
- 旋转示意:
x y / \ / \ A y --> x C / \ / \ B C A B
-
左右旋(LR旋转):
- 当某个结点的左子树的右子树插入导致不平衡。
- 先对左子结点进行左旋,再对根结点进行右旋。
- 旋转示意:
z z y / \ / \ / \ x D --> y D --> x z / \ / \ / \ / \ A y x C A B C D / \ / \ B C A B
-
右左旋(RL旋转):
- 当某个结点的右子树的左子树插入导致不平衡。
- 先对右子结点进行右旋,再对根结点进行左旋。
- 旋转示意:
x x y / \ / \ / \ A z --> A y --> x z / \ / \ / \ / \ y D B z A B C D / \ / \ B C C D
2.4.4 AVL树的操作
基本操作包括插入、删除和查找:
-
插入:
- 插入结点后,可能导致某些结点失去平衡。需要回溯检查并通过旋转操作来恢复平衡。
-
删除:
- 删除结点后,也可能导致某些结点失去平衡。通过旋转操作恢复平衡。
-
查找:
- 查找操作与普通的二叉搜索树相同,时间复杂度为 (O(log n))。
2.4.5 AVL树的时间复杂度
由于 AVL 树保持了高度平衡,树的高度 (h) 始终在 (O(log n)) 级别。因此,插入、删除和查找操作的时间复杂度均为 (O(log n))。
2.4.6 AVL树的结构例子
考虑以下一个 AVL 树例子:
30
/ \
20 40
/ \ \
10 25 50
在这个例子中:
- 每个结点的左右子树高度差不超过 1,满足 AVL 树的条件。
2.4.6 AVL树的优缺点
优点:
- 查找、插入和删除操作都有较好的时间复杂度 (O(log n))。
- 适用于需要频繁插入和删除操作的动态数据集。
缺点:
- 旋转操作相对复杂,可能引入一些开销。
- 维护树的平衡需要一定的额外计算资源。
2.4.7 结论
AVL 树以其平衡二叉搜索树的特性,在需要高效进行查找和动态插入、删除操作的场景中尤为重要。通过旋转操作保持平衡,使得树的高度始终控制在对数级别,从而保证高效的操作时间。
2.5 二叉搜索树(Binary Search Tree, BST)
- 每个结点的左子树上的所有结点值都小于该结点的值,而右子树上的所有结点值都大于该结点的值。
2.5.1 二叉搜索树的定义
二叉搜索树是一种二叉树,它具有以下特性:
- 每个结点有一个唯一的键值或者数据。
- 左子树所有结点的键值小于其根结点的键值。
- 右子树所有结点的键值大于其根结点的键值。
- 每个子树也是一个二叉搜索树。
2.5.2 二叉搜索树的性质
- 中序遍历:对二叉搜索树进行中序遍历(左-根-右)可以得到一个递增的有序序列。
- 查找、插入与删除操作的时间复杂度:在平均情况下,这些操作的时间复杂度均为 (O(log n)),但在最坏情况下(树退化成链表),时间复杂度为 (O(n))。
- 最低与最高结点:最小值结点是一直沿左子树遍历得到的结点,最大值结点是一直沿右子树遍历得到的结点。
2.5.3 二叉搜索树的基本操作
1. 查找(Search):
在树中查找一个值,按照以下步骤:
- 从根结点开始比较。
- 若值等于根结点,查找成功。
- 若值小于根结点,递归查找左子树。
- 若值大于根结点,递归查找右子树。
def search(root, key):
if root is None or root.val == key:
return root
if key < root.val:
return search(root.left, key)
else:
return search(root.right, key)
2. 插入(Insert):
在树中插入一个新值,需要保持树的性质:
- 若树为空,把新结点作为根结点。
- 若树非空,比较新值与当前结点值:
- 若小于当前结点,递归插入到左子树。
- 若大于当前结点,递归插入到右子树。
def insert(root, key):
if root is None:
return TreeNode(key)
if key < root.val:
root.left = insert(root.left, key)
else:
root.right = insert(root.right, key)
return root
3. 删除(Delete):
删除结点时需要维护树的性质,有三种情况:
- 结点无子结点:直接删除。
- 结点有一个子结点:用子结点替代被删除结点。
- 结点有两个子结点:找到右子树的最小值结点或左子树的最大值结点替换被删除结点,然后递归删除最小值/最大值结点。
def delete(root, key):
if root is None:
return root
if key < root.val:
root.left = delete(root.left, key)
elif key > root.val:
root.right = delete(root.right, key)
else:
if root.left is None:
return root.right
elif root.right is None:
return root.left
temp = minValueNode(root.right) # 找右子树最小值结点
root.val = temp.val
root.right = delete(root.right, temp.val)
return root
def minValueNode(node):
current = node
while current.left is not None:
current = current.left
return current
4. 前序遍历(Pre-order Traversal):
遍历顺序为根结点 -> 左子树 -> 右子树。
5. 中序遍历(In-order Traversal):
遍历顺序为左子树 -> 根结点 -> 右子树。对BST的中序遍历结果是一个排序的序列。
6. 后序遍历(Post-order Traversal):
遍历顺序为左子树 -> 右子树 -> 根结点。
2.5.4 二叉搜索树的应用
- 查找表:如字典、集合等,基于BST可以高效地实现查找、插入和删除操作。
- 排序:通过中序遍历可以得到排序后的数据。
- 动态数据集合:特别是在需要动态维护数据集的场景中,BST非常有效。
2.5.5 二叉搜索树的例子
考虑一个简单的二叉搜索树:
15
/ \
10 20
/ \ / \
8 12 17 25
在这个例子中:
- 查找 12,通过根结点 15 向左,再向右,找到了结点 12。
- 插入 16,首先找到 17 的位置,然后插入 16 作为 17 的左子结点。
- 删除 20,选择 25 替代 20 的位置,然后从右子树中删除 25,再连接。
2.5.6 二叉搜索树的优缺点
优点:
- 实现简单,直观易懂。
- 可以高效地实现查找、插入和删除操作,特别是在平均情况下。
缺点:
- 在最坏情况下,树可能退化为链表,导致操作时间复杂度为 (O(n))。
- 不自带平衡机制,需要引入平衡策略(如 AVL 树或红黑树)来优化。
2.5.7 总结
二叉搜索树作为一种基本且重要的数据结构,在很多算法和数据存储中扮演重要角色。理解并掌握它的基本操作和性质,有助于设计高效的算法,并打下坚实的数据结构基础。
2.6 N叉树(B-Tree、B+树)
- 每个结点可以有最多N个子结点,常用于数据库和文件系统。
2.6.1 N叉树的定义
N叉树(N-ary Tree)是一种广义的树形数据结构,其中每个结点最多可以有 N 个子结点。N叉树是二叉树的推广,在二叉树中每个结点至多有两个子结点,而在N叉树中,每个结点可以有更多的子结点。
2.6.2 N叉树的性质
- 结点和子结点:每个结点最多可以有 N 个子结点。
- 树的高度:由根结点到叶子结点所经过的边的最大数量。
- 叶子结点:没有子结点的结点。
- 树的度:树中结点拥有的最大子结点数。
2.6.3 N叉树的表示
2.6.3.1 数组表示法
在数组表示法中,N叉树的每一个结点都包含自己的数据和一个子结点数组。这个数组包含指向所有子结点的指针。
class Node:
def __init__(self, val=None):
self.val = val
self.children = []
2.6.3.2 链表表示法
在链表表示法中,N叉树的每个结点不仅存储数据,还包含一个指向其子结点的列表(可以是链表、数组等)。
2.6.4 N叉树的基本操作
1. 插入(Insert):
插入操作根据特定的需求和策略,可以将新结点插入到不同的位置。例如,可以选择插入到某个结点的子结点列表中。
def insert(parent, child):
if parent is not None:
parent.children.append(child)
2. 删除(Delete):
删除操作通常也根据特定要求进行,可以删除某个结点及其子树,或者仅删除某个结点而保留其子结点。
def delete(parent, child):
if parent is not None:
parent.children.remove(child)
3. 遍历(Traversal):N叉树的遍历可以类比二叉树的遍历方法,分别有前序、中序、后序和层序遍历。
- 前序遍历(Pre-order Traversal):根 -> 子结点。
def preorder(node):
if not node:
return
print(node.val, end=' ')
for child in node.children:
preorder(child)
- 后序遍历(Post-order Traversal):子结点 -> 根。
def postorder(node):
if not node:
return
for child in node.children:
postorder(child)
print(node.val, end=' ')
- 层次遍历(Level-order Traversal):一层一层地遍历树的结点。
from collections import deque
def level_order(root):
if not root:
return
queue = deque([root])
while queue:
node = queue.popleft()
print(node.val, end=' ')
for child in node.children:
queue.append(child)
2.6.5 N叉树的应用
N叉树由于其灵活的结点数量,广泛应用于多种数据结构和现实问题中:
- 文件系统:N叉树可以用来表示目录和文件的层次结构,每个目录包含多个子目录和文件。
- 分类体系:如产品分类,种属分类等,每个分类下可以有多个子分类。
- 游戏开发:用于表示场景中的层次结构,如场景图和对象之间的层次关系。
- 组织架构:公司或组织的层次结构,每个部门可以有多个子部门。
2.6.6 N叉树的例子
考虑一个3叉树的结构示例:
1
/ | \
2 3 4
/|\ |
5 6 7 8
在这个3叉树中:
- 根结点是 1。
- 结点 1 有 3 个子结点(2、3、4)。
- 结点 2 有 3 个子结点(5、6、7)。
- 结点 4 有 1 个子结点(8)。
2.6.7 N叉树的优缺点
优点:
- 可以高效管理具有层次性和多子结点的数据结构。
- 适用于需要灵活控制结点子结点数量的场景。
缺点:
- 实现和操作相对复杂,特别是对于一些较高阶的操作。
- 内存消耗较高,因为每个结点存储多个子结点的信息。
2.6.8 总结
N叉树是一种广义的树形数据结构,能够有效地表示和操作具有多子结点的复杂层次关系。其灵活性和扩展性使其在许多应用场景中非常实用。理解其基本结构和操作方法,可以帮助设计更复杂和更高效的数据管理系统。
3.树的遍历
树的遍历是一种访问结点的方式,通常有几种基本的遍历方式:
3.1 先序遍历(Pre-order Traversal)
- 访问根结点→左子树→右子树。
3.2 中序遍历(In-order Traversal)
- 访问左子树→根结点→右子树。
- 用于二叉搜索树时,中序遍历可以生成排序的序列。
3.3 后序遍历(Post-order Traversal)
- 访问左子树→右子树→根结点。
3.4 层次遍历(Level-order Traversal)
- 逐层访问结点(使用队列实现)。
4.树的应用
4.1 文件系统
- 操作系统的文件系统中目录结构通常用树表示。
4.2 数据库索引
- B树和B+树广泛用于数据库和文件系统的索引。
4.3 表达式解析
- 树用于编译器中表达式解析。
4.4 网络路由
- 路由协议经常使用树来存储路由信息。
4.5 人工智能
- 游戏中的决策树和搜索树。