Python-数据结构与算法(九、二分搜索树)

保证一周更两篇吧,以此来督促自己好好的学习!代码的很多地方我都给予了详细的解释,帮助理解。好了,干就完了~加油!
声明:本python数据结构与算法是imooc上liuyubobobo老师java数据结构的python改写,并添加了一些自己的理解和新的东西,liuyubobobo老师真的是一位很棒的老师!超级喜欢他~
如有错误,还请小伙伴们不吝指出,一起学习~
No fears, No distractions.


一、基本知识点

1. 二叉树

对于每个节点来说,它最多只有两个分支。
和链表一样,是一种动态数据结构。

# 为了更清楚的表示节点结构,这里用C++语法来阐述
class Node{
    E e;           # 节点携带的元素
    Node left;     # 左孩子
    Node right;    # 右孩子
}

所以这么看来就可以把链表看成是只有一个孩子的一叉树- -
特点:二叉树具有唯一根节点

  1. 每个节点都有指向做孩子和右节点的指针,只不过有可能指向空(叶子节点)
  2. 二叉树每个节点最多有两个孩子(注意是最多,可以只有一个孩子,而另一个为None哦)
  3. 二叉树每个节点最多有一个父亲(根节点没有父亲节点!)
  4. 具有天然的递归结构
    每个节点的左子树也是二叉树(以其左孩子为根的二叉树)
    每个节点的右子树也是二叉树(以其右孩子为根的二叉树)
  5. 二叉树不一定是“满”的(有的节点可能只有左孩子,没有右孩子,或者只有右孩子,没有左孩子),当然一个节点也可以看做成是二叉树(None也可以看成二叉树!,是否是树取决与你的主观判断),相应的代码也会发生一定的更改,因为判断条件不一样了嘛。

2. 典型的树结构对象

  1. 树结构是一种天然的组织结构,比如文件系统。
  2. 有时将数据使用树结构存储后,出奇的高效。
  3. 树结构的典型对象:
    二分搜索树
    平衡二叉树:AVL;红黑树
    堆;并查集
    线段树;Trie(字典树,前缀树)

3. 二分搜索树

  1. 是二叉树的一种
  2. 对于二分搜索树的每个节点的值(这里讨论的是无重复元素的二分搜索树):
    大于其左子树的所有节点的值
    小于其右子树所有节点的值
  3. 每一棵子树也是一棵二分搜索树
  4. 二分搜索树不一定是“满”树(满树的定义前面有讲)
  5. 存储的元素必须有可比较性
  6. 我们实现的二分搜索树不包含重复元素
    如果想包含重复元素的话,只需要定义:
    左子树小于等于节点,或者右子树大于等于节点
    注意:我们之前实现的数组和链表,可以有重复元素
    二分搜索树添加元素的非递归写法,和链表很像

二、二分搜索树的实现

# -*- coding: utf-8 -*-
# Author:           Annihilation7
# Data:             2018-10-13   11:27 am
# Python version:   3.6

from Stack.arrayStack import ArrayStack  # 我们实现的栈
from queue.loopqueue import LoopQueue    # 我们实现的循环队列

class Node:
    def __init__(self, elem):
        """
        节点构造函数,三个成员:携带的元素,指向左孩子的指针(标签),指向右孩子的指针(标签)
        :param elem: 携带的元素
        """
        self.elem = elem
        self.left = None        # 左孩子设为空
        self.right = None       # 右孩子设为空


class BST:    # binary search tree
    def __init__(self):
        """
        二分搜索树的构造函数——————空树
        """
        self._root = None       # 根节点设为None
        self._size = 0          # 有效元素个数初始化为0

    def getSize(self):
        """
        返回节点个数
        :return: 节点个数
        """
        return self._size

    def isEmpty(self):
        """
        判断二分搜索树是否为空
        :return: bool值,空为True
        """
        return self._size == 0

    def add(self, elem):
        """
        向二分搜索树插入元素elem
        时间复杂度:O(logn)
        :param elem: 待插入的元素
        :return: 二分搜索树的根
        """
        self._root = self._add(self._root, elem)  # 调用私有函数self._add

    def contains(self, elem):
        """
        查看二分搜索树中是否包含elem
        时间复杂度:O(logn)
        :param elem: 待查询元素
        :return:     bool值,查到为True
        """
        return self._contains(self._root, elem)   # 调用私有函数self._contains

    def preOrfer(self):
        """
        二分搜索树的前序遍历
        时间复杂度:O(n)
        前序遍历、中序遍历以及后续遍历是针对当前的根节点来说的。前序就是把对根节点的操作放在遍历左、右子树的前面,相应的中序遍历以及后序遍历以此类推
        前序遍历是最自然也是最常用的二叉搜索树的遍历方式
        """
        self._preOrder(self._root)        # 调用self._preOrder函数

    def inOrder(self):
        """
        二分搜索树的中序遍历
        时间复杂度:O(n)
        特点:输出的元素是从小到大排列的,因为先处理左子树,到底后再处理当前节点,最后再处理右子树,而左子树的值都比当前节点小,
              右子树的值都比当前节点大,所以是排序输出
        """
        self._inOrder(self._root)         # 调用self._inOrder函数

    def postOrder(self):
        """
        二分搜索树的后序遍历
        应用场景:二叉搜索树的内存回收,例如C++中的析构函数
        时间复杂度:O(n)
        """
        self._postOrder(self._root)       # 调用self._postOrder函数

    def preOrderNR(self):
        """
        前序遍历的非递归写法
        此时需要借助一个辅助的数据结构————栈
        时间复杂度:O(n)
        空间复杂度:O(n)
        技巧:压栈的时候先右孩子,再左孩子,从而左孩子先出栈。
        """
        self._preOrderNR(self._root)      # 调用self._preOrderNE函数

    def levelOrder(self):
        """
        层序遍历(广度优先遍历)
        时间复杂度:O(n)
        空间复杂度:O(n)
        """
        self._levelOrder(self._root)      # 调用self._levelOrder函数

    def minimum(self):
        """
        Description: 返回当前二叉搜索树的最小值
        时间复杂度:O(n)
        """
        if self.getSize() == 0:  # 空树直接报错
            raise Exception('Empty binary search tree!')
        return self._minimum(self._root).elem  # 调用self._minimum函数,它传入当前的根节点

    def maximum(self):
        """
        Description: 返回当前二叉搜索树的最大值
        时间复杂度:O(logn)
        """ 
        if self.getSize() == 0:  # 空树直接报错
            raise Exception('Empty binary search tree!')
        return self._maximum(self._root).elem  # 调用self._maxmum函数,它传入当前的根节点

    def removeMin(self):
        """
        Description: 删除当前二叉搜索树的最小值的节点
        时间复杂度:O(logn)
        Returns: 被删除节点所携带的元素的值
        """
        # 注意这里不用再判空了,因为self.minimum已经帮我们干了这个活
        ret = self.minimum()   # 找到当前二叉搜索树的最小值
        self._root = self._removeMin(self._root)  # 调用self._removeMin函数,该函数返回删除节点后的二叉搜索树的根节点
        return ret  # 返回最小值

    def removeMax(self):
        """
        Description: 删除当前二叉搜索树的最大值的节点
        时间复杂度:O(logn)
        Returns: 被删除节点所携带的元素的值
        """
        ret = self.maximum()   # 找到当前二叉搜索树的最大值
        self._root = self._removeMax(self._root)  # 调用self._removeMax函数,该函数返回删除节点后的二叉搜索树的根节点
        return ret  # 返回最大值

    def remove(self, elem):
        """
        Description: 删除二叉搜索树中值为elem的节点,注意我们的二叉搜索树中的元素的值是不重复的,所以删除就是真正的删除,无残余
                     这个算法是二叉搜索树中最难的一个算法
                     note: 因为删除的是指定的值,用户已经直到该值了,所以就不需要返回这个值了。
                     时间复杂度:O(logn)
        """
        # 也不用判空了,以为self._remove中对于空树的处理是直接返回的,即什么也不做
        self._root = self._remove(self._root, elem)  # 调用self._remove函数,该函数返回删除节点后二叉搜索树的根节点

        


    # private
    def _add(self, node, elem):
        """
        向以Node为根的二分搜索树插入元素elem,递归算法,这个根可以是任意节点哦,因为二分搜索树的每一个节点都是一个新的二分搜索树的根节点
        :param Node: 根节点
        :param elem: 带插入元素
        :return:     插入新节点后二分搜索树的根
        """
        if node is None:        # 递归到底的情况,此时已经到了None的位置。注意None也算一棵二分搜索树
            self._size += 1     # 维护self._size
            return Node(elem)   # 新建一个携带elem的节点Node,并将它返回

        if elem < node.elem:                            # 待添加元素小于当前节点的elem值
            node.left = self._add(node.left, elem)      # 继续递归向node的左子树添加elem,设想此时node.left已经为空,根据上面的语句,
            # 将返回一个新节点,而此时这个节点与二叉搜索树没有任何联系,所以要用node.left接住这个新节点,从而让新节点挂接到二叉搜索树上
        elif node.elem < elem:                          # 当前节点的elem值小于待添加元素,原理同上
            node.right = self._add(node.right, elem)
        # 注意我们实现的是一个不带重复元素的二分搜索树,所以要用elif,而不是else,相当于对于插入了一个重复元素,我们什么也不做
        return node     # 最后要把node返回,还是这个根,满足定义。

    def _contains(self, node, elem):
        """
        在以node为根的二叉搜索树中查询是否包含元素elem
        :param node:    根节点
        :param elem:    带查找元素
        :return:        bool值,存在为True
        """
        if node is None:             # 递归到底的情况,已经到None了,还没有找到,返回False
            return False

        if node.elem < elem:         # 节点元素小于带查找元素,就向右子树的根节点递归查找
            return self._contains(node.right, elem)
        elif elem < node.elem:       # 带查找元素小于节点元素,就向左子树的根节点递归查找
            return self._contains(node.left, elem)
        else:                        # 最后一种情况就是相等了,此时返回True
            return True

    def _preOrder(self, node):
        """
        对以node为根的节点的二叉搜索树的前序遍历
        :param node: 当前根节点
        """
        if node is None:            # 同样的,先写好递归到底的情况
            return
        print(node.elem, end=' ')   # 在这里我只是对当前节点进行了打印操作,并没有什么别的操作
        self._preOrder(node.left)   # 前序遍历以node.left为根节点的二叉搜索树
        self._preOrder(node.right)  # 最后才是右子树

    def _inOrder(self, node):
        """
        对以node为根节点的二叉搜索树的中序遍历
        :param node: 当前根节点
        """
        if node is None:            # 递归到底的情况
            return
        self._inOrder(node.left)    # 先左子树
        print(node.elem, end=' ')   # 再当前节点的操作,这里只是打印
        self._inOrder(node.right)   # 最后右子树

    def _postOrder(self, node):
        """
        对以node为根节点的二叉搜索树的后序遍历
        :param node: 当前根节点
        """
        if node is None:            # 递归到底的情况
            return
        self._postOrder(node.left)  # 先左子树
        self._postOrder(node.right) # 再右子树
        print(node.elem, end=' ')   # 最后进行当前节点的操作

    def _preOrderNR(self, node):
        """
        对以node为根节点的二叉搜索树的非递归的前序遍历
        :param node: 当前根节点
        """
        stack = ArrayStack()            # 初始化一个我们以前实现的栈
        if node:
            stack.push(node)            # 如果根节点不为空,就首先入栈
        else:
            return
        while not stack.isEmpty():          # 栈始终不为空
            ret = stack.pop()               # 出栈并拿到栈顶的节点
            print(ret.elem, end=' ')        # 打印(我这里就只选择打印操作了,当然可以对这个节点执行任何你想要的操作)
            if ret.right:                   # 出栈后,看一下ret的左右孩子,先入右孩子
                stack.push(ret.right)
            if ret.left:                    # 再入栈左孩子,想想为什么是先右后左
                stack.push(ret.left)
    
    def _levelOrder(self, node):
        """
        Description: 对以node为根节点的二叉搜索树的广度优先遍历
        Params:
        - node: 当前根节点
        """
        if node is None:                # node本身就是None
            return
        queue = LoopQueue()             # 建立一个我们以前实现的循环队列,作为辅助数据结构
        queue.enqueue(node)             # 当前根节点入队
        while not queue.isEmpty():      # 如果队列不为空
            tmp_node = queue.dequeue()  # 取出队首的元素
            print(tmp_node.elem, end=' ')        # 这里仅仅是打印,无其他操作
            if tmp_node.left:           # 如果左孩子不是None
                queue.enqueue(tmp_node.left)     # 将它的左孩子入队,注意是先左孩子后右孩子哦,想象为什么是这样
            if tmp_node.right:          # 如果右孩子不是None
                queue.enqueue(tmp_node.right)    # 右孩子入队

    def _minimum(self, node):
        """
        Description: 返回以node为根的二叉搜索树携带最小值的节点
        """
        if node.left is None:           # 递归到底的情况,二叉搜索树的最小值就从当前节点一直向左孩子查找就好了
            return node
        return self._minimum(node.left) # 否则向该节点的左子树继续查找

    def _maximum(self, node):
        """
        Description: 返回以node为根的二叉搜索树携带最大值的节点
        """
        if node.right is None:          # 递归到底的情况,二叉搜索树的最大值就从当前节点一直向右孩子查找就好了
            return node
        return self._maximum(node.right) # 否则向该节点的右子树继续查找
        
    def _removeMin(self, node):
        """
        Descriptoon: 删除以node为根节点的二叉搜索树携带最小值的节点
        Returns: 删除后的二叉搜索树的根节点,与添加操作有异曲同工之处
        """
        if node.left is None:          # 递归到底的情况
            right_node = node.right    # 记录当前节点的右节点,即使是None也没关系
            node.right = None          # 将当前节点的右节点置为None,便于垃圾回收
            self._size -= 1            # 维护self._size
            return right_node          # 返回当前节点的右子树的根,因为删除最小节点有两种情况,一种是node是叶子节点,直接用None来代替就好了。另外一种就是node还有右子树
                                       # 此时需要用node的右节点来代替当前的节点

        node.left = self._removeMin(node.left)  # 没到底就继续向左子树前进,注意要用node.left接住被删除节点的右节点,从而与整棵树产生连接。
        return node  # 将节点返回,从而在递归算法完成后的回归过程中逐层返回直到最后到根节点

    def _removeMax(self, node):
        """
        Description: 删除以node为根节点的二叉搜索树携带最大值的节点
        Returns: 删除后的二叉搜索树的根节点,与添加操作有异曲同工之处
        """
        if node.right is None:         # 与self.removeMin原理差不多,不再赘述
            left_node = node.left 
            node.left = None
            self._size -= 1
            return left_node
        
        node.right = self._removeMax(node.right)
        return node 

    def _remove(self, node, elem):
        """
        Description: 删除以node为根节点的二叉搜索树中携带值为elem的节点
        Returns: 删除节点后的二叉搜索树的根节点
        """
        if node is None:    # 没找到携带elem的节点
            return None
        
        if elem < node.elem:    # 要寻找的元素小于当前节点的elem值
            node.left = self._remove(node.left, elem)  # 向node的左子树继续寻找,注意要用node.left接住返回值,从而让代替被删除节点的节点与搜索树产生连接
            return node   # 返回node,从而在递归完事后的回归过程中最终返回到搜索树的根节点
        elif node.elem < elem:  # 同理
            node.right = self._remove(node.right, elem)
            return node
        
        else:    # 此时 elem == node.elem
            if node.left is None:   # node左子树为空的情况,单独处理,与前面的删除最大/最小节点的方法一致,不再赘述
                ret = node.right
                node.right = None
                self._size -= 1
                return ret                 
            elif node.right is None:  # node右子树为空的情况
                ret = node.left 
                node.left = None
                self._size -= 1
                return ret            
            else:     # 此时node左右子树均不为空,此时是该算法重头戏
                # 既然node的左右子树均不为空,那么删除node后究竟要用谁来接替这个删除后的空位呢,答案是node的前驱或者后继节点!前驱节点就是node左子树携带最大值的节点
                # 这个节点满足:它的elem一定小于node右子树全部元素的elem,但是还大于左子树全部元素的elem(除了他自己--),同理后继是node右子树的最小值,代替
                # node后也满足二叉搜索树的要求,本文通过node的后继来实现,小伙伴们可以用前驱来实现,也非常简单。
                successor = self._minimum(node.right)    # 通过self._minimum方法找到node的后继节点,并记为seccessor
                successor.right = self._removeMin(node.right)   # 通过self._removeMin方法将node的右子树的最小节点删除,注意返回的删除节点的新的右子树的根节点,所以
                # 此时直接将返回值作为successor的右节点就可以了
                self._size += 1  # 但是我们的目的是让后继来取代被删除的位置的节点,并不是要删除它,而self._removeMin方法中已经对self._size进行了维护,所以在这里我们要加回来
                successor.left = node.left # successor的左孩子就是node的左孩子就好了,代替嘛,画个图看看就懂啦
                node.left = node.right = None  # 可以把node扔了,他已经没用了,让node从树中脱离
                self._size -= 1  # 把二叉搜索树上的节点都扔了,肯定要维护一下self._size
                return successor # 返回取代node后的后继节点

三、测试

# -*- coding: utf-8 -*-
from bst import BST   # 在这py文件中实现的binary search tree

test_bst = BST()
print('初始大小:', test_bst.getSize())
print('是否为空:', test_bst.isEmpty())

add_list = [15, 4, 25, 22, 3, 19, 23, 7, 28, 24]
print('待添加元素:', add_list)
for add_elem in add_list:
    test_bst.add(add_elem)
    
    ##################################################
    #                    15                          #
    #               /           \                    #
    #               4           25                   #
    #            /      \     /    \                 #
    #           3        7    22    28               #
    #                        /   \                   #
    #                      19     23                 #
    #                               \                #
    #                                24              #
    ##################################################

print('添加元素后,树的大小:', test_bst.getSize())
print('是否包含28?', test_bst.contains(28))
print('前序遍历:(递归版本)')
test_bst.preOrfer()
print()         # 为了美观,起换行作用
print('前序遍历:(非递归版本)')
test_bst.preOrderNR()
print()
print('中序遍历:')
test_bst.inOrder()
print()
print('后序遍历:')
test_bst.postOrder()
print()
print('广度优先遍历(层序遍历):')
test_bst.levelOrder()
print()
print('树中最小值:', test_bst.minimum())
print('树中最大值:', test_bst.maximum())
print('-------------------------------------------')
print('删除最小值后的层序遍历以及树的大小')
print('删除的最小值为:', test_bst.removeMin())
print('层序遍历:', end=' ')
test_bst.levelOrder()
print()
print('最小值删除后的size:', test_bst.getSize())
print('-------------------------------------------')
print('删除最大值后的层序遍历以及树的大小')
print('删除的最大值为:', test_bst.removeMax())
print('层序遍历:', end=' ')
test_bst.levelOrder()
print()
print('最大值删除后的size:', test_bst.getSize())
print('-------------------------------------------')
print('删除特定元素22,以及删除后树的大小')
test_bst.remove(22)
print('层序遍历:', end=' ')
test_bst.levelOrder()
print()
print('删除22后的size:', test_bst.getSize())
print('-------------------------------------------')

四、输出

初始大小: 0
是否为空: True
待添加元素: [15, 4, 25, 22, 3, 19, 23, 7, 28, 24]
添加元素后,树的大小: 10
是否包含28True
前序遍历:(递归版本)
15 4 3 7 25 22 19 23 24 28
前序遍历:(非递归版本)
15 4 3 7 25 22 19 23 24 28
中序遍历:
3 4 7 15 19 22 23 24 25 28
后序遍历:
3 7 4 19 24 23 22 28 25 15
广度优先遍历(层序遍历):
15 4 25 3 7 22 28 19 23 24
树中最小值: 3
树中最大值: 28
-------------------------------------------
删除最小值后的层序遍历以及树的大小
删除的最小值为: 3
层序遍历: 15 4 25 7 22 28 19 23 24
最小值删除后的size: 9
-------------------------------------------
删除最大值后的层序遍历以及树的大小
删除的最大值为: 28
层序遍历: 15 4 25 7 22 19 23 24
最大值删除后的size: 8
-------------------------------------------
删除特定元素22,以及删除后树的大小
层序遍历: 15 4 25 7 23 19 24
删除22后的size: 7
-------------------------------------------

五、总结

本节关于二分搜索树的代码稍微有点多,其实除了remove操作,其他的操作都很好理解。这是一个很重要的数据结构,需要慢慢消化、理解哦~。

若有还可以改进、优化的地方,还请小伙伴们批评指正!

猜你喜欢

转载自blog.csdn.net/Annihilation7/article/details/83036920
今日推荐