数据结构与算法——27. 二叉查找树的实现及算法分析

一、二叉查找树(Binary Search Tree)

在ADT Map(抽象数据类型 映射)的实现方案中,可以采用不同的数据结构和搜索算法来保存和查找Key,前面已经实现了两个方案:

  1. 有序表数据结构+二分搜索算法
  2. 散列表数据结构+散列及冲突解决算法

下面我们来试试用二叉查找树保存key,实现key的快速搜索。

二叉查找树BST的性质

**比父节点小的key都出现在左子树(是整个左子树,不是子节点),比父节点大的key都出现在右子树。**话句话说,二叉查找树中任意一个节点,左子树的每个节点的值都要小于这个节点的值,而右子树每个节点的值都应大于这个节点的值。

比如,构造一个二叉查找树,按照70,31,93,94,14,23,73的顺序插入:

  • 31比70小,放到左子节点
  • 93比70大,放到右子节点
  • 94比93大,放到右子节点
  • 14比31小,放到左子节点
  • 23比14大,放到其右
  • 73比93小,放到其左

在这里插入图片描述

注意:插入顺序不同,生成的BST也不同

二、二叉查找树的实现(python代码)

节点和链接结构

二叉查找树的实现需要用到BST和TreeNode两个类,BST的root成员引用根节点TreeNode:

class BinarySearchTree:
    def __init__(self):
        self.root = None
        self.size = 0

    def length(self):
        return self.size

    def __len__(self):
        return self.size

    def __iter__(self):
        return self.root.__iter__()


class TreeNode:
    def __init__(self, key, val, left=None, right=None, parent=None):
        self.key = key  # 键
        self.payload = val  # 值
        self.leftChild = left  # 左子节点
        self.rightChild = right  # 右子节点
        self.parent = parent  # 父节点

    def hasLeftChild(self):
        """是否有左子节点"""
        return self.leftChild

    def hasRightChild(self):
        """是否有右子节点"""
        return self.rightChild

    def isLeftChild(self):
        """是否是左子节点"""
        return self.parent and self.parent.leftChild == self

    def isRightChild(self):
        """是否是右子节点"""
        return self.parent and self.parent.rightChild == self

    def isRoot(self):
        """是否是根节点"""
        return not self.parent

    def isLeaf(self):
        """是否是叶节点"""
        return not (self.rightChild or self.leftChild)

    def hasAnyChildren(self):
        """是否拥有子节点"""
        return self.rightChild or self.leftChild

    def hasBothChildren(self):
        """是否拥有兄弟节点"""
        return self.rightChild and self.leftChild

    def replaceNodeData(self, key, value, lc, rc):
        """更换当前节点的键值以及左右子节点,并将子节点的父节点指向当前节点"""
        self.key = key
        self.payload = value
        self.leftChild = lc
        self.rightChild = rc
        if self.hasLeftChild():
            self.leftChild.parent = self
        if self.hasRightChild():
            self.rightChild.parent = self

1. BST.put方法

put(key, val)方法:插入key构造BST。

首先看BST是否为空,如果一个节点都没有,那么key成为根节点root;否则,就调用一个递归函数_put(key, val,root)来放置key。

def put(self, key, val):
    # 如果有根节点,就调用_put递归函数
    if self.root:
        self._put(key, val, self.root)
    # 如果没有根节点,就使用当前键值构造根节点
    else:
        self.root = TreeNode(key, val)
    self.size = self.size + 1


def _put(self, key, val, currentNode):
    # 如果key比当前节点小,那么放到到左子树
    if key < currentNode.key:
        # 如果有左子树,就递归进入左子树继续查找合适的空位置
        if currentNode.hasLeftChild():
            self._put(key, val, currentNode.leftChild)
        # 如果没有左子树,那么key就成为左子节点
        else:
            currentNode.leftChild = TreeNode(key, val, parent=currentNode)
    # 如果key比当前节点大,那么放到到右子树
    else:
        # 如果有右子树,就递归进入右子树继续查找合适的空位置
        if currentNode.hasRightChild():
            self._put(key, val, currentNode.rightChild)
        # 如果没有右子树,那么key就成为右子节点
        else:
            currentNode.rightChild = TreeNode(key, val, parent=currentNode)

2. BST.__setitem__方法

在python中,索引赋值(如:my_tree[2]="18")是由__setitem__(前后双下划线)方法实现的,我们这里只需重写即可:

def __setitem__(self,k,v):
    self.put(k,v)

3. BST.get方法

在树中找到key所在的节点取到val值。

def get(self, key):
    # 判断是否是空树
    if self.root:  
        # 调用_get递归函数,查找节点
        res = self._get(key, self.root)
        # 找到则返回值
        if res:
            return res.val
        # 找不到返回None
        else:
            return None
    # 是空树,直接返回None
    else:
        return None


def _get(self, key, currentNode):
    # 如果当前节点不存在,直接返回None
    if not currentNode:
        return None
    # 如果当前节点的key等于要找的key,则返回当前节点
    elif currentNode.key == key:
        return currentNode
    # 如果要找的key小于当前节点的key,进入左子树查找
    elif key < currentNode.key: 
        return self._get(key, currentNode.left)
    # 如果要找的key大于当前节点的key,进入右子树查找
    else:
        return self._get(key, currentNode.right)

4. BST.__getitem__BST.__contains__方法

前者的作用是根据值来获取索引,后者是判断给定索引是否包含在树中,也就是in运算符:

def __getitem__(self,key):
    return self.get(key)


def __contains__(self,key):
    if self._get(key,self.root):
        return True
    else:
        return False

5. TreeNode类的__iter__方法(迭代器)

__iter__方法可以使我们用for循环遍历BST中的所有key。我们的想法是以“左、根、右”的顺序进行遍历,也就是进行中序遍历。

TreeNode类中的__iter__迭代器用了for循环迭代,而for则会调用__iter__方法,所以这个方法实际上是个递归函数。

def __iter__(self):
    # 基本结束条件:当前节点为空
    if self:
        # 如果左子树不为空
        if self.hasLeftChild():
            # 遍历左子树,实质是递归调用
            for elem in self.leftChild: 
                # yield每次都会返回一个elem
                yield elem
        # 返回根节点
        yield self.key
        # 如果右子树不为空
        if self.hasRightChild():
            # 遍历右子树,实质是递归调用
            for elem in self.rightChild:
                yield elem

6. BST.delete方法

先用_get找到要删除的节点,然后调用remove来删除,找不到则提示错误:

def delete(self, key):
    # 剩余元素数量大于1个
    if self.size > 1:
        # 查找要删除的节点
        nodeToRemove = self._get(key, self.root)
        # 如果节点存在就调用remove方法删除
        if nodeToRemove:
            self.remove(nodeToRemove)
            self.size -= 1
        # 不存在就抛出异常
        else:
            raise KeyError("Error, key not in tree")
    # 剩余元素数量等于1个
    elif self.size == 1 and self.root.key == key:
        # 删除根节点
        self.root = None
        self.size -= 1
    # 剩余元素数量小于1个,抛出异常
    else:
        raise KeyError("Erro, key not in tree")

def __delitem__(self, key):
    """实现del myTree[8]这样的索引删除操作"""
    self.delete(key)

7. BST.remove方法

从BST中remove一个节点,还要求仍然保持BST的性质,分以下3种情形:

  • 被删节点没有子节点:直接删除;

    # 如果是叶节点,说明没有子节点,直接删除
    if currentNode.isLeaf():
    	if currentNode == currentNode.parent.leftChild:
    	     currentNode.parent.leftChild = None
    	 else:
    	     currentNode.parent.rightChild = None
    
    
  • 被删节点有1个子节点:将这个唯一的子节点上移,替换掉被删节点的位置。

在这里插入图片描述

但替换操作需要区分几种情况:

  • 被删节点的子节点是左子节点?还是右子节点?
  • 被删节点本身是其父节点的左子节点?还是右子节点?
  • 被删节点本身就是根节点?
# 否则就是有一个子节点
else:
    # 如果被删节点有一个左子节点
    if currentNode.hasLeftChild():
        # 如果当前节点是父节点的左子节点
        if currentNode.isLeftChild():
            # 将当前节点的左子节点的父节点设置为当前节点的父节点
            currentNode.leftChild.parent = currentNode.parent
            # 将当前节点的父节点的左子节点设置为当前节点的左子节点
            currentNode.parent.leftChild = currentNode.leftChild
        # 如果当前节点是父节点的右子节点,方法与左子节点类似
        elif currentNode.isRightChild():
            currentNode.leftChild.parent = currentNode.parent
            currentNode.parent.rightChild = currentNode.leftChild
        # 如果当前节点是根节点,直接调用根节点的替换方法
        else:
            currentNode.replaceNodeData(
                currentNode.leftChild.key,
                currentNode.leftChild.payload,
                currentNode.leftChild.leftChild,
                currentNode.leftChild.rightChild,
            )
    # 如果被删节点有一个右子节点
    else:
        if currentNode.isLeftChild():
            currentNode.rightChild.parent = currentNode.parent
            currentNode.parent.leftChild = currentNode.rightChild
        elif currentNode.isRightChild():
            currentNode.rightChild.parent = currentNode.parent
            currentNode.parent.rightChild = currentNode.rightChild
        else:
            currentNode.replaceNodeData(
                currentNode.rightChild.key,
                currentNode.rightChild.payload,
                currentNode.rightChild.leftChild,
                currentNode.rightChild.rightChild,
            )

  • 被删节点有2个子节点:选择被删节点右子树所有节点中最小的那个作为“后继”,替换掉被删节点的位置,然后再去调整。由于二叉查找树的性质,“后继”要么本身是叶节点,要么只有1个右子节点。所以只要把原先的“后继”摘除掉就可以了。

在这里插入图片描述

elif currentNode.hasBothChildren():
    # 寻找“后继”
    succ = currentNode.findSuccessor()
    # 将“后继”摘除
    succ.spliceOut()
    # 用“后继”替换当前的节点
    currentNode.key = succ.key
    currentNode.payload = succ.payload

TreeNode类:寻找“后继”节点

def findSuccessor(self):
    succ = None
    # 如果当前节点有右子节点,就调用findMin方法
    if self.hasRightChild():
        succ = self.rightChild.findMin()
        # 下面的情况暂时用不到
    else:
        if self.parent:
            if self.isLeftChild():
                succ = self.parent
            else:
                self.parent.rightChild = None
                succ = self.parent.findSuccessor()
                self.parent.rightChild = self
        return succ

def findMin(self):
    current = self
    # 如果有左子树,就进入左子树继续寻找
    while current.hasLeftChild():
        current = current.leftChild
    return current

def spliceOut(self):
    if self.isLeaf():
        # 如果是叶节点,直接“摘除”
        if self.isLeftChild():
            self.parent.leftChild = None
        # 下面else的情况暂时用不到
        else:
            self.parent.rightChild = None
    elif self.hasAnyChildren():
        # 下面if的情况暂时用不到
        if self.hasLeftChild():
            if self.isLeftChild():
                self.parent.leftChild = self.leftChild
            else:
                self.parent.rightChild = self.leftChild
            self.leftChild.parent = self.parent
        # 如果有一个右子节点,“摘除”
        else:
            if self.isLeftChild():
                self.parent.leftChild = self.rightChild
            # 下面else的情况暂时用不到
            else:
                self.parent.rightChild = self.rightChild
            self.rightChild.parent = self.parent

8. 完整代码

class TreeNode:
    def __init__(self, key, val, left=None, right=None, parent=None):
        self.key = key
        self.payload = val
        self.leftChild = left
        self.rightChild = right
        self.parent = parent

    def hasLeftChild(self):
        return self.leftChild

    def hasRightChild(self):
        return self.rightChild

    def isLeftChild(self):
        return self.parent and self.parent.leftChild == self

    def isRightChild(self):
        return self.parent and self.parent.rightChild == self

    def isRoot(self):
        return not self.parent

    def isLeaf(self):
        return not (self.rightChild or self.leftChild)

    def hasAnyChildren(self):
        return self.rightChild or self.leftChild

    def hasBothChildren(self):
        return self.rightChild and self.leftChild

    def spliceOut(self):
        if self.isLeaf():
            if self.isLeftChild():
                self.parent.leftChild = None
            else:
                self.parent.rightChild = None
        elif self.hasAnyChildren():
            if self.hasLeftChild():
                if self.isLeftChild():
                    self.parent.leftChild = self.leftChild
                else:
                    self.parent.rightChild = self.leftChild
                self.leftChild.parent = self.parent
            else:
                if self.isLeftChild():
                    self.parent.leftChild = self.rightChild
                else:
                    self.parent.rightChild = self.rightChild
                self.rightChild.parent = self.parent

    def findSuccessor(self):
        succ = None
        if self.hasRightChild():
            succ = self.rightChild.findMin()
        else:
            if self.parent:
                if self.isLeftChild():
                    succ = self.parent
                else:
                    self.parent.rightChild = None
                    succ = self.parent.findSuccessor()
                    self.parent.rightChild = self
        return succ

    def findMin(self):
        current = self
        while current.hasLeftChild():
            current = current.leftChild
        return current

    def replaceNodeData(self, key, value, lc, rc):
        self.key = key
        self.payload = value
        self.leftChild = lc
        self.rightChild = rc
        if self.hasLeftChild():
            self.leftChild.parent = self
        if self.hasRightChild():
            self.rightChild.parent = self


class BinarySearchTree:
    def __init__(self):
        self.root = None
        self.size = 0

    def length(self):
        return self.size

    def __len__(self):
        return self.size

    def put(self, key, val):
        if self.root:
            self._put(key, val, self.root)
        else:
            self.root = TreeNode(key, val)
        self.size = self.size + 1

    def _put(self, key, val, currentNode):
        if key < currentNode.key:
            if currentNode.hasLeftChild():
                self._put(key, val, currentNode.leftChild)
            else:
                currentNode.leftChild = TreeNode(key, val, parent=currentNode)
        else:
            if currentNode.hasRightChild():
                self._put(key, val, currentNode.rightChild)
            else:
                currentNode.rightChild = TreeNode(key, val, parent=currentNode)

    def __setitem__(self, k, v):
        self.put(k, v)

    def get(self, key):
        if self.root:
            res = self._get(key, self.root)
            if res:
                return res.payload
            else:
                return None
        else:
            return None

    def _get(self, key, currentNode):
        if not currentNode:
            return None
        elif currentNode.key == key:
            return currentNode
        elif key < currentNode.key:
            return self._get(key, currentNode.leftChild)
        else:
            return self._get(key, currentNode.rightChild)

    def __getitem__(self, key):
        return self.get(key)

    def __contains__(self, key):
        if self._get(key, self.root):
            return True
        else:
            return False

    def delete(self, key):
        if self.size > 1:
            nodeToRemove = self._get(key, self.root)
            if nodeToRemove:
                self.remove(nodeToRemove)
                self.size = self.size - 1
            else:
                raise KeyError("Error, key not in tree")
        elif self.size == 1 and self.root.key == key:
            self.root = None
            self.size = self.size - 1
        else:
            raise KeyError("Error, key not in tree")

    def __delitem__(self, key):
        self.delete(key)

    def remove(self, currentNode):
        if currentNode.isLeaf():  # leaf
            if currentNode == currentNode.parent.leftChild:
                currentNode.parent.leftChild = None
            else:
                currentNode.parent.rightChild = None
        elif currentNode.hasBothChildren():  # interior
            succ = currentNode.findSuccessor()
            succ.spliceOut()
            currentNode.key = succ.key
            currentNode.payload = succ.payload

        else:  # this node has one child
            if currentNode.hasLeftChild():
                if currentNode.isLeftChild():
                    currentNode.leftChild.parent = currentNode.parent
                    currentNode.parent.leftChild = currentNode.leftChild
                elif currentNode.isRightChild():
                    currentNode.leftChild.parent = currentNode.parent
                    currentNode.parent.rightChild = currentNode.leftChild
                else:
                    currentNode.replaceNodeData(
                        currentNode.leftChild.key,
                        currentNode.leftChild.payload,
                        currentNode.leftChild.leftChild,
                        currentNode.leftChild.rightChild,
                    )
            else:
                if currentNode.isLeftChild():
                    currentNode.rightChild.parent = currentNode.parent
                    currentNode.parent.leftChild = currentNode.rightChild
                elif currentNode.isRightChild():
                    currentNode.rightChild.parent = currentNode.parent
                    currentNode.parent.rightChild = currentNode.rightChild
                else:
                    currentNode.replaceNodeData(
                        currentNode.rightChild.key,
                        currentNode.rightChild.payload,
                        currentNode.rightChild.leftChild,
                        currentNode.rightChild.rightChild,
                    )

三、算法分析(以put方法为例)

其性能决定因素在于二叉搜索树的高度(最大层次),而其高度又受数据项key
插入顺序的影响。

如果key的列表是随机分布的话,那么大于和小于根节点key的键值大致相等,BST的高度就是 log ⁡ 2 n \log _{2}n log2n(n是节点的个数),而且,这样的树就是平衡树。put方法最差性能为 O ( log ⁡ 2 n ) O(\log _2n) O(log2n)

但key列表分布极端情况就完全不同,按照从小到大顺序插入的话,就会如下图:

在这里插入图片描述

此时,put方法的性能为 O ( n ) O(n) O(n),其他方法的情况也是类似的。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
描述 用函数实现如下二排序算法: (1) 插入新结点 (2) 前序、中序、后序遍历二 (3) 中序遍历的非递归算法 (4) 层次遍历二 (5) 在二查找给定关键字(函数返回值为成功1,失败0) (6) 交换各结点的左右子 (7) 求二的深度 (8) 叶子结点数 Input 第一行:准备建的结点个数n 第二行:输入n个整数,用空格分隔 第三行:输入待查找的关键字 第四行:输入待查找的关键字 第五行:输入待插入的关键字 Output 第一行:二的先序遍历序列 第二行:二的中序遍历序列 第三行:二的后序遍历序列 第四行:查找结果 第五行:查找结果 第六行~第八行:插入新结点后的二的先、中、序遍历序列 第九行:插入新结点后的二的中序遍历序列(非递归算法) 第十行:插入新结点后的二的层次遍历序列 第十一行~第十三行:第一次交换各结点的左右子后的先、中、后序遍历序列 第十四行~第十六行:第二次交换各结点的左右子后的先、中、后序遍历序列 第十七行:二的深度 第十八行:叶子结点数 Sample Input 7 40 20 60 18 50 56 90 18 35 30 Sample Output 40 20 18 60 50 56 90 18 20 40 50 56 60 90 18 20 56 50 90 60 40 1 0 40 20 18 30 60 50 56 90 18 20 30 40 50 56 60 90 18 30 20 56 50 90 60 40 18 20 30 40 50 56 60 90 40 20 60 18 30 50 90 56 40 60 90 50 56 20 30 18 90 60 56 50 40 30 20 18 90 56 50 60 30 18 20 40 40 20 18 30 60 50 56 90 18 20 30 40 50 56 60 90 18 30 20 56 50 90 60 40 4 4

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

花_城

你的鼓励就是我最大的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值