【C语言数据结构(基础版)】第五站:树和二叉树

目录

一、树的概念及结构

1.树的概念

2.树的表示

3.树在实际中的应用

 二、二叉树概念及结构

1.概念

2.特殊的二叉树

3.二叉树的性质

4.二叉树的存储结构

(1)顺序存储

(2)链式存储

三、二叉树链式结构的实现

1.二叉树的前序中序后序(深度优先遍历)

(1)树的分割

(2)先序遍历

(3)中序遍历

(4)后序遍历

 (5)先序中序后序的代码实现

2.计算二叉树中结点的个数

3.计算二叉树中叶子结点的个数

4.二叉树的层序遍历(广度优先遍历)

5.二叉树的销毁

6.二叉树的一些选择题

7.牛客题目之二叉树遍历

(1)思路分析

(2)解题代码

 四、哈夫曼树的建立以及编码

总结


一、树的概念及结构

1.树的概念

树是一种非线性的数据结构,它是由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
节点的层次从根开始定义起,根为第一层,根的子节点为第二层,以此类推
树的高度或深度树中节点的最大层次;如上图:树的高度为4
节点的祖先

从根到该节点所经分支上的所有节点;如上图:A是所有节点的祖先

子孙以某节点为根的子树中任一节点都称为该节点的子孙。如上图:所有节点都是A的子孙
森林由m棵互不相交的多颗树的集合称为森林;

 了解了这些概念的话,那么我们肯定就知道了如何看待这样一棵树,下面是一颗普通的树,这些树我们都可有看作是根和多颗子树组成的

当然还有一些长得比较像树,但他其实不是树,如下图中三棵都不是树

因为树需要满足:

1.子树是不相交的

2.除了根节点外,每个结点有且仅有一个父节点

3.一颗N个结点的树有N-1条边

2.树的表示

我们知道,像这种树的定义就是一件比较头疼的事情,比如我们一个节点里面该定义几个指针,我们根本不知道,除非它说了树的度为6。那这样我们就可有定义六个指针,但是这样浪费又很大。如果大部分的地方只有一个子树,那么这个节点的浪费就很大了。

 所以有的人就想出了这样一种方式,在c++中,有的人使用顺序表来存储这个指针,不规定一开始又多少个子树,而是采用动态增容的方式来实现的

 当然还有另外一种比较巧的方式

也就是左孩子右兄弟表示法,这又是什么表示法呢?我们先写出它的节点定义,如下图所示,它永远都是两个指针,一个只指向最左边的孩子,另外一个指向它的兄弟节点,这样的话,如果还有右边的兄弟的话,那就是让兄弟节点继续指向下一个兄弟。直到为空。然后孩子节点指向它这个节点的孩子节点,同样也是分为左孩子和右兄弟

 具体他们的指向图如下

 还有一种方式是双亲表示法,如下图所示

我们将每一个节点都给他一个下标,放到一个数组里面,比如R是0,A是1等,我们让这些不指向他们的孩子,而是让孩子指向父亲

然后R因为它是根了,所以它里面是-1。而像ABCD之类的节点就让他们与他们的双亲结点的下标相联系起来。这样我们也可以表示出整个树

3.树在实际中的应用

我们这里举一个简单的例子,比如我们的文件夹就是一个树

 二、二叉树概念及结构

1.概念

一颗二叉树是结点的一个有限集合,该集合或者为空,或者是由一个根结点加上两颗分别称为左子树和右子树的二叉树组成

二叉树的特点

1.每个结点最多有两个子树,即二叉树不存在度大于2的结点

2.二叉树的子树有左右之分,其子树的次序不能颠倒

如下图所示就是一个二叉树

2.特殊的二叉树

1.满二叉树:

一个二叉树,如果每一层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果二叉树的层数为k,它的结点总数为2^k-1,那么这是一个满二叉树

如下图所示就是一个满二叉树

 看着图我们也能理解这个满二叉树了,而它的层数为k的话,它的第一层是2^0个结点,第二层是2^1个结点,第三层是2^2个结点.......第k层是2^(k-1)个结点,那么对其求和之后它的总结点数当然就是2^k-1个结点了。同样的我们也可以解出来有N个结点的满二叉树层数为log(N+1),这里的对数是以2为底的。

2.完全二叉树:

完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为k的,有n个结点的二叉树,当且仅当其每一个结点都与深度为k的满二叉树中编号从1至n的结点一一一对应时称之为完全二叉树。要注意的是满二叉树是一种特殊的完全二叉树

需要特别注意的是,完全二叉树的最后一层如果是不满的话,那么最后一层结点必须是按顺序的,从左到右两个子树中不能出现空树,否则不是完全二叉树。

也就是说假设树的高度是h

1.前h-1层都是满的

2.最后一层不满,但是最后一层从左到右都是连续的

完全二叉树的结点个数我们也很容易的得到是2^k-1-x,x为完全二叉树对应的满二叉树所缺的元素个数。

所以如果知道结点的个数N,我们也可以反推出高度为log(N+1+X),其中该对数是以2为底数的。而且这个高度我们可以近似认为是logN,以2为底数。因为X是小于等于N二大于等于0的。我们只需要近似的算出高度,向上取整即可

3.二叉树的性质

1.若规定根节点的层数为1,则一颗非空二叉树的第i层上最多有2^(i-1)个结点。

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

3.对于任何一颗二叉树,如果度为0的叶结点个数为n0,度为2的分支结点个数为n2,则有n0=n2+1

4.若规定根结点的层数为1,具有N个结点的满二叉树的深度为logN

这里是一些二叉树的一些选择题

1. 某二叉树共有 399 个结点,其中有 199 个度为 2 的结点,则该二叉树中的叶子结点数为( )

A 不存在这样的二叉树         B 200        C 198        D 199

对于这道题,其实如果不知道性质3的话是很难想出来的,由性质三的,度为0的结点个数是度为2的结点个数+1直接得到答案是200

也就是说这道题跟这个399没有关系

2.在具有 2n 个结点的完全二叉树中,叶子结点个数为( )

A n         B n+1         C n-1         D n/2

对于这道题,我们可能一开始也是很懵的。但是其实还是考察二叉树的性质

设度为0的结点个数为x0,度为1的结点个数为x1,度为2的结点个数为x2

所以我们就知道了,x0+x1+x2=2n        //这是题目给出的第一个方程

然后由性质三得到x0=x2+1        //第二个方程

其实到了这里,我们看似没有条件,但是题目中说了是完全二叉树,所以它的度为1的结点个数要么是0要么是1。所以我们无非就是将两种情况都算一遍就可以了

最终解出来的答案只有A符合

3.一棵完全二叉树的节点数位为531个,那么这棵树的高度为( )

A 11         B 10         C 8        D 12

对于这道题,也是考察二叉树的性质,按照我们的公式就是直接logN,然后最终结果向上取整即可

我们知道1024是2的十次方,512是2的9次方,而它刚好落在这个范围内,所以最终的计算结果应该是9.xxxxx,而它向上取整刚好就是10,所以高度为10

4.二叉树的存储结构

二叉树一般可以使用两种结构存储:一种顺序结构,另外一种链式结构

(1)顺序存储

顺序结构存储就是使用数组来存储,一般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空间的浪费。而现实中只有堆才会使用数组来存储。二叉树顺序存储在物理上是一个数组,在逻辑上是一颗二叉树

(2)链式存储

二叉树的链式存储结构是指,用链表表示一颗二叉树,即用链来指示元素的逻辑关系。通常的方法是链表中每个结点由三个域组成。数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在链结点的存储地址。链式结构又分为二叉链,三叉链。我们现在所用的都是二叉链

下面是二叉链与三叉链的声明

三、二叉树链式结构的实现

1.二叉树的前序中序后序(深度优先遍历)

(1)树的分割

我们得先将一颗树的结构给理清楚,一棵树由三部分构成,分别是根节点,左子树,右子树。

比如说下面这棵树

我们是这样看这棵树的

这棵树首先分为根节点A,左子树B,右子树C

 然后我们继续具体细分,左子树B中,又可分为根节点B,左子树D,右子树E。而右子树C中,又可分为根节点C,左子树空树,右子树空树

 然后继续具体细分,D这棵树可分为根节点D和左子树空,右子树空,E这颗树可分为根节点C,左子树空,右子树空

这就是我们看待一颗树时候的看法

这也是分治算法:分而治之,大问题分为类似的子问题,子问题在分为子问题...直到子问题不可再分割

(2)先序遍历

那么这个树的分割我们直到了,它对我们的先序中序后序遍历树有什么用呢?

我们先看先序遍历,其实先序也称作先根,如下图所示,先根就很通俗易懂了,先访问根,再访问左子树,再访问右子树。

 那么我们按照这个思路用先序的方式去访问一下这棵树吧,首先这棵树得先访问根节点A

 然后我们开始访问左子树B,访问这颗左子树的时候,我们又先访问左子树的根,也就是B

 访问完B的根了,我们就要访问它的左子树D,而它的左子树D,又要先访问它的根,也就是D

 访问了D的根节点,那么我们又要访问它的左子树,而它的左子树是空

然后继续访问D的右子树,刚好它也是空

 D这棵树访问完了,而D是属于B的左子树,那么我们就应该访问B的右子树E了,而它的访问也一样,先访问根节点,再访问左子树,在访问右子树

 然后我们就发现,其实B这颗树也访问完成了,那么这下也就是A的左子树访问完了,该访问A的右子树了,而A的右子树C,它也是先访问根节点C,然后访问左右两颗树,而它的左右两颗树刚好都是空的

 这就是我们的先序遍历了。当然有的书上没有NULL这个东西,这个其实也不影响的

(3)中序遍历

有了先序遍历的理解那么其实,中序遍历我们也很容易就能写出来了,因为中序遍历其实就是先左子树,根,最后右子树

 那么我们简单的分析一下:

首先这颗树分为根节点A,左子树B,右子树C。那么我们因为是中序遍历,所以得先访问B这颗左子树。而B这颗左子树又分为根节点B,左子树D,右子树E。所以我们还得先访问D这颗树,而D这颗树,又分为根节点D,左子树NULL,右子树NULL。到了这里左子树已经到头了,所以我们该返回去了,所以目前的访问顺序就是NULL D NULL B,这样也就是B的左子树以及它的根已经访问完了,现在该访问B的右子树了,而这个根D是一样的过程,所以目前我们的访问顺序就成了NULL D NULL B NULL E NULL。这样也就意味着B这颗树访问完了,而B又是A这颗树上的左子树,所以现在将A一访问,就该访问右子树C了,而C的右子树又分为左子树NULL根C右子树NULL。

所以最终的访问顺序为NULL D NULL D NULL E NULL A NULL C NULL

(4)后序遍历

这个的访问我们就更加熟悉了,它的访问过程是

NULL NULL D NULL NULL E B NULL NULL C A

 我们在看一个例子

 这颗树的前序中序后序是

 (5)先序中序后序的代码实现

根据了上面的分析,其实我们也不难得知这个是通过递归实现的,代码如下:

#define _CRT_SECURE_NO_WARNINGS 1

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
typedef char BTDateType;
typedef struct BinaryTreeNode
{
	struct BinaryTreeNode* left;
	struct BinaryTreeNode* right;
	BTDateType date;
}BTNode;
//先序
void PrevOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("NULL ");
		return;
	}
	printf("%c ", root->date);
	PrevOrder(root->left);
	PrevOrder(root->right);
}
//中序
void InOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("NULL ");
		return;
	}
	InOrder(root->left);
	printf("%c ", root->date);
	InOrder(root->right);
}
//后序
void PostOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("NULL ");
		return;
	}
	PostOrder(root->left);
	PostOrder(root->right);
	printf("%c ", root->date);
}
int main()
{
	BTNode* A = (BTNode*)malloc(sizeof(BTNode));
	A->date = 'A';
	A->left = NULL;
	A->right = NULL;

	BTNode* B = (BTNode*)malloc(sizeof(BTNode));
	B->date = 'B';
	B->left = NULL;
	B->right = NULL;	

	BTNode* C = (BTNode*)malloc(sizeof(BTNode));
	C->date = 'C';
	C->left = NULL;
	C->right = NULL;

	BTNode* D = (BTNode*)malloc(sizeof(BTNode));
	D->date = 'D';
	D->left = NULL;
	D->right = NULL;

	BTNode* E = (BTNode*)malloc(sizeof(BTNode));
	E->date = '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);
	printf("\n");
}

 运行结果为

2.计算二叉树中结点的个数

这个也很简单,只要我们理解了递归,那么这个就很容易了,代码如下

//计算二叉树中结点的个数
int TreeSize(BTNode* root)
{
	if (root == NULL)
	{
		return 0;
	}
	else
	{
		return 1 + TreeSize(root->left) + TreeSize(root->right);
	}
}

 这是运行结果

3.计算二叉树中叶子结点的个数

这个同样很简单,如果树是空树,那么直接返回0即可,如果该结点的左孩子和右孩子都是空指针,那么返回1就可以了,说明他是一个叶子结点,其他情况我们就采用递归的思想去计算他的左孩子和右孩子的结点就可以了,代码如下

//计算叶子结点的个数
int TreeLeftSize(BTNode* root)
{
	if (root == NULL)
	{
		return 0;
	}
	else if (root->left == NULL && root->right == NULL)
	{
		return 1;
	}
	else
	{
		return TreeLeftSize(root->left) + TreeLeftSize(root->right);
	}
}

这是运行结果

4.二叉树的层序遍历(广度优先遍历)

我们先说一下广度优先遍历的基本思路,他的基本思路是这样的,需要使用一个队列

如下图所示,我们先将A入队列

 然后 A出队列的同时打印他并且将他的左右孩子入队列

 然后继续出B,打印他 入他的左右孩子

然后我们继续出C,入他的孩子,打印C

后面的也都是同理,出队列的同时打印他并且入他的孩子,如果没有孩子,那就不用入孩子了

 这就是我们的大致思路,而要实现这个首先,我们得导入我们队列,导入之后,我们需要修改的部分就是这两个,前置声明,因为我们的树是在他的里面定义的,所以在队列的头文件里面是不认识树结点的,所以我们得先声明一下,定义就在后面让他去找去。

  所以他最终的代码为

//层序遍历
void LevelOrder(BTNode* root)
{
	Queue q;
	QueueInit(&q);
	if (root != NULL)
	{
		QueuePush(&q, root);
	}
	while (!QueueEmpty(&q))
	{
		BTNode* front = QueueFront(&q);
		QueuePop(&q);
		printf("%c ", front->date);

		if (root->left != NULL)
		{
			QueuePush(&q, front->left);
		}
		if (root->right != NULL)
		{
			QueuePush(&q, front->right);
		}
	}
	printf("\n");
	QueueDestory(&q);
}

运行结果为

5.二叉树的销毁

想要销毁这颗二叉树,那么我们就必须得使用后序的方式来进行销毁,如果根节点是NULL,那么什么也不用做,如果不是空,那么就先销毁他的左子树,然后销毁右子树,最后销毁该结点。如此递归下去即可,代码如下

void DestoryTree(BTNode* root)
{
	if (root == NULL)
	{
		return;
	}

	DestoryTree(root->left);
	DestoryTree(root->right);

	free(root);
	root = NULL;
}

6.二叉树的一些选择题

1.某完全二叉树按层次输出(同一层从左到右)的序列为 ABCDEFGH 。该完全二叉树的前序序列为( )

A、 ABDHECFG         B、 ABCDEFGH         C、 HDBEAFCG         D、 HDEBFGCA

对于这道题,最关键的部分是完全二叉树这几个字眼。由此我们直接得出该完全二叉树的图

 而他的先序遍历我们很容易就得知,A B D H E C F G

所以选A

2.二叉树的先序遍历和中序遍历如下:先序遍历:EFHIGJK;中序遍历:HFIEJKG.则二叉树根结点为 ()

A、E         B、 F         C、 G         D、 H

对于这道题其实不用中序遍历我们也能做出来,因为我们知道先序遍历第一个肯定是根节点,所以就直接得出答案为E

但是呢我们也要知道先序遍历和中序遍历其实是可以确定一个二叉树的。我们现在画出他的二叉树,首先先序遍历我们可以确定根,所以我们知道首先有一个根E,有了根以后,我们根据这个根在中序遍历中的位置就能确定出左右区间,所以HFI部分为左子树,JKG部分为右子树。而HFI这一部分我们又回到先序遍历中,我们可以看出F是根,我们由这个F是根又回到中序遍历中确定他的左右子树区间,我们不难得出,H就是F的左子树,I就是F的右子树。同理,我们可以得出JKG部分的结构,G为根节点,J和K都是左子树部分,而JK部分中,J又是根节点,而K 是J 的右子树

最终我们得出这颗二叉树为

3.设一课二叉树的中序遍历序列:badce,后序遍历序列:bdeca,则二叉树前序遍历序列为____。

A、 adbce         B、 decab         C、 debac         D、 abcde 

这道题和上一道题是一样的,我们可以根据中序遍历和后序写出他的二叉树。这里直接给出他的二叉树

所以他的先序遍历为abcde

7.牛客题目之二叉树遍历

在这里也给出一道题

题目链接:二叉树遍历_牛客题霸_牛客网

题目描述:

(1)思路分析

首先我们先将思路给理清楚,二叉树的中序遍历并不难,难的是如何根据输入的字符串去构建二叉树。他的构建过程是这样的,因为他是根据先序遍历来构建的,那么第一个a肯定就是根结点,然后第二个b就是左子树的根节点,如此下来,c也是b的左子树的根结点,这时候我们该看c的左子树了,但是下一个数据是#,也就是说c的左子树是一个空,那么就该插入c的右子树了,而这个右子树恰好也是一个空。所以就返回到b的右子树了,b的右子树是一个d,d的左子树是一个e,e的左子树又是空,e的右子树是g,g的左右子树都是空。所以现在回到了d的右子树了,d的右子树刚好是一个f,而f的左右子树都是空,所以现在回到了a,a的右子树是一个空。

这样一来二叉树就构建完成了,如下图所示

 有了二叉树构建完成,那么这道题简直就是易如反掌了

(2)解题代码

#include <stdio.h>
#include <stdlib.h>
typedef struct TreeNode
{
    struct TreeNode* left;
    struct TreeNode* right;
    char val;
}TreeNode;
TreeNode* CreateTree(char* str,int* i)
{
    if(str[*i]=='#')
    {
        (*i)++;
        return NULL;
    }
    TreeNode* root=(TreeNode*)malloc(sizeof(TreeNode));
    if(root==NULL)
    {
        printf("malloc fail\n");
        exit(-1);
    }
    root->val=str[*i];
    (*i)++;
    root->left=CreateTree(str,i);
    root->right=CreateTree(str,i);
    return root;
}
void InOrder(TreeNode* root)
{
    if(root==NULL)
    {
        return ;
    }
    InOrder(root->left);
    printf("%c ",root->val);
    InOrder(root->right);
}
int main() 
{
    char str[101]={0};
    scanf("%s",str);
    int i=0;
    TreeNode* root = CreateTree(str,&i);
    InOrder(root);
    return 0;
}

 四、哈夫曼树的建立以及编码

在这里也简单提及一下哈夫曼树,实际上哈夫曼树应用并不多。一般只应用于文件压缩。在这里简单的提及一下哈夫曼树的建立以及他的编码

假设我们有如下四个结点,a、b、c、d相当于他们的编号,7、5、2、4则是他们的权值,我们利用这四个结点来构造一颗哈夫曼树

 我们的构造过程是这样的,先找出最小的和次最小的两个结点,然后让最小的放在左边,次最小的放在右边,然后再他们这两个结点上面构造一个双亲结点。这个双亲结点的权值是他们两个的权值之和

 然后接下来将这个6这个结点和剩余的结点放在一块,找出他们的最小的两个结点,同样最小的放在左边,次最小的放在右边

 跟上面同样的道理,继续将剩余的找出最小的两个

这样一来,我们就构建出哈夫曼树了,上面的就是哈夫曼树

有了哈夫曼树,我们还会对其进行编码,我们将每一个左子树编码为0,右子树编码为1

有了这些编码,我们就可以知道每一个结点的编码是多少了

比如a的编码为0、b的编码为10、c的编码为110、d的编码为111

而这些编码就是哈夫曼编码

这棵树也叫做加权路径最优二叉树,也就是所有原结点的权值*路径长度和最小

即:权值越大、路径越短、编码越短;权值越小、路径越长、编码越长。


总结

本小节讲解了树的概念,二叉树的性质,二叉树的先序中序后序层序遍历以及计算叶子结点,总结点个数的实现,最后还有哈夫曼树的建立以及编码

如果对你有帮助,不要忘记点赞加收藏哦!!!

想获得更多优质的内容, 一定不要忘记关注我哦!!!

  • 22
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 11
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

青色_忘川

你的鼓励是我创作的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值