一、二叉排序树:又称二叉查找树,或二叉搜索树。
二叉排序树或者为空,或者具有以下性质:
- 如果其左子树不空,那么其左子树上所有结点的值均小于其根结点的值。
- 如果其右子树不空,那么其右子树上所有结点的值均大于其根结点的值。
- 非空的左子树或右子树也是二叉排序树。
如果对二叉排序树做中序遍历,将得到一个递增序列。利用二叉排序树可以提高查找、插入和删除元素的速度。
下图是数据集[36,65,18,7,60,89,43,57,96,52,74]所生成的两棵二叉排序树。可见,同一个数据集对应的二叉排序树不唯一。
以下利用二叉排序树来实现一个字典类:
class Assoc: # 定义一个关联类 def __init__(self, key, value): self.key = key # 键(关键码) self.value = value # 值 def __lt__(self, other): # Python解释器中遇到比较运算符<,会去找类里定义的__lt__方法(less than) return self.key < other.key def __le__(self, other): # (less than or equal to) return self.key < other.key or self.key == other.key def __str__(self): return 'Assoc({0},{1})'.format(self.key, self.value) # key和value分别替换前面{0},{1}的位置。 class BinTNode: def __init__(self, dat, left=None, right=None): self.data = dat self.left = left self.right = right class DictBinTNode: def __init__(self, root=None): self.root = root def is_empty(self): return self.root is None def search(self, key): # 检索是否存在关键码key bt = self.root while bt is not None: entry = bt.data if key < entry.key: bt = bt.left elif key > entry.key: bt = bt.right else: return entry.value return None if __name__ == "__main__": # t = BinTNode(5, BinTNode(3,BinTNode(2)), BinTNode(8)) t = BinTNode(Assoc(5, 'a'), BinTNode(Assoc(3, 'b'), BinTNode(Assoc(2, 'd'))), BinTNode(Assoc(8, 'c'))) dic = DictBinTNode(t) print(dic.search(3))
二、向二叉排序树字典中插入数据的基本算法:
- 如果二叉树为空,直接将数据插在根结点上
- 如果小于根结点的值,则转向左子树,若左子树为空,则将数据插在这里。
- 如果大于根结点的值,则转向右子树,若右子树为空,则将数据插在这里。
- 如果等于结点的值,则将结点的关联值直接替换掉。
def insert(self, key, value): bt = self.root if bt is None: self.root = BinTNode(Assoc(key, value)) return while True: entry = bt.data if key < entry.key: # 如果小于当前关键码,转向左子树 if bt.left is None: # 如果左子树为空,就直接将数据插在这里 bt.left = BinTNode(Assoc(key, value)) return bt = bt.left elif key > entry.key: if bt.right is None: bt.right = BinTNode(Assoc(key, value)) return bt = bt.right else: bt.data.value = value return if __name__ == "__main__": t = BinTNode(Assoc(5, 'a'), BinTNode(Assoc(3, 'b'), BinTNode(Assoc(2, 'd')), BinTNode(Assoc(4, 'e'))), BinTNode(Assoc(8, 'c'))) dic = DictBinTree(t) dic.insert(7, 'e')
三、下面定义一个可以将字典中所有数据值都打印出来的迭代器方法,以便于可以利用for循环查看里面所有的数据。(其实就是中序遍历)。 SStack类是基于之前文章中的定义的栈类:点击打开链接
def print_all_values(self): bt, s = self.root, SStack() while bt is not None or not s.is_empty(): # 最开始时栈为空,但bt不为空;bt = bt.right可能为空,栈不为空;当两者都为空时,说明已经全部遍历完成了 while bt is not None: s.push(bt) bt = bt.left bt = s.pop() # 将栈顶元素弹出 yield bt.data.value bt = bt.right # 将当前结点的右子结点赋给bt,让其在while中继续压入栈内 if __name__ == "__main__": t = BinTNode(Assoc(5, 'a'), BinTNode(Assoc(3, 'b'), BinTNode(Assoc(2, 'd')), BinTNode(Assoc(4, 'e'))), BinTNode(Assoc(8, 'c'))) dic = DictBinTree(t) for i in dic.print_all_values(): print(i)
四、有时希望将字典中的键值对取出,只需修改上面函数中的一个语句即可。将yield bt.data.value换为yield bt.data.key, bt.data.value。
def entries(self): bt, s = self.root, SStack() while bt is not None or not s.is_empty(): while bt is not None: s.push(bt) bt = bt.left bt = s.pop() yield bt.data.key, bt.data.value bt = bt.right def print_key_value(self): for k, v in self.entries(): print(k, v)
五、二叉排序树的删除有三种情况
- 如果待删除结点是叶结点:则将其直接删除即可。
- 如果待删除结点仅有左子树或者右子树:则将左子树或者右子树直接上移,取代删除结点的位置即可。
- 如果待删除结点既有左子树又有右子树:则用待删除结点的直接前驱、或直接后继来取代待删除结点,然后调整前驱或后继结点处的树的位置。
以下是一个例子:
(1)如果删除的是以下带有颜色的叶子结点的位置,直接将其删除即可。
(2)如果删除的结点只有左子树或者右子树,则将左子树或者右子树直接上移
(3)如果删除的结点105既只有左子树又有右子树,此处选择用其前驱结点104来取代删除结点,然后将103结点上移到104结点的位置。这里注意104结点不可能再有右子树,因为104结点已经是105结点的直接前驱,如果104结点存在右子树,则105的直接前驱将是其他的值。
def delete(self, key): # 以下这一段用于找到待删除结点及其父结点的位置。 del_position_father, del_position = None, self.root # del_position_father是待删除结点del_position的父结点 while del_position is not None and del_position.data.key != key: # 通过不断的比较,找到待删除结点的位置 del_position_father = del_position if key < del_position.data.key: del_position = del_position.left else: del_position = del_position.right if del_position is None: print('There is no key') return if del_position.left is None: # 如果待删除结点只有右子树 if del_position_father is None: # 如果待删除结点的父结点是空,则说明待删除结点是根结点 self.root = del_position.right # 则直接将根结点置空 elif del_position is del_position_father.left: # 如果待删除结点是其父结点的左结点 del_position_father.left = del_position.right # ***改变待删除结点父结点的左子树的指向 else: del_position_father.right = del_position.right return # 如果既有左子树又有右子树,或者仅有左子树时,都可以用直接前驱替换的删除结点的方式,只不过得到的二叉树与原理中说明的不一样,但是都满足要求。 pre_node_father, pre_node = del_position, del_position.left while pre_node.right is not None: # 找到待删除结点的左子树的最右结点,即为待删除结点的直接前驱 pre_node_father = pre_node pre_node = pre_node.right del_position.data = pre_node.data # 将前驱结点的data赋给删除结点即可,不需要改变其原来的连接方式 if pre_node_father.left is pre_node: pre_node_father.left = pre_node.left if pre_node_father.right is pre_node: pre_node_father.right = pre_node.left if __name__ == "__main__": t = BinTNode(Assoc(5, 'a'), BinTNode(Assoc(3, 'b'), BinTNode(Assoc(2, 'd')), BinTNode(Assoc(4, 'e'), BinTNode(Assoc(3.5, 'f'))))) dic = DictBinTree(t) dic.delete(3) for i in dic.print_all_values(): print(i)
六、最后单独定义一个可以生成一棵二叉排序树。它不是二叉排序树类的一部分
def build_dictBinTree(entries): dic = DictBinTree() for k, v in entries: dic.insert(k, v) return dic if __name__ == "__main__": entries = [(5, 'a'), (3, 'b'), (2, 'd'), (4, 'e'), (3.5, 'f')] dic = build_dictBinTree(entries) dic.print_key_value()
总结:
以上的查找,插入,删除等操作的效率,都与二叉树的高度有密切的关系。
如果一棵二叉树的结构良好,其高度与树中结点个数成对数关系O(log n)。
如果一棵二叉树的结构畸形,则查找的时间复杂度可能达到O(n)。这是最坏的情况。
如果build_dictBinTree(entries)中的entries序列按照递增或者递减顺序排列,则得到的就会是一棵高度等于结点个数的二叉排序树。
最后附上全部代码,省的一点点复制了。
class StackUnderflow(ValueError):
pass
class SStack():
def __init__(self):
self.elems = []
def is_empty(self):
return self.elems == []
def top(self): # 取得栈里最后压入的元素,但不删除
if self.elems == []:
raise StackUnderflow('in SStack.top()')
return self.elems[-1]
def push(self, elem):
self.elems.append(elem)
def pop(self):
if self.elems == []:
raise StackUnderflow('in SStack.pop()')
return self.elems.pop()
class Assoc: # 定义一个关联类
def __init__(self, key, value):
self.key = key # 键(关键码)
self.value = value # 值
def __lt__(self, other): # Python解释器中遇到比较运算符<,会去找类里定义的__lt__方法(less than)
return self.key < other.key
def __le__(self, other): # (less than or equal to)
return self.key < other.key or self.key == other.key
def __str__(self):
return 'Assoc({0},{1})'.format(self.key, self.value) # key和value分别替换前面{0},{1}的位置。
class BinTNode:
def __init__(self, dat, left=None, right=None):
self.data = dat
self.left = left
self.right = right
class DictBinTree:
def __init__(self, root=None):
self.root = root
def is_empty(self):
return self.root is None
def search(self, key): # 检索是否存在关键码key
bt = self.root
while bt is not None:
entry = bt.data
if key < entry.key:
bt = bt.left
elif key > entry.key:
bt = bt.right
else:
return entry.value
return None
def insert(self, key, value):
bt = self.root
if bt is None:
self.root = BinTNode(Assoc(key, value))
return
while True:
entry = bt.data
if key < entry.key: # 如果小于当前关键码,转向左子树
if bt.left is None: # 如果左子树为空,就直接将数据插在这里
bt.left = BinTNode(Assoc(key, value))
return
bt = bt.left
elif key > entry.key:
if bt.right is None:
bt.right = BinTNode(Assoc(key, value))
return
bt = bt.right
else:
bt.data.value = value
return
def print_all_values(self):
bt, s = self.root, SStack()
while bt is not None or not s.is_empty(): # 最开始时栈为空,但bt不为空;bt = bt.right可能为空,栈不为空;当两者都为空时,说明已经全部遍历完成了
while bt is not None:
s.push(bt)
bt = bt.left
bt = s.pop() # 将栈顶元素弹出
yield bt.data.key, bt.data.value
bt = bt.right # 将当前结点的右子结点赋给bt,让其在while中继续压入栈内
def entries(self):
bt, s = self.root, SStack()
while bt is not None or not s.is_empty():
while bt is not None:
s.push(bt)
bt = bt.left
bt = s.pop()
yield bt.data.key, bt.data.value
bt = bt.right
def print_key_value(self):
for k, v in self.entries():
print(k, v)
def delete(self, key):
# 以下这一段用于找到待删除结点及其父结点的位置。
del_position_father, del_position = None, self.root # del_position_father是待删除结点del_position的父结点
while del_position is not None and del_position.data.key != key: # 通过不断的比较,找到待删除结点的位置
del_position_father = del_position
if key < del_position.data.key:
del_position = del_position.left
else:
del_position = del_position.right
if del_position is None:
print('There is no key')
return
if del_position.left is None: # 如果待删除结点只有右子树
if del_position_father is None: # 如果待删除结点的父结点是空,则说明待删除结点是根结点
self.root = del_position.right # 则直接将根结点置空
elif del_position is del_position_father.left: # 如果待删除结点是其父结点的左结点
del_position_father.left = del_position.right # ***改变待删除结点父结点的左子树的指向
else:
del_position_father.right = del_position.right
return
# 如果既有左子树又有右子树,或者仅有左子树时,都可以用直接前驱替换的删除结点的方式,只不过得到的二叉树与原理中说明的不一样,但是都满足要求。
pre_node_father, pre_node = del_position, del_position.left
while pre_node.right is not None: # 找到待删除结点的左子树的最右结点,即为待删除结点的直接前驱
pre_node_father = pre_node
pre_node = pre_node.right
del_position.data = pre_node.data # 将前驱结点的data赋给删除结点即可,不需要改变其原来的连接方式
if pre_node_father.left is pre_node:
pre_node_father.left = pre_node.left
if pre_node_father.right is pre_node:
pre_node_father.right = pre_node.left
def build_dictBinTree(entries):
dic = DictBinTree()
for k, v in entries:
dic.insert(k, v)
return dic
if __name__ == "__main__":
# t = BinTNode(5, BinTNode(3,BinTNode(2)), BinTNode(8))
# t = BinTNode(Assoc(5,'a'), BinTNode(Assoc(3,'b'),BinTNode(Assoc(2,'d')), BinTNode(Assoc(4,'e')) ), BinTNode(Assoc(8,'c')))
# t = BinTNode(Assoc(5,'a'))
# t = BinTNode(Assoc(5,'a'), BinTNode(Assoc(3,'b'),BinTNode(Assoc(2,'d')), BinTNode(Assoc(4,'e'),BinTNode(Assoc(3.5,'f')) ) ) )
# dic = DictBinTree(t)
# print(dic.search(3))
# dic.insert(7,'e')
# dic.delete(5)
# for i in dic.print_all_values():
# print(i)
entries = [(5, 'a'), (3, 'b'), (2, 'd'), (4, 'e'), (3.5, 'f')]
dic = build_dictBinTree(entries)
dic.print_key_value()