由先序+后序遍历确定序列是否唯一并输出一个中序序列

来源:互联网 发布:淘宝天猫详情页尺寸 编辑:程序博客网 时间:2024/06/05 19:06

由先序+后序遍历确定序列是否唯一并输出一个中序序列

@(算法学习)

在前面讨论过如何确定两种唯一二叉树的情况。

  • 先序+中序
  • 后序+中序

中序是必须要有的,因此按照这个提示原则,我们根据根在先序或后序的位置特殊性,在中序序列中找到根的位置,迅速完成了对树的切割。

反向构造时,问题也很简洁,首先确认的是根结点,因此,我们专注于当前层。然后思考子问题是不是相同的结构,很显然是的。因此,写好递归出口+递归子结构,问题可解。特别需要注意的是下标的关系。我每次在想的时候都是用0,1,2这个最简单的序列来想。比如2-1=1,表示两个下标之间差值,那么1+1=2,即左边的下标加上二者的差值得到的是右边的下标。这么说,差值就是右边告诉左边,再努力多少步就可以到达右边的位置了。这的确是小学计数问题,但是在递归的边界上,还是需要万分小心的!

http://blog.csdn.net/u011240016/article/details/53111527?locationNum=1&fps=1

回到问题里来。如何由先序+后序遍历判断是不是唯一的树呢?

我们已经熟练掌握了一个可能不知道为什么的知识点:先序+后序可能无法唯一确认一棵二叉树

注意是可能,因为还是有可能就是能做到唯一确认的。

可能会存在多种情况,这种情况就是一个非叶结点只有一个孩子。这个孩子可能是该非叶结点的左孩子也有可能是该非叶结点的右孩子

而唯一的情况是,除了叶子结点,每个结点都是有两个孩子。

如果是不唯一的情况,我们姑且认为这个是结点的右孩子进行处理,那么就可以得到一个确定的二叉树了。

#include <cstdio>#include <vector>using namespace std;vector<int> ans;int *pre, *post, unique = 1;int mapIndex[256];void mapToIndices(int pre[], int n){    int i;    for (i=0; i<n; i++) {        mapIndex[pre[i]] = i;    }}void setIn (int prel, int prer, int postl, int postr) {    if (prel == prer) {        ans.push_back(pre[prel]);//递到尽头时先序序列被切割到只剩下一个结点,这是中序序列的开始        return;    }    if (pre[prel] == post[postr]) {        // int x = findFromPre(post[postr - 1], prel + 1, prer);        //找到在后序序列中根的前面一位的元素在先序中的下标        // x指示的下标是右子树的开始        // 在当前层次设计出口,不要把思考放到递归的细节,那不是人脑擅长的,等非常熟练后可以做到        int x = mapIndex[post[postr-1]];         if (x - prel > 1) // 左子树不为空        {            setIn(prel + 1, x - 1, postl, postl + x - prel - 2); //递归处理左子树            ans.push_back(post[postr]); // 开始回归时,最小子树的根被push进来            setIn(x, prer, postl + x - prel - 2 + 1, postr - 1);//递归处理右子树        }         else  // 只要任何一个子树为空时,表示结点只有一个孩子,则为不唯一        {            unique = 0;            ans.push_back(post[postr]);//认为这个是右孩子,所以递归只有一只,如果想当成左子树处理也可以            setIn(x, prer, postl + x - prel - 2 + 1, postr - 1);        }    } }int main() {    int n = 0;    scanf("%d", &n);    pre = new int [n];    post = new int [n];    for (int i = 0; i < n; i++) {        scanf("%d", &pre[i]);    }    mapToIndices(pre,n);//将先序序列打表到哈希表,使得查询为O(1)    for (int i = 0; i < n; i++) {        scanf("%d", &post[i]);    }    setIn(0, n - 1, 0, n - 1);    printf("%s\n", unique ? "Yes" : "No");    printf("%d", ans[0]);    for (int i = 1; i < ans.size(); i++) {        printf(" %d", ans[i]);    }    printf("\n");    return 0;}

很显然这是又是一个递归的应用。在树的情形下,递归是非常靠谱的工具。这个算法的核心是如何考虑划分。

  • 先序第一个元素是根,后序最后一个元素是根。

这个是最基本的特征。假设以T表示树根,L,R分别表示左右子树。先序是TLR, 后序是LRT.

看起来TLR与LRT中的LR是一回事,实际不然。LRT中的LR最后一个元素是右子树的根!注意到这个特征,就找到了如何划分左右子树的终极秘诀。然后在前序序列中,右子树的根前面的除了根T以外元素都是左子树的值。

这是递归第一层的划分,递归处理时,分:

  • 左子树
  • 右子树

左子树在两种序列下的表示:

  • 先序:起点是prel+1,因为prel是原来树根的位置。左子树从prel+1开始,x指向右子树的第一个元素,左边就是左子树的最后一个元素,因此左子树范围是:先序序列[prel+1...x1]
  • 后序:起点是postl不变,终点用左子树的个数来推导:个数是x-prel-1, 那么从posl+个数指向的是什么?比如下标1,2,3共三个元素,1+3 = 4,指向的是3后面的元素。因此,要想指向序列的最后一个,还要减1.于是左子树在后序序列中是:[posl...postl+xpre2]

右子树再两种序列下表示:

  • 先序:起点是x,终点是prer,即:[x,prer]
  • 后序:起点是左子树后序中结尾右边的元素,即postl+x-pre-2+1,终点是postr-1不必多说,即:[postl+xpre2+1,postr1]

下标理清楚了,递归就可以写了。另外,递归写法不必自己像电脑一样,压栈,压栈…弹出,弹出…这样模拟递归的过程。我们并不擅长这种操作,除非你觉得这样好玩,那就have fun,不然理解的原理,并且学习时模拟过,那就不必每次都这么做。我们只需要关注策略:划分子问题,写好递归出口,然后就静静的看奇迹的发生。。。背后是计算机帮助我们做事情。

至于为什么中序可以这样收集,是因为我们说递归,是先递到出口才往回归,所以当prel==prer时,表示只有一个元素了,按照先处理的左子树,因此,这是最左的,也即:这是中序的起点。在递归左子树和递归右子树中间加入的push,即最小子树的根结点,这样不断归回去,就神奇的按照中序顺序收集好了中序的序列。

前面说不一定唯一,判断标准是非叶结点是否有两个孩子。x-prel>1 即为x-prel-1表示左子树的结点个数,最小时都大于0.表示有左孩子。我们认为只有一个孩子时假设为右孩子,所以,左孩子为空时,就是树形不唯一的状况。这个时候只用递归处理右子树即可。左子树没了。

罗里吧嗦写了这么多,希望对你有帮助。参考了下面两篇文章。其中第二篇没有特别看懂,作者只是解释了基本的判定,但是牵涉到具体的下标关系,递归设计没有解释,代码精炼,但是并不易读,我着重研究了上面的用vector实现的代码。此外,第二篇文章中用哈希表的形式存储,借助空间首先了查找时间的O(1)优化,在三种构造树的情况中都可以用到。

以上就是关于这个问题的思考过程。
如果后来觉得补图会更加有助于思考,再来补图。主要是已经1点多了,说好了要两点前睡,那就写到这里。

天,真啰嗦啊我。

【参考】

  • http://www.liuchuo.net/archives/2484

  • http://blog.csdn.net/rain722/article/details/52596149

0 0