二叉树详解(概念+遍历实现)

news2024/11/18 16:37:46

 

一、基本概念

 

1.最左孩子结点:一个结点的孩子结点中位于最左边的孩子结点。例如,A——B,B——E;
2.树的高度:树的最高层数;
3.路径长度:树中的任意两个顶点之间都存在唯一的一条路径。一条路径所经过的边的数量称为路径长度;
4.树的直径:树中的最长路径,图中树的直径为6;
5.兄弟节点:拥有共同父节点(B、C、D),E的右兄弟节点为F,F的左兄弟节点为E;
6.堂兄弟结点:父结点的父结点(祖父结点,对应就是子孙结点)相同(K和L);
7.叔叔结点:C为E的叔叔结点
8.:一个结点子树的数量(A-3,E-1)
 

二、二叉树

二叉树是最常使用的树形结构,其存储结构及其相关操作都较为简单,堆、AVL树、红黑树等都属于二叉树。

定义:

二叉树是节点的度不超过2的树形结构。

性质:
1.二叉树的第i层上最多有2^{i-1}个结点(i>=1);
2.在一棵高度为h的二叉树中,最多有2^{h}-1个结点,最少有h个结点;
3.如果一棵二叉树有n个结点,则该二叉树有n-1条边;
4.在一棵二叉树中,如果度为0的结点(叶结点)数为n0,度为2的结点数为n2,则n0=n2+1。

创建如下二叉树: (以下代码皆基于这张图)

 
        前面创建二叉树的方法是多次调用 create_lrpTree函数,操作比较烦琐,这里介绍如何利用扩展二叉树的先根序列创建二叉树。扩展二叉树是指将二叉树的空指针域指向一个特殊的结点,并将这特殊结点赋予一个特殊的值(如#)。扩展二叉树中称原结点为内结点,而添加的结点为外结点,显然,在扩展二叉树中,外结点的数量比内结点的数量多1,且每个内结点都有两个孩子。

        以上图所示的扩展二叉树的先根序列为:ABD##EG###CF#H###。

#include<iostream>
#include<stack>
using namespace std;

/*
	左右链指针表示法,这是左右链指针表示二叉树结点的常见表示方法
	这种方法只能自顶向下遍历二叉树,通常用二叉树的根结点表示二叉树。
	由于二叉树的一个结点的左右子树结构与原二叉树具有相似的结构,
	因此对二叉树的操作通常采用递归的方法。
*/
// 孩子表示法
typedef char datatype;
typedef struct lrpNode {
	datatype data;   //数据域
	lrpNode* lc, * rc; //左右链域
	lrpNode():lc(NULL), rc(NULL) {
		data = 0;
	}
}*lrpTree;

// 孩子双亲表示法
// 在结点中添加parent之后不仅可以对二叉树进行自顶向下的操作,而且可以进行自底向上的操作
//typedef struct lrpNode {
//	datatype data;   //数据域
//	lrpNode* lc, * rc,*parent; //左右链域
//	lrpNode() :lc(NULL), rc(NULL),parent(NULL) {
//		data = 0;
//	}
//}*lrpTree;

// 创建一棵二叉树,其根节点的数据值为data,左右孩子分别为lc,rc
// 用该函数创建一棵二叉树,应该从二叉树的最底层开始,自下而上依次创建
lrpTree create_lrpTree(datatype data, lrpTree lc, lrpTree rc) {
	lrpTree tmp = new lrpNode; //为根结点动态分配存储空间
	tmp->data = data;
	tmp->lc = lc;
	tmp->rc = rc;
	return tmp;
}

//根据扩展二叉树的先根序列创建二叉树
lrpTree create_lrpTree(string str, int& idx) {
	lrpTree ret;
	if (str[idx] == '#' || idx == str.size()) {
		idx++;
		return NULL;
	}
	ret = new lrpNode;
	ret->data = str[idx++];
	//通过递归更新ret的位置
	ret->lc = create_lrpTree(str, idx);  //创建ret的左子树,直到#结束
	ret->rc = create_lrpTree(str, idx);	 //创建ret的右子树,直到#结束
	return ret;
}

/*
	二叉树的遍历
	二叉树的遍历是指按某种顺序访问二叉树中的结点,每个结点都被访问且访问一次。
	遍历也是二叉树进行其他运算的基础。
*/

// 先根遍历:即首先先访问根结点,然后先根遍历左子树,最后先跟遍历右子树。
// 顺序:ABDEGCFH
// 递归算法
void preOrder(lrpTree rt) {
	if (rt == NULL)return;
	cout << rt->data; // 访问当前结点
	preOrder(rt->lc); // 先根遍历左子树
	preOrder(rt->rc); // 后根遍历右子树
}

// 利用递归方法实现先根遍历的代码清晰易懂,但当树的高度较大时,递归的层次较高,甚至可能会造成爆栈。
// 为了避免该情况,可以采用非递归方法实现二叉树的先根遍历,非递归用栈来模拟函数递归过程
// 左右每边遍历完后,栈都为空的状态;
// 每一个结点都要考虑右结点,所以每个都会被top,top后即失去了所有价值,需pop掉
// 先根遍历的非递归算法
void preOrder1(lrpTree rt) {
	stack<lrpTree>stk;
	lrpTree t = rt;
	while (t || !stk.empty()) {
		while (t) {  //输出t,并沿t的左分支向下遍历,直到没有
			cout << t->data;
			stk.push(t);
			t = t->lc;
		}
		t = stk.top();
		stk.pop();
		t = t->rc;  //考虑栈顶元素的右分支
	}
}

//中根遍历:首先中根遍历左子树,然后访问根节点,最后中根遍历右子树
//顺序:DBGEAFHC
//递归算法
void midOrder(lrpTree rt) {
	if (rt == NULL)return;
	midOrder(rt->lc);
	cout << rt->data;
	midOrder(rt->rc);
}

//后根遍历:首先后根遍历左子树,然后后根遍历右子树,最后访问根节点
//顺序:DGEBHFCA
//递归算法
void postOrder(lrpTree rt) {
	if (rt == NULL)return;
	postOrder(rt->lc);
	postOrder(rt->rc);
	cout << rt->data;
}
 
//为了检查所构建的二叉树是否正确,可将二叉树的每个结点的数据输出,并输出其两个孩子的数据
void print_lrpTree(lrpTree rt) {
	if (rt == NULL) return;
	cout << rt->data << " ";
	if (rt->lc) cout << "lChind:" << rt->lc->data << " ";
	else cout << "lChind:NULL" << " ";
	if (rt->rc) cout << "rChind:" << rt->rc->data << " "<<endl;
	else cout << "rChind:NULL" << " ";
	print_lrpTree(rt->lc);
	print_lrpTree(rt->rc);
}

int main()
{
	//初始版创建二叉树
	lrpTree t1 = new lrpNode, t2 = new lrpNode, t = new lrpNode;
	//构建左子树
	t1 = create_lrpTree('G',NULL,NULL), t2 = create_lrpTree('E', t1,NULL);
	t1 = create_lrpTree('D',NULL,NULL), t2 = create_lrpTree('B', t1, t2);
	//构建右子树
	t1 = create_lrpTree('H', NULL, NULL), t1 = create_lrpTree('F', NULL, t1);
	t1 = create_lrpTree('C', t1, NULL), t = create_lrpTree('A', t2, t1);

	//扩展二叉树创建先根序列
	string s = "ABD##EG###CF#H###";
	lrpTree prestr = new lrpNode;
	int i = 0;
	prestr = create_lrpTree(s, i);

	//先根遍历
	preOrder(t);
	cout << endl;
	preOrder1(t);
	cout << endl;
	preOrder(prestr);
	cout << endl;
	preOrder1(prestr);
	cout << endl;

	//中根遍历
	midOrder(t);
	cout << endl;

	//后根遍历
	postOrder(prestr);
	cout << endl;

	//print_lrpTree(t);
}

二叉树括号表示法 

 将左右链指针表示的二叉树转化t为括号的二叉树的括号表达式:

//将左右链指针表示的二叉树转化t为括号的二叉树的括号表达式
string convertToBracket(lrpTree t) {
	if (t == NULL)return "";
	return "(" + convertToBracket(t->lc) + t->data + convertToBracket(t->rc) + ")";
}

输出:(((D)B((G)E))A((F(H))C)) 

分析二叉树括号表示法,可得出一下特点:


1.叶结点直接被一对括号所包含 ,如D/G/H


2.如果一个节点有左子树,则其左端的字符为‘)’,如结点A/B/C;如果没有左子树,则其左端字符为‘(’,如结点F


3.如果一个节点有右子树,则其右端的字符为‘(’,如结点A/B/C;如果没有右子树,则其右端字符为‘)’,如结点C/E


4.每一个左括号代表一颗子树结束,且结点被括号所包含的重数与其在二叉树中的层数一致,如A被一层括号包括,为第一层;D/E/F被三重括号包括,为第三层。

 

 注意:图3-7需要配合代码来看,先看代码后看图

//将左右链指针表示的二叉树转化t为括号的二叉树的括号表达式
string convertToBracket(lrpTree t) {
	if (t == NULL) return "";
	return  "(" + convertToBracket(t->lc) + t->data + convertToBracket(t->rc) + ")";
}

//s = convertToBracket(t);
//	cout << s;

//将二叉树的括号表示形式b转化为左右链指针表示形式,结果作为函数的返回值
lrpTree converToTree(string b) {
	lrpTree rt = new lrpNode, lc = new lrpNode, rc = new lrpNode;
	if (b.size() <= 2) return NULL;
	stack<lrpTree>stk;
	for (int i = 1; i < b.size(); i++) {
		if (b[i] == '(') continue; //跳过左括号
		else if (b[i] == ')') { //右括号,构建第一棵子树,并入队
			rc = stk.top(); stk.pop(); //栈中第一个元素为右子树
			rt = stk.top(); stk.pop(); //栈中第二个元素为根结点
			lc = stk.top(); stk.pop(); //栈中第三个元素为左子树
			rt->lc = lc; rt->rc = rc;
			stk.push(rt); //新的子树入队
		}
		else { //结点
			if (b[i-1] == '(') stk.push(NULL);//结点没有左子树
			rt = new lrpNode;
			rt->data = b[i], stk.push(rt);
			if (b[i + 1] == ')') stk.push(NULL);//结点没有右子树
		}
	}
	return stk.top();
}

//s = "(((D)B((G)E))A((F(H))C))";
//	t1 = converToTree(s);
//	preOrder(t1);

 根据二叉树的括号表示法直接求二叉树的后根序列:该算法可用于求一个表达式的逆波兰表达式。

/*
	可以由二叉树的括号形式直接求该二叉树的后根序列。
	由于二叉树的括号表示b为中根序,b[i]=')'表示一颗子树结束,将该子树输出
	如果b[i+1]为结点,则该子树为b[i+1]的左子树,将b[i+1]保存到一个栈中
	再考虑b[i+1]的右子树,当右子树输出后,再输出b[i+1]
*/
//根据二叉树的括号表示法直接求二叉树的后根序列
string postOrder(string b) {
	string ret;
	stack<char>stk;
	for (int i = 0; i < b.size(); i++){
		if (b[i] != ')')  //b[i]为左括号或结点,直接入栈
			stk.push(b[i]);
		else {  //b[i]为右括号
			while (!stk.empty() && stk.top() != '(') //将栈顶的结点加入ret,直到左括号为止
				ret += stk.top(), stk.pop();
			if (!stk.empty())
				stk.pop(); //左括号出栈
		}
	}
	return ret;
}

 层次遍历:

二叉树的层次遍历是从根节点出发,逐层访问二叉树,并且每一层按从左到右的顺序访问每一个结点。层次遍历所得到的结点序列称为层次序列。例如,上面二叉树图的层次遍历序列为ABCDEFGH。

实现层次遍历需要借助队列,实现方法与广度优先探索算法类似。 

参考: 

1.数据结构:树(Tree)【详解】_UniqueUnit的博客-CSDN博客_数据结构树

2. 树(Tree)和二叉树_快到锅里来呀的博客-CSDN博客

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

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

相关文章

我们这样做容器分层性能测试

前言目前闲鱼不少业务正在从H5/Weex升级到Kun&#xff08;基于W3C标准&Flutter打造的混合高性能终端容器&#xff09;&#xff0c;从测试角度来看&#xff0c;我们希望这种升级迭代对于用户体验是正向的&#xff0c;所以用好性能测试这把标准尺就显得格外重要。早期做性能保…

有什么比较好用的低代码开发平台?

国内有特色的低代码快速开发平台产品有哪些&#xff1f;这篇就来介绍下目前市面上主要的几家零代码开发平台&#xff01; 简道云、明道云、IVX这几家目前是无代码赛道的明星选手&#xff0c;在市场综合表现上名列前茅。宜创、红圈营销虽也极具潜力&#xff0c;但在市场表现力上…

Java开发技术之成为高级java工程师必须学习的三个技术

所谓的Java高级程序员往往是经验和能力的结合&#xff0c;并不是说掌握了哪几个技术就是高级程序员了&#xff0c;能否把掌握的知识运用到实际的项目中&#xff0c;并且解决了具体的问题&#xff0c;这个才是衡量一个Java程序员的标准。 那么对于一名Java程序员来说&#xff0…

Java项目:房屋租赁系统设计和实现(java+ssm+mysql+spring+jsp)

源码获取&#xff1a;博客首页 "资源" 里下载&#xff01; 主要功能描述&#xff1a; 1.登录管理&#xff1a;主要有管理员登录和租客登录 2.房源列表以及添加房源功能&#xff1a; 3.租赁合同管理以及在租房源和已退租房源信息管理: 4.看房申请和退租申请管理&a…

【 java 集合】HashMap源码分析

&#x1f4cb; 个人简介 &#x1f496; 作者简介&#xff1a;大家好&#xff0c;我是阿牛&#xff0c;全栈领域优质创作者。&#x1f61c;&#x1f4dd; 个人主页&#xff1a;馆主阿牛&#x1f525;&#x1f389; 支持我&#xff1a;点赞&#x1f44d;收藏⭐️留言&#x1f4d…

python基础篇之列表(增删改查)

大家好&#xff0c;我是csdn的博主&#xff1a;lqj_本人 这是我的个人博客主页&#xff1a;lqj_本人的博客_CSDN博客-微信小程序,前端,vue领域博主lqj_本人擅长微信小程序,前端,vue,等方面的知识https://blog.csdn.net/lbcyllqj?spm1000.2115.3001.5343 哔哩哔哩欢迎关注&…

excel数据统计:三个公式提高统计工作效率

善于在工作中使用函数、公式可以提高工作效率&#xff0c;结合近期学员们遇到的问题&#xff0c;老菜鸟总结了三个非常实用的公式&#xff0c;每个公式都可以解决一类问题。学会这三个公式套路&#xff0c;就能解决日常遇到的很多麻烦事。第一类问题&#xff1a;对指定时间段的…

通过nvm 控制node的常见命令

通过nvm 控制node查看本电脑安装的node版本号切换到对应的node版本号可以查看nvm的全部命令查看node可安装的全部版本号下载对应node的版本查看本电脑安装的node版本号 nvm ls 查看本电脑安装的node版本号 切换到对应的node版本号 nvm use 版本号 切换到对应的node版本号 注意…

基于Python + Django 的密码自助平台项目(完整代码)

场景说明&#xff1a;因为本公司 AD 是早期已经在用&#xff0c;用户的个人信息不是十分全面&#xff0c;例如:用户手机号。 钉钉是后来才开始使用&#xff0c;钉钉默认是使用手机号登录。 用户自行重置密码时如果通过手机号来进行钉钉与 AD 之间的验证就行不通了。逻辑&#x…

Linux驱动开发基础_在设备树中指定中断以及在代码中获得中断

目录 1 设备树里中断节点的语法 1.1 设备树里的中断控制器 1.2 设备树里使用中断 2 设备树里中断节点的示例 3 在代码中获得中断 3.1 对于 platform_device 3.2 对于 I2C 设备、SPI 设备 3.3 调用 of_irq_get 获得中断号 3.4 对于 GPIO 1 设备树里中断节点的语法…

OVN实验----L3互通

概述 物理拓扑 如上一个实验OVN实验----L2互通 逻辑拓扑 按照上个实验OVN实验----L2互通 的操作方式&#xff0c;再配置一组容器blue&#xff0c;网段192.168.2.0/24 配置完成后可以在central上ovn-sbctl show看到如下4个绑定接口 此时&#xff0c;red和blue两个网段内是可…

EasyTrans,一个注解搞定数据翻译,减少30%SQL代码量

介绍easy trans适用于3种场景1 有userId/idCardNo(身份证号码-唯一键场景) 需要 userName&#xff0c;无需联表查询。2 有gender code 0 需要 男。3 枚举指定属性给前端亮点1 缓存支持2 跨微服务翻译支持(User和Order 是2个不同微服务&#xff0c;order里面有userId 需要userNa…

strapi系列--如何自定义非界面化的接口,定制化自己的业务逻辑

为什么要进行后端定制呢&#xff1f; 在实际开发过程中&#xff0c;项目中有些需求是不需要创建界面化接口的&#xff0c;需要我们定制化自己的业务逻辑&#xff0c;那么我们该如何处理这个需求呢&#xff1f;本文以图文并茂的形式&#xff0c;定制一个我们自己的业务逻辑接口…

blender 应用物体变换的作用

编辑模式和物体模式操作的区别 旋转 在物体模式下旋转时物体旋转值会发生变换** 在编辑模式下旋转时物体不会发生变化** 缩放 在物体模式下缩放会导致缩放尺寸发生变化 在编辑模式下缩放时&#xff0c;缩放属性不会发生变化 应用物体变换 把物体模式下的缩放旋转变换应…

Android---TabLayout

目录 TabLayout TabItem ​编辑 演示效果的xml TabLayout TabLayout 在开发中一般作为选项卡使用&#xff0c;常与 ViewPager2 和 Fragment 结合起来使用。 常用属性&#xff1a; app:tabBackground 设置 TabLayout 的背景色&#xff0c;改变整个TabLayout 的颜色&#xf…

绪论的习题

刘佳瑜*&#xff0c;王越 *, 黄扬* , 张钊* (淮北师范大学计算机科学与技术学院&#xff0c;安徽 淮北) *These authors contributed to the work equllly and should be regarded as co-first authors. &#x1f31e;欢迎来到机器学习的世界 &#x1f308;博客主页&#xff1…

idea调试常用的快捷键

一、F7 步入调试&#xff0c;进入当前函数内部。 说明&#xff1a; 如果步入的是自己编的函数&#xff0c;可读性会好很多。 如果是系统函数&#xff0c;我个人目前水平&#xff0c;觉得很难读。而且idea系统已编写好的函数&#xff0c;除非是研究源码&#xff0c;否则感觉…

javaweb08 javaweb、tomcat、maven简介、servlet原理和实例、Mapping映射、请求转发和读取properties文件

文章目录一、javaweb简介二、Tomcat三、Maven四、Servlet简介和HelloWorld五、Servlet原理六、Mapping映射七、ServletContext八、请求转发九、读取资源文件properties一、javaweb简介 在java中&#xff0c;动态web资源开发的技术成为javaweb 人们访问到的任何一个网页和资源…

C语言字符串库函数模拟实现

字符串检验 strlen 函数原型 /// brief 返回给定空终止字符串的长度&#xff0c;即首元素为 str 所指&#xff0c;且不包含首个空字符的字符数组中的字符数 /// param str 指向要检测的字符串的指针 /// return 字符串 str 的长度 size_t strlen( const char *str );空终止字…

C语言实现通讯录静态版本

通讯录中首先要有人的信息&#xff0c;然后是存放多少个人的信息 再丰富一下通讯录的功能&#xff0c;例如增删查改、显示、排序。 我们分三个文件来实现。 1、实现简易的菜单&#xff0c;通讯录的整体逻辑 #include"contact.h"void menu() {printf("*****…