数据结构之二叉搜索树

100 篇文章 0 订阅

 

二叉搜索树(binary search tree)
二叉树(binary tree)可以看作是二维的列表,是列表在维度上的扩充
二叉搜索树同时借鉴了二叉树和有序向量(sorted vector)的特点
与其他数据结构相同,二叉搜索树也是由一组数据项构成的数据集合
二叉搜索树对其中数据项的访问方式:循关键码访问
二叉搜索树中每个节点的数据项都具有唯一的关键码,并以关键码为特征互相区分
在二叉搜索树的数据集中,定位数据项的问题可以转换成定位关键码
这样的对于数据集中的数据项的访问方式称之为循关键码访问
数值的比较:判断两个数据的大小关系
数值的比对:判断两个数据是否相等
二叉搜索树数据集中的数据项统一地实现和表示为词条(entry)形式
二叉搜索树中的每个数据项称之为词条(entry),词条包括key和value
其中key称之为关键码,这是因为比较词条的大小(或者比对)实际上就是比较两个数据项的关键码的大小(或者比对两个词条的关键码是否相等)

由此可见:

二叉搜索树的原本定义并不等价于:对于任意一个节点,其左孩子节点的关键码都小于或者等于当前节点的关键码,其右孩子节点的关键码都大于或者等于当前节点的关键码。而只能是对于左后代或者右后代中的所有节点而言的。

只含有单个节点的二叉树必然是BST(二叉搜索树)

任何BST的中序遍历序列都是单调非降序列。而如果二叉树的中序遍历序列是单调非降序列,则它一定是BST

即中序遍历序列是单调非降序列是BST的充要条件。

如何证明二叉搜索树的中序遍历序列单调非降?

对于任何一个二叉搜索树,首先考虑它的树根,树根节点的关键码将二叉树的中序遍历序列一分为二,前缀子序列(在中序遍历序列中根节点关键码之前的序列)表示根节点的左子树所有节点的中序遍历序列,后缀子序列(在中序遍历序列中根节点关键码之后的序列)表示根节点的右子树所有节点的中序遍历序列,根据二叉搜索树的定义,对于根节点而言同样满足二叉搜索树的定义/性质,即根节点左子树中的所有节点都小于或者等于根节点的关键码,而根节点右子树中的所有节点都大于或者等于根节点的关键码,则当前的根节点可以看作是使用快速排序算法得到的轴点(满足在排序后的子序列中即中序遍历序列,轴点之前的所有元素都小于等于轴点数值,轴点之后的所有元素都大于或等于节点数值),同理分别在左子树序列和右子树序列中,再找到子树根节点,可以证明子序列的有序性。

简明判断二叉树是否是二叉搜索树的方法:考察二叉树所有节点的垂直投影,得到其中序遍历序列。

二叉搜索树的节点删除算法(双分支情况)


class BinNode(object):#二叉树的基本组成单位是二叉树中的节点
    def __init__(self,e):
        self.data=e
        self.parent=None
        self.lChild=None
        self.rChild=None
        self.height=0#记录二叉树中当前节点的高度
    def size(self):
        # 返回包含当前节点在内的所有后代节点的总数,递归统计
        # 统计当前节点具有多少个后代节点数必须遍历树中的每个节点
        s=1
        if self.lChild is not None:
            s+=self.lChild.size()
        if self.rChild is not None:
            s+=self.rChild.size()
        return s
    def insertAsLC(self,e):# 将某一个具体的数值生成一个新节点,并且作为当前节点的做孩子插入树中
        new_node=BinNode(e)
        new_node.parent=self
        self.lChild=new_node# 将当前节点的左孩子引用给新节点
        self.height+=1

    def insertAsRC(self,e):# 根据数值e产生新节点,作为当前节点的右孩子插入到二叉树中
        new_node = BinNode(e)
        new_node.parent = self
        self.rChild = new_node
        self.height+=1

    def succ(self,node):
        # 返回中序遍历意义下当前节点的直接后继
        # 即在整个二叉搜索树中进行遍历,找到不小于当前节点node的最小的节点
        return

class BinTree(object):
    def __init__(self,e=0):
        self.root=BinNode(e)
        self.size=1 # 当前二叉树中的节点总数,即树的规模
        self.height=0
        # 只包含一个根节点的树,高度为0,不包含任何节点的树的高度为-1
        # 可以将二叉树的高度理解成是从根节点出发到所有叶子节点的路径最大值
        # 路径指的是边长而不是节点数
        self.output=[]# 用于记录输出的序列(遍历的序列)

    def updateHeight(self,node):
        # 只更新节点node的高度
        if node.lChild is not None and node.rChild is not None:
            node.height=1+max(node.lChild.height,node.rChild.height)
        elif node.lChild is None and node.rChild is not None:
            node.height=1+node.rChild.height
        elif node.rChild is None and node.lChild is not None:
            node.height = 1 + node.lChild.height
        else:
            node.height=0

    def updateHeightAbove(self,node):
        # 更新从当前节点开始以及其所有父代节点的高度
        while(node is not None):# 不断回溯node节点的父代节点
            self.updateHeight(node)
            node=node.parent

    def insertAsRC(self,node,e):
        self.size+=1
        node.insertAsRC(e)
        self.updateHeightAbove(node)
        return node.rChild

    def insertAsLC(self,node,e):
        self.size+=1
        node.insertAsLC(e)
        self.updateHeightAbove(node)
        return node.lChild

class BST(BinTree):
    # def __init__(self,e=0):
    #     '''
    #     :param e: 根节点起始数值
    #     '''
    #     super.__init__(self,BinTree)
    def search_recursion(self,e):
        '''
        在当前的二叉搜索树中查找是否存在关键码e,如果存在,则返回关键码为e的树节点,如果不存在,返回None
        :param e:
        :return:
        '''
        self._hot=None
        return self.searchIn(self.root,e,self._hot)
    def searchIn(self,node,e,_hot):
        '''
        以node为根节点的某个子树中查找关键码e
        :param node: 当前需要比对的节点,即当前接收到比较控制权的节点,必须是BinNode类对象
        :param e: 目标关键码
        :param _hot: 当前接受比较控制权的节点的父亲节点
        :return:
        '''
        if node is None:# 递归基:如果当前子树为空,则直接返回查找失败
            return None
        # print(node.data)
        if node.data==e:# 当前子树的根节点与目标关键码正好相等,查找成功
            # print(node.data,'right')
            return node
        _hot=node
        self._hot=_hot
        # 对_hot节点的修改总是发生在每次试图进行深入递归之前
        # _hot 节点将会记录下此前刚刚接受访问的非空节点
        if node.data>e:
            return self.searchIn(node.lChild,e,_hot)
        else:
            return self.searchIn(node.rChild,e,_hot)
        # searchIn算法每递归一次,当前子树的根节点node的高度都会下降一层
        # 故而searchIn算法在最坏情况下的递归深度不会超过树的高度
        # 故而算法的时间复杂度正比于树的高度O(h)
        # searchIn算法的递归实现代码是典型的尾递归,很容易改写成迭代形式
        '''
        _hot 节点的语义
        查找成功时,算法的返回值将会是真实存在并且数据域等于目标关键码的节点
        查找失败时,算法的返回值将会是None,准确而言是在二叉树上查找路径末端节点当前不存在的一个孩子
        
        _hot节点
        查找成功时,_hot节点将会指向命中返回将节点的父亲节点
        查找失败时,_hot节点将会指向在整个查找过程中最后访问的一个真实存在的节点 
        '''
    def search_iteration(self,node,e):
        '''
        在二叉搜索树中以node为根节点的子树中,查找是否包含关键码为e的节点
        :param node:
        :param e:
        :return:
        '''
        self._hot=None
        if node is None:
            return None
        while(node):
            if node.data==e:
                return node
            else:
                self._hot=node
                if e>node.data:
                    if node.rChild is None:
                        return None
                    else:
                        node=node.rChild
                else:
                    if node.lChild is None:
                        return None
                    else:
                        node=node.lChild

    def mid_order(self,node):
        if node is None:
            return
        stack=[]# 构造左侧链栈
        curr=node
        while(curr):
            stack.append(curr)
            curr=curr.lChild
        result=[]
        while(stack):
            temp=stack.pop(-1)# 弹出栈顶元素
            result.append(temp.data)
            if temp.rChild is not None:
                curr=temp.rChild
                while(curr):
                    stack.append(curr)
                    curr=curr.lChild
        return result
    def Insertion(self,e):
        '''
        将关键码e封装成节点,并将节点插入到二叉搜索树中的合适位置
        向二叉搜索树新插入的节点必然是叶子结点
        插入算法的时间复杂度 O(h)  不会超过二叉树的高度
        :param e:
        :return:
        '''
        self.search_recursion(e)
        # 为了便于分析,假设二叉搜索树中不包含任何重复的元素
        # 则说明将要插入的关键码必然不存在于原始的二叉搜索树中
        # 故而搜索算法返回值必然为None,但是运行了搜索算法之后
        # self._hot 就将记录在搜索关键码e的路径中的末端节点(_hot变量将会指向查找路径的末端节点)
        # 即需要将关键码e插入的父亲节点
        new_node=BinNode(e)
        if e>self._hot.data:
            # 如果e大于当前节点,则说明当前节点的右孩子一定为空,否则搜索算法就会将_hot节点定位到当前节点的右孩子
            self._hot.rChild=new_node
            self.updateHeightAbove(new_node)
        else:
            self._hot.lChild=new_node
            self.updateHeightAbove(new_node)
        self.size+=1
        return
    def remove(self,e):
        '''
        BST的节点删除操作    从二叉搜索树中删除关键码为e的节点
        :param e:
        :return:
        '''
        target_node=self.search_recursion(e)# 对于目标关键码进行查找和定位
        if target_node is None:
            return False #表示二叉搜索树中不存在目标关键码,删除失败
        self.removeAt(self._hot,target_node)
        self.size-=1
        self.updateHeightAbove(self._hot)
        # 删除操作将会导致:被删除的节点target_node的后台节点高度不变,而是它的历代祖先的高度会减小
        return True # 删除成功与否,由函数返回值指示
    def removeAt(self,parent,target_node):
        '''
        将target_node节点从二叉搜索树中删除,target_node节点的父亲节点是parent
        :param parent:
        :param target_node:
        :return:

        情况一:所要删除的目标节点target_node最多只有一个孩子非空,即它至少有一颗子树是空的
              实际上情况一包含三种情况:
                  (1)所要删除的目标节点target_node左孩子为None,右孩子非空
                  (2)所要删除的目标节点target_node右孩子为None,左孩子非空
                  (3)所要删除的目标节点target_node左孩子和右孩子都为None
        情况二:所要删除的目标节点target_node左孩子和右孩子都非空
              算法思路:化繁为简
              step1  找到target_node节点的直接后继,succ
                  # 找到target_node即需要被删除的节点在中序遍历意义下当前节点的直接后继succ
                  # 即在整个二叉搜索树中进行遍历,找到不小于当前节点node的最小的节点
                  由于现在的target_node节点同时具有左后代子树和右后代子树
                  故而在二叉搜索树中寻找target_node节点的直接后继必然是这样的:
                  首先要在当前节点的右子树中搜索,因为根据二叉搜索树的处处顺序性
                  只有在所要删除的节点的右子树后代中的节点才会大于当前节点
                  于是访问控制权交给target_node.rChild,然后再在以target_node.rChild
                  为根节点的子树中找到最小值,也就是说中序遍历序列的第一个节点
                  不难发现,就是以target_node.rChild为根节点的左侧链的末端节点
                  即从target_node.rChild开始,沿着左侧分支不断下行,直到最终不能继续下行
                  最终所抵达的节点就是当前节点的直接后继
                  由于需要将当前节点target_node删除,则以target_node为根节点的子树需要设定新的根节点
                  新的根节点的数值应该是target_node节点的直接后继,即在原始的二叉搜索树(未进行删除操作之前的)
                  中大于target_node的最小的节点,记作succ

                  在数值上,实际被删除的节点是target_node的关键码
                  在结构上,实际被删除的节点是target_node的直接后继succ

              step2  将succ的关键码与target_node节点的关键码交换,这样target_node根节点的关键码就变成了其直接后继的关键码
                  相当于将target_node的关键码转移到了其直接后继上,而其直接后继必然没有左孩子,可能有右孩子
                  故而现在将问题转换成删除target_node关键码(此时的target_node没有左孩子,可能有右孩子)
              step3


        '''
        if parent is not None:
            flag=0
            if parent.data<target_node.data:
                # 说明所要删除的节点是它的父亲节点的右孩子
                flag=1
            # 指示变量 flag=0 表示parent.lChild=target_node
            # flag=1 表示parent.rChild=target_node
            if target_node.lChild is None:
                if not flag:
                    parent.lChild=target_node.rChild
                else:
                    parent.rChild = target_node.rChild
                return
            if target_node.rChild is None:
                if not flag:
                    parent.lChild = target_node.lChild
                else:
                    parent.rChild = target_node.lChild
                return
            # 如果target_node 节点的左右孩子都是空,同样可以处理

        # 情况2  此时target_node的左孩子和右孩子都非空
        # step1  找到当前节点的直接后继succ
        succ=target_node.rChild
        while(succ.lChild):
            self._hot = succ
            succ = succ.lChild
        # step2 将succ中的数值与target_node的关键码交换,从而等效的将需要被删除的节点转移到一个新的位置
        # 并且这个新的位置至多只有一个分支
        temp_value=succ.data
        succ.data=target_node.data
        target_node.data=temp_value

        # step3 问题转化为删除succ(succ没有左孩子,可能有右孩子,succ的关键码就是需要被删除的数值)
        # 将带删除的节点succ顶替为它的右孩子,在右孩子与它的祖父(succ的父亲节点)之间完成单向连接
        assert self._hot.lChild==succ
        self._hot.lChild=succ.rChild
        return
    # BST的插入和删除操作的时间复杂度,在最坏的情况下都不会超过全树的高度,即O(h)

if __name__=="__main__":
    # tree=BinTree(0)
    # tree.insertAsLC(tree.root,1)
    # tree.insertAsRC(tree.root, 2)
    # tree.insertAsLC(tree.root.lChild,3)
    # tree.insertAsRC(tree.root.lChild, 4)
    # tree.insertAsLC(tree.root.rChild, 5)
    # tree.insertAsRC(tree.root.rChild, 6)
    # print(tree.mid_order(tree.root))
    # tree.preOrder(tree.root)
    # # tree.Inorder_iteration(tree.root)
    # tree.posOrder_recursion(tree.root)
    # print(tree.midOrder(tree.root))
    # print(tree.root.height)
    # print(tree.root.lChild.height)
    '''
    所构建的二叉树
        0
       /  \
      1    2
     / \  / \
    3  4  5  6
    二叉树先序遍历的结果为  0 1 3 4 2 5 6
    其中4个叶子结点的高度为0,根节点的高度为2,左右子树根节点的高度为1
    节点的高度可以理解成从该节点到二叉树根节点的唯一路径的长度(边的长度)
    二叉树中序遍历的结果为  3 1 4 0 5 2 6
    二叉树后序遍历的结果为  3 4 1 5 6 2 0
    '''
    tree=BST(16)

    tree.insertAsLC(tree.root,10)
    tree.insertAsRC(tree.root, 25)

    tree.insertAsLC(tree.root.lChild, 5)
    tree.insertAsRC(tree.root.lChild, 11)

    tree.insertAsLC(tree.root.rChild, 19)
    tree.insertAsRC(tree.root.rChild, 28)

    tree.insertAsLC(tree.root.rChild.lChild, 17)
    tree.insertAsRC(tree.root.rChild.lChild, 22)

    # print(tree.root.data)
    # print(tree.preOrder(tree.root))
    print(tree.mid_order(tree.root))
    print(tree.size)
    # print(tree.)
    print(tree.search_recursion(22).data)
    print(tree.search_recursion(23))

    # print(tree.search_iteration(tree.root,22).data)
    # print(tree.search_iteration(tree.root, 23))

    # tree.Insertion(23)
    # print(tree.size)
    # print(tree.mid_order(tree.root))

    tree.remove(16)
    print(tree.mid_order(tree.root))

    tree.remove(19)
    print(tree.mid_order(tree.root))

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值