1 AVL树
2 AVL树的旋转-左旋和右旋
2.1 AVL树的旋转实现
3 二叉搜索树的扩展应用-B树
3.2 B+树
1 AVL树
AVL树:AVL树是一棵自平衡的二叉搜索树。
AVL树具有以下性质:
根的左右子树的高度之差的绝对值不能超过1
根的左右子树都是平衡二叉树
平均情况下,二叉搜索树进行搜索的时间复杂度为O(lgn)。
最坏情况下,二又搜索树可能非常偏斜。
解决方案:
1 随机化插入
2 D AVL树
2 AVL树的旋转-左旋和右旋
1 插入一个节点可能会破坏AVL树的平衡,可以通过"旋转"操作来进行修正
2 插入一个节点后,只有从插入节点到根节点的路上的节点的平衡可能被改变。
需要找出第一个破坏了平衡条件的节点,称之为K。K的两颗子树的高度差2。
3 不平衡的出现可能有4种情况
2.1 AVL树的旋转实现
class BiTreeNode:
def __init__(self, data):
"""
初始化二叉树节点
:param data: 节点的值
"""
self.data = data # 节点的值
self.lchild = None # 左子节点
self.rchild = None # 右子节点
self.parent = None # 父节点
class BST:
def __init__(self, li: list):
"""
初始化二叉搜索树,并插入给定的值
:param li: 包含插入值的列表
"""
self.root = None # 初始化根节点为空
if li:
for val in li:
self.insert_no_rec(val) # 使用非递归插入方法将列表中的值插入树中
def insert_no_rec(self, val):
"""
非递归插入节点到二叉搜索树中
:param val: 要插入的值
:return: None
"""
p = self.root
if not p:
# 如果树为空,将新节点设置为根节点
self.root = BiTreeNode(val)
return
while True:
if val < p.data:
# 如果要插入的值小于当前节点的值,则移动到左子树
if p.lchild:
p = p.lchild
else:
# 如果左子树为空,则在此位置插入新节点
p.lchild = BiTreeNode(val)
p.lchild.parent = p # 更新新节点的父节点
return
elif val > p.data:
# 如果要插入的值大于当前节点的值,则移动到右子树
if p.rchild:
p = p.rchild
else:
# 如果右子树为空,则在此位置插入新节点
p.rchild = BiTreeNode(val)
p.rchild.parent = p # 更新新节点的父节点
return
else:
# 如果要插入的值等于当前节点的值,则不做任何操作(BST中不允许重复值)
return
def query_no_rec(self, val):
"""
非递归查询二叉搜索树中的节点
:param val: 要查询的值
:return: 节点对象(如果找到)或 None(如果未找到)
"""
p = self.root
while p:
if p.data < val:
# 如果当前节点的值小于要查询的值,则移动到右子树
p = p.rchild
elif p.data > val:
# 如果当前节点的值大于要查询的值,则移动到左子树
p = p.lchild
else:
# 如果当前节点的值等于要查询的值,返回当前节点
return p
# 如果遍历结束仍未找到值,返回 None
return None
def pre_order(self, root):
"""
二叉树的前序遍历
:param root:
:return:
"""
if root:
print(root.data, end=',')
self.pre_order(root.lchild)
self.pre_order(root.rchild)
def mid_order(self, root):
"""
二叉树的中序遍历
:param root:
:return:
"""
if root:
self.mid_order(root.lchild)
print(root.data, end=',')
self.mid_order(root.rchild)
def post_order(self, root):
"""
二叉树的后序遍历
:param root:
:return:
"""
if root:
self.post_order(root.lchild)
self.post_order(root.rchild)
print(root.data, end=',')
class AVLNode(BiTreeNode):
def __init__(self, data):
BiTreeNode.__init__(self, data)
self.bf = 0 # balance_factor
class AVLTree(BST):
def __init__(self, li: list = None):
BST.__init__(self, li)
def insert_no_rec(self, val):
"""
插入节点到二叉搜索树中
:param val: 要插入的值
:return: None
"""
# 1.插入节点
p = self.root
if not p:
# 如果树为空,将新节点设置为根节点
self.root = AVLNode(val)
return
while True:
if val < p.data:
# 如果要插入的值小于当前节点的值,则移动到左子树
if p.lchild:
p = p.lchild
else:
# 如果左子树为空,则在此位置插入新节点
p.lchild = AVLNode(val)
p.lchild.parent = p # 更新新节点的父节点
node = p.lchild # node 存储的就是插入的节点
break
elif val > p.data:
# 如果要插入的值大于当前节点的值,则移动到右子树
if p.rchild:
p = p.rchild
else:
# 如果右子树为空,则在此位置插入新节点
p.rchild = AVLNode(val)
p.rchild.parent = p # 更新新节点的父节点
node = p.rchild
break
else: # val = p.data
# 如果要插入的值等于当前节点的值,则不做任何操作(BST中不允许重复值)
return
# 2.更新balance factor
while node.parent: # node.parent不为空
if node.parent.lchild == node: # 传递是从左子树来的,左子树更沉了
# 更新node.parent的bf -= 1
if node.parent.bf < 0: # 原来的node.parent.bf == -1,更新变成-2
# 做旋转
# 看node哪边沉
g = node.parent.parent # 为了连接旋转之后的子树
x = node.parent # 旋转前的子树的根
if node.bf > 0: # 右边沉
n = self.rotate_left_right(node.parent, node)
else: # 左边沉
n = self.rotate_right(node.parent, node)
# 记得把n和g连起来
elif node.parent.bf > 0: # 原来node.parent.bf = 1,更新之后变成0
node.parent.bf = 0
break
else: # 原来的node.parent.bf = 0,更新之后变成-1
node.parent.bf = -1
node = node.parent
continue
else: # 传递是从右子树来的,右子树更沉了
# 更新node.parent.bf+=1
if node.parent.bf > 0: # 原来node.parent.bf = 1,更新之后变成2
# 做旋转
# 看哪边沉
g = node.parent.parent # 为了连接旋转之后的子树
x = node.parent # 旋转前的子树的根
if node.bf < 0: # 左边沉
n = self.rotate_right_left(node.parent, node)
else: # 右边沉
n = self.rotate_left(node.parent, node)
# 记得把n和g连起来
elif node.parent.bf < 0: # 原来node.parent.bf = -1,更新之后变成0
node.parent.bf = 0
break
else: # 原来node.parent.bf = 0,更新之后变成1
node.parent.bf = 1
node = node.parent
continue
# 连接旋转后的子树,把n和g连起来
n.parent = g
if g: # g不是空
if x == g.lchild:
g.lchild = n
else:
g.rchild = n
break
else:
self.root = n
break
def rotate_left(self, p, c):
"""
左旋操作
:param p: 当前节点 p(旋转前的根节点)
:param c: 右孩子节点 c(旋转后的新根节点)
:return: 新的子树根节点 c
"""
s2 = c.lchild # s2 是 c 的左孩子
p.rchild = s2 # p 的右孩子指向 s2
if s2:
s2.parent = p # 如果 s2 存在,更新 s2 的父节点为 p
c.lchild = p # c 的左孩子指向 p
p.parent = c # 更新 p 的父节点为 c
# 更新平衡因子
p.bf = 0
c.bf = 0
return c # 返回新的根节点 c
def rotate_right(self, p, c):
"""
右旋操作
:param p: 当前节点 p(旋转前的根节点)
:param c: 左孩子节点 c(旋转后的新根节点)
:return: 新的子树根节点 c
"""
s2 = c.rchild # s2 是 c 的右孩子
p.lchild = s2 # p 的左孩子指向 s2
if s2:
s2.parent = p # 如果 s2 存在,更新 s2 的父节点为 p
c.rchild = p # c 的右孩子指向 p
p.parent = c # 更新 p 的父节点为 c
# 更新平衡因子
p.bf = 0
c.bf = 0
return c # 返回新的根节点 c
def rotate_right_left(self, p, c):
"""
右旋-左旋复合操作
:param p: 当前节点 p(旋转前的根节点)
:param c: 右孩子节点 c(旋转前的右孩子)
:return: 新的子树根节点 g(旋转后的新根节点)
"""
g = c.lchild # g 是 c 的左孩子(旋转后的新根节点)
s3 = g.rchild # s3 是 g 的右孩子
c.lchild = s3 # c 的左孩子指向 s3
if s3:
s3.parent = c # 如果 s3 存在,更新 s3 的父节点为 c
g.rchild = c # g 的右孩子指向 c
c.parent = g # 更新 c 的父节点为 g
s2 = g.lchild # s2 是 g 的左孩子
p.rchild = s2 # p 的右孩子指向 s2
if s2:
s2.parent = p # 如果 s2 存在,更新 s2 的父节点为 p
g.lchild = p # g 的左孩子指向 p
p.parent = g # 更新 p 的父节点为 g
# 更新平衡因子
if g.bf > 0:
p.bf = -1
c.bf = 0
elif g.bf < 0:
p.bf = 0
c.bf = 1
else:
p.bf = 0
c.bf = 0
g.bf = 0
return g # 返回新的根节点 g
def rotate_left_right(self, p, c):
"""
左旋-右旋复合操作
:param p: 当前节点 p(旋转前的根节点)
:param c: 左孩子节点 c(旋转前的左孩子)
:return: 新的子树根节点 g(旋转后的新根节点)
"""
g = c.rchild # g 是 c 的右孩子(旋转后的新根节点)
s2 = g.lchild # s2 是 g 的左孩子
c.rchild = s2 # c 的右孩子指向 s2
if s2:
s2.parent = c # 如果 s2 存在,更新 s2 的父节点为 c
g.lchild = c # g 的左孩子指向 c
c.parent = g # 更新 c 的父节点为 g
s3 = g.rchild # s3 是 g 的右孩子
p.lchild = s3 # p 的左孩子指向 s3
if s3:
s3.parent = p # 如果 s3 存在,更新 s3 的父节点为 p
g.rchild = p # g 的右孩子指向 p
p.parent = g # 更新 p 的父节点为 g
# 更新平衡因子
if g.bf < 0:
p.bf = 1
c.bf = 0
elif g.bf > 0:
p.bf = 0
c.bf = -1
else:
p.bf = 0
c.bf = 0
g.bf = 0
return g # 返回新的根节点 g
tree = AVLTree([9, 8, 7, 6, 10, 5, 4, 3, 2, 1])
tree.pre_order(tree.root)
print('')
tree.mid_order(tree.root)
执行结果:
8,4,2,1,3,6,5,7,9,10,
1,2,3,4,5,6,7,8,9,10,
3 二叉搜索树的扩展应用-B树
"B树(B-Tree): B树是一棵自平衡的多路搜索树。常用于数据库的索引。"
B-Tree(B树)是一种自平衡的树数据结构,它可以保持数据有序,并允许二分查找、顺序访问、插入和删除。
B树广泛用于数据库和文件系统的实现中,因为它能够很好地管理大量数据,并且可以有效地减少磁盘I/O操作。
B-Tree的特点:
1 节点:每个节点可以包含多个键(数据),而不是像二叉树那样每个节点只包含一个键。
2 有序性:在每个节点中,键是按照非递减顺序排列的。
3 孩子节点数量:一个节点有 m 个孩子,则节点中最多有 m-1 个键。
B树的阶数 m 决定了节点的最小和最大子节点数量。
4 平衡性:B树是自平衡的,所有叶子节点都位于同一层,保证了查找、插入和删除操作的效率。
B-Tree的性质:
1 一个B树的节点包含至多 m-1 个键(数据),和至多 m 个子树。
2 如果根节点不是叶子节点,则根节点至少有两棵子树。
3 每个内部节点至少包含 ⌈m/2⌉ - 1 个键和 ⌈m/2⌉ 个子树。
4 所有叶子节点都在同一层。
B-Tree的插入和删除:
1 插入:
1 从根节点开始,递归找到应插入的叶子节点。
2 如果叶子节点未满,则直接插入。
3 如果叶子节点已满,则需要将该节点分裂成两个节点,并将中间键提升到父节点。
若父节点也满,则继续向上分裂,直到根节点。
2 删除:
1 如果删除的是叶子节点中的键且节点中的键数大于最小值,则直接删除。
2 如果删除的键在内部节点,需要选择合适的替代键(例如前驱或后继),然后递归删除该键。
3 如果节点在删除后键数不足,则需要进行合并或从兄弟节点借键。
class BTreeNode:
def __init__(self, leaf=False):
"""
初始化B树节点。
:param leaf: 布尔值,表示该节点是否为叶子节点。如果为True,则该节点是叶子节点。
"""
self.leaf = leaf # 是否为叶子节点
self.keys = [] # 节点中存储的键值列表
self.children = [] # 子节点列表,存储指向子节点的引用
class BTree:
def __init__(self, t):
"""
初始化B树。
:param t: B树的最小度数,表示每个节点中最少包含t-1个键,最多包含2t-1个键。
"""
self.root = BTreeNode(True) # 创建B树的根节点,初始时默认为叶子节点
self.t = t # 最小度数
def insert(self, k):
"""
插入键值k到B树中。
:param k: 要插入的键值。
"""
root = self.root
# 如果根节点已满,需要分裂
if len(root.keys) == (2 * self.t) - 1:
# 创建一个新的根节点,并设置它为非叶子节点
temp = BTreeNode()
# 将旧的根节点作为子节点挂载到新的根节点
self.root = temp
temp.children.append(root)
# 分裂旧的根节点
self._split_child(temp, 0)
# 将键值插入到适当的位置
self._insert_non_full(temp, k)
else:
# 如果根节点未满,直接插入
self._insert_non_full(root, k)
def _split_child(self, x, i):
"""
分裂子节点。假设x是一个非满节点,且x的第i个子节点是满的。
:param x: 需要分裂的节点的父节点。
:param i: 需要分裂的子节点在父节点子节点列表中的索引。
"""
t = self.t
# 获取需要分裂的子节点
y = x.children[i]
# 创建一个新的节点z来存储y的后半部分键值和子节点
z = BTreeNode(y.leaf)
# 将z插入到x的子节点列表中
x.children.insert(i + 1, z)
# 将y的中间键值提升到x中
x.keys.insert(i, y.keys[t - 1])
# 将y的后半部分键值移动到z中
z.keys = y.keys[t:(2 * t) - 1]
# 将y的前半部分键值保留在y中
y.keys = y.keys[0:t - 1]
# 如果y不是叶子节点,将它的后半部分子节点也移动到z中
if not y.leaf:
z.children = y.children[t:(2 * t)]
y.children = y.children[0:t]
def _insert_non_full(self, x, k):
"""
在非满节点中插入键值k。
:param x: 当前节点。
:param k: 要插入的键值。
"""
i = len(x.keys) - 1
if x.leaf:
# 如果是叶子节点,直接在合适位置插入键值
x.keys.append(None)
while i >= 0 and k < x.keys[i]:
x.keys[i + 1] = x.keys[i]
i -= 1
x.keys[i + 1] = k
else:
# 如果不是叶子节点,找到合适的子节点进行递归插入
while i >= 0 and k < x.keys[i]:
i -= 1
i += 1
# 如果子节点已满,先分裂子节点
if len(x.children[i].keys) == (2 * self.t) - 1:
self._split_child(x, i)
# 如果新插入的键值大于分裂后的中间键值,则移到右边的子节点进行插入
if k > x.keys[i]:
i += 1
self._insert_non_full(x.children[i], k)
def search(self, k, x=None):
"""
在B树中查找键值k。
:param k: 要查找的键值。
:param x: 当前节点,默认为None表示从根节点开始查找。
:return: 如果找到返回包含键值k的节点和索引,否则返回None。
"""
if x is not None:
i = 0
# 找到第一个大于或等于k的位置
while i < len(x.keys) and k > x.keys[i]:
i += 1
# 如果找到k,返回当前节点和索引
if i < len(x.keys) and k == x.keys[i]:
return (x, i)
# 如果当前节点是叶子节点且没有找到,返回None
elif x.leaf:
return None
else:
# 否则递归到适当的子节点继续查找
return self.search(k, x.children[i])
else:
# 如果x为None,从根节点开始查找
return self.search(k, self.root)
def print_tree(self, x, l=0):
"""
打印B树结构。
:param x: 当前节点。
:param l: 当前节点的层级,用于格式化输出。
"""
print("Level", l, " ", len(x.keys), end=": ")
for i in x.keys:
print(i, end=" ")
print()
l += 1
# 递归打印每个子节点
if len(x.children) > 0:
for i in x.children:
self.print_tree(i, l)
# 示例使用:
btree = BTree(3) # 创建最小度数为3的B树
data = [10, 20, 5, 6, 12, 30, 7, 17] # 要插入的键值列表
for item in data:
btree.insert(item) # 将键值逐个插入B树
btree.print_tree(btree.root) # 打印B树的结构
3.2 B+树
B+树 是 B树 的一种扩展版本,主要用于数据库和文件系统中的索引结构。
与 B树 相比,B+树 有以下几个显著特点:
1 所有的键值都存在叶子节点:
在 B+树 中,所有的数据记录都存储在叶子节点中,而内部节点仅存储索引键值,用于指引搜索路径。
2 链表结构的叶子节点:
B+树 的所有叶子节点通过一个链表连接起来,便于范围查询的实现。
这样可以很方便地遍历数据范围,而不需要重新从根节点开始查找。
3内部节点的键值用于索引:
B+树 的内部节点存储的是子树中最小键值的副本,而不包含指向具体记录的指针。
class BPlusTreeNode:
def __init__(self, leaf=False):
"""
初始化B+树节点。
:param leaf: 布尔值,表示该节点是否为叶子节点。
"""
self.leaf = leaf # 是否为叶子节点
self.keys = [] # 节点中存储的键值列表
self.children = [] # 子节点列表或叶子节点中的数据记录指针
self.next = None # 指向下一个叶子节点的指针(仅在叶子节点中使用)
class BPlusTree:
def __init__(self, t):
"""
初始化B+树。
:param t: B+树的最小度数,表示每个节点中最少包含t-1个键,最多包含2t-1个键。
"""
self.root = BPlusTreeNode(True) # 创建B+树的根节点,初始时默认为叶子节点
self.t = t # 最小度数
def insert(self, k):
"""
插入键值k到B+树中。
:param k: 要插入的键值。
"""
root = self.root
if len(root.keys) == (2 * self.t) - 1: # 根节点已满,需要分裂
temp = BPlusTreeNode() # 创建新的根节点
self.root = temp # 更新根节点
temp.children.append(root) # 原来的根节点成为新根节点的子节点
self._split_child(temp, 0) # 分裂根节点的子节点
self._insert_non_full(temp, k) # 插入键值到新的根节点
else:
self._insert_non_full(root, k) # 插入键值到非满节点
def _split_child(self, x, i):
"""
分裂子节点。假设x是一个非满节点,且x的第i个子节点是满的。
:param x: 需要分裂的节点的父节点。
:param i: 需要分裂的子节点在父节点子节点列表中的索引。
"""
t = self.t
y = x.children[i] # 要分裂的子节点
z = BPlusTreeNode(y.leaf) # 新建一个节点z用于存储分裂出的部分
# 在x中插入新节点z,并在x的keys中插入分裂点的中间键
x.children.insert(i + 1, z)
x.keys.insert(i, y.keys[t - 1])
# 将y中大于等于中间键的键和值分裂到z中
z.keys = y.keys[t:]
y.keys = y.keys[:t - 1]
if y.leaf:
z.children = y.children[t:] # 如果y是叶子节点,复制y的部分数据到z
y.children = y.children[:t]
z.next = y.next # 更新叶子节点链表的链接
y.next = z
else:
z.children = y.children[t:] # 如果y不是叶子节点,复制y的部分子节点到z
y.children = y.children[:t]
def _insert_non_full(self, x, k):
"""
在非满节点中插入键值k。
:param x: 当前节点。
:param k: 要插入的键值。
"""
i = len(x.keys) - 1 # 找到要插入的位置
if x.leaf: # 如果是叶子节点,直接插入
x.keys.append(None) # 占位
while i >= 0 and k < x.keys[i]:
x.keys[i + 1] = x.keys[i] # 依次后移,腾出插入位置
i -= 1
x.keys[i + 1] = k # 插入键值
else: # 如果不是叶子节点,需要递归插入到合适的子节点中
while i >= 0 and k < x.keys[i]:
i -= 1
i += 1
if len(x.children[i].keys) == (2 * self.t) - 1: # 如果子节点已满,先分裂
self._split_child(x, i)
if k > x.keys[i]: # 如果k大于分裂出的中间键,插入到右侧的子节点
i += 1
self._insert_non_full(x.children[i], k) # 递归插入到子节点
def search(self, k, x=None):
"""
在B+树中查找键值k。
:param k: 要查找的键值。
:param x: 当前节点,默认为None表示从根节点开始查找。
:return: 如果找到返回包含键值k的叶子节点,否则返回None。
"""
if x is not None: # 如果指定了节点,从该节点开始查找
i = 0
while i < len(x.keys) and k > x.keys[i]: # 找到大于等于k的位置
i += 1
if i < len(x.keys) and k == x.keys[i] and x.leaf: # 如果在叶子节点中找到k
return x # 返回该叶子节点
elif x.leaf: # 如果在叶子节点中未找到k
return None # 查找失败
else: # 如果是内部节点,递归查找合适的子节点
return self.search(k, x.children[i])
else: # 如果未指定节点,从根节点开始查找
return self.search(k, self.root)
def print_tree(self, x, l=0):
"""
打印B+树结构。
:param x: 当前节点。
:param l: 当前节点的层级,用于格式化输出。
"""
print("Level", l, " ", len(x.keys), end=": ") # 输出当前节点的层级和键值数量
for i in x.keys: # 输出当前节点的所有键值
print(i, end=" ")
print()
l += 1
if len(x.children) > 0: # 如果有子节点,递归打印子节点
for i in x.children:
self.print_tree(i, l)
# 示例使用:
btree = BPlusTree(3) # 创建一个B+树,最小度数为3
data = [10, 20, 5, 6, 12, 30, 7, 17] # 要插入的数据列表
for item in data: # 逐个插入数据
btree.insert(item)
btree.print_tree(btree.root) # 打印B+树结构