c++判断二叉树是否为二叉搜索树_原创 | 好端端的数据结构,为什么叫它SB树呢?...

点击上方蓝字,关注并星标,和我一起学技术。

2e27c035d2b329857988a4896749efa2.png

大家好,今天给大家介绍一个很厉害的数据结构,它的名字就很厉害,叫SB树,业内大佬往往叫做傻叉树。这个真不是我框你们,而是它的英文缩写就叫SBT。

SBT其实是英文Size balanced tree的缩写,翻译过来可以理解成节点平衡树,这是大牛陈启峰在高中参加算法竞赛时期发明的数据结构。不得不说大牛实在是大牛,在高中的时候就已经难以望其项背了。

二叉搜索树

SBT本质上是一棵二叉搜索树,我们之前介绍过二叉搜索树,但是从来没有真正实现过。我们今天先来复习一下二叉搜索树的概念。

对于一棵二叉树而言,如果它满足对于每一个节点都有,以它右孩子构成的右子树中的所有元素大于它,左孩子为根构成的左子树所有元素都小于它,那么这样一棵二叉树就可以被认为是一棵二叉搜索树。比如下图,就是一棵经典的二叉搜索树。

9082efab266acc04aa3736857a478428.png

二叉搜索树有什么好处呢?我们观察一下上图,其实很容易发现,当我们想要查找某个元素是否存在于二叉树当中的时候,我们可以利用刚才提到的性质进行快速地查找。比如我们想要判断15这个元素在不在树当中,我们首先和根节点的11进行判断,由于15大于11,如果15存在一定在11的右子树。所以我们移动到它的右子树16上,继续判断。由于15小于16,所以15要存在一定在它的左子树当中,以此类推,我们只需要经过最多4次比较,就可以找到15。

对于一棵二叉树而言,如果它是完美二叉树,每一层的元素都是满的。我们假设它的层数是k,那么它一共可以存放个元素。反过来说,如果一个完美二叉树当中存在n个元素,那么它的层数应该是。换句话说,我们只需要次操作就可以判断元素是否存在

但是这是完美的情况,大多数情况下普通方法构建出来的二叉搜索树并不是完美的,其中可能存在倾斜。在极端情况下,甚至可以蜕化成链表。比如这样:

163ea639e410a4aa297aa045175a6fd6.png

正因为如此,所以我们才需要设置一些机制来保证二叉搜索树的平衡性。平衡性有了,二叉搜索树的查找效率才能得到保障。

关于让二叉树维持平衡的方法,现在有很多种,比如大名鼎鼎的红黑树、AVL树等等,其实本质上都是二叉搜索树。只是它们维护二叉树平衡性的方法不同。今天我们介绍的SBT同样也是一种自平衡二叉搜索树的实现方法,它的核心机制是旋转。

旋转

旋转是二叉搜索树维持平衡的常用机制,说是旋转,其实可以理解成树上的某些节点之间互换位置。当然位置不能随意更换,我们必须要保证更换位置之后不会破坏二叉搜索树的特性,同时让二叉树整体更加趋向于平衡。

旋转分为两种,一种是左旋,另外一种是右旋。我们一个一个来介绍。

左旋

左旋可以理解成逆时针旋转,我们来看一个例子:

48ca388e2d73245c233eb1d7d1c820c1.png

是不是看着有点蒙,不知道这个旋转怎么实现的?这里我有一个方法,我们可以想象一下,我们把左边的二叉树以B为轴逆时针旋转90度,之后得到的结果是这样:

230be49058f4a82672ae40a00a6817de.png

我们可以发现B节点拥有三个孩子节点了,这显然就违反了二叉树的规则。那么我们就需要断掉它的一个孩子,重新分配。那么为什么重新分配是把E分配给D而不是把C分配给E或者是D呢?

首先我们观察一下就知道,C子树的元素都是比B要大的,那么把它分配给D显然不合适。对于把C分配给E,看起来似乎没有问题,但其实仔细想下也会发现不妥。不妥的原因在哪里?不妥的地方在于我们不知道E节点的情况,如果E没有右子树还好,如果E存在右子树,那么怎么处理?

所以我们只有一种解法,就是把E分配给D做右子树,因为D原先的右子树是B,旋转之后一定就不存在右子树了。

我们试着写出伪代码:

def left_rotate(u):
    ur = u.right
    u.right = ur.left
    ur.left = u
    u = ur

看起来旋转一通操作猛如虎,但是写成代码也就这么几行。

右旋

左旋理解了,右旋也就好办了,实际上右旋就是左旋的逆操作。左旋刚才是逆时针的旋转,那么右旋自然就是顺时针的旋转。这个不需要死记,你只需要记住是向左旋转或者是向右旋转就可以了。

对于这样一棵子树我们要进行右旋:

3a226971d687e3a32a9927bfd63a183c.png

之前左旋的时候我们是以右孩子作为旋转轴,那么右旋自然就要以左孩子作为旋转轴了。旋转90度之后,我们得到了这样的结果:

c535022fd3b9c9cf911cec3b6c91f577.png

同样我们发现A节点的孩子数量超过了限制,我们需要断开重连。根据刚才一样的判断方法,我们可以发现只有一种重连的方式,就是把C节点作为D的左孩子。因为D的左孩子原本是A,由于旋转,D没有左孩子了,这样连接一定不会引起冲突和问题。最终,我们得到的结果就是:

7d69c6a88fbdb23477b8f75d5ac300f1.png

同样,我们可以写出伪代码:

def right_rotate(u):
    ul = u.left
    u.left = ul.right
    ul.right = u
    u = ul

旋转很好理解,但是我们为什么要旋转呢?我们观察一下会发现旋转最重要的功能就是改变了一些节点的位置,这样可以扭转一些不平衡的情况。

比如在右旋之前,可能E或者C子树当中元素过多,引发了不平衡。当我们旋转之后,我们把E和C分别放到了树的两边。这样旋转之后的树距离平衡也就更接近了一些,但是如何严格地保证完全达到完美平衡呢?这里就需要引入本数据结构的核心概念——size balance了。

Size Balance

前面我们也说过了,实现二叉树平衡的方法有很多,同样定义一棵二叉树是否平衡的标准也有很多。比如在AVL树当中是通过左右两棵子树的树深来判断的,两边的树深差不超过1,那么就认为是平衡了。而在SBT当中,我们对二叉树平衡的定义是基于节点数量的,也就是Size。

这里呢,我们需要定义一下size的概念,对于树上某个节点而言,它的size指的是以它为树根的子树当中节点的数量。接下来,我们就要结合size来讨论平衡树不平衡的情况以及我们让它变得平衡能够使用的办法。

首先,我们先来看看我们认为平衡树达到平衡的条件。这里呢我们一共有两个条件,这两个条件都是对称的。我们先来看下一个一般意义上的平衡树。

bd2c4264e6d47c48ce3d0466836cd8b8.png

我们观察一下上面的图,来思考一下,什么情况下可以认为这棵树达成平衡了呢?是L.size == R.size吗?这其实是有问题的,因为可能L的节点都落在了A上,R的节点都落在了C上。这同样是不平衡的,但这种情况除非我们继续往下递归,否则很难识别。

所以这里换了一种方法,我们判断R和AB的size的关系,以及L和CD size的关系。我们要求L.size >= C.size, D.size, R.size >= A.size, B.size。

也就是说高层的size一定要比底层来的大,这两个条件都很直观,也都很好记。这两个条件理解了,我们再去分析它不平衡的条件就很清楚了。一共有四种情况:

  1. Size of A > size of R

  2. Size of B > size of R

  3. Size of C > size of L

  4. Size of D > size of L

在这4种情况当中,1和3是对称的,2和4也是对称的。所以我们只需要着重分析其中两种就可以了,另外两种可以通过对称性得到。

由于我们使用递归来维护树的平衡性的时候,是从底往上的。因此我们可以假设ABCDLR这六棵子树都是平衡的,这样可以简化我们的分析。我们假设我们现在有了一个函数叫做maintain,它可以将一棵不平衡的子树旋转到平衡状态。我们先假设已经有了这个函数,再去看看它里面需要实现哪些逻辑。

接下来我们来看看上面四种情况如果不满足的话,我们应该怎么处理。

情况1

情况1当中A.size > R.size,也就是A当中的节点比较多,为了能够趋近于平衡。我们将原子树右旋,得到:

b6e85833594062c8a65112f3a3f6df5f.png

我们右旋之后,A的层级向上提升了一层。我们观察一下旋转之后的结果,会发现R子树的平衡性得到了保持,没有被破坏,A子树本身就是平衡的。所以旋转之后,还有还有两个节点的平衡性没有保证。一个是T节点,一个是L节点。那么,我们递归调用maintain(T)和maintain(L)即可。

我们写成伪代码就是:

right_rotate(T)
maintain(T)
maintain(L)

情况2

下面我们来看情况2,也就是B.size > R.size的情况。和上面一种情况类似,由于B的节点比较多,我们希望能够把B往上提。但是B节点在内部,我们无论对L左旋还是右旋都

既然对T旋转不行,那么我们可以对L进行旋转啊,这样不就可以影响到B节点了吗?为了展示地更加清楚,我们把B子树的孩子节点也画出来。

f9c024fec4cbc5d37870e41b48400e7a.png

接着我们对L进行左旋,这样可以把B往上提升一层,得到:

3c9fc96fc9549ed201c3eff6b80e52ce.png

虽然我们把B往上提了一层,但是对于T子树而言,左重右轻的局面仍然没有改变。要想改变T的不平衡,我们还需要对T进行右旋,得到:

9c8fe8335583e61a714cfe73b25e2135.png

对于这个结果而言,除了L、T和B这三棵树而言,其他所有的子树都满足平衡了。所以我们按顺序维护L、T和B即可。

我们写成代码就是:

left_rotate(L)
right_rotate(T)
maintain(L)
maintain(T)
maintain(B)

情况3和情况1刚好相反,我们把左旋和右旋互换即可,情况4和情况2也一样。

所以我们可以写出所有的情况来了:

def maintain(t):
    if t.left.left.size > t.right.size:
        right_rotate(t)
        maintain(t.right)
        maintain(t)
    elif t.left.right.size > t.right.size:
        left_rotate(t.left)
        right_rotate(t)
        maintain(t.left)
        maintain(t.right)
        maintain(t)
    elif t.right.right.size > t.left.size:
        left_rotate(t)
        maintain(t.left)
        maintain(t)
    else:
        right_rotate(t.right)
        left_rotate(t)
        maintain(t.left)
        maintain(t.right)
        maintain(t)                                                                                                                                                            

这里的四种情况罗列出来当然就可以,但是有很多代码重复了,我们可以设置一个flag标记,表示我们判断的是左子树还是右子树。这样我们可以把一些情况归并在一起,让代码显得更加简洁:

def maintain(t, flag):
    if flag:
        if t.left.left.size > s.right.size:
            right_rotate(t)
        elif s.left.right.size > t.right.size:
            left_rotate(t.left)
            right_rotate(t)
        else:
            return
    else:
        if t.right.right.size > t.left.size:
         left_rotate(t)
     elif t.right.left.size > t.left.size:
         right_rotate(t.right)
            left_rotate(t)
        else:
            return
    maintain(t.left, False)
    maintain(t.right, True)
    maintain(t, False)
    maintain(t, True)                                                                                                                                                                

这里其实我们省略了maintain(t.left, True)和maintain(t.right, False)这两种情况,这两种情况我们稍微分析一下会发现其实已经被包含了。

我们搞清楚了这些之后,还有一个疑问没有解开,就是为什么旋转操作可以让二叉树趋向于平衡呢,而不是无穷无尽地旋转下去呢?

尽管我们已经知道了不会,但是还是想要来证明一下。我们以情况一举例,我们右旋之后的结果是:

b0a822ad4ca9004bee84197884e29f48.png

我们对比一下旋转之前的结果,会发现T、R、C、D的高度增加1,而L和A的高度减小了1。由于A.size > R.size,A的size最小等于R.size + 1,也就刚好是T加上R子树的size。这两个部分一增一减互相抵消之后,至少还有L这个节点的深度减小了1。也就是说旋转之后的所有元素的深度和是在减小的,不仅是情况1如此,其他的情况也是一样。

既然深度和是在减小的,那么maintain这个操作就一定不是无限的。并且它也的确可以让树趋向于稳定,因为完美平衡的情况下所有元素的深度和才是最小的。

实现细节

到这里我们就已经把SBT的原理都讲解完了,但是还存在一些细节上的问题。由于我们是使用Python是引用语言,所以当我们在旋转的时候进行赋值只是指针之间改变了引用的目标, 并没有实际对原本的结构进行改变。

我们来看下刚才上面的伪代码:

def right_rotate(u):
    ul = u.left
    u.left = ul.right
    ul.right = u
    u = ul

由于我们把u的左孩子右旋,代替了u本来的位置。当我们执行u = ul的时候,只是u这个指针改变了指向的位置。至于原本的数据结构当中的内容,并没有发生变化。因为u、ul这些变量都是临时变量,都是拷贝出来的,我们随便更改,也不会影响类当中的值。

在C++当中我们可以传入引用,这样我们修改引用就是修改原值了。但是Python当中不行,想要解决这个问题,只有一种方法,就是对于每个节点我们都记录它父节点的位置。当我们旋转完了之后,我们需要去更新它父节点中储存的孩子节点的地址,这样的话,我们就不只是局部变量之间互相修改了,就真正落实到了数据结构上了。

我们以右旋为例:

    def reset_child(self, node, child, left_or_right='left'):
        """
        Since Python pass instance by reference, in order to rotate the node in tree, we need to reset the child of father node
        Otherwise the modify won't be effective
        """
        if node is None:
            self.root = child
            self.root.father = None
            return 
        if left_or_right == 'left':
            node.lchild = child
        else:
            node.rchild = child
        if child is not None:
            child.father = node

 def rotate_right(self, node, left_or_right='left'):
        """
        Right rotate operation of Treap.
        Example: 
                D
              /   \
             A     B
            / \
           E   C
        After rotate:
                A
               / \
              E   D
                 / \
                C   B 
        """
        father = node.father
        lchild = node.lchild
        node.lchild = lchild.rchild
        if lchild.rchild is not None:
            lchild.rchild.father = node
        lchild.rchild = node
        node.father = lchild
        # 要重新reset父节点的孩子节点,这样整个改动才是真的生效了。
        self.reset_child(father, lchild, left_or_right)
        # 更新节点买的size
        node.size = node_size(node.lchild) + node_size(node.rchild) + 1
        lchild.size = node_size(lchild.lchild) + node_size(lchild.rchild) + 1
        return lchild

由于每个节点的孩子节点有两个,所以我们还需要一个变量来记录我们当前要改变的节点究竟是它父亲节点的左孩子还是右孩子,这样我们才能在reset的时候正确地修改。不仅是旋转如此,删除和添加也是一样的,我们都需要修改父节点当中的信息,否则我们修改来修改去,改的都只是局部变量而已。

另外一点是我们旋转之后还需要更新每个节点的size,这个逻辑如果忘记了,那么后面的maintain就无从谈起了。

最后我们思考一个问题,我们在什么情况下需要maintain操作呢,也就是什么情况下会破坏树的平衡性呢?其实很简单,就是当树中的元素数量发生改变的时候。无论是增多或者是减少都有可能破坏树的平衡。所以我们在完成了插入和删除之后都需要maintain一次树的平衡。

论文当中对于maintain这个操作还有详细的分析,可以证明maintain的均摊复杂度是,也就是常数级的操作,这也是为什么SBT运行效率高的原因。

论文的最后还附上了SBT和其他常用平衡树数据结构的比较,我们可以看出SBT无论是运行效率还是质量都是其中佼佼者。

92976d6f9216c27781cea01bf4a44759.png

最后,我们聊一聊SBT的实现。关于SBT这类复杂数据结构的实现还是C++要更方便一些,主要原因就是因为C++当中带有引用和指针的传递操作。我们可以在函数内部修改全局的值,而Python当中则不行。参数传递默认传递的是拷贝,我们在函数内部赋值并不会影响结果。所以如果使用Python实现会更加复杂一些,并且需要一些修改父节点的额外操作。

因此网上关于SBT的Python实现非常非常少,我有自信说我的代码目前是我能找到的实现得比较好的一个。相关代码很长,足足有五百多行,不适合放在文章当中。如果大家感兴趣,可以在公众号内回复SBT关键字进行获取。

今天的文章就到这里,衷心祝愿大家每天都有所收获。如果还喜欢今天的内容的话,请来一个三连支持吧~(点赞、在看、转发)

- END -

bdd9a6d9de38831dec6be4eceb8cf39b.png

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/332925.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

ide 日志 乱码_IDE日志分析方法pt。 1个

ide 日志 乱码介绍 我认为大多数软件工程师都了解日志的重要性。 它们已成为软件开发的一部分。 如果无法解决问题,我们尝试在日志中查找原因。 对于一些简单的情况,当错误阻止应用程序打开窗口时,这可能就足够了。 您可以在日志中找到问题&a…

.bash_profile vs .bashrc

请参阅:https://joshstaiger.org/archives/2005/07/bash_profile_vs.html

生成ssh证书(windows)

ssh -keygen -t rsa 生成ssh证书 /home/work/.ssh authorized_keys 客户端建立私钥和公钥 在客户端终端运行命令 ssh-keygen -t rsa https://www.cnblogs.com/ggjucheng/archive/2012/08/19/2646346.html https://blog.csdn.net/qq_36667170/article/details/79094257

日志间隔_在日志中搜索时间间隔

日志间隔介绍 这篇文章与我有关日志分析的迷你系列文章间接相关。 最好阅读两个主要部分,以更好地理解我在说什么。 第1 部分 , 第2部分 。 这篇文章描述了我在实现IDE方法时遇到的一个重要问题。 任务描述 当某人使用日志时,通常只需要调查…

如果在iTerm2中复制命令特别卡,就跟慢动作似的,怎么办?

如果在 iTerm2 中复制命令特别卡,就跟慢动作似的。你可以编辑 ~/.zshrc: vim ~/.zshrc增加如下内容: pasteinit() {OLD_SELF_INSERT${${(s.:.)widgets[self-insert]}[2,3]}zle -N self-insert url-quote-magic # I wonder if youd need .ur…

lambda表达式的使用

package com.asx.application.common.utils;import org.junit.Test;import java.util.Comparator; import java.util.function.Consumer;/*** lambda表达式的使用* 1.举例:(o1,o2) -> Integer.compare(o1,o2) ;* 2.格式* ->;lambda操作符 或 箭头操作符* ->…

centos桥接模式怎么联网_今日回收 | 互联网+废品回收模式是怎么兴起的呢?

随着社会的不断发展和进步,废品回收已不再是传统和低效的行业,而是我国现如今整合资源的重要手段。而该行业之所以能够有如此成就,只因其中98%的企业结合了互联网,成功实现了企业转型。据统计,我国目前废品回收的相关企…

文档 修订 非修订区别_修订和不变性

文档 修订 非修订区别这是一个简短的帖子。 我不确定如何启动它。 这是审阅一些现有代码时“为什么我没有想到这一刻”之一。 由于存在NDA,我无法共享实际代码。 它与处理修订有关。 我能与之联系最紧密的是WordPress(WP)如何处理博客文章和修…

终端界面如何改成彩色的

很多朋友说自己的终端一直是黑白的,如何改成彩色的呢?在用户目录的 .profile 里加上这两行即可: export CLICOLOR 1 export LSCOLORSgxfxcxdxbxegedabagacad

深度解析Java可变参数类型以及与数组的区别

可变参数类型:variable argument type 1.可变参数是兼容数组类参数的,但是数组类参数却无法兼容可变参数 //说明:可变参数可以兼容数组参数 public class TestVarArgus {public static void dealArray(int... intArray) {for (int i : intA…

ios nslog 例子_iOS Block实例

iOS之Block详解:Block详解ViewController.h(ARC)#import interface ViewController : UIViewController// 属性声明的block都是全局的__NSGlobalBlock__property (nonatomic, copy) void (^copyBlock)();property (nonatomic, weak) void (^weakBlock)();endViewCon…

boot gwt_带Spring Boot的GWT

boot gwt介绍 我最近一直在研究用Java编写UI代码的选项。 在我以前的文章中,我研究了Eclipse RAP,发现它可以与Spring Boot集成在一个可执行jar中。 这次,我想对GWT做同样的技巧。 每个人都喜欢Spring Boot。 它使很多事情变得更加干净和容易…

工作占用了太多私人时间_下班后还要被逼谈工作,我们应该如何处理?

老板总是下班后在跟我说工作的事情。不理吧,怕领导不高兴,回复了又怕没完没了的占用了自己的私人时间去完成工作,并且以后老板会觉得这样是理所当然,会变本加厉。“幻想花开”是一家装修公司的设计师,公司里的业务量越…

oh-my-zsh中如何去掉命令提示符前缀

终端的提示符前面存在着一长串前缀:用户名主机名,有时候命令稍微长点,一整行就放不下,于是找到了消除前缀的办法: 输入快捷键 Shift Command G,在前往文件夹输入框中输入 ~/.oh-my-zsh/themes/&#xff…

迁移学习 简而言之_简而言之SPIFFE

迁移学习 简而言之我一直在研究SPIFEE(每个人的安全生产身份框架)[1],在这里,我正在按照我现在的理解起草流程,以使任何其他试图了解流程的人受益。 身份注册表 – SPIRE服务器具有自己的身份注册表,该注册…

MyBatisPlus使用教程

lt是小于 gt是大于

cap理论具体含义_架构设计之「 CAP 定理 」

在计算机领域,如果是初入行就算了,如果是多年的老码农还不懂 CAP 定理,那就真的说不过去了。CAP可是每一名技术架构师都必须掌握的基础原则啊。现在只要是稍微大一点的互联网项目都是采用 分布式 结构了,一个系统可能有多个节点组…

用于zsh的高亮插件 zsh-syntax-highlighting

文章目录简介安装配置简介 zsh-syntax-highlighting 插件为 shell zsh 提供语法高亮显示。当命令在 zsh 提示符下输入到交互式终端时,它可以突出显示命令。这有助于在运行命令之前检查命令,特别是捕获语法错误。 主页地址:https://github.c…

项目不能使用fn标签_无服务器,Java和FN项目的第一步

项目不能使用fn标签无服务器不是什么新事物,但是可以说,仍然有很多关于它的炒作,以及它将如何改变一切,以及未来将如何成为无服务器。 除了云提供商提供的无服务器/功能之外,还有越来越多的无服务器项目正在我们的路上…

tomcat目录下创建临时文件,长时间没有使用会被系统清理掉

原因 原因:在linux系统中,spring boot应用服务每次使用java -jar启动后都会在/tmp目录下生成如下目录: hsperfdata_root tomcat.***.9008(中间是一串数字,结尾是应用端口号) tomcat-docbase..9008&#x…