大家好啊✨
先简单介绍一下自己💎
本人目前大一在读,专业是计算机科学与技术。
写博客的目的是督促自己记好每一章节的笔记,同时也希望结交更多同仁,大家互相监督,一起进步!☀️
那么今天,就开始数据结构第三讲的学习----二叉树的初级和进阶。
👀注意,本篇文章并不会介绍二叉树的增删查改,因为它在实际案例中意义不大。其次,本篇文章并不会讲二叉树的链式存储,该内容会在后面的文章中体现。
但是,只要把本篇文章理解透彻,应对学校里的相关考试题目是没有问题的!
👀 如果各位同仁觉得本篇文章还不错的话,不妨先收藏起来,以后也好复习,也当做是给我的小小鼓励了!

文章可能过长,但全是干货,请大家耐心品读🔥🔥🔥
文章目录
树是一种非线性的数据结构,它是由n(n>=0)个有限结点组成一个具有层次关系的集合。把它叫做树是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。
下面是树的三个特点:👇👇👇
•有一个特殊的结点,称为根结点,根节点没有前驱结点
•除根节点外,其余结点被分成M(M>0)个互不相交的集合T1、T2、……、Tm,其中每一个集合Ti(1<= i <= m)又是一棵结构与树类似的子树。 每棵子树的根结点有且只有一个前驱,可以有0个或多个后继
•因此,树是递归定义的。
只有文字,不太方便理解,下面用图解来深入了解一下!

注意:树形结构中,子树之间不能有交集,否则就不是树形结构!

先贴上一张图方便大家理解:

节点的度:一个节点含有的子树的个数称为该节点的度; 如上图:A的为6
叶节点或终端节点:度为0的节点称为叶节点; 如上图:B、C、H、I…等节点为叶节点
非终端节点或分支节点:度不为0的节点; 如上图:D、E、F、G…等节点为分支节点
双亲节点或父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点; 如上图:A是B的父节点
孩子节点或子节点:一个节点含有的子树的根节点称为该节点的子节点; 如上图:B是A的孩子节点
兄弟节点:具有相同父节点的节点互称为兄弟节点; 如上图:B、C是兄弟节点
树的度:一棵树中,最大的节点的度称为树的度; 如上图:树的度为6
节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推;
树的高度或深度:树中节点的最大层次; 如上图:树的高度为4
堂兄弟节点:双亲在同一层的节点互为堂兄弟;如上图:H、I互为兄弟节点
节点的祖先:从根到该节点所经分支上的所有节点;如上图:A是所有节点的祖先
子孙:以某节点为根的子树中任一节点都称为该节点的子孙。如上图:所有节点都是A的子孙
森林:由m(m>0)棵互不相交的树的集合称为森林
温馨提示:上面黄色背景的概念是必须要熟练掌握的,其他的只需了解即可。
还需要注意的一点是:有一些教材中,在计算数的深度(高度)时,会把根结点所在的那一层记为第0层。
这种说法也是正确的,但我建议大家还是按照第一种方法来。原因在于如果根结点所在层数为0,当一棵树的结点数为空(即没有根结点时)该怎么规定这棵树的高度呢? 好像我们还没有听说过有高度为-1的树吧😇
树结构相对线性表就比较复杂了,要存储表示起来就比较麻烦了,既然保存值域,也要保存结点和结点之间的关系,实际中树有很多种表示方式如:双亲表示法,孩子表示法、孩子双亲表示法以及孩子兄弟表示法等。我们这里就简单的了解其中最常用的孩子兄弟表示法。
这里说明一下为什么不采用其他的方法:
当采用其他方法定义一棵树时,一个树的结构体成员数量是不确定的(当一个结点的孩子或者度过多时,很难将其完美地表示出来)。所以这里主要介绍三种比较简单有效的方法👇👇👇
方法一:顺序表存孩子的指针(这种方法主要在C++中采用:
struct TreeNode
{
int data;
vector<struct TreeNode*>childs;
};
即在一个结构体中存放一个数据,再存放一个大小并不固定的数组,用来存储其他的结点。这种方法也可以在C语言中运用,但其操作要比C++麻烦许多。
方法二:左孩子右兄弟
typedef int DataType;
struct Node
{
struct Node* _firstChild1; // 第一个孩子结点
struct Node* _pNextBrother; // 指向其下一个兄弟结点
DataType _data; // 结点中的数据域
};
定义一棵树的结构体时,当一个结点有多个孩子时,只存储左边第一个孩子的结点,再用一个兄弟指针存放旁边一个孩子的结点
以下图为例:

这种方法是非常实用的一种方法,也是我最推荐的一种方法哦✨
方法三:双亲表示法
这种方法是用数组来存储每一个结点的父亲,每个结点从上到下,从左到右依次从0开始编号,对应数组中的每个下标。每个节点中只存储其父亲的数组下标。

这种方法也是非常实用的一种方法,只不过它所应用的场景是初学者没有见过的。它往往出现在并查集中,这个还需要各位小伙伴们在以后的学习中慢慢体会,这里就先不讲这个了。
想必大家都很好奇,树在现实生活中到底有什么用途呢?其实,我们手机和电脑里保存文件的各种目录就是树结构的一个很好地应用。

如上图所示,最开始的一阶目录就相当于树的根结点,它里面包含的下一阶目录就相当于每一个节点的孩子。当文件为空或者该文件所在目录为最后一级时,这个文件就相当于叶节点。
更加深入的内容,相信大家在以后的学习中会遇到的,我们这里就不再继续讨论了。
一棵二叉树是结点的一个有限集合,该集合:
- 或者为空
- 由一个根节点加上两棵别称为左子树和右子树的二叉树组成

从上图可以看出:
- 二叉树不存在度大于2的结点
- 二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树
注意:对于任意的二叉树都是由以下几种情况复合而成的:

我们现实生活中的二叉树长什么样子呢?这里有两张图:


当普通人看到这两棵树时的反应往往是:giao,好对称的两棵树啊!
但是当一个程序猿看到它们时的反应就会是:giao,二叉树成精了!
当然,这两张图只是让大家换个方式理解二叉树,我们接着往下看!
- 满二叉树:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为K,且结点总数是 2^k-1,则它就是满二叉树。

那么,我们可以得到一个结论:
如果一棵满二叉树的高度为h,则其节点数为2^h-1
如果一个满二叉树的节点数为N,则其高度为log(以2为底N+1的对数)
- 完全二叉树:完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树。 要注意的是满二叉树是一种特殊的完全二叉树。注意:完全二叉树要求当树的高度为h时,前h-1层必须是满的,最后一层可以不满,但要求最后一层的节点必须从左到右依次排列,不能断开。

1.若规定根节点的层数为1,则一棵非空二叉树的第i层上最多有2^(i-1)个节点。
2.若规定根节点的层数为1,则深度为h的二叉树的最大节点数为2^n-1。
3. 对任何一棵二叉树, 如果度为0其叶结点个数为n0 , 度为2的分支结点个数为 n1,则有 n0=n2 +1。
4.若规定根节点的层数为1,则具有n个节点的满二叉树的深度为log(以2为底n+1的对数)。
以上性质1、2、4都已经在上文中出现,这里主要讲解一下性质三。

这个结论是一个高手总结出来的,而且非常有用,在很多题目中都能用到!
某二叉树共有 399 个结点,其中有 199 个度为 2 的结点,则该二叉树中的叶子结点数为( )
A 不存在这样的二叉树
B 200
C 198
D 199
根据上面的二叉树性质,度为0的结点(即叶子结点)的个数为度为2的结点的个数+1.所以该题答案为199+1=200.
其实这道题跟399个结点半毛钱关系都没有,我们只需要掌握二叉树性质就能秒杀它,所以,上面的性质真的是十分重要!
在具有 2n 个结点的完全二叉树中,叶子结点个数为( )
A n
B n+1
C n-1
D n/2

一棵完全二叉树的节点数位为531个,那么这棵树的高度为( )
A 11
B 10
C 8
D 12

对于这种类型的题,当写到这一步的时候,就只需将选项中的值带入到式子中去,如果解出一个合理的值,那就是正确答案,本题的答案是B,小伙伴们可以自己动手解一下哦!
在学习二叉树的前中后序之前,我们需要知道每一棵树(一整棵树也好,一颗子树也好)都由三部分组成----根、左子树、右子树
以下图为例:

这里可能大家会疑惑为什么存在空这种说法,好像学校里老师从来没这样说过。
但是,既然每一个结构体变量里都要存储左右子树,那么到了叶节点的时候,指向左右子树的指针就只能指向空了,所以叶节点也有自己的左右子树,只不过它们都为空。
而当一棵树的左右子树都为空时,也就不用再继续向下找它的子树了。
还需要注意的是,在处理树的问题的时候,我们通常采用分治(分而治之)的方法,这就需要我们不断地将一棵树分为根和它的子树,直到不可再分。
不知道大家有没有想到,这种方式好像和递归有些相似?
哈哈,现实也确实是这样的,在处理树的问题时,我们大多时候都离不开递归,大家慢慢体会吧!
下面来举例讲解前、中、后序:
前序----按照根->左子树->右子树的顺序遍历整个树
大家这里注意到,遍历时,出现了多次NULL,这时就是所谓的“不可再分”。而我们平常在课本或者习题中见到的都是把其中的NULL去掉之后的样子,本质是一样的!
中序----按照左子树->根->右子树的顺序遍历整棵树
后序----按照左子树->右子树->根的顺序遍历整棵树
下面我们用代码分别把上面三种情况表示出来:👇👇👇
先创建一个结构体,分别存储一个节点的左子树、右子树和结点中所存储的值:
typedef char BTDataType;
typedef struct BinaryTreeNode
{
struct BinaryTreeNode* left;//存储节点的左孩子
struct BinaryTreeNode* right;//存储节点的右孩子
BTDataType data;//存储结点所代表的值
}BTNode;
然后运用递归对树进行前中后序的遍历:
void PrevOrder(BTNode* root)//前序
{
if (root == NULL)
{
printf("NULL ");
return;
}
printf("%c ", root->data);//先打印根,然后是左子树和右子树
PrevOrder(root->left);
PrevOrder(root->right);
}
void InOrder(BTNode* root)//中序
{
if (root == NULL)
{
printf("NULL ");
return;
}
PrevOrder(root->left);//先打印左子树,然后是根和右子树
printf("%c ", root->data);
PrevOrder(root->right);
}
void PostOrder(BTNode* root)//后序
{
if (root == NULL)
{
printf(" NULL ");
return;
}
PrevOrder(root->left);//先打印左右子树,再打印根
PrevOrder(root->right);
printf("%c ", root->data);
}
接下来仅需要为结点开辟空间,再存储相应的值,最后把节点相互连接起来即可:
int main()
{
BTNode* A = (BTNode*)malloc(sizeof(BTNode));
A->data = 'A';
A->left = NULL;
A->right = NULL;
BTNode* B = (BTNode*)malloc(sizeof(BTNode));
B->data = 'B';
B->left = NULL;
B->right = NULL;
BTNode* C = (BTNode*)malloc(sizeof(BTNode));
C->data = 'C';
C->left = NULL;
C->right = NULL;
BTNode* D = (BTNode*)malloc(sizeof(BTNode));
D->data = 'D';
D->left = NULL;
D->right = NULL;
BTNode* E = (BTNode*)malloc(sizeof(BTNode));
E->data = 'E';
E->left = NULL;
E->right = NULL;
A->left = B;
A->right = C;
B->left = D;
B->right = E;
PrevOrder(A);
printf("\n");
InOrder(A);
printf("\n");
PostOrder(A);
return 0;
}
运行结果如下:

可以看到,结果和我们之前分析的相同!
仅仅看代码,可能大家还是没有彻底理解递归的整个过程,我们用一个图例来体会一下:

那对于递归的知识掌握得还不是很深的小伙伴可以尝试自己画一下代码的递归展开图,会帮助自己更快地理解其中奥秘❗️❗️❗️
接下来再来看一下如何计算二叉树中所有结点的个数:(该二叉树和前中后序遍历中为同一棵二叉树)
方法一:遍历计数法:
typedef struct BinaryTreeNode
{
struct BinaryTreeNode* left;//存储节点的左孩子
struct BinaryTreeNode* right;//存储节点的右孩子
BTDataType data;//存储结点所代表的值
}BTNode;
int TreeSize(BTNode* root)
{
if (root == NULL)
{
return;
}
int size = 0;
size++;
TreeSize(root->left);
TreeSize(root->right);
}
int main()
{
BTNode* A = (BTNode*)malloc(sizeof(BTNode));
A->data = 'A';
A->left = NULL;
A->right = NULL;
BTNode* B = (BTNode*)malloc(sizeof(BTNode));
B->data = 'B';
B->left = NULL;
B->right = NULL;
BTNode* C = (BTNode*)malloc(sizeof(BTNode));
C->data = 'C';
C->left = NULL;
C->right = NULL;
BTNode* D = (BTNode*)malloc(sizeof(BTNode));
D->data = 'D';
D->left = NULL;
D->right = NULL;
BTNode* E = (BTNode*)malloc(sizeof(BTNode));
E->data = 'E';
E->left = NULL;
E->right = NULL;
A->left = B;
A->right = C;
B->left = D;
B->right = E;
printf("TreeSize:%d\n", TreeSize(A));
return 0;
}
上面的代码就是对一棵二叉树进行遍历,运用递归,若节点不为空,就进行计数。可是,这种方法得出的结果真的是正确的吗?

可见,该方法得到的结果错误!
上面的代码错误的原因在于:每次递归调用TreeSize函数时,都会重新创建一个size,即每次进行计数的size都不一样,也就达不到累加的效果。
为了避免出现上面的错误,我们可以使用一个全局变量的size,这样,就能把每次递归的值加到size上。如下:
int size = 0;
void TreeSize(BTNode* root)
{
if (root == NULL)
{
return;
}
size++;
TreeSize(root->left);
TreeSize(root->right);
}
随后,直接打印size即可:

但是,这种方法就万无一失了吗❓❓❓
接下来,我们来同时用这种方法求一下A和B的结点数:
TreeSize(A);
printf("TreeSize:%d\n", size);
TreeSize(B);
printf("TreeSize:%d\n", size);

正确的答案应该是5 3,很明显,编译器输出的结果错误。原因在于,每次调用函数用的都是同一个size,这就导致计算B时累加了A的结点数。
为了避免累加,就需要在计算下一棵树的结点之前,将size置为0.
TreeSize(A);
printf("TreeSize:%d\n", size);
size = 0;
TreeSize(B);
printf("TreeSize:%d\n", size);

这样就得到正确答案了!
但是,这种方法还有一个潜在的漏洞。当有多个线程同时调用该函数时,最终的结果还是会累加。这时候就不妨直接传参调用该函数,即每次使用都传一个参数用来记录节点的个数。
void TreeSize(BTNode* root,int* p)
{
if (root == NULL)
{
return;
}
(*p)++;
TreeSize(root->left, p);
TreeSize(root->right, p);
}
int main()
{
BTNode* A = (BTNode*)malloc(sizeof(BTNode));
A->data = 'A';
A->left = NULL;
A->right = NULL;
BTNode* B = (BTNode*)malloc(sizeof(BTNode));
B->data = 'B';
B->left = NULL;
B->right = NULL;
BTNode* C = (BTNode*)malloc(sizeof(BTNode));
C->data = 'C';
C->left = NULL;
C->right = NULL;
BTNode* D = (BTNode*)malloc(sizeof(BTNode));
D->data = 'D';
D->left = NULL;
D->right = NULL;
BTNode* E = (BTNode*)malloc(sizeof(BTNode));
E->data = 'E';
E->left = NULL;
E->right = NULL;
A->left = B;
A->right = C;
B->left = D;
B->right = E;
int sizeA = 0;
TreeSize(A, &sizeA);
printf("TreeSize:%d\n", sizeA);
int sizeB = 0;
TreeSize(B, &sizeB);
printf("TreeSize:%d\n", sizeB);
return 0;
}

可见,结果正确。
方法二:分治算法:
int TreeSize(BTNode* root)
{
return root == NULL ? 0 : TreeSize(root->left) + TreeSize(root->right) + 1;
}
int main()
{
BTNode* A = (BTNode*)malloc(sizeof(BTNode));
A->data = 'A';
A->left = NULL;
A->right = NULL;
BTNode* B = (BTNode*)malloc(sizeof(BTNode));
B->data = 'B';
B->left = NULL;
B->right = NULL;
BTNode* C = (BTNode*)malloc(sizeof(BTNode));
C->data = 'C';
C->left = NULL;
C->right = NULL;
BTNode* D = (BTNode*)malloc(sizeof(BTNode));
D->data = 'D';
D->left = NULL;
D->right = NULL;
BTNode* E = (BTNode*)malloc(sizeof(BTNode));
E->data = 'E';
E->left = NULL;
E->right = NULL;
A->left = B;
A->right = C;
B->left = D;
B->right = E;
printf("TreeSizeA:%d\n", TreeSize(A));
printf("TreeSizeB:%d\n", TreeSize(B));
return 0;
}
一棵树的结点数为左子树的结点数+右子树的结点数+根结点。利用分治思想,每次递归,直到不可再分求出一棵树的总结点数。

计算叶子结点个数和计算结点总数有异曲同工之妙,都是运用递归和分治的思想。这里就不再赘述了。
int TreeLeafSize(BTNode* root)
{
if (root == NULL)
return 0;
if (root->left == NULL && root->right == NULL)
return 1;
return TreeLeafSize(root->left) + TreeLeafSize(root->right);
}
本篇文章介绍了二叉树的一些基本概念和性质,太深入的知识还没有涉及。因为博主现在临近期末,没有太多时间更新博客,所以有关二叉树更多深入的知识就留在暑假更新了~
不过相信大家学习了本篇内容之后,也就可以应对学校里考试的大多数题目了!
博主也只是一个大一学生,也在不断学习,如果文章中有哪些不太严谨的地方,还请私信告诉我,我将不胜感激!大家互相帮助,互相监督,学习的过程也会更加快乐!
一起加油吧~
我想将html转换为纯文本。不过,我不想只删除标签,我想智能地保留尽可能多的格式。为插入换行符标签,检测段落并格式化它们等。输入非常简单,通常是格式良好的html(不是整个文档,只是一堆内容,通常没有anchor或图像)。我可以将几个正则表达式放在一起,让我达到80%,但我认为可能有一些现有的解决方案更智能。 最佳答案 首先,不要尝试为此使用正则表达式。很有可能你会想出一个脆弱/脆弱的解决方案,它会随着HTML的变化而崩溃,或者很难管理和维护。您可以使用Nokogiri快速解析HTML并提取文本:require'nokogiri'h
我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i
有时我需要处理键/值数据。我不喜欢使用数组,因为它们在大小上没有限制(很容易不小心添加超过2个项目,而且您最终需要稍后验证大小)。此外,0和1的索引变成了魔数(MagicNumber),并且在传达含义方面做得很差(“当我说0时,我的意思是head...”)。散列也不合适,因为可能会不小心添加额外的条目。我写了下面的类来解决这个问题:classPairattr_accessor:head,:taildefinitialize(h,t)@head,@tail=h,tendend它工作得很好并且解决了问题,但我很想知道:Ruby标准库是否已经带有这样一个类? 最佳
给定一个复杂的对象层次结构,幸运的是它不包含循环引用,我如何实现支持各种格式的序列化?我不是来讨论实际实现的。相反,我正在寻找可能会派上用场的设计模式提示。更准确地说:我正在使用Ruby,我想解析XML和JSON数据以构建复杂的对象层次结构。此外,应该可以将该层次结构序列化为JSON、XML和可能的HTML。我可以为此使用Builder模式吗?在任何提到的情况下,我都有某种结构化数据-无论是在内存中还是文本中-我想用它来构建其他东西。我认为将序列化逻辑与实际业务逻辑分开会很好,这样我以后就可以轻松支持多种XML格式。 最佳答案 我最
我正在尝试使用Curbgem执行以下POST以解析云curl-XPOST\-H"X-Parse-Application-Id:PARSE_APP_ID"\-H"X-Parse-REST-API-Key:PARSE_API_KEY"\-H"Content-Type:image/jpeg"\--data-binary'@myPicture.jpg'\https://api.parse.com/1/files/pic.jpg用这个:curl=Curl::Easy.new("https://api.parse.com/1/files/lion.jpg")curl.multipart_form_
无论您是想搭建桌面端、WEB端或者移动端APP应用,HOOPSPlatform组件都可以为您提供弹性的3D集成架构,同时,由工业领域3D技术专家组成的HOOPS技术团队也能为您提供技术支持服务。如果您的客户期望有一种在多个平台(桌面/WEB/APP,而且某些客户端是“瘦”客户端)快速、方便地将数据接入到3D应用系统的解决方案,并且当访问数据时,在各个平台上的性能和用户体验保持一致,HOOPSPlatform将帮助您完成。利用HOOPSPlatform,您可以开发在任何环境下的3D基础应用架构。HOOPSPlatform可以帮您打造3D创新型产品,HOOPSSDK包含的技术有:快速且准确的CAD
本教程将在Unity3D中混合Optitrack与数据手套的数据流,在人体运动的基础上,添加双手手指部分的运动。双手手背的角度仍由Optitrack提供,数据手套提供双手手指的角度。 01 客户端软件分别安装MotiveBody与MotionVenus并校准人体与数据手套。MotiveBodyMotionVenus数据手套使用、校准流程参照:https://gitee.com/foheart_1/foheart-h1-data-summary.git02 数据转发打开MotiveBody软件的Streaming,开始向Unity3D广播数据;MotionVenus中设置->选项选择Unit
文章目录一、概述简介原理模块二、配置Mysql使用版本环境要求1.操作系统2.mysql要求三、配置canal-server离线下载在线下载上传解压修改配置单机配置集群配置分库分表配置1.修改全局配置2.实例配置垂直分库水平分库3.修改group-instance.xml4.启动监听四、配置canal-adapter1修改启动配置2配置映射文件3启动ES数据同步查询所有订阅同步数据同步开关启动4.验证五、配置canal-admin一、概述简介canal是Alibaba旗下的一款开源项目,Java开发。基于数据库增量日志解析,提供增量数据订阅&消费。Git地址:https://github.co
我正在尝试在Rails上安装ruby,到目前为止一切都已安装,但是当我尝试使用rakedb:create创建数据库时,我收到一个奇怪的错误:dyld:lazysymbolbindingfailed:Symbolnotfound:_mysql_get_client_infoReferencedfrom:/Library/Ruby/Gems/1.8/gems/mysql2-0.3.11/lib/mysql2/mysql2.bundleExpectedin:flatnamespacedyld:Symbolnotfound:_mysql_get_client_infoReferencedf
文章目录1.开发板选择*用到的资源2.串口通信(个人理解)3.代码分析(注释比较详细)1.主函数2.串口1配置3.串口2配置以及中断函数4.注意问题5.源码链接1.开发板选择我用的是STM32F103RCT6的板子,不过代码大概在F103系列的板子上都可以运行,我试过在野火103的霸道板上也可以,主要看一下串口对应的引脚一不一样就行了,不一样的就更改一下。*用到的资源keil5软件这里用到了两个串口资源,采集数据一个,串口通信一个,板子对应引脚如下:串口1,TX:PA9,RX:PA10串口2,TX:PA2,RX:PA32.串口通信(个人理解)我就从串口采集传感器数据这个过程说一下我自己的理解,