平衡二叉树、线索二叉树、B-树和B+树
五、平衡二叉树
平衡二叉树它是一棵二叉排序树,它或者为空树,或者它的左右子树的高度差的绝对值不超过1,并且左右两个子树均为平衡二叉树。
5.1 AVL树
5.1.1 AVL树的定义
AVL 树是一种平衡二叉树,得名于其发明者的名字( Adelson-Velskii 以及 Landis)。
为了保证二叉树的平衡,AVL树引入了一种监督机制,在树的某一部分不平衡度超过一个阈值后触发相应的平衡操作。保证树的平衡度在可以接受的范围内。既然引入了监督机制,引入了一种叫“平衡因子”的监督指标。
**平衡因子:**某个结点的左子树的高度减去右子树的高度得到的差值。
在AVL树中,所有结点的平衡因子的绝对值都不超过1。为了计算平衡因子,需要在结点中引入高度这一属性。
class AVLTNode(object):
def __init__(self, val, left=None, right=None):
self.val = val
self.left = left
self.right = right
self.height = 0
5.1.2 AVL树的调整
在对AVL树进行插入或删除操作时,可能会破坏AVL树的平衡性,需要通过旋转操作来进行修正,包括:LL型调整,RR型调整,RL型调整以及LR型调整。
- LL型调整:它表示在插入或删除一个结点后,根结点的左孩子的左孩子还有非空结点,导致根结点的左子树的高度比右子树的高度高2,AVL树失去平衡。在调整的时候,只需要进行一个右单旋操作就可以了。
-
RR型调整:它表示在插入或删除一个结点后,根结点的右孩子的右孩子还有非空结点,导致根结点的右子树的高度比左子树的高度高2,AVL树失去平衡。在调整的时候只需要进行一个左单旋操作就可以。
-
RL型调整:它表示在插入或删除一个结点后,根结点的右孩子的左孩子还有非空结点,导致根结点的右子树的高度比左子树的高度高2,AVL树失去平衡。先对右子树进行一个右单旋操作,然后整体进行一个左单旋操作。这里可以是B位置高,也可以是C位置高,以B位置高为例绘图。
-
LR型调整:它表示在插入或删除一个结点后,根结点的左孩子的右孩子还有非空结点,导致根结点的左子树的高度比右子树的高度高2,AVL树失去平衡。先对左子树进行一个左单旋操作,然后对整体进行一个右单旋操作。这里可以是B位置高,也可以是C位置高,以B位置高为例绘图。
我们上述说的根结点不是整棵树的根结点,而是平衡因子超出阈值的那个结点。
例如,我们对下面a图插入元素5,那么结点3位置的平衡因子被破坏,那么3就是上面所说的调整中的根结点,我们只需要对b图中的部分进行左单旋调整即可,得到c图。
5.1.3 AVL树的实现
class AVLTree:
def __init__(self, num_list=[]):
self.root = None
for num in num_list:
self.insert(num)
def insert(self, num):
if self.root is None:
self.root = AVLTNode(num)
else:
self.root = self._insert(num, self.root)
def _insert(self, num, root):
if root is None:
root = AVLTNode(num)
elif num < root.val:
root.left = self._insert(num, root.left)
if self.height(root.left) - self.height(root.right) == 2:
if num < root.left.val:
root = self.single_right_rotate(root)
else:
root = self.right_left_rotate(root)
elif num > root.val:
root.right = self._insert(num, root.right)
if self.height(root.right) - self.height(root.left) == 2:
if num > root.right.val:
root = self.single_left_rotate(root)
else:
root = self.left_right_rotate(root)
root.height = max(self.height(root.right), self.height(root.left)) + 1
return root
def remove(self, num):
if self.root is None:
raise KeyError('The AVLTree is empty!')
else:
self.root = self._remove(num, self.root)
def _findMin(self, node):
if node.left:
return self._findMin(node.left)
else:
return node
def findMin(self):
if self.root is None:
return None
else:
return self._findMin(self.root)
def _findMax(self, node):
if node.right:
return self._findMax(node.right)
else:
return node
def findMax(self):
if self.root is None:
return None
else:
return self._findMax(self.root)
def _remove(self, num, node):
if node is None:
raise KeyError('The key {} is not in this AVLTree!'.format(num))
elif num < node.val:
node.left = self._remove(num, node.left)
if self.height(node.right) - self.height(node.left) == 2:
if self.height(node.right.right) >= self.height(node.right.left):
node = self.single_left_rotate(node)
else:
node = self.left_right_rotate(node)
node.height = max(self.height(node.left), self.height(node.right)) + 1
elif num > node.val:
node.right = self._remove(num, node.right)
if self.height(node.left) - self.height(node.right) == 2:
if self.height(node.left.left) >= self.height(node.left.right):
node = self.single_right_rotate(node)
else:
node = self.right_left_rotate(node)
node.height = max(self.height(node.left), self.height(node.right)) + 1
elif node.left and node.right:
if node.left.height <= node.right.height:
min_node = self._findMin(node.right)
node.val = min_node.val
node.right = self._remove(node.val, node.right)
else:
max_node = self._findMax(node.left)
node.val = max_node.val
node.left = self._remove(node.val, node.left)
node.height = max(self.height(node.left), self.height(node.right)) + 1
else:
if node.right:
node = node.right
else:
node = node.left
return node
def single_right_rotate(self, node):
"""
这是右单旋操作,对LL型进行调整时执行的操作。
:param node:
:return:
"""
k1 = node.left
node.left = k1.right
k1.right = node
node.height = max(self.height(node.right), self.height(node.left)) + 1
k1.height = max(self.height(k1.left), node.height) + 1
return k1
def single_left_rotate(self, node):
"""
这是左单旋操作,对RR型进行调整时执行的操作。
:param node:
:return:
"""
k1 = node.right
node.right = k1.left
k1.left = node
node.height = max(self.height(node.right), self.height(node.left)) + 1
k1.height = max(self.height(k1.right), node.height) + 1
return k1
def left_right_rotate(self, node):
"""
对RL型进行调整时进行的操作。
:param node:
:return:
"""
self.single_right_rotate(node.right)
return self.single_left_rotate(node)
def right_left_rotate(self, node):
"""
对LR型进行调整时进行的操作。
:param node:
:return:
"""
self.single_left_rotate(node.left)
return self.single_right_rotate(node)
@staticmethod
def height(node):
return node.height if node else -1
5.2 红黑树
红黑树是一种自平衡的二叉查找树,除了符合二叉查找树的基本特性之外,还具有以下特性:
- 结点有黑红两种颜色
- 根结点为黑色
- 每个叶子结点都是黑色的空结点
- 每个红色结点的两个子结点都是黑色
- 从任何一个结点到其每个叶子结点的所有路径都包含相同数目的黑色结点。
插入操作:
- 将红黑树当做一棵二叉查找树将结点插入。
- 将插入的结点着色为红色
- 通过一系列的旋转或者着色等操作,使之重新成为一棵红黑树。
为什么着色为红色?
将插入的结点着色为红色不会违背最后一条特性,减少一条特性的违背,就意味着能少处理一些情况。
具体的调整操作可以参考面试常问:什么是红黑树。
六、线索二叉树
6.1 产生背景和定义
常用的二叉树的链表表示一个结点共有三个域,包括一个数据域和两个指针域,一棵二叉树的结构可以如下图所示,从图中我们可以看出,一棵有4个结点的树会有五个空指针域,这些空指针域造成了很大的空间浪费。
当我们在对上面这棵树进行中序遍历时,得到的遍历序列为4, 2, 1, 3
,只有在遍历结束之后我们才能知道某个结点的前驱和后继是谁,因此,为了加快查找结点前驱和后继的速度,我们可以使用空指针域来存放结点的前驱和后继,这就形成了线索二叉树。
在对二叉树进行线索化的时候,通常规定:若结点无左子树,则让其左孩子指向其前驱结点,若结点无右子树,则让其右孩子指向其后继结点。
当然这样也导致了一个问题,我们无法判断这个结点的左孩子或者右孩子指向的左子树或右子树还是前驱结点或后继结点。为了解决这一问题,需要引入两个标志位ltag
和rtag
。
这样我们一个线索二叉树的结点可以定义如下:
class TreeNode:
def __init__(self, val):
self.val = val
self.lchild = None
self.rchild = None
self.ltag = 0
self.rtag = 0
"""
这里0表示它指向一棵树,1表示它指向前驱或后继结点
"""
6.2 线索二叉树的构造
下图为对6.1中的二叉树进行中序线索化之后的结果:
对二叉树的线索化,实质上就是遍历一次二叉树,然后在遍历的过程中,检查当前节点左、右指针域是否为空,若为空,将它们改为指向前驱或者后继结点的线索。
以中序遍历的线索化为例,实现代码如下:
class ThreadTree:
def __init__(self, root):
self.root = root
self.pre = None
def create_in_thread(self):
if not self.root:
return
self._create_in_thread(self.root)
self.pre.rchild = None
self.pre.rtag = 1
def _create_in_thread(self, node):
if not node:
return
self._create_in_thread(node.lchild)
if node.lchild is None:
node.lchild = self.pre
node.ltag = 1
if self.pre and self.pre.rchild is None:
self.pre.rchild = node
self.pre.rtag = 1
self.pre = node
self._create_in_thread(node.rchild)
def trav_thread_in_order(self):
"""
遍历线索二叉树
"""
if self.root is None:
return
tmp = self.root
while tmp:
while tmp.ltag == 0:
tmp = tmp.lchild
print(tmp.val, end=' ')
while tmp.rtag == 1:
tmp = tmp.rchild
print(tmp.val, end=' ')
tmp = tmp.rchild
root = TreeNode(1)
n1 = TreeNode(2)
root.lchild = n1
n2 = TreeNode(3)
root.rchild = n2
root.lchild.lchild = TreeNode(4)
thread_tree = ThreadTree(root)
thread_tree.create_in_thread()
thread_tree.trav_thread_in_order()
6.3 访问线索二叉树
还是以中序线索二叉树为例,假设p为线索二叉树中的某个结点,查找其后继结点的方法如下:
p.rtag
的值为1,则其右孩子指针即为其后继结点。p.rtag
的值为0,那么该结点的中序遍历的后继结点是其右子树进行中序遍历时得到的第一个结点。也就是从其右子树的根结点的开始沿左孩子指针向下遍历,直到没有左孩子的那个结点,即为p结点的后继。
查找其后继结点的方法如下:
p.ltag
的值为1,则其左孩子指针即为其前驱结点。p.ltag
的值为0,那么该结点的中序遍历的前驱结点是其左子树进行中序遍历时得到的最后一个结点。也就是从起左子树的根结点爱是沿右孩子指针向下遍历,直到没有右孩子的那个结点,即为p结点的前驱。
七、B-树和B+树
7.1 B-树
首先它不叫“B减树”,它就叫“B树”。
B-树是一种自平衡树,它维护的是有序的数据,并且可以以对数时间复杂度进行搜索、顺序访问、插入和删除。B-树是二叉搜索树的一般化,因为在B-树中一个结点可以有多个子节点。B-树非常适合读取和写入相对较大的数据块的存储系统,通常用于数据库和文件系统。
B-树是一种平衡的多分支树,通常我们说的m阶B树需要满足以下条件:
- 每个节点最多有m个子节点;
- 每个非叶子结点(除了根节点)具有至少⌈ m/2⌉个子节点;
- 如果根节点不是叶子节点那么它至少有两个子节点;
- 具有k个子节点的非叶子节点包含k-1个键;
- 所有的叶子节点都位于同一层。
上图即为一棵4阶B树。
针对m阶B树进行插入操作时,首先判断其在B-树中是否已经存在,如果不存在,即在叶子节点处结束,然后在该叶子节点插入新的元素。
- 若该结点元素个数小于m-1,则直接插入即可;
- 若该结点元素个数等于m-1,则会引起结点分裂;以该结点中间元素为分界,取中间元素(偶数个元素时,中间两个随机选择一个)插入到父结点;
- 重复上面的操作,直到所有结点都符合B树的规则;最坏的情况是一直分裂到根结点,生成新的根结点,树的高度加一。
7.2 B+树
B+树是应文件系统所需而产生的B树的变形树,一棵m阶B+树或者为空,或者是满足以下条件:
- 树中每一个分支结点至多有m棵子树,除根结点之外的分支结点至少有⌊m/2⌋棵子树,如果根结点不是叶结点,至少有两棵子树。
- 关键码在结点里顺序存放。分支结点里每一个关键码关联着一棵子树,这个关键码等于其所关联子树的根结点里的最大关键码。叶结点里的每个关键码关联着一个数据项的存储位置,数据项另行存储。
与B树不同的是,分支结点中的关键码不是子树区分的关键码,可以将其看作子树的索引关键码,分支结点中的关键码不关联数据项,只有叶子结点的关键码关联数据项。
例如上图为一棵4阶B+树。
B+树的优势:
- 单一结点存储更多的元素,使得查询的IO次数更少;
- 所有查询都要查找到叶子结点,查询性能稳定;
- 所有叶子结点形成有序链表,便于范围查询。