线索二叉树的遍历以及二叉树拓展知识+算法

在探讨这个问题之前,如果对于线索二叉树的遍历存在疑问的可以先看我的另一篇博客:二叉树的遍历和线索二叉树的深刻理解,这样或许你能更好的理解这一部分的内容。

线索二叉树的遍历

在线索二叉树上进行遍历,只要先找到序列中的第一个结点,然后依次找结点后继直至后继为空时而止。本篇博客就这三种线索二叉树的遍历展开讨论。

中序线索二叉树的遍历

线索二叉树的遍历主要是为访问运算服务的,这种遍历不再需要借助栈,因为其结点中隐含了线索二叉树的前驱和后继信息。利用线索二叉树,可以实现二叉树遍历的非递归算法。遍历的依据都是建立在线索二叉树上的,所以一定要结合二叉树的线索化来理解遍历的过程。
中序线索二叉树下,第一个结点是最左结点,最后一个结点是最右结点。每一个根结点都是其左子树在中序线索二叉树下的最后一个结点的后继结点,同时也是其右子树在中序线索二叉树下的第一个结点的前驱结点。所以不含头结点的中序索二叉树的遍历算法如下:
1、求中序线索二叉树中,中序序列下的第一个结点:

ThreadNode *FirstNode(ThreadNode *p)
{
    while (p->ltag == 0) // 最左下结点(不一定是叶结点)
        p = p->lchild;
    return p;
}

2、求中序线索二叉树中结点p的后继结点:

ThreadBTNode *NextNode(ThreadBTNode *p)
{
	// 当存在右孩子时,问题就转化为求以p的右孩子为根结点的中序线索二叉树下的第一个结点
    if (p->rtag == 0)
        return FirstNode(p->rchild);
    // 若无右孩子则其rchild域指针指示的就是后继结点
    else
    	return p->rchild;
}

3、求中序线索二叉树中,中序序列下的最后一个结点:

ThreadBTNode *LastNode(ThreadBTNode *p)
{
    while (p->rtag != 0) // 最右下结点(不一定是叶结点)
        p = p->rchild;
    return p;
}

4、求中序线索二叉树中结点p的前驱结点:

ThreadBTNode *PreNode(ThreadBTNode *p)
{
	// 当存在左孩子时,问题就转化为求以p的左孩子为根结点的中序线索二叉树下的最后一个结点
    if (p->ltag == 0)
        return PreNode(p->lchild);
  	// 若无左孩子则其lchild域指针指示的就是后前驱结点
    else
        return p->lchild;
}

5、利用上面四个算法,可以写出不含头结点的中序线索二叉树的中序遍历的算法:

void InOrder(ThreadBTNode *BT)
{
    for (ThreadNode *p = BT; p != NULL; p = NextNode(p))
        Visit(p);
}

后序线索二叉树的遍历

后序线索二叉树的遍历相对来说较为复杂,所以放在先序线索二叉树的遍历之前。我们知道,遍历的关键就是找后继。在后序线索二叉树中找后继可分3种情况:
1、若结点p是二叉树的根结点,则其后继为空。
后序线索二叉树中根结点是最后一个被访问的,所以其rchild指针域是空的。
2、若结点p是其双亲的右孩子,或是其双亲的左孩子且其双亲没有右子树,则其后继为双亲结点。
由后序遍历的顺序“左右中”可知,若其为右孩子,说明左子树已经遍历完,其后继就为双亲结点;若其为左孩子且双亲无右子树,则跳过“右”,其后继也为双亲结点。
3、若结点p是其双亲的左孩子,且其双亲有右子树,则其后继为双亲的右子树上按后序遍历列出的第一个结点。
由后序递归的定义很容易得到这一结论。
1、求后序线索二叉树中,后序序列下的第一个结点:

ThreadBTNode *FirstNode(ThreadBTNode *p)
{
    // 遍历找到左子树最左下结点
    // 有的同学可能会在这样一个特殊的结点卡壳
    // 即只有右子树的结点,由“左右中”的顺序,后序遍历应该到其右子树去
    // 可是我们只设置了p = p->lchild,并没有到右子树去啊?
    // 原因在于这是后序遍历线索化的二叉树
    // 如果该结点没有左子树,其lchild指针在线索化时指向的是其前驱
    // 也即其右子树,因为“左右中”,所以p = p->lchild是没有问题的
    while (p->ltag == 0)
        p = p->lchild;
    return p;
}

2、求后序线索二叉树中结点p的前驱结点:

ThreadBTNode *PreNode(ThreadBTNode *p)
{
	// 有右孩子时,前驱结点是右孩子
	if (p->rtag == 0)
		return p->rchild;
	// 无右孩子无左孩子时,lchild就是前驱结点
	// 无右孩子有左孩子时,左孩子就是前驱结点
	else
		return p->lchild;
}

3、求后序线索二叉树中结点p的后继结点:

ThreadBTNode *NextNode(ThreadBTNode *p, ThreadBTNode *par)
{
    if (p == BT) // 第一种情况返回空
        return NULL;
    else if ((par->rchild == p) || (par->lchild == p && par->rtag == 1))
        return par; // 第二种情况返回双亲结点
    else if (par->lchild == p && pre->rtag == 0)
        return FirstNode(par->rchild); // 第三种情况调用FirstNode
}

4、由上面两个算法我们也知道,在遍历过程中需要记录结点p的双亲结点,因此需要借助栈来实现。由此我们可以写出不含头结点的后序线索二叉树的遍历算法:

void PostOrder(ThreadBTNode *BT)
{
    ThreadBTNode *p = BT;
    ThreadBTNode *Stack[maxSize]; // 定义并初始化一个指针栈
    int top = -1;
    Stack[++top] = p; // 根结点入栈
    while (top != -1 && p != NULL)
    {
        while (p->ltag == 0) // 寻找最左结点
        {
            p = p->lchild; // 逐个入栈
            Stack[++top] = p;
        }
        if (p->rtag == 0) // 检查该最左结点是否有右孩子,有就右孩子入栈
        {
            p = p->rchild;
            Stack[++top] = p;
        }
        else // 没有右孩子
        {
            top--; // 栈顶结点出栈,该结点是叶结点
            Visit(p); // 访问它
            ThreadBTNode *par = Stack[top]; // 此时栈顶结点就为双亲结点
            p = NextNode(p, par); // 令p为其后继
        }
    }
}

前序线索二叉树的遍历

在前序线索二叉树中找前驱可分2种情况:
1、若结点p是二叉树的根结点,则其前驱为空。
前驱线索二叉树中根结点是第一个被访问的,所以其lchild指针域是空的。
2、若结点p是其双亲的左孩子,或是其双亲的右孩子且其双亲没有左子树,则其前驱为双亲结点。
由前序遍历的顺序“中左右”可知,只要结点是双亲的左孩子,其前驱结点一定是双亲结点;若其为右孩子且双亲无左子树,则其后继也为双亲结点。
由前序递归的定义很容易得到这一结论。
1、求前序线索二叉树中,前序序列下的最后一个结点:

ThreadBTNode *LastNode(ThreadBTNode *p)
{
	if (p->rtag == 0)
		p = p->rchild;
    return p;
}

2、求前序线索二叉树中结点p的前驱结点:

ThreadBTNode *PreNode(ThreadBTNode *p, ThreadBTNode *par)
{
	if (p == BT)
		return NULL;
	else if (par->lchild == p || (par->rchild == p && par->ltag == 1))
		return par;
}

3、求前序线索二叉树中结点p的后继结点:

ThreadBTNode *NextNode(ThreadBTNode *p)
{
	// 有左孩子时,左孩子就是后继结点
    if (p->ltag == 0)
        return FirstNode(p->rchild);
    // 无左孩子无右孩子时,rchild就是后继结点
    // 无左孩子有右孩子时,右孩子就是后继结点
    else
    	return p->rchild;
}

4、不含头结点的前序线索二叉树的遍历算法:

void PreOrder(ThreadBTNode *BT)
{
	ThreadBTNode *p = BT;
	while (p != NULL)
	{
		while (p->ltag == 0) // 存在左孩子就访问
		{
			Visit(p);
			p = p->lchild;
		}
		Visit(p); // 不存在左孩子,先访问p,然后指向右孩子
		p = p->rchild; // 不管右指针是否为空,rchild都是p的后继
	}

前序后序遍历线索二叉树的不足

二叉树线索化后,仍不能有效求解的问题是前序线索二叉树中求前序前驱以及后续线索二叉树中求后序后继结点。理由如下:
1、在前序线索二叉树中,若某一结点有左孩子,那么它的lchild就必须指向左孩子,就没有指向前驱的指针了。
2、在后序线索二叉树中,若某一结点有右孩子,那么它的rchild就必须指向右孩子,就没有指向后继的指针了。
3、不能有效的求解,但不代表不能求解。因为在前文中我解释过,1、2两种情况要能求解,必须知道双亲结点,所以求解的过程需要设置栈,从而降低了求解的效率。
4、在中序线索二叉树中,如果该结点有左孩子,则lchild指向左孩子,通过左孩子我们也能找到该结点的前驱结点;若没有左左孩子,lchild指向的就是前驱结点。后继结点同样如此。因此中序线索二叉树能有效地求解结点的前驱和后继。
由此可见,在中序线索二叉树上遍历二叉树,虽然时间复杂度亦为 O ( n ) O(n) O(n),但常数因子要比前序和后序讨论的算法小,且不需要设栈。因此,若在某程序或题目中所用二叉树需要经常遍历或查找结点在遍历所得线性序列中的前驱和后继,则应采用中序线索链表作存储结构。

二叉树拓展知识

一颗完全二叉树上有1001个结点,求其叶子结点的个数
完全二叉树只有最后一层不是满的,依据这个特点有两种求法:

  1. 二叉树第k层以上至多有 2 k − 1 2^k - 1 2k1个结点,第k层至多有 2 k − 1 2^{k-1} 2k1个结点。因为 2 8 − 1 = 255 < 2 9 − 1 = 511 < 2 10 − 1 = 1023 2^8-1=255<2^9-1=511<2^{10}-1=1023 281=255<291=511<2101=1023,所以该完全二叉树的高度为10。第9层有256个结点,9层以上有511个结点,所以第10层有1001-511=490个叶结点。这490个结点的双亲结点是第9层前490/2=245个结点,所以第9层有256-245=11个叶结点。综上该完全二叉树有490+11=501个叶结点。
  2. 完全二叉树的最后一个叶结点,其要么是双亲结点的唯一孩子且为左孩子,要么是双亲结点的右孩子。所以完全二叉树只可能有一个单分支结点或没有。设叶结点的个数为n。没有单分支结点时,有 n + n 2 = 1001 n+n_2=1001 n+n2=1001,即 n + n − 1 = 1001 n+n-1=1001 n+n1=1001,解得n=501;有单分支结点时,有 n + 1 + n 2 = 1001 n+1+n_2=1001 n+1+n2=1001,即 n + n = 1001 n+n=1001 n+n=1001,解得n不为整数,错误。所以该完全二叉树有501个叶结点。

一棵有n个结点的完全二叉树的高度h是 ( ⌊ l o g 2 n ⌋ + 1 ) ∼ n (\left \lfloor log_2n \right \rfloor+1)\sim n (log2n+1)n。相反,一棵高度为h的完全二叉树至少有 2 h − 1 2^{h-1} 2h1个结点,此时最后一层只有一个结点;至多有 2 h − 1 2^h-1 2h1个结点,此时它是一个满二叉树。


先序遍历序列和后序遍历序列相反的二叉树的高度=结点数。


先序遍历序列和后序遍历序列不能确定唯一的一棵二叉树,中序遍历序列不一定能确定唯一的一棵二叉树。中序+先序或者中序+后序唯一确定一棵二叉树,但先序+后序不一定能确定唯一的一棵二叉树。


要清楚的是,在二叉树结点的先序序列、中序序列和后序序列中,所有叶子结点在遍历序列中的先后顺序都是相同的。不同的仅仅是访问根结点的顺序。


交换二叉树所有分支结点左右子树的位置,采用后序遍历的方法最合适。
理由:交换根结点的左右子树,可以递归处理交换左子树的左右子树。递归处理到叶结点为止,即交换左右孩子。这对应了先左后右再中的顺序,所以采用后序遍历的方法。


在度为m的Huffman树中,叶结点个数为n,则非叶结点个数为 ⌈ ( n − 1 ) / ( m − 1 ) ⌉ \left \lceil (n-1)/(m-1) \right \rceil (n1)/(m1)
理由:可以这么理解,在构造度为m的Huffman树的过程中,每次把m个叶结点合并为一个父结点,从而每次合并减少m个叶结点,增加一个非叶结点。最后一次不足m个时,补权值为0的叶结点以凑出m个结点。因而从n个叶结点减少到只剩一个结点需要(参与合并的叶结点个数 / 每次合并减少的叶结点个数),也即 ⌈ ( n − 1 ) / ( m − 1 ) ⌉ \left \lceil (n-1)/(m-1) \right \rceil (n1)/(m1)次合并。


二叉树拓展算法

二叉树采用二叉链表存储结构,设计一个算法,利用结点的右孩子指针rchild将一棵二叉树的叶结点从左往右的顺序串成一个单链表。
Note:显然我们需要遍历这棵二叉树。不管采用哪种遍历顺序,对其叶结点的访问顺序都是从左到右的。所以我们需要做的,不过是在访问每个叶结点的过程中,判断此结点是否是叶结点,如果是就修改其rchild指针。既然是单链表,我们就需要设置一个head指针和一个tail指针,分别指向第一个和最后一个叶结点。

void Link(BTNode* p, BTNode*& head, BTNode*& tail)
{
    // 为什么用指针的引用呢?
    // 用指针做函数参数时,只是创建了指针的副本,比如这里的p,它是二叉树中结点地址的副本
    // 我们对p进行的操作,仅仅改变的是形参指针的值,改变的是副本的内容,并未改变结点的地址
    // 比如执行语句p = p->lchild;,不过是让形参p的值从p指向结点的地址变成了p->lchild指向结点的地址
    // 而p原本指向的结点没有任何改变,不管是它的地址还是地址上存储的内容
    // 而head和tail指针是外部变量,是明确的指向单链表第一个和最后一个结点的指针
    // 所以我们不希望只改变它的副本,而是改变它本身的值,这就要用到指针的引用
    // head和tail是引用,引用的对象是一个指向BTNode类型的指针
    // 所以我们对head和tail进行操作,就是对引用,即外部的指针本身进行操作
    // 总结一下就是:指针做形参,操作的是地址的拷贝,给它赋值改变的是指针形参的值
    //               引用做形参,操作的是地址,给它赋值改变的是指针实参的值
    if (p != NULL)
    {
        if (p->lchild == NULL && p->rchild == NULL) // 判断是不是叶结点
        {
            if (head == NULL) // head没有指向,表明该叶结点是第一个被访问的叶结点
            {
                head = p; // 令head和tail都指向它
                tail = p;
            }
            else
            {
            	// head不为NULL,说明head已经指向第一个叶结点
            	// 此时就可以将此结点链接到链表的尾部,然后让tail指向它
                tail->rchild = p;
                tail = p;
            }
        }
        Link(p->lchild, head, tail); // 如果不用引用,这里传入的是形参
        Link(p->rchild, head, tail); // 实际上head和tail真正的指向并不会改变
    }
}

假设满二叉树b的先序遍历序列已经存在于数组中PreOrder中,设计一个算法将其转换为后序遍历序列。
Note:因为是满二叉树的先序遍历序列,所以数组的第一个元素应该是根结点。剩下的元素对半分,分别就是左子树的结点和右子树的结点。将根结点移动到整个序列的末尾,然后分别递归的去处理左子树和右子树即可。

void Change(int PreOrder[], int L1, int R1, int PostOrder[], int L2, int R2)
{
    // L1, R1, L2, R2分别是两个数组第一个元素的下标和最后一个元素的下标
    // 初始时L1 = L2, R1 = R2,因为两者元素个数相等
    if (L1 <= R1)
    {
        // 将PreOrder[]中的第一个元素放在PostOrder[]的末尾
        PostOrder[R2] = PreOrder[L1];
        // 递归地处理PreOrder[]中的前一半序列,将其存放在PstOrder[]中的前一半位置
        // 传入(L1 + 1 + R1) / 2是没问题的,即使是奇数也是向下取整,刚好是左子树最后一个元素的下标
        Change(PreOrder, L1 + 1, (L1 + 1 + R1) / 2, PostOrder, L2, (L2 + R2 - 1) / 2);
        // 递归地处理PreOrder[]中的后一半序列,将其存放在PstOrder[]中的后一半位置
        // 传入(L1 + 1 + R1) / 2 + 1也是没问题的,其刚好是右子树第一个元素的下标
        Change(PreOrder, (L1 + 1 + R1) / 2 + 1, R1, PostOrder, (L2 + R2 - 1) / 2 + 1, R2 - 1);
    }
}

  • 4
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值