bst解题报告 - 二叉排序树的访问次数问题

【题面】

bst

相信大家对二叉查找树都很熟悉了,现在给你N个整数的序列,每个整数都在区间[1,N]内,且不重复。现在要你按照给定序列的顺序,建立一个二叉查找树,把第一整数作为根,然后依次插入后面的整数。

每个结点X的插入过程其实就是模拟下面的 insert(X, root)过程:

insert( number X, node N )

{

increase the counter  C by  1  //每次进来都会使C1

if X is less than the numberin node N //如果X小于结点N的值

{

if  N has no left child //N没有左孩子把X作为N左孩子

create a new node with thenumber X and set it to be the left child of node N

else  insert(X, left child of node N)//递归,从N左孩子插入

}

else (X is greater than thenumber in node N)

{

if N has no right child

create a new node with thenumber X and set it to be the right child of node N

else

insert(X, right child of nodeN)

    }

}

你要求的是:每次把序列的一个整数插入到二叉查找数后,当目前为止计数累加器C的值是多少?请把它输出。注意:第一次插入根,计数器C的值是0,你可以理解为插入根是不执行insert()操作的,其后每插入一个结点,C都类加,也就是每次进入过程insert( number X, node N ),都会执行increase the counter  C by  1,使得C不断增大。

输入:

第一行:一个整数:N, 表示序列有多少个整数。 1 <= N <=300000

接下来有N行,每行一个整数X, X在区间[1,N]内,且不重复,这N个整数就组成了一个有序的序列。

输出:

N行,每行一个整数,第一行表示你把序列的第一个数插入到二叉查找树后,当前计数器C的值是多少。 50%的数据:N <= 1000

样例1bst.in

4

1

2

3

4

输出:bst.out

0

1

3

6

样例2

输入:bst.in

5

3

2

4

1

5

输出:bst.out

0

1

2

4

6


【题目分析】

这道题目的题意很明显,考虑计数器C每一次加的数目,就是指在二叉排序树中,插入一个值时经过的节点数量。很容易看出,这个经过的节点数量是唯一确定的,因为二叉排序树的形态也其实是唯一确定的。

【题目解析】

这道题的综合性相当强,涉及到线段树、逆向思维+链表等多个有代表性的技能。根据【题目分析】,我们要求的就是求一个节点插入后的深度。为什么呢?因为插入过程不会改变二叉排序树的原来形态(不是平衡树!)(【定理1】),只是在原来的基础上增加了一个节点,所以说这个节点的深度就等同于其插入时经过的节点数量。

先考虑一些简单的优化。首先我们发现,二叉排序树如果按照上面所说的方式直接插入的话,深度最大是N,而这种情况的出现当且仅当给出的数列纯升序或降序,有注意到所有数字都在[1,N]内,又没有重复,所以我们就可以考虑将一段连续的数,压缩成一个节点。比如说我们给出1,2,3,4,就可以给出一个节点{1,2,3,4},只不过下面我们再走的时候,向左走(走到左孩子去)C就只加1,而向右走(走到右孩子去)C要加4。经测试大概能过70%的数据,但是时间复杂度仍然是bst解题报告 - 二叉排序树的访问次数问题 - wenjianwei1 - 算法的设计的。

继续,我们考虑下一个优化。一般来说,我们平时确定了树后是怎样求深度的?一般来说都是用BFS或DFS遍历整棵树,接着求出距离。但是如果我们给出一个节点,并且除了这个节点外的所有节点都有深度,那我们怎么求呢?答案是这个节点的深度就等于其父节点深度加1。所以我们就将这个问题化约成了怎样求这一个节点的父亲节点。如果我们能够在小于bst解题报告 - 二叉排序树的访问次数问题 - wenjianwei1 - 算法的设计的时间内快速求出某一个节点的父亲节点并打到一个表中,那这样就很简单了。因为我们前面的【定理1】,某一个节点的父亲节点一定在它之前加进过到二叉排序树中,所以说必然知道其深度,那么我们也能够很快(常数时间)求出当前节点的深度,而后面的节点也能够加以引用了。

那么我们下面剩下来的问题就是快速找到某一个节点的父亲节点。首先因为【定理1】,我们知道这个节点相当于是“附加”的节点,所以说这个节点肯定是一个叶节点(根据程序也可以知道)。然后我们根据二叉排序树的性质,也就是二叉排序树的中序遍历的序列有序,结合可能的与其父节点的“挂接”的两种形态,设当前节点为P,那么有可能P是其父亲节点W的左孩子或右孩子,两种情况分情况讨论,对于第一种,既然是其左孩子,那么根据二叉排序树的性质,刚刚好应该是P<W,而且W应该是比P大的中最小的一个!另一种是右孩子的则W应是比P小的中最大的一个(【定理2】)!

所以我们的任务就变成了:依次读入每一个值,对于每一个值,找到之前输入过的值中最接近它的两个。但是,我们怎样才能够知道哪一个才是这个值的父亲呢?想一想看【定理2】,我们又可以发现,这两个值,我们设为lower和upper(lower<P,upper>P),那么恰好lower和upper也必定其中一个是另一个的父亲。因为P还没有加进去,所以说lower和upper也是相邻的(二叉排序树的性质)。那么,我们只需要比较其的深度即可,或者由于【定理1】,我们很容易知道,越后的一个,深度越大。

于是我们就得出了算法:如果插入一个P,那么在其前面找最接近于它的两个,这可以用线段树完成。时间复杂度为bst解题报告 - 二叉排序树的访问次数问题 - wenjianwei1 - 算法的设计

   从另一个角度来考虑,我们来算一下,某一个节点P被算进了计数器一共多少次。很容易看出,每一个节点的被插入的路径,都是节点到根的路径 ,那么某一个节点对于计数器的“贡献”就是其被到根的路径包含的数量。这样看来,很显然某一个节点造成的计数器增量,就是其子树结点的数量。那么我们的算法——由于是逆向思考——大概是这样的:首先从最后的二叉排序树出发,维护每个节点子树的大小,并记到节点上。然后,对于每一个输入的询问,很容易找到相应的节点上的值。那么么问题就变成了:怎样找到这棵二叉树……(好吧,这个问题仍然悬而未决)
  那么我们仍然考虑原来的路,但是仍然采用逆向的思维。利用逆向思维,怎样找到某一个值离其大小最相近的的两个值,
然后还能够维护删除当前的这个值的操作呢?注意我们其实并不用动态地维护,上面的几个算法都几乎是静态的。找到某一个
值离它大小最小的一个……删除……于是我们突然就想到了这样的一个算法:链表+逆向思维!首先将整一个输入按从小到大
或从大到小(保持数值上的有序性就好)排序,这可以用桶排或计数排序或基数排序,时间复杂度为
bst解题报告 - 二叉排序树的访问次数问题 - wenjianwei1 - 算法的设计。然后将这个出来的有序表造成一个链表,每一次我们找到一个相应值,然后找其前后的两个值,这两个值,就是残余的两个可能的父亲。很显然,我们的判断是只用常数时间的——只需比较一下哪个先哪个后即可,只需要记一下坐标即可。然后,找到父亲后容易输出答案,接着,将这个节点删除。最后,逆序输出相应的答案。
  这似乎很简单……几句代码就完成了,而且时间复杂度有了很大的提升。这样的算法考量中主要运用到了两种算法的优
化手段:静态化和逆向思维(其实后一种可能不算……叫逆序化也好)。算是一个稀有的应用例子吧。这道题目所描述的问
题很经典,所以同时也应该读懂每一行文字,并尝试给出一些证明(其实好像就两三个)。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值