14.4 2-3 树
2-3 树是一种平衡树,其形状定义如下:
- 每一个结点可储存1个或2个关键码,分别以k1和k2表示;
- 每一个结点有2~3个子结点,分别为左子结点、中子结点和右子结点;
- 所有的叶结点都在同一层;
- 每一个内结点如果只有一个关键码,则其只有两个子结点,如果有两个关键码,则其有三个子结点。
- 所有小于该结点第一个关键码的关键码都在左子树中;
- 如果结点只含有两个子结点,则大于该结点关键码的都在中子树中;
- 如果结点含有三个子结点,则在该结点两个关键码之间的关键在中子树中,所有大于该结点第二个关键码的关键码在右子树种。
14.4.1 查找
2-3树的查找与二叉查找树类似,通过比较目标关键字与结点处的关键码,如果位于该结点则表明目标关键字在2-3树中,如果不在则定位该结点的正确子树,继续上述过程。要注意到的是,如果目标关键字不存在,那么指针最后肯定停留在叶结点上。
14.4.2 插入
2-3树的插入较二叉查找树更为复杂,但基本思路是相同的,首先是进行查找,判断目标关键字是否已存在树中,如果存在,则修改相应值即可,如果不存在,由前面的查找操作可知,指针最后停留在叶结点上,首先要确定的是该叶结点的是否还能储存目标关键字,如果可以存储,那么还要比较其与该叶结点已存在的关键码的大小,根据大小来插入。
分裂叶结点
但是如果该叶结点的两个数据域都满了,情况就变复杂了。之前的二叉查找树是在原结点处分支出新结点,但是2-3树不同,它的所有叶结点都在同一层。因此,只能将该叶结点分裂,与此同时在同一层创建新的一个叶结点。
这一过程具体如下,首先在同一层创建一个新结点,然后将要插入的关键码与原叶结点的两个关键码比较,最小的关键码储存在原结点中,最大的关键码存在新创建的结点中,而处在中间的关键码则上溢至原叶结点的父结点中,即插入父结点中。
处于中间的关键码上溢至父结点后,其插入方式与插入叶结点类似,尤其是父结点只有一个关键码的情况。
若父结点也已经有两个关键码,则重复上述分裂结点的过程,唯一不同的是要改变父结点与子结点的连接。
如果要分裂根结点,需要创建一个新的根结点,用来储存上溢的关键码。
实现
#-*-coding: utf-8-*-
# 2-3树实现的映射ADT
class Tree23Map(object):
def __init__(self):
self._root = None
self._size = 0
def _23Search(self, subtree, target):
if subtree is None:
return
elif subtree.hasKey(target):
return subtree.getData(target)
else:
branch = subtree.getBranch(target)
return self._23Search(branch, target)
def _23Insert(self, key, newitem):
if self._root is None: # 空树情形
self._root = _23TreeNode(key, newitem)
else: # 查找正确的叶结点插入
(pKey, pData, pRef) = self._23SplitNode(self._root, key, newitem)
# 判断叶结点是否分裂
if pKey is not None:
newRoot = _23TreeNode(pKey, pData)
newRoot.left = self._root
newRoot.middle = pRef
self._root = newRoot
# 向2-3树中插入结点的递归方法,这一方法也是关于根结点的两种特殊情况,首先是空树,创建结点作为根结点,其次是分裂根结点
def _23RecInsert(self, subtree, key, newitem):
# 首先确保目标关键字不在2-3树中
if subtree.hasKey(key):
return (None, None, None)
# 判断传入结点subtree是否是叶结点
elif subtree.isALeaf(): # 是叶结点,就直接创建新结点,然后插入2-3树
return self._23AddToNode(subtree, key, newitem, None)
# 如果传入结点subtree是内结点
else:
branch = subtree.getBranch(key) # 确定目标关键码要插入的位置位于传入结点subtree的哪一个分支
(pKey, pData, pRef) = self._23RecInsert(branch, key, newitem) # pKey、pData和pRef表示上溢关键字、相应值以及新创建的结点
if pKey is None:
return (None, None, None)
else:
return self._23AddToNode(subtree, pKey, pData, pRef)
# 向一个结点中插入关键字,其中pRef表示新创建的子结点
def _23AddToNode(self, subtree, key, data, pRef):
# 判断结点subtree是否已满
if subtree.isFull(): # 满了,就分裂结点
return self._23SplitNode(subtree, key, data, None)
# 如果没满,就找出正确插入位置
else:
if key < subtree.key1:
subtree.key2 = subtree.key1
subtree.data2 = subtree.data1
subtree.key1 = key
subtree.data1 = data
if pRef is not None: # 上溢关键字插入父结点的第一种情况
subtree.right = subtree.middle
subtree.middle = pRef
else:
subtree.key2 = key
subtree.data2 = data
if pRef is not None: # 上溢关键字插入父结点的第二种情况
subtree.right = pRef
return (None, None, None)
# 分裂非根结点,返回一个元组,包括上溢关键字pKey、上溢关键字的相应值pData以及新创建的结点newnode,其中pRef(新创建的子结点)不为None时,表示分裂内结点。
def _23SplitNode(self, node, key, data, pRef):
# 创建新的空结点
newnode = _23TreeNode(None, None)
# 接下来是确定上溢关键字
# 目标关键字小于插入结点的第一个关键字
if key < node.key1:
pKey = node.key1
pData = node.data1
node.key1 = key
node.data1 = data
newnode.key1 = node.key2
newnode.data1 = node.data2
if pRef is not None: # 上溢关键字插入父结点的第一种情况
newnode.left = node.middle
newnode.middle = node.right
node.middle = pRef
# 目标关键字介于插入结点的两个关键字之间
elif key < node.key2:
pKey = key
pData = data
newnode.key1 = node.key2
newnode.data1 = node.data2
if pRef is not None: # 上溢关键字插入父结点的第二种情况
newnode.left = pRef
newnode.middle = node.right
# 目标关键字大于插入结点的第二个关键字
else:
pKey = node.key2
pData = node.data2
newnode.key1 = key
newnode.data1 = data
if pRef is not None:
newnode.left = node.right
newnode.middle = pRef
node.key2 = None
node.data2 = None
return (pKey, pData, newnode)
# 2-3树的储存类
class _23TreeNode(object):
def __init__(self, key, data):
self.key1 = key
self.key2 = None # 第二个数据域初始化为空
self.data1 = data
self.data2 = None
self.left = None
self.middle = None
self.right = None
# 判断叶结点
def isALeaf(self):
return self.left is None and self.middle is None and self.right is None
# 判断该结点数据域是否已满
def isFull(self):
return self.key2 is not None
# 判断目标关键码是否位于该结点
def hasKey(self, target):
if (target == self.key1) or (self.key2 is not None and target == self.key2):
return True
else:
return False
# 假定目标关键码在该结点,返回对应值
def getData(self, target):
if target == self.key1:
return self.data1
elif self.key2 is not None and target == self.key2:
return self.data2
else:
return None
# 如果目标关键码不在该结点,判断其在哪个子树中
def getBranch(self, target):
if target < self.key1:
return self.left
elif self.key2 is None:
return self.middle
elif target < self.key2:
return self.middle
else:
return self.right
2-3树的最坏情况是每一个结点只有一个值,两个子结点时,显然想要遍历整个2-3树的时间复杂度是O(n),查找操作依赖于树的高度,而2-3树的最大高度就是log n,所以时间复杂度是O(log n)。
插入操作和删除操作是类似的,都是先进行查找操作,然后根据情况插入。最坏情况是,每一层都需要分裂结点,直到根结点为止,而分裂结点所需时间显然是固定时间的,所以插入操作的时间复杂度是O(log n)。