lec4 Trees(2)
lec4 Trees(2)
来到弱项,AVL树,旋转操作至今都没这么搞懂哈哈,不过没关系,今天就会解决。
前言
二叉搜索树(BST)操作的时间复杂度
平均时间复杂度:
对于二叉搜索树(BST)的所有基本操作(查找、插入、删除),其平均时间复杂度是 O(d),其中 \(d\) 是树的深度。
最佳情况:
最小的树深度是 $ _2 N $,其中 \(N\) 是树的节点数。对于一个完全平衡的二叉树,树的深度接近 $ _2 N \(,因此,在最佳情况下,BST 的操作时间复杂度为 O(\)N$)。最差情况:
最差情况是树的深度为 \(N\),即树退化为一条链表。在这种情况下,BST 的操作时间复杂度为 O(N)。
最佳树形:
最佳树形是 平衡的二叉搜索树,即每个节点的左子树和右子树的高度差不超过 1。
例如,插入节点顺序为:4, 2, 6, 1, 3, 5, 7
,构成一个平衡的二叉搜索树:1
2
3
4
54
/ \
2 6
/ \ / \
1 3 5 7在这种树形下,深度 \(d\) 为 $ _2 N \(,因此操作时间复杂度为 O(\)N$)。
最差树形:
最差树形是 退化的二叉搜索树,即树的节点按升序或降序插入,导致树形退化为一条链表。
例如,插入节点顺序为:2, 4, 6, 8, 10, 12
,构成一个退化的树:1
2
3
4
5
6
7
8
9
10
112
\
4
\
6
\
8
\
10
\
12在这种情况下,树的深度 \(d = N\),即树退化为一条链表,操作时间复杂度为 O(N)。
插入升序元素的情况:
- 如果你按升序插入元素,例如
2, 4, 6, 8, 10, 12
,则会导致 缺乏平衡,树形会退化为一条 不平衡的链表。这种树的深度最大,等于 \(N\),因此查找、插入、删除操作的时间复杂度为 O(N)。
树的平衡方法
1. 不平衡(Don’t Balance)
- 描述:树在操作时不进行任何平衡调整。
- 优点:实现简单,操作较为直接。
- 缺点:随着插入和删除操作,树可能会变得非常不平衡,导致某些节点变得非常深,进而影响操作效率。
2. 严格平衡(Strict Balance)
- 描述:树必须始终保持完全平衡,即每个节点的左右子树高度差不超过 1(例如 AVL 树),或者树的高度始终为 $ N $。
- 优点:树保持最优的操作效率,查找、插入和删除操作的时间复杂度为 O(\(\log N\))。
- 缺点:维护严格平衡需要频繁的旋转操作,增加了实现的复杂度。
3. 较好的平衡(Pretty Good Balance)
- 描述:树允许轻微的不平衡,平衡条件比严格平衡宽松。例如,红黑树允许每个节点的左右子树的黑色节点数相同,但不要求每个节点的左右子树高度差不超过 1。
- 优点:比严格平衡更容易实现且保持良好的操作效率。
- 缺点:尽管不完全平衡,但依然能保证操作的时间复杂度为 O(\(\log N\)),适合大部分应用。
4. 访问时调整(Adjust on Access)
- 描述:在访问(查找、插入、删除)时根据树的结构动态调整,常见的如伸展树(Splay Tree)。
- 优点:在访问时自动优化树的形状,减少未来访问的成本。
- 缺点:可能会导致某些操作后的树形较差,无法保证最优平衡。
完全二叉树(Complete Binary Tree)
1. 完全二叉树的定义
满二叉树:在满二叉树中,每个节点要么是一个内部节点,拥有两个非空子节点,要么是一个叶子节点。
完全二叉树:在完全二叉树中,除了可能在最后一层(深度为 $ d-1 $)没有完全填满外,所有的层都被完全填满。最后一层的节点按从左到右的顺序填充。即:
- 每一层的节点都被填满,只有最底层可能不完全。
- 最底层的节点从左侧开始填充,直到该层结束。
2. 完全二叉树的构建
- 完全二叉树是通过从根节点开始,逐层填充节点,从左到右填充的方式构建的。每一层都填满后,才开始填充下一层,直到最后一层。
每一次操作之后都想成为一颗完全二叉树,这会是一个很昂贵的操作。
平衡二叉搜索树(Balanced Binary Search Trees)
平衡二叉搜索树简介
- AVL树:是高度平衡的二叉搜索树(Height-Balanced Tree),其中每个节点的左子树和右子树的高度差不超过1。
- 自调整树(如:Splay树):是一种通过不断地进行调整来保持树结构的平衡的树。
- B树及多路查找树:这些是树的一种变体,允许每个节点有多个子节点,适用于需要大量数据存储的场景。
AVL树的平衡因子
- 平衡因子:一个节点的平衡因子是其左子树高度减去右子树高度的差值。
- 平衡因子计算公式:
$ = - $
- 平衡因子计算公式:
- AVL树的特点:
- 每个节点的左右子树高度差不超过1。
- 如果一个节点的平衡因子的绝对值大于1,则该节点是不平衡的,必须进行旋转操作来恢复平衡。
- 为了确保树保持平衡,AVL树中会存储每个节点的高度和/或平衡因子。
AVL树的高度
- AVL树的高度函数:
$ S(h) $ 表示高度为 $ h $ 的最小AVL树的节点数。
基础条件:- $ S(0) = 1 $,表示高度为0时的AVL树只有一个节点(根节点)。
- $ S(1) = 2 $,表示高度为1时的AVL树有两个节点(根节点和一个子节点)。
- 递推关系: $ S(h) = S(h-1) + S(h-2) + 1 $
- 其中 $ S(h-1) $ 表示左子树的最小节点数,$ S(h-2) $ 表示右子树的最小节点数。
- 递推的结果:
$ S(h) > ^h $,其中 $ $ 约等于1.62,表示随着树的高度增加,AVL树的节点数呈现出斐波那契数列的增长趋势。
插入操作与AVL树的平衡调整
1. 插入操作与平衡因子
插入操作的影响:
当向AVL树插入新节点时,可能会导致某些节点的平衡因子(balance factor)变为2或-2,表示该节点的左右子树高度差异过大,树失去了平衡。平衡因子的更新:
插入节点后,树的平衡性可能会受到影响,但只有从插入点到根节点的路径上的节点的高度会发生变化。我们需要沿着这条路径逐一更新每个节点的高度,并重新计算其平衡因子。
2. 更新节点高度
更新高度:
在插入新节点后,我们需要沿着从插入点到根节点的路径,依次向上更新节点的高度。每个节点的高度更新规则是: $ = (, ) + 1 $更新平衡因子:
在更新节点高度的同时,还要计算并更新该节点的平衡因子。平衡因子的计算公式为: $ = - $ 如果平衡因子的绝对值大于1(即平衡因子为2或-2),则表明该节点不平衡,需要进行旋转操作。
3. 旋转操作
- 旋转的必要性:
当某个节点的平衡因子为2或-2时,表示该节点失衡。此时需要通过旋转操作来恢复平衡。旋转操作通常有四种情况,分别为:- 左旋(Left Rotation):当右子树较高时,通过左旋调整。
- 右旋(Right Rotation):当左子树较高时,通过右旋调整。
- 左-右旋(Left-Right Rotation):当左子树的右子树较高时,先左旋再右旋。
- 右-左旋(Right-Left Rotation):当右子树的左子树较高时,先右旋再左旋。
单旋转图示:
双旋转图示
AVL树插入与平衡调整实现
1. 树节点定义
在AVL树中,每个节点包含:
element
:存储数据的元素。left
:指向左子节点的指针。right
:指向右子节点的指针。height
:节点的高度(可选)。可以通过平衡因子来计算高度,减少不必要的存储。
1 | struct AvlNode{ |
2. 获取节点的高度
为了计算树的平衡因子,可以定义一个辅助函数,获取给定节点的高度。如果节点为空,返回-1。
1 | int height( AvlNode *t ) const { |
3. 插入操作
插入操作遵循二叉搜索树(BST)的基本规则:
- 如果插入节点为空,创建一个新节点。
- 否则,按照大小关系递归地插入元素。
插入操作的特殊之处在于:
- 插入后需要沿着路径向上更新每个节点的高度。
- 每次更新高度后,检查平衡因子,若超出容忍范围(2或-2),则需要进行旋转调整。
1 | void insert(const Comparable &x, AvlNode * &t) { |
4. 平衡操作
平衡操作的核心是计算每个节点的平衡因子,并根据需要进行旋转。如果平衡因子大于1或小于-1,进行旋转操作来恢复平衡。
- 平衡因子:
height(left subtree) - height(right subtree)
。 - 平衡条件:如果平衡因子的绝对值大于1,表示不平衡,必须进行旋转。
1 | void balance(AvlNode* &t) { |
5. 旋转操作
旋转是为了修复不平衡的AVL树。以下是几种旋转方式:
- 单旋转:
- 左旋:当左子树过高时,进行右旋。
- 右旋:当右子树过高时,进行左旋。
1 | void rotateWithLeftChild(AvlNode* &k2) { |
- 双旋转:
- 左-右旋:首先对左子树进行右旋,再对根节点进行左旋。
- 右-左旋:首先对右子树进行左旋,再对根节点进行右旋。
1 | void doubleWithLeftChild(AvlNode* &k3) { |
完整代码
1 |
|
AVL树举例:一看就会(Bushi)
每个圆圈上面的数字是高度。
AVL树的优缺点
AVL树的优点
搜索操作时间复杂度为 O(log N)
由于AVL树始终保持平衡,因此无论是查找、插入还是删除操作,树的高度都保持在对数级别(log N)。因此,AVL树的查找操作时间复杂度为 O(log N),保证了高效性。插入和删除操作时间复杂度为 O(log N)
插入和删除操作虽然需要通过旋转来保持树的平衡,但由于AVL树是自平衡的,插入和删除操作的时间复杂度也为 O(log N),确保了对数级别的效率。高度平衡增加的开销不大
AVL树在每次插入和删除时,会调整树的高度平衡。虽然这增加了一些操作的时间,但由于平衡因子的存储并不会占用太多空间,且平衡调整操作的开销通常是常数级的,因此对性能的影响相对较小。
AVL树的缺点
编程和调试难度较大
实现AVL树时,需要处理复杂的旋转和双旋操作,保持树的平衡。这使得AVL树的实现相对复杂,容易出错,调试起来也比较困难。额外的空间开销
为了存储每个节点的平衡因子(即每个节点的左右子树高度差),AVL树比普通的二叉搜索树(BST)需要额外的空间。虽然平衡因子通常只占用一个整数大小,但在节点数量非常大的情况下,这也会增加存储开销。旋转操作可能影响性能
每次插入或删除时,AVL树可能需要进行旋转操作来恢复平衡。虽然每次旋转的时间复杂度为 O(1),但当多次操作连续发生时,频繁的旋转操作会增加总的操作时间,影响整体性能。对于磁盘上的大量数据,B树更为合适
在大型数据库系统中,尤其是需要处理磁盘上的数据时,AVL树的平衡方式可能并不是最优的。B树(或B+树)通常是更合适的选择,因为B树能够减少磁盘访问次数,并在树的每个节点中存储多个元素,减少树的高度。某些情况下,O(N)的操作可能是可接受的
对于一些应用,特别是像Splay树那样的自调整树,如果执行许多操作时能保证整体时间较快,即使某些单个操作达到 O(N) 的时间复杂度,仍然是可以接受的。Splay树通过频繁调整访问路径,尽管某一操作可能很慢,但总体性能较为优秀。