1、二叉排序树,又称二叉查找树,或二叉搜索树。
二叉排序树或者为空,或者具有以下性质:
- 如果其左子树不空,那么其左子树上所有结点的值均小于其根结点的值。
- 如果其右子树不空,那么其右子树上所有结点的值均大于其根结点的值。
- 非空的左子树或右子树也是二叉排序树。
如果对二叉排序树做中序遍历,将得到一个递增序列。利用二叉排序树可以提高查找、插入和删除元素的速度。
下图是数据集[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))
2、向二叉排序树字典中插入数据的基本算法:
- 如果二叉树为空,直接将数据插在根结点上
- 如果小于根结点的值,则转向左子树,若左子树为空,则将数据插在这里。
- 如果大于根结点的值,则转向右子树,若右子树为空,则将数据插在这里。
- 如果等于结点的值,则将结点的关联值直接替换掉。
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')3、下面定义一个可以将字典中所有数据值都打印出来的迭代器方法,以便于可以利用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)
4、有时希望将字典中的键值对取出,只需修改上面函数中的一个语句即可。将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)
5、二叉排序树的删除有三种情况
- 如果待删除结点是叶结点:则将其直接删除即可。
- 如果待删除结点仅有左子树或者右子树:则用左子树或者右子树直接上移,取代删除结点的位置即可。
- 如果待删除结点既有左子树又有右子树:则用待删除结点的直接前驱后者直接后继来取代待删除结点,然后调整前驱或后继结点处的树的位置。
以下是一个例子:
(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)
6、最后单独定义一个可以生成一棵二叉排序树。它不是二叉排序树类的一部分
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()