【数据结构】二叉树的概念和实现

目录

一. 树概念及结构

1、树概念

2、树的表示

二. 二叉树概念及结构

1、概念

2、二叉树的特点

 3、满二叉树和完全二叉树 

4、二叉树的存储结构

三、二叉树的实现

1、二叉树顺序存储的实现

2、二叉树链式存储的实现 

①、四种遍历顺序的实现 

1、前序遍历

2、中序遍历 

3、后序遍历 

4、层序遍历和判断一个树是否为完全二叉树: 

②、获得树节点个数---TreeSize

 ③、获得叶子结点个数---TreeLeafSize

④、二叉树第k层结点个数--BinaryTreeLevelKSize 

⑤、二叉树查找值为x的节点--BinaryTreeFind

⑥、销毁二叉树--DestroyTree

四、二叉树的性质


一. 树概念及结构

1、树概念

树是一种非线性的数据结构,它是由n(n>=0)个有限结点组成一个具有层次关系的集合。把它叫做树是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。

根结点:根节点没有前驱结点。
除根节点外,其余结点被分成是一棵结构与树类似的子树。每棵子树的根结点有且只有一个前驱,可以有0个或多个后继。树是递归定义的。

  • 节点的度:一个节点含有的子树的个数称为该节点的度; 如上图:A的为2
  • 叶节点:度为0的节点称为叶节点; 如上图:G、H、I节点为叶节点
  • 非终端节点或分支节点:度不为0的节点; 如上图:B、D、C、E、F节点为分支节点
  • 双亲节点或父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点; 如上图:A是B的父节点
  • 孩子节点或子节点:一个节点含有的子树的根节点称为该节点的子节点; 如上图:B是A的孩子节点
  • 兄弟节点:具有相同父节点的节点互称为兄弟节点; 如上图:B、C是兄弟节点
  • 树的度:一棵树中,最大的节点的度称为树的度; 如上图:树的度为2
  • 节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推;
  • 树的高度或深度:树中节点的最大层次; 如上图:树的高度为4
  • 堂兄弟节点:双亲在同一层的节点互为堂兄弟;如上图:H、I互为堂兄弟节点
  • 节点的祖先:从根到该节点所经分支上的所有节点;如上图:A是所有节点的祖先
  • 子孙:以某节点为根的子树中任一节点都称为该节点的子孙。如上图:所有节点都是A的子孙
  • 森林:由m棵互不相交的树的集合称为森林;

2、树的表示

树结构相对线性表就比较复杂了,要存储表示起来就比较麻烦了,实际中树有很多种表示方式,如:双亲表示法,孩子表示法、孩子兄弟表示法等等。我们这里就简单的了解其中最常用的孩子兄弟表示法。

    typedef int DataType;
    struct Node
    {
         struct Node* firstChild1; 
         struct Node* pNextBrother; 
         DataType data; 
    };

二. 二叉树概念及结构

1、概念

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

2、二叉树的特点

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

②、二叉树的子树有左右之分,其子树的次序不能颠倒。

 3、满二叉树和完全二叉树 

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


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

4、二叉树的存储结构

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

①、顺序存储

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

②、链式存储

 二叉树的链式存储结构是指,用链表来表示一棵二叉树,即用链表来指示元素的逻辑关系。 通常的方法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址 。链式结构又分为二叉链和三叉链,当前我们学习中一般都是二叉链,后面课程学到高阶数据结构如红黑树等会用到三叉链

   // 二叉链
   struct BinaryTreeNode
   {
    struct BinTreeNode* _pLeft; // 指向当前节点左孩子
    struct BinTreeNode* _pRight; // 指向当前节点右孩子
    BTDataType _data; // 当前节点值域
   }
   // 三叉链
   struct BinaryTreeNode
   {
    struct BinTreeNode* _pParent; // 指向当前节点的双亲
    struct BinTreeNode* _pLeft; // 指向当前节点左孩子
    struct BinTreeNode* _pRight; // 指向当前节点右孩子
    BTDataType _data; // 当前节点值域
   };

三、二叉树的实现

1、二叉树顺序存储的实现

顺序存储主要实现堆的,我写过的,可以看看【数据结构】堆的实现(向下调整和向上调整法)和堆排序的实现_姜暮、的博客-CSDN博客


2、二叉树链式存储的实现 

遍历:前序 中序 后序 层序 前三个又叫深度优先遍历,层序又叫做广度优先遍历,树的遍历和线性表的遍历是不一样的。在图中的岛屿等问题爱考深度优先问题。

深度优先遍历:指节点均往最深的地方走,无路可走时(即NULL)再往回退,退回来有岔路再往最深的地方走,一般用到递归和栈。

层序优先遍历:以根为中心,一层一层往外走,一般用到队列。

任何一个二叉树都要看做三个部分:根节点 左子树 右子树 (因为左子树又可以分为根,左子树,右子树,左子树又可以分为根,左子树,右子树......直到遇到空的子树,即NULL,那么右子树同理,所以这个问题可以分为子问题,子问题又可分为子问题,直到NULL,即递归)

针对下图,前中后序遍历顺序是如何的?

前序遍历(先序遍历):

根 左(子树)右(子树)

A B D NULL NULL E NULL NULL C NULL NULL (本质的遍历顺序)

我们常说的是:A B D E C

但是一般我们是不会写这个NULL的,但是了解本质的遍历顺序才有利于理解后面讲的递归遍历二叉树。

中序遍历:

左(子树) 根 右(子树)

NULL D NULL B NULL E NULL A NULL C NULL(本质的遍历顺序)

我们常说的是:D B E A C

后序遍历:

左(子树) 右(子树)根

NULL NULL D NULL E NULL B NULL NULL C A(本质的遍历顺序)

我们常说的是: D E B C A 

二叉树的链式存储实现

已知二叉树结构:

typedef char BTDataType;
typedef struct BinaryTreeNode
{
	BTDataType _data;//根的数据
	struct BinaryTreeNode* _left;//左子树
	struct BinaryTreeNode* right;//右子树
}BTNode;

①、四种遍历顺序的实现 

1、前序遍历

 

前序遍历:用递归实现,根左右,

递归过程:先A,打印A的值,再B,B又作为根,打印B的值,再D,D又作为根,打印D的值,访问它的左子树,为NULL,所以终止,访问它的右子树,为NULL,也终止,至此,B的左子树D访问完毕,再访问B的右子树E,它的左右子树均为NULL,故终止,至此,B的左右子树访问完毕,A的左子树访问完毕,再C,先打印C的值,C的左右子树均为NULL,C访问完毕,至此整个二叉树遍历完毕。

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

	printf("%c", root->_data);
	PrevOrder(root->_left);
	PrevOrder(root->_right);
}

2、中序遍历 

 

中序遍历:用递归实现,左根右

递归过程:A的左子树B,B的左子树D,①、先D的左子树NULL,故打印D的值,右为NULL,至此D访问完毕②访问B,故打印B的值③、访问E时,要先访问它的左子树,为NULL,故打印E的值,右子树为NULL,至此E访问完毕,B子树访问完毕④、再访问A,故打印A的值⑤、访问C,C的左子树右子树均为NULL,至此C访问完毕,A访问完毕

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

	InOrder(root->_left);
	printf("%c", root->_data);
	InOrder(root->_right);
}

3、后序遍历 

 

 后序遍历:用递归实现,左右根

递归过程:①、D的左子树NULL,右也为NULL,再访问D,打印D的值②、访问E之前,因其左右子树为NULL,故打印E的值③、访问B,打印B的值④、访问C之前,先访问其左右子树,均为NULL,故打印C的值⑤、最

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

	PostOrder(root->_left);
	PostOrder(root->_right);
	printf("%c", root->_data);
}

4、层序遍历和判断一个树是否为完全二叉树: 

层序遍历:

  层序遍历只保存节点的值可以吗?不行,因为比如你存值A,就无法找到它的左右孩子B和C,所以要存这个节点,但存节点又太大了,所以我们一般会用存节点的指针,注意:存节点的指针,那么队列入的和出的的元素都是节点的指针,那么出队列删除的是指针,不会真正把节点删除

【若用c++实现,就不用以下操作了,c++直接调用STL库中的】

因为需要用队列,所以我们可以在当前项目下添加以前写的队列的一系列操作,先在写的队列的所在文件路径下,找到写的队列的两个文件

然后打开当前要写的二叉树项目->添加现有项->粘贴刚才复制的两个文件,这样这个二叉树项目就能用队列的两个文件了。

回到刚才所说的,我们要存节点的指针,即要在Queue.h中typedef BTNode* QDataType,但是BTNode在Queue文件中未定义,法一、在Queue.h文件中定义二叉树,这显然有些不妥,在一个队列的实现中竟然定义二叉树,故采用法二、在Queue.h文件中声明外部变量,extern struct BinaryTreeNode  再typedef struct BinaryTreeNode* QDataType 

void BinaryTreeLevelOrder(BTNode* root)
{
	Queue q;
	QueueInit(&q);//要初始化,不然就为随机值了
	if (root == NULL)
		return;//如果为空树,直接return;
	QueuePush(&q, root);//先插入根节点到队列中

	while (!QueueEmpty(&q))
	{
		BTNode* front = QueueFront(&q);//获得的数据是指针
		QueuePop(&q);//pop并没有删除这个队头,而是出指针,那就找不到节点了

		printf("%d", front->_data);//用指针访问对应元素值
		if (front->_left)
		{
			QueuePush(&q, front->_left);//入队列也是入指针
		}
		if (front->_right)
		{
			QueuePush(&q, front->_right);
		}
	}
	QueueDestory(&q);
	printf("\n");
}

 判断一个树是否为完全二叉树:

由二叉树层序遍历的结果知: 

若是完全二叉树:有效节点(不是NULL的就是有效节点)的后面应全为NULL,若不是,有效节点后面会插有有效节点,所以利用层序遍历找有效节点后面的区别就可判断二叉树是否为完全二叉树。

思路:

①、在入队列的同时出队列,出队列时,如果出到NULL,说明有效节点判断完毕,直接break,判断有效节点后面的部分

②、如果有效节点后面的部分全为NULL,则为完全二叉树,如果有效节点的后面存在有效节点,则不是完全二叉树。

int BinaryTreeComplete(BTNode* root)
{
	Queue q;
	QueueInit(&q);
	if (root == NULL)
		return 1;

	QueuePush(&q, root);

	while (!QueueEmpty(&q))
	{
		BTNode* front = QueueFront(&q);
		QueuePop(&q);

		if (front == NULL)
		{//出队列时碰到NULL了说明前面有效节点都走完了
        //这时就应该检查队列现存的是不是全是NULL即可
			break;
		}

        //因为NULL也要入队列,所以不用判断是否为NULL
        //入NULL是为了方便去判断是否为完全二叉树
		QueuePush(&q, front->_left);
		QueuePush(&q, front->_right);
	}

    //再判断现在队列里面是否全为NULL,是则为完全二叉树
	while (!QueueEmpty(&q))
	{
		BTNode* front = QueueFront(&q);
		QueuePop(&q);

		if (front)
		{//非空则说明在NULL中存在有效节点,一定不是完全二叉树
			QueueDestroy(&q);
			return 0;
		}
	}
	
	QueueDestroy(&q);
	return 1;//均为NULL,则是完全二叉树
}

如果我们想验证自己写的遍历,可以自己构建一个树,然后再验证

②、获得树节点个数---TreeSize

用前中后序都可以, 下面这个代码表面看是可以的,访问一次根,就相当于有一个节点,size就++,因为每一个节点在它的子树中都可以看做一次根。类似于根左右的遍历方式,只是这下是只统计几个,不是打印根的数据而已。但下面的代码存在错误,因为每次递归调用函数,因为size是局部变量,每次的size++的都不是一个size。

int TreeSize(BTNode* root)
{
	if(root == NULL)
		return 0;

	int size = 0;
	++size;
	TreeSize(root->_left);
	TreeSize(root->_right);

	return size;
}

解决方案考虑:

①、全局变量可以吗(在函数外部定义一个size)?不完全可以。因为调用一次TreeSize会给你正常返回大小,你调用第二次呢?第二次调用会在第一次上面累加,所以就错了。因为我们以后会学多线程,两个程序会同时在访问,那假如两个程序访问同一个全局变量,那不都加在同一个全局变量上了吗,会出问题。

②、static静态变量可以吗(static int size)?全局变量和静态变量区别就在于访问位置不一样,用static int size 后,size出了函数还会在,可加在同一个size上,但是和全局变量同样的问题,第二次调用TreeSize函数就不行了。

③、利用参数解决,每次都传一个指针就可保存下来了,但是不太简洁

void TreeSize(BTNode* root,int* psize)
{
	if(root == NULL)
		return 0;

	(*psize)++;
	TreeSize(root->_left,psize);
	TreeSize(root->_right,psize);
}

④、分治思想,把大问题不断分解为小问题,根,左子树,右子树,而左右子树又可以分为根,左子树,右子树。当前树的节点个数=1+左子树的节点个数+右子树的节点个数(这个1就是当前树的根节点)

递归过程:①、A不为空,return 1+A的左子树,进入A的左子树,B不为空,return 1+B的左子树,D不为空,return 1+D的左子树,D的左子树为NULL,故返回0,右子树也为空,故也返回0,所以return 1 + 0 + 0, 再访问B的右子树E,E不为空,return 1 + E的左子树 + E的右子树,即返回1,所以对于B return 1+1+1,至此A的左子树访问完毕,再访问A的右子树C,同理,C返回1,所以对于A来说return 1+3+1,至此整个二叉树访问完毕,返回5

int TreeSize(BTNode* root)
{
	if (root == NULL)
	{
		return 0;
	}

	return 1 + TreeSize(root->_left) + TreeSize(root->_right);
}

 ③、获得叶子结点个数---TreeLeafSize

法一、利用参数求解


int TreeLeafSize(BTNode* root, int* psize)
{
	if (root == NULL)
	{
		return 0;
	}
	else if (root->_left == NULL && root->_right == NULL)
	{
		(*psize)++;
	}

	TreeLeafSize(root->_left, psize);
	TreeLeafSize(root->_right, psize);
}

法二、分治思想:

当前树的叶子结点个数=左子树叶子节点个数+右子树叶子结点个数,而判断是否为叶子结点,看他的左子树和右子树是否同时为NULL即可。

①、如果是空树,直接返回0

②、如果是叶子结点,返回1,作为递归出口

③、如果不是叶子结点,则不断访问它的左右子树,不断逼近递归出口

int TreeLeafSize(BTNode* root)
{
	if (root == NULL)
	{
		return 0;//空树的叶子结点是0个
	}
	if (root->_left == NULL && root->_right == NULL)//如果是叶子结点
		return 1;
	
	//如果不是叶子结点,则看左右子树,直到遇到叶子结点
	return TreeLeafSize(root->_left) + TreeLeafSize(root->_right);

}

④、二叉树第k层结点个数--BinaryTreeLevelKSize 

思路:当前树的第k层可以转换成当前树的左子树和右子树的第k-1层,而左子树的第k-1层又=它的左子树和右子树的第k-2层,直到层数=1时就无需再分解

当k==1成立时,有两种情况,一种情况是该层数有节点,另一种情况是该层数无节点,所以应先判断当前是否为NULL,不是再又因k==1成立,return 1即可

int BinaryTreeLevelKSize(BTNode* root, int k)
{
	if (root == NULL)
		return 0;

	if (k == 1)
		return 1;

	return BinaryTreeLevelKSize(root->_left, k - 1)
		+ BinaryTreeLevelKSize(root->_right, k - 1);
}

⑤、二叉树查找值为x的节点--BinaryTreeFind

思路:先序遍历,假设要找x,先判断当前树的根节点是不是x,是->返回该节点,不是则看左子树和右子树,但这里注意接收一下节点,如果非空,说明找到了。

如图:假如我们想找E(即x=E)

①、A不是E,故判断A的左子树,B不是,故判断B的左子树,D不是,故判断D的左子树,为NULL故返回NULL,再判断D的右子树也返回NULL,故以D为根节点的整棵树不存在E,故返回NULL,再判断B的右子树,B的右子树==E成立,所以return E(此时E是根),node为真,所以函数返回E,对于以A为根来说,左子树返回的node为真,故返回node,递归结束。

BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{	//按照根左右的遍历顺序来
	if (root == NULL)
	{
		return NULL;
	}
	//先判断当前树的根是否满足
	if (root->_data == x)
	{
		return root;
	}
	//再判断当前树的左右子树是否有满足的
	BTNode* node = BinaryTreeFind(root->_left, x);
	if (node)
		return node;//如果左子树存在满足的就无需判断右子树了
	BTNode* node = BinaryTreeFind(root->_right, x);
	if (node)
		return node;
	//如果上述都不满足就返回NULL即可
	return NULL;
}

⑥、销毁二叉树--DestroyTree

思路:采用后序遍历,因为如果采取先序遍历,你先释放根节点,左右节点就找不到了,所以应该先看左右子树,再释放根 

实现方式一、参数用一级指针

有时我们为了接口型的一致性,可能就采用一级指针 

void DestroyTree(BTNode* root)
{
	if (root == NULL)
	{
		return;
	}
	DestroyTree(root->_left);
	DestroyTree(root->_right);
	free(root);
}

 实现方式二、参数用二级指针

有时我们为了防止野指针问题,会传二级指针,以便对传入的参数置为NULL

void DestroyTree(BTNode** root)
{
	if (*root == NULL)
	{
		return;
	}
	DestroyTree((*root)->_left);
	DestroyTree((*root)->_right);
	free(*root);
	*root = NULL;
}

⑦、 给先序遍历构造二叉树【c++实现】

思路:每次读入一个字符,用'#'来代表递归构建左右子树即可

//前序构造二叉树
Node* create_by_prev()
{
	char m;
	cin >> m;//每次都会读入一个字符
	Node* root;
	if (m == '#')
	{//用'#'来代表空
		root = nullptr;
	}
	else
	{
		root = new Node(m);
		root->_left = create_by_prev();//构建左子树
		root->_right = create_by_prev();//构建右子树
	}

	return root;//返回根节点
}


⑧、用先序和中序构造一颗二叉树【c++实现】

思路:先序第一个节点就是根节点,则在中序中找到根节点,根节点左边的就是左子树,根节点右边的就是右子树,就是一个递归结构

Node* bulidTree(char prev[], int l1, int r1, char in[], int l2, int r2)
{
	if (l1 > r1)
	{
		return nullptr;
	}

	Node* root = new Node(prev[l1]); //构造根节点
	int index = 0; //寻找中序遍历中的根节点
	for (int i = 0; i < strlen(prev); ++i)
	{
		if (in[i] == prev[l1])
		{
			index = i;
			break;
		}
	}
	int len = index - l2; //利用中序遍历获取左子树的长度
	root->_left = bulidTree(prev, l1 + 1, l1 + len, in, l2, index - 1);
	root->_right = bulidTree(prev, l1 + len + 1, r1, in, index + 1, r2);

	return root;
}


四、二叉树的性质

  • 1、若规定根节点的层数为1,则一棵非空二叉树的第i层上最多有2^(i-1) 个结点.
  • 2、若规定根节点的层数为1,则深度为h的二叉树的最大结点数是2^h- 1.
  • 3、对任何一棵二叉树, 如果度为0其叶结点个数为 n0, 度为2的分支结点个数为 n2,则有n0=n2+1
  • 下面是针对满二叉树和完全二叉树的:
  • 4、若规定根节点的层数为1,具有n个结点的满二叉树的深度,h=Log2(n+1). (ps:Log2(n+1)是log以2为底,n+1为对数)

5、对于具有n个结点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有节点从0开始编号,则对于序号为i的结点有: 

   1. 若i>0,i位置节点的双亲序号:(i-1)/2;i=0,i为根节点编号,无双亲节点
   2. 若2i+1<n,左孩子序号:2i+1,2i+1>=n否则无左孩子
   3. 若2i+2<n,右孩子序号:2i+2,2i+2>=n否则无右孩子

部分例题如下:

①、某二叉树共有399个结点,其中度为2的节点有199个,则此二叉树中叶子节点数为(200个)

分析:因为n0 = n2 + 1

②、若有2n个结点的完全二叉树中,叶子结点个数为(n个) 

分析:关于完全二叉树给节点数让你求叶子结点数的问题

对于二叉树,度数只有三种情况,度为0、1,2个,总结点数=度为0、1,2的各节点数相加。又因为是完全二叉树,因为其特殊性,度为1的最多只有1个(即要么1个,要么没有,具体有没有看题中给的节点个数)。故假设度数为0的节点(即叶子结点)有x个,则度为2的节点个数为x-1个(n0=n2+1),x+x-1=2x-1,又因为题中已给节点数为2n,即偶数个,所以度为1的节点有1个,所以2x-1+1=2n,故x=n  =>   叶子结点数有n个

③、若一颗完全二叉树的节点数为531个,此树的深度为(10)

分析:公式log2(n+1) = 深度(又称高度),其中n=531,所以解得h约等于9.几,深度为整数,所以h=10

④、一个具有767个结点的完全二叉树,其叶子结点的个数为(384)个

分析:同理第②题,假设叶子结点x个,x+x-1 +1或者+0 = 767,本题度为1的节点数应为0,故x=384个

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值