[数据结构]长文带你玩转二叉树,期末考试必看篇

目录

 

树的相关概念和结构

 概念

树如何去表示?

树与非树

二叉树

概念

特殊的二叉树

二叉树性质

性质 

例题

二叉树存储形式

顺序存储

链式存储

链式遍历

前序遍历(先根遍历)

中序遍历(中根遍历)

后序遍历

求节点数和各种变形

 

二叉树oj题练习

题1 二叉树前序遍历

题2.二叉树的深度

题3.平衡二叉树


 

树的相关概念和结构

 概念

树是一种非线性的结构,根朝上,而叶朝下,是一棵倒挂的树。

538c078d410b245a7a8bcadfccbf3ad7.jpeg

来认识些简单的概念

节点的度:一个节点含有的子树的个数。(通俗的讲就是有多少个子节点)。 例如5号的度为2

树的度:最大的节点的度才是树的度。

叶节点/终端节点:度为0的节点。1,4,6,8都是叶节点。

双亲节点/父节点:若这个节点含有子节点,则这个节点为其子节点的父节点。

子节点/孩子节点:例如,5是2,7的父节点,2是5的子节点。

兄弟节点:具备相同父节点的才是兄弟节点。例如2,7就是兄弟节点,但1,6就不是,因为其不具备相同父节点。

节点的层次:从根开始,根为1开始(有些资料认为是从0开始,但不建议,因为这样的话空树的高度就是-1)

树的高度或深度:树中节点的最大层次。例如上图为3

节点的祖先:从根到该节点所经分支的所有节点。5是所有节点的祖先。

子孙:以某节点为根的子树中任一节点都称为改节点子孙。所有节点都是5的子孙。

森林:由多棵互不相交的树的集合称为森林。

树如何去表示?

不管怎样还是会用到结构体的,但是有一个很明显的问题,你怎么知道他有多少个孩子呢?定义多了有些节点用不上,定义少了不够用。

在c++里可以采取类似于这样的方法去实现。

vector<struct treenode*>childs

亦或者“左孩子右兄弟”的表示方法。

不管一个节点多少孩子,都只存两个指针,一个叫左孩子,一个叫右兄弟。

13fb0958b7040255f074053a65c1627f.jpeg

(图源百度百科)

总之就是只指向左边第一个孩子,再用孩子的兄弟指针去指向右边的兄弟。

还有双亲表示法等等。都仅作了解。

树与非树

树的子树是不相交的。

除了根节点,每个节点只有一个父节点。

一棵n个节点的树有n-1条边。

二叉树

概念

不存在超过度大于2的节点。

二叉树的子树具备左右的分别,且子树的次序是不可颠倒的。

20063d5014327a604fc40ae0bd92ef6b.png

特殊的二叉树

 满二叉树:每一层的节点都达到最大值。也就是这个而二叉树的层数为k,则节点总数为(2^k)-1,可以用数学知识去进行推导。

上图就是一个满二叉树。

完全二叉树:满二叉树是一种特殊的完全二叉树。完全二叉树是一种效率很高的数据结构,当且仅当每一个节点都与满二叉树中编号1-n的节点一一对应。

format,png

 

若树高度为h,那么前h-1层必须是满的,且最后一层可以不满但从左到右必须连续(也就是h-1的子节点有右子树就有左子树)。

二叉树性质

性质 

 规定根节点层数为1,则非空二叉树第i层最多2^(i-1)个节点。

 

 规定根节点层数为1,则深度为h的二叉树最大节点数为2^h-1 。

分析:假设深度为h,则总节点个数为2^0+2^1+2^3·······2^(h-1)=N

那么2^h-1=N

 

对于任意一棵二叉树,如果度为0的叶节点个数为n0,度为2的有n2,则n0=n2+1

 

若规定根节点层数为1,具有n个节点的满二叉树的深度,h=log2n(2为底)+1

 

例题

题1.某二叉树399个节点,有199个度为2的节点,求叶子节点的个数。

199+1=200,则度为0的节点有200个。

题2.在具有2n个节点的完全二叉树中,求叶子节点个数。

设度为0有a0个,度1有a1个,度2有a2个。

则a0+a1+a2=2n,a0=a2+1

即得到2a0+a1=2n-1,又因为是完全二叉树,所以度为1的点最多只有一个。

即2a0+1(或者0)-1=2n,但为0的话会得到小数,所以度为1的点有1个。

题3.一棵完全二叉树节点位数为531个,求树的高度。

设高度为h,且最后一层缺x个,则有2^h-1-x=531,又因为是完全二叉树,则x范围为[0,2^(h-1)-1]

(至少要剩一个,或者全缺),然后就只有拿值去套了。所以对2的各种次方一定要敏感。

可以得到h=10即可。

(用到遍历知识)

题4.某二叉树按层次输出(同一层从左到右)序列为abcdefgh,求该完全二叉树的前序序列。

因为是完全二叉树,则a在第一层,bc在第二层,defg在第三层,h在第四层第一个的位置。

所以把图画出来就可以得到abdhecfg是前序排列。

题5.二叉树先序为efhigjk 中序为hfiejkg,求后序。

因为前序先走根,所以可以得到e为根节点。但f就不确定是左还是右了。

但这里还给出了后序,因为e为根节点,所以可以确定hfi是左子树,jkg是右子树。

再结合前序,可以得到f是左子树的根了。依次类推得到

b857447924c549f993408e0a4c46a9d0.png

且这种题必须要有中序,只有中序可以确定左右子树有哪些。

 

二叉树存储形式

顺序存储

使用数组来存储,一般来讲呢,数组只适合于表示完全二叉树。因为在现实中只有在使用堆时才会用到数组来存储,所以在这里就不再过多的展开了。

链式存储

 常采用定义两个指针,分别指向左子树和右子树。

也有三个指针的写法。还有一个parent指针。

typedef struct treenode
{
	struct treenode* left;
	struct treenode* right;
	char data;
}tn;

 

链式遍历

当研究一棵二叉树时,都应该有意识地认为他是由三个部分去构成的。

分别是左子树,右子树还有根节点。

例如上图,根节点为a时,那么bde构成左子树,cfg构成右子树。

当看b时,d为左子树,e为右子树,c也是同理。

那么为何要这样去分的这么细呢?因为等会会用到分治算法。

分治算法的思想是:分而治之,将大问题分成类似的子问题,子问题再接着往下分。直到子问题不可分割。

去求深度高度啥的会用到遍历。而且遍历我们分成三种。

前序遍历(先根遍历)

20063d5014327a604fc40ae0bd92ef6b.png

如其名,先访问根,再访问左子树,再访问右子树。

还是如上图,先访问a,a的左子树为b,右子树为c,先访问左子树,所以先访问b,再去访问b的左子树d,d仍然可以接着往下访问,但为空,则d的左右子树都为NULL,接着是b的右子树,e。

e如d其子树为空。

此时b访问完成,接着可以访问a的右子树。

所以大概这一圈走下来就是a  b  d  null  null  e  null  null  c  f  null  null  g  null  null

用代码来测试一下。

结构体定义左右节点和data。遍历我们采取递归的形式。

typedef struct treenode
{
	struct treenode* left;
	struct treenode* right;
	char data;
}tn;
//前序
void prevorder(tn* root)
{
	if (root == NULL)
	{
		printf("NULL ");
		return;
	}
	printf("%c ", root->data);
	prevorder(root->left);
	prevorder(root->right);
}
int main()
{
	tn* a = (tn*)malloc(sizeof(tn));
	a->data = 'a';
	a->left = NULL;
	a->right = NULL;
	tn* b = (tn*)malloc(sizeof(tn));
	b->data = 'b';
	b->left = NULL;
	b->right = NULL;
	tn* c = (tn*)malloc(sizeof(tn));
	c->data = 'c';
	c->left = NULL;
	c->right = NULL;
	tn* d = (tn*)malloc(sizeof(tn));
	d->data = 'd';
	d->left = NULL;
	d->right = NULL;
	tn* e = (tn*)malloc(sizeof(tn));
	e->data = 'e';
	e->left = NULL;
	e->right = NULL;
	tn* f = (tn*)malloc(sizeof(tn));
	f->data = 'f';
	f->left = NULL;
	f->right = NULL;
	tn* g = (tn*)malloc(sizeof(tn));
    g->data = 'g';
	g->left = NULL;
	g->right = NULL;
	a->left = b;
	a->right = c;
	b->left = d;
	b->right = e;
	c->left = f;
	c->right = g;
	prevorder(a);


}

结果

29d7ecacce9a42178bb835ae9079fa10.png

 

中序遍历(中根遍历)

先左子树,再是根,再是右子树。

null  d  null  b null e null a null f null c null g null 

代码如下,和前序如出一辙

//中序
void inorder(tn* root)
{
	if (root == NULL)
	{
		printf("NULL ");
		return;
	}
	inorder(root->left);
	printf("%c ", root->data);
	inorder(root->right);

}

后序遍历

先左子树,再右子树,最后根。

null null d null null e b null null f null null g c a

void postorder(tn* root)
{
	if (root == NULL)
	{
		printf("NULL ");
		return;
	}
	postorder(root->left);
	postorder(root->right);
	printf("%c ", root->data);

}

求节点数和各种变形

做法1:仍然采取递归的做法。但是请定义一个全局的size变量。否则size无法进行计数。

//求节点个数
int size = 0;
int treesize(tn* root)
{
	if (root == NULL)
		return ;
	size++;
	treesize(root->left);
	treesize(root->right);
	return size;
}

但使用全局变量会出现问题。当你第二次使用此函数时,size未清零。所以注意在每次调用前需要置为0即可。

但我仍旧不推荐这样去写,比如在多线程中进行处理,就极易出现问题。

那怎么处理这个问题比较好呢?可以考虑传参。

但不能传值,因为在不断的调用中调用的都是拷贝。所以可以传址,也就是指针。

//求节点个数
int size = 0;
void treesize(tn* root,int*psize)
{
	if (root == NULL)
	{
		return;
	}
	++(*psize);
	treesize(root->left,psize);
	treesize(root->right,psize);
}

但还有其他的思考,比如分治思想。

从根开始往下走,不断的进行分治。这样的代码就非常的简洁了。

int treesize2(tn* root)
{
	return root == NULL ? 0 : treesize2(root->left) + treesize2(root->right) + 1;
}

那么参照分治算法,试着写一下如何去求叶子节点的个数吧。

如果此节点为空,则返回0,是叶子节点,就返回1,不是空又不是叶子节点,那就代表可以接着向下进行遍历。

//求叶子节点个数
int treeleafsize(tn* root)
{
	if (root == NULL)
		return 0;
	if (root->left == NULL && root->right == NULL)
	{
		return 1;
	}
	return treeleafsize(root->left) + treeleafsize(root->right);
}

 

二叉树oj题练习

几道经典的力扣题目,可以更好的去理解在二叉树中递归的使用。 

题1 二叉树前序遍历

链接:https://leetcode.cn/problems/binary-tree-preorder-traversal

给你二叉树的根节点 root ,返回它节点值的 前序 遍历。

 

示例 1:


输入:root = [1,null,2,3]
输出:[1,2,3]
示例 2:

输入:root = []
输出:[]
示例 3:

输入:root = [1]
输出:[1]
示例 4:


输入:root = [1,2]
输出:[1,2]
示例 5:


输入:root = [1,null,2]
输出:[1,2]
 

提示:

树中节点数目在范围 [0, 100] 内
-100 <= Node.val <= 100

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     struct TreeNode *left;
 *     struct TreeNode *right;
 * };
 */


/**
 * Note: The returned array must be malloced, assume caller calls free().
 */
int* preorderTraversal(struct TreeNode* root, int* returnSize){

}

由题可知,返回类型是数组,所以需要malloc一个数组。和前面讲的差不多,只需要将打印换成将值放到数组里去即可。

同时,不知道数组要放多少个,所以直接用求节点的方法返回一个size即可。

int treesize(struct TreeNode* root)
 {
     return root==NULL?0:treesize(root->left)+treesize(root->right)+1;
 }
 void prevorder(struct TreeNode* root,int*a ,int *p)
 {
     if(root==NULL)
     return ;
     a[*p]=root->val;
     (*p)++;
     prevorder(root->left,a,p);
     prevorder(root->right,a,p);
 }
int* preorderTraversal(struct TreeNode* root, int* returnSize){
    int size=treesize(root);
    int*a =(int*)malloc(size*sizeof(int));
     int i=0;
     int* p=&i;
     prevorder(root,a,p);
     *returnSize=size;
     return a;
}

记得传进来的数组a和i传参传的是地址哦。否则只是一层拷贝,在调用中切记不要犯这种错误。

不要乱用全局变量来简单处理,可能会导致越界。更何况在多线程的处理中会出现问题,之前也提到过。

 

题2.二叉树的深度

输入一棵二叉树的根节点,求该树的深度。从根节点到叶节点依次经过的节点(含根、叶节点)形成树的一条路径,最长路径的长度为树的深度。

例如:

给定二叉树 [3,9,20,null,null,15,7],

    3
   / \
  9  20
    /  \
   15   7
返回它的最大深度 3 。

 

提示:

节点总数 <= 10000

分析:采用分治的思想。分为左右子树分别求,取大的深度进行返回。

最简单容易想到的就是如下代码

int maxDepth(struct TreeNode* root){
  if(root==NULL)
  return 0;
  return maxDepth(root->left)>maxDepth(root->right)?maxDepth(root->left)+1:maxDepth(root->right)+1;
}

但是在力扣中会给出一个非常巨大的树,导致超出时间限制。

可以将计算的值进行一个存储,减少计算时间。

int maxDepth(struct TreeNode* root){
  if(root==NULL)
  return 0;
  int leftdepth=maxDepth(root->left);
  int rightdepth=maxDepth(root->right);
  return leftdepth>rightdepth?leftdepth+1:rightdepth+1;
}

 

题3.平衡二叉树

链接:https://leetcode.cn/problems/ping-heng-er-cha-shu-lcof

输入一棵二叉树的根节点,判断该树是不是平衡二叉树。如果某二叉树中任意节点的左右子树的深度相差不超过1,那么它就是一棵平衡二叉树。

 

示例 1:

给定二叉树 [3,9,20,null,null,15,7]

    3
   / \
  9  20
    /  \
   15   7
返回 true 。

示例 2:

给定二叉树 [1,2,2,3,3,null,null,4,4]

       1
      / \
     2   2
    / \
   3   3
  / \
 4   4
返回 false 。

 

限制:

0 <= 树的结点个数 <= 10000

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     struct TreeNode *left;
 *     struct TreeNode *right;
 * };
 */


bool isBalanced(struct TreeNode* root){

}

分析:和上面那道题思路是类似的。去检查每棵树是否满足。只有满足了才去检查其左树和右树。

直到不可分割的子问题。

int maxDepth(struct TreeNode* root){
  if(root==NULL)
  return 0;
  int leftdepth=maxDepth(root->left);
  int rightdepth=maxDepth(root->right);
  return leftdepth>rightdepth?leftdepth+1:rightdepth+1;
}
bool isBalanced(struct TreeNode* root){
if(root==NULL)
return true;
int leftdepth=maxDepth(root->left);
int rightdepth=maxDepth(root->right);
return abs(leftdepth-rightdepth)<2&&isBalanced(root->left)&&isBalanced(root->right);
}

 

大概应付学校的期末考试如上就差不多了。就算超过也一般不会超过太多。

祝期末考试顺利。

 

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
(1)非递归定义 树(tree)是由n(n≥0)个结点组成的有限集合。n=0的树称为空树;n>0的树T: ① 有且仅有一个结点n0,它没有前驱结点,只有后继结点。n0称作树的根(root)结点。 ② 除结点外n0 , 其余的每一个结点都有且仅有一个直接前驱结点;有零个或多个直接后继结点。 (2)递归定义 一颗大树分成几个大的分枝,每个大分枝再分成几个小分枝,小分枝再分成更小的分枝,… ,每个分枝也都是一颗树,由此我们可以给出树的递归定义。 树(tree)是由n(n≥0)个结点组成的有限集合。n=0的树称为空树;n>0的树T: ① 有且仅有一个结点n0,它没有前驱结点,只有后继结点。n0称作树的根(root)结点。 ② 除根结点之外的其他结点分为m(m≥0)个互不相交的集合T0,T1,…,Tm-1,其中每个集合Ti(0≤i<m)本身又是一棵树,称为根的子树(subtree)。 2、掌握树的各种术语: (1) 父母、孩子与兄弟结点 (2) 度 (3) 结点层次、树的高度 (4) 边、路径 (5) 无序树、有序树 (6) 森林 3、二叉树的定义 二叉树(binary tree)是由n(n≥0)个结点组成的有限集合,此集合或者为空,或者由一个根结点加上两棵分别称为左、右子树的,互不相交的二叉树组成。 二叉树可以为空集,因此根可以有空的左子树或者右子树,亦或者左、右子树皆为空。 4、掌握二叉树的五个性质 5、二叉树的二叉链表存储。
MySQL多数据源是指在一个应用程序中同时使用多个不同的MySQL数据库来存储和管理数据的技术。它可以帮助开发人员更灵活地处理各种数据库操作,提高程序的性能和可扩展性。下面是一个完整的MySQL多数据源教程。 一、设置数据库连接信息 1. 在应用程序的配置文件中,创建多个数据库连接的配置项。例如,可以为每个数据源创建一个配置项,分别命名为db1、db2等。 2. 在配置项中,设置每个数据源的连接信息,包括数据库地址、用户名、密码等。 二、创建数据源管理器 1. 创建一个数据源管理器类,用于管理多个数据源。该类需要实现数据源的动态切换和获取。 2. 使用Java的线程安全的数据结构,如ConcurrentHashMap来存储数据源信息。将配置文件中的数据库连接信息加载到数据结构中。 3. 实现方法来切换不同的数据源,通过传入数据源的名称来切换到对应的数据库。 三、实现数据源切换 1. 在应用程序中,根据业务需求选择需要使用的数据源。可以通过调用数据源管理器的方法来切换数据源。 2. 在DAO层的代码中,根据当前使用的数据源名称,选择对应的数据源进行数据库操作。 四、使用多数据源进行数据库操作 1. 在DAO层的代码中,区分不同的数据源,并将数据库操作的代码包装在对应的数据源中。 2. 在业务层的代码中,调用DAO层的方法来进行数据库操作。不同的数据源会自动切换。 五、处理事务 1. 如果需要在一个事务中操作多个数据源,可以使用分布式事务的方式来处理。 2. 可以使用开源的分布式事务框架,如Atomikos、Bitronix等来实现多数据源的事务管理。 六、监控和维护 1. 使用监控工具来监控多个数据源的使用情况,包括连接数、查询次数等。 2. 定期对数据库进行维护,包括索引优化、数据清理等工作,以保证数据库的性能和稳定性。 通过以上步骤,我们可以实现MySQL多数据源的配置和使用。使用多数据源可以更好地管理和处理不同的数据库操作,在提高程序性能和可扩展性的同时,也提供了更灵活的数据操作方式。同时,需要注意合理选择和配置数据源,以及监控和维护数据库,以保证系统的运行效率和数据的安全性。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值