树其一——二叉树

树(一)

二叉树

树结构,是介于网(图)结构与线性表(链表)结构的一种逻辑结构,节点间可能具有父子关系,兄弟关系两种关系。关系的确定与他们各自所在的层数有关。

然而,树结构由于具有区别于线性表的性质,在研究其性质之前,可以说,确定述的存储单元与利用相应结构来进行树的建立是一个绕不开的坎。

见到很多树结构后,可能会进行简单的构想,树也许需要一种这样的数据结构:每个节点拥有的指针个数永远等于其拥有的子节点的个数,各个指针分别指向其各个子节点的地址。

但这种结构明显难于实现(不是不能实现,可以借助vector<btnode< element _type>*>这种结构然后对各个子节点进行编号添加即可),而且无规律可言,研究意义不大。

对于上述的想法,我会在系列的后面的文章里分享代码实现。而本篇文章介绍的主体,二叉树,就将要来了:

为了更有规律的对树的性质进行研究,我们不妨限制每个节点都有且仅有两个子节点,一左一右,互相连接,一个以指数方式繁衍的树结构。这便是二叉树的由来。

所以进入我们的第一部分,研究二叉树,我们应该想想怎么建吧?

二叉树的构建

首先,每个节点都有自己的数据,并且包含一左一右两个子节点,则易得出,二叉树的节点的结构为:

template<class T>
struct btnode{
	btnode<T>* lchild;//指向左子节点
	btnode<T>* rchild;//指向右子节点
	T data;//节点包含数据
};

那么怎么连接呢?

1.手连

没错,手连是可以的,在处理固定的图的时候,手连甚至可以成为安全性稳定性最高的一种方式!但是,1ww个结点的树也不是不存在啊,总不能:

btnode<int> a;
btnode<int> la;
btnode<int> ra;
a.data=10;
la.data=20;
ra.data=30;
a.lchild = &la;
a.rchild = &ra;

这种“屎山”代码写1ww遍吧!所以手连的非自动建图在某些情况下弊端会很大的。

2.抛开实际结构,建线性树!

定义T a[n],其中a[1]存放第一个节点,a[2]存放第二个……有几个放几个,这种线性数组化二叉树可以吗?可以。在存放完全二叉树的时候,这甚至可以说是最佳选择。我们按从上到下,从左到右的顺序编号的话,容易发现:左子结点编号永远是其父结点编号的二倍,右子节点编号是其父节点的二倍加一。这样一来,标明父子关系的指针完全可以变成数组下标(迭代器)的运算。

而这种写成数组的结构,也让求两节点最近公共祖先的算法变成了简单的数理逻辑问题,现给出对应的实现代码:

#include<iostream>
using namespace std;
void pub_ancestor(int i,int j){
	int x = i,y = j;
  if(x == y){//不能保证两节点本来就相同!结点编号相同,则公共祖先为本结点。
    cout<< x <<endl;
    return;
  }
	while(x != y){//如果不同。
		(x > y)?(x = x / 2):(y = y / 2);//双目运算符,在二者相等前,两数中大数不断除二,由性质得,除以二即可向上追溯到上层父节点编号,相等时便为最大公共祖先。(用左移右移运算更佳哦~)
		if(x == y){
			cout<< x <<endl;//打出来
			break;
		}
	}
	return;
}
int main(){
	int i = 0,j = 0;
	cin >> i >> j;//输入两节点对应的编号
	pub_ancestor(i,j);//打印两节点的最近公共祖先
	return 0;
}

然而,这种建树方法遇到稀疏图,会造成极大的浪费空间现象。不适合非完全二叉树,而且对于图的编号顺序有特殊要求!

3.递归建树

那么接下来就回到我们最难想到的也是相对于树来讲最自然的构建方法。递归建法。在介绍递归建树之前,插入一小部分树的模拟遍历的知识:

树的模拟遍历:

第二种结构的遍历简单——和普通数组几乎一模一样,但它的实现结构抛开了二叉树的逻辑结构。如果按照二叉树的特殊结构,来进行特别的遍历操作,应该如何做到不重不漏呢?有三种方法:

  1. 先序遍历: 当前节点(父节点)->左子树->右子树。
  2. 后序遍历: 左子树->右子树->当前节点(父节点)。
  3. 中序遍历: 左子树->当前节点(父节点)->右子树。

其中,对于每个遍历方法的左子树与右子树,都以相同的方式递推遍历下去。以先序遍历为例,左子树会变成左子节点->左子节点左子树->左子节点右子树,而左子节点的树也是以这个规律进行的,直到子树为空为止。

那假设我们已经按照二叉树的结构建立了树,我们应该如何用算法先序遍历他们呢?上述的模拟遍历本质上是递归描述的,我们也不妨采用递归函数:

template<class T>
void preorder(btnode<T>* a) {//遍历以*a为根节点的二叉树
	if (a != nullptr) {
		cout << a->data << " " << endl;//vist()函数
		preorder(a->lchild);
		preorder(a->rchild);
	}
	return;
}

用数学归纳法易证得上述递归先序遍历函数是正确的。那么,中序遍历函数呢?

template<class T>
void midorder(btnode<T>* a) {//遍历以*a为根节点的二叉树
	if (a != nullptr) {
		midorder(a->lchild);
    cout << a->data << " " << endl;//vist()函数
		midorder(a->rchild);
	}
	return;
}

你现在能写出后序遍历函数了吗?学会了之后,尝试理解递归过程,在之后的算法中,注意vist()函数部分!

那么我们就来根据先序遍历,来写出先序递归构建函数吧?(跨度较大,但也容易理解,不做过多解释)代码如下:

template<class T>
btnode<T>* creat_bina_tree(T value[],int n,T flagvalue) {
	static int num = -1;
	if (num < n) {
		T temp = value[++num];
		btnode<T>* a = nullptr;
		if (temp != flagvalue) {
			a = new btnode<T>;
			a->data = temp;
			a->lchild = creat_bina_tree(value, n, flagvalue);
			a->rchild = creat_bina_tree(value, n, flagvalue);
		}
		return a;
	}
}

这里对于这个函数,是本节的重难点,希望你能静下心来细品,而不要依靠我的注释来,这样收获会很大,同时为降低阅读门槛,在此点出:“value”是以先序遍历顺序输入的结点所含数据。n为结点总个数。flagvalue是空结点标志数据——遇到此符号建立空节点。

看懂了吗?看懂的话请跟我往下,没有?请继续品味。

以上三种便是经常用于构建二叉树的方法。

但我们注意到,在第三种方法建立二叉树的过程中,用到了new这一操作,需要进行手动释放,如果你读懂了上述建立节点的代码,那么相信你肯定能轻松的写出类似于链表析构函数的释放树内存的函数。当然,为了方便大家,我依旧不做任何解释的放出相关代码,切记,对于每个调用递归建二叉树方法的程序,都要在最后调用此函数,而且,此函数必须写在main函数的return语句前!

template<class T>
void dest_bina_tree(btnode<T> *a) {
	btnode<T>* f = a;
	if (f != nullptr) {
		dest_bina_tree(f->rchild);
		dest_bina_tree(f->lchild);
	}
	delete f;
	return;
}

以上便是二叉树的所有基础操作。至于求树高,求节点数,将放到下节:树其二——递归函数的计数规律里探讨。

写完本稿之时,又是深夜,真诚希望这篇文章能对你的学习有所帮助。感谢您光临本站,感谢您的阅读,感谢您的打赏。我们下节再见。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值