数据结构——二叉树

本文介绍了二叉树的基本概念,包括节点的关系、高度、深度和层的定义。重点讲解了二叉查找树,包括其查找、插入、删除操作,并讨论了完全二叉树的特性。此外,还涉及了支持重复数据的二叉查找树及其时间复杂度分析,指出在某些情况下,二叉查找树可能退化为链表,导致时间复杂度变为O(n)。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

最近开始学习王争老师的《数据结构与算法之美》,通过总结再加上自己的思考的形式记录这门课程,文章主要作为学习历程的记录。

树是一种非线性表结构。树这种数据结构比线性表的数据结构要复杂得多。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lZ2bxIAc-1574047398677)(C:\Users\Public\Pictures\Sample Pictures\二叉树.jpg)]

树中的每个元素叫做“节点”,用来连线相邻节点之间的关系,叫做“父子关系”。如上图,A节点是B节点的父节点,B节点是A节点的子节点。B、C、D这三个节点的父节点是同一个节点。因此,它们互称兄弟节点。把没有父节点的节点叫做根节点,也就是图中的节点E。将没有子节点的节点叫做叶子节点或叶节点,其中G、H、I、J、K、L均为叶子节点。

关于树,还有三个比较相似的概念:高度、深度和层,定义为:

节点的高度 = 节点到叶子节点的最长路径(边数)

节点的深度 = 根节点到这个节点所经历的边的个数

节点的层数 = 节点的深度+1

树的高度 = 根节点的高度

以下图为例

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ulzEzzCj-1574047398679)(C:\Users\Public\Pictures\Sample Pictures\二叉树2.jpg)]

树的结构多种多样,最常用的还是二叉树。二叉树,顾名思义,每个节点最多有两个“叉”,也就是两个子节点,左子节点和右子节点。

其中,叶子节点均在最底层,除了叶子节点之外,每个节点都有左右两个子节点。这种二叉树叫做满二叉树。如下图中②:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HNbx1eLC-1574047398679)(C:\Users\Public\Pictures\Sample Pictures\二叉树3.jpg)]

叶子节点都在最底下两层,最后一层的叶子节点都靠左排列,并且除了最后一层,其他层的节点个数都要达到最大,这种二叉树叫做完全二叉树,如上图③。

要理解完全二叉树定义的由来,需要了解如何表示(或存储)一棵二叉树?

想要存储一棵二叉树,有两种方法:一种是基于指针或引用的二叉树链式存储法,一种是基于数组的顺序存储法。首先看一下比较简单,直观的链式存储法,每个结点有三个字段,其中一个存储数据,只要拎着根节点,就可以通过左右子节点的指针,把整棵树串起来。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CSi0Jgqp-1574047398679)(C:\Users\Public\Pictures\Sample Pictures\二叉树4.jpg)]

接下来,再看基于数组的顺序存储法。我们把根结点存储在下标为i的位置,那左子节点存储在下标 2 ∗ i = 2 2*i=2 2i=2的位置,右子节点存储在 2 ∗ i + 1 = 3 2*i+1=3 2i+1=3的位置。以此类推,B节点的左子节点存储在 2 ∗ i = 2 ∗ 2 = 4 2*i=2*2=4 2i=22=4的位置,右子节点存储在 2 ∗ i + 1 = 2 ∗ 2 + 1 = 5 2*i+1=2*2+1=5 2i+1=22+1=5的位置。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ch4lxTCP-1574047398679)(C:\Users\Public\Pictures\Sample Pictures\二叉树5.jpg)]

这个例子是一棵完全二叉树,仅仅浪费了一个下标为0的存储位置。如果是非完全二叉树,会浪费比较多的数组存储空间。如下图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-elubQLMx-1574047398680)(C:\Users\Public\Pictures\Sample Pictures\二叉树6.jpg)]

因此,如果一棵树是完全二叉树,那用数组存储无疑是最省内存的。

二叉树的遍历

二叉树的遍历方法分为三种:前序遍历,中序遍历和后序遍历。

前序遍历是指:对于树中的任意节点来说,先打印这个节点,然后再打印它的左子树,最后打印它的右子树。

中序遍历是指:对于树中的任意节点来说,先打印它的左子树,然后再打印本身,最后打印它的右子树。

后序遍历是指:对于树中的任意节点来说,先打印它的左子树,然后再打印它的本身,最后打印节点本身。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QfbaWxPJ-1574047398680)(C:\Users\Public\Pictures\Sample Pictures\遍历.jpg)]

因为每个节点最多会被访问两次,因此遍历操作的时间复杂度跟节点的个数n成正比,也就是说,二叉树遍历的时间复杂度为O(n)。

采用递归方式实现遍历

#先序遍历
def preOrder(root):
    if root is not None:
        print(root.data,end='')
        BiTree.preOrder(root.lchild)
        BiTree.preOrder(root.rchild)
#中序遍历
def inOrder(root):
    if root is not None:
        BiTree.inOrder(root.lchild)
        print(root.data,end='')
        BiTree.inOrder(root.rchild)
#后序遍历
def postOrder(root):
    if root is not None:
        BiTree.postOrder(root.lchild)
        BiTree.postOrder(root.rchild)
        print(root.data,end='')

采用非递归算法实现递归

#先序遍历
def preOrder2(root):
    p = root
    s = LinkStack()
    s.push(p)
    while not s.isEmpty():
        p = s.pop()
        print(p.data,end='')
        while p is not None:
            if p.lchild is not None:
                print(p.lchild.data,end='')
            if p.rchild is not None:
                s.push(p.rchild)
            p = p.lchild
#中序遍历
def inOrder2(root):
    p = root
    s = LinkStack()
    s.push(p)
    while not s.isEmpty():
        while p.lchild is not None:
            p = p.lchild
            s.push(p)
        p = s.pop()
        print(p.data,end='')
        if p.rchild is not None:
            s.push(p.rchild)
#后序遍历
def postOrder2(root):
    p = root
    t = None
    flag = True
    s = LinkStack()
    if p is not None:
        s.push(p)
        while p.child is None:
            p = p.lchild
            s.push(p)
        while not s.isEmpty() and flag:
            if p.rchild == t or p.rchild is None:
                print(p.data,end='')
                flag = True
                t = p
                s.pop()
            else:
                s.push(p.rchild)
                flag = False

二叉查找值

二叉查找树最大特点为支持动态集合的快速插入、删除和查找操作。二叉查找树也叫二叉搜索树,查找、插入、删除都依赖二叉查找树的特殊结构。二叉查找树要求,在树中的任意一个节点,其左子树中的每个节点的值,都要小于这个节点的值,而右子树节点的值都大于这个节点的值。

1、二叉查找树的查找操作

先取根节点,如果它等于我们要查找的数据,那就返回。如果要查找的数据比根节点的值小,那就在左子树中递归查找;如果要查找的数据比根节点的值大,那就在右子树中递归查找。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aLBjaj5R-1574047398680)(C:\Users\Public\Pictures\Sample Pictures\二叉查找树_查找.jpg)]

2、二叉查找树的插入操作

新插入的数据一般都在叶子节点上,所以我们只需要从根节点开始,依次比较要插入的数据和节点的大小。如果插入的数据比节点的数据大,并且节点的右子树为空,则将新数据插到右子节点的位置;如果不为空,就递归遍历右子树,查找插入位置。同理,如果要插入的数据比节点数值小,并且左子树为空,就将新数据插入到左子节点的位置。如果不为空,就再递归遍历左子树,查找插入位置。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tKb1YWxh-1574047398682)(C:\Users\Public\Pictures\Sample Pictures\二叉查找树_插入.jpg)]

3、二叉查找树的删除操作

二叉查找树的删除操作比较复杂,针对要删除节点的子节点个数不同,需要分三种情况来处理:

第一种情况是,如果要删除的节点没有子节点,只需要将父节点中,指向要删除节点的指针置为NULL,比如图中的删除节点55。

第二种情况是,如果要删除的节点只有一个子节点(只有左子节点或右子节点),我们只需要更新父节点,指向要删除节点的指针,让它指向要删除节点的子节点就可以了,比如图中的删除节点13。

第三种情况是,如果要删除的节点有两个子节点,就比较复杂了。我们需要找到这个节点的右子树的最小节点,把它替换到要删除的节点上,然后再删除掉这个最小节点,因为最小节点肯定没有左子节点(如果有左子节点,那就不是最小节点了)。所以,可以应用上面的两条规则来删除这个最小节点,比如删除节点18。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-S7AdLLTr-1574047398682)(C:\Users\Public\Pictures\Sample Pictures\二叉查找树_删除.jpg)]

实际上,关于二叉查找树的删除操作,还有个非常简单、取巧的方法就是将要删掉的节点标记为“已删除”,但是并不真正从树中将这个节点去掉。这样原本删除的节点还需要存储在内存中,比较浪费内存空间,但删除操作就变得简单了,而且这种处理方法也没有增加。

4、二叉查找树的其他操作

二叉查找树还支持快速查找最大节点和最小节点、前驱节点和后驱节点。除了支持上述操作,还有一个重要特性就是中序遍历二叉查找树,可以输出有序的数据序列,时间复杂度为O(n)。

支持重复数据的二叉查找树

之前说的二叉查找树的操作,针对的都是不存在键值相同,当存储两个对象键值相同,则有两种处理方法。

第一种方法:二叉查找树中每一个节点不仅会存储一个数据,因此我们通过链表和支持动态扩容的数组等数据结构,把值相同的数据都存储在同一个节点上。

第二种方法:每个节点仍然只存储一个数据。在查找、插入数据的过程中,如果碰到一个节点的值,与要插入数据的值相同,就将这个要插入的数据放在这个节点的右子树,也就是说,把这个新插入的数据当做大于这个节点的值来处理。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LuxHsNYh-1574047398682)(C:\Users\Public\Pictures\Sample Pictures\二叉查找树_插入2.jpg)]

当要查找数据时,遇到值相同的节点,并不停止查找操作,而是继续再查找,直到遇到叶子节点才停止。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tIS41P0l-1574047398683)(C:\Users\Public\Pictures\Sample Pictures\二叉查找树_插入2.jpg)]

对于删除操作,也需要先查找到每个要删除的节点,然后再按前面讲的删除操作的方法,依次删除。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lG9HwmKF-1574047398683)(C:\Users\Public\Pictures\Sample Pictures\二叉查找树_删除2.jpg)]

二叉查找树的时间复杂度分析

先分析一个最理想情况,二叉查找树是一棵完全二叉树,这样时间复杂度其实都跟树的高度成正比,也就是O(height)。这时,问题就转化为如何求一棵包含n个节点的完全二叉树的高度?

对于完全二叉树,n满足这样一种关系(最大层数为L)

n > = 1 + 2 + 4 + 8 + . . . + 2 L − 2 + 1 n >= 1+2+4+8+...+2^{L-2}+1 n>=1+2+4+8+...+2L2+1

n < = 1 + 2 + 4 + 8 + . . . + 2 L − 2 + 2 L − 1 n <= 1+2+4+8+...+2^{L-2}+2^{L-1} n<=1+2+4+8+...+2L2+2L1

因此L的范围是[ l o g 2 ( n + 1 ) log_2{(n+1)} log2(n+1), l o g 2 n + 1 log_2{n}+1 log2n+1],完全二叉树的层数小于等于 l o g 2 n + 1 log_2{n}+1 log2n+1,也就是说,完全二叉树的高度小于等于 l o g 2 n log_2{n} log2n,因此时间复杂度为O( l o g n logn logn)。

不过,二叉查找树在频繁地动态更新过程,可能会出现树的高度远大于 l o g 2 n log_2{n} log2n的情况。极端情况下,二叉树会退化成链表,时间复杂度为O(n)。因此,需要设计一种平衡二叉查找树。

l o g 2 n log_2{n} log2n,因此时间复杂度为O( l o g n logn logn)。

不过,二叉查找树在频繁地动态更新过程,可能会出现树的高度远大于 l o g 2 n log_2{n} log2n的情况。极端情况下,二叉树会退化成链表,时间复杂度为O(n)。因此,需要设计一种平衡二叉查找树。

参考资料:王争《数据结构与算法之美》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值