二叉树详解

1.树概念及结构

1.1 树的相关概念

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

(1)有一个特殊的节点,称为根节点,根节点没有前驱节点。

(2)除根节点外,其余节点被分成M(M>0)个互不相交的集合T1、T2、...、Tm,其中每一个集合Ti(1<=i<=m)又是一棵结构与树类似的子树。每棵子树的根节点有且只有一个前驱,可以有0个或多个后继。

(3)因此,树是递归定义的。

树形结构中,子树之间不能有交集,否则就不是树形结构。除了根节点之外,每个节点有且仅有一个父节点。一棵N个节点的树有N-1条边。

上图就不是树形结构。

节点的度:一个节点含有的子树的个数称为该节点的度;如上图中:节点A的度为2。

叶节点或终端节点:度为0的节点称为叶节点;如上图中:节点G、H、I为叶节点。

非终端节点或分支节点:度不为0的节点;如上图:B、C等节点为分支节点。

双亲节点或父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点;如上图:A是B的父节点。

孩子节点或子节点:一个节点含有的子树的根节点称为该节点的子结点;如上图:B是A的孩子节点/子节点。

兄弟节点:具有相同父节点的节点互相称为兄弟节点;如上图:E、F是兄弟节点。

树的度:一棵树中,最大的节点的度称为树的度;如上图:树的度为2。

节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推。

树的高度或深度:树中节点的最大层析;如上图:树的高度为4。

堂兄弟节点:双亲在同一层的节点互相称为堂兄弟;如上图:H、I互称为兄弟节点。

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

子孙:以某节点为根的子树中任一节点都称为该节点的子孙;如上图:所有节点都是A的子孙。

森林:由m(m>0)棵互不相交的树的集合称为森林。

1.2 树的表示

树结构相对于线性表来说比较复杂,既要保存值域,也要保存节点和节点之间的关系,实际中树有很多种表示方式,如:双亲表示法、孩子表示法、孩子双亲表示法、孩子兄弟表示法等。我们这里就简单的了解其中最常用的孩子兄弟表示法。

typedef int DataType;
struct Node
{
    struct Node* _firstChild1;//第一个孩子节点
    struct Node* _pNextBrother;//指向其下一个兄弟节点
    DataType _data;//节点中的数据域
};

2.二叉树的概念及结构

2.1 概念

一棵二叉树是节点的一个有限集合,该集合:1、可能为空;2、由一个根节点加上两棵称为左子树和右子树的二叉树组成。

从上图中可以看出:1、二叉树不存在度大于2的节点;2、二叉树的子树有左子树和右子树之分,次序不能颠倒,因此二叉树是有序树。

对于任意的二叉树,都是由以下几种情况组合而成的。

2.2 特殊的二叉树

1.满二叉树:一个二叉树,如果每一层的节点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为k,且节点总是2^k-1,则他就是满二叉树。

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

2.3 二叉树的性质

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

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

3.对任何一棵二叉树,如果度为0的叶节点的个数为n0,度为2的分支节点的个数为n2,则存在公式n0=n2+1。

4.若规定根节点的层数为1,具有n个节点的满二叉树的深度为h= log(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 无右孩子。

2.4 二叉树的存储结构

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

2.4.1 顺序存储

顺序结构存储就是使用数组来存储,一般数组只适合表示完全二叉树,因为不是完全二叉树会有空间的浪费。实际使用中只有堆才会使用数组来存储,关于堆后面会有专门讲解。二叉树顺序存储在物理上是一个数组,在逻辑上是一棵二叉树。

2.4.2 链式存储

二叉树的链式存储结构是指,用链表来表示一棵二叉树,也就是使用链来指示元素间的逻辑关系。通常的方法是链表中的每一个节点由三个域组成,即数据域、左指针域、右指针域。左右指针分别存储的是该节点左右孩子节点的地址。链式结构又分为二叉链和三叉链,目前只涉及到二叉链,高阶数据结构,如红黑树等会用到三叉链。

//二叉链
typedef int BTDataType;
struct BinaryTreeNode
{
    struct BinaryTreeNode* _pLeft;//指向当前节点左孩子
    struct BinaryTreeNode* _pRight;//指向当前节点右孩子
    BTDataType _data;//当前节点的值域
}

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

2.5 二叉树的顺序结构及实现

2.5.1 二叉树的顺序结构

前面我们已经介绍过,普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结构存储。我们通常把堆(一种二叉树)使用顺序结构的数组来存储。需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。

2.5.2 堆的介绍

堆的概念及结构、堆的实现以及堆的应用可参考文章关于堆的介绍

2.6 二叉树链式结构的实现

2.6.1 二叉树的遍历

2.6.1.1 前序、中序以及后续遍历

所谓二叉树遍历(Traversal)是按照某种特定的规则,依次对二叉树中的节点进行相应的访问操作,并且每个节点只访问操作一次。访问节点所做的操作依赖域具体的应用问题。遍历是二叉树上最重要的运算之一,是二叉树上进行其他运算的基础。

按照规则,二叉树的遍历有:前序/中序/后序的递归结构遍历:

1.前序遍历(Preorder Traversal亦称先序遍历)--访问根节点的操作发生在遍历其左右子树之前。

2.中序遍历(Inorder Travelsal)--访问根节点的操作发生在遍历其左右子树之中(间)。

3.后续遍历(Postorder Traversal)--访问根节点的操作发生在遍历其左右子树之后。

由于被访问的节点必是某子树的根,所以N(Node)、L(Left subtree)和R(Right subtree)又可解释为根、根的左子树和根的右子树。NLR、LNR和LRN分别又称为先根遍历、中根遍历和后根遍历。

先根遍历即遇到一棵二叉树按此顺序遍历:根->左子树->右子树,上图中的二叉树的前序遍历的顺序是:A->B->D->NULL->NULL->E->NULL->NULL->C->NULL->NULL,很多时候空节点的值不显示,故二叉树的前序遍历顺序是:ABDEC

中根遍历即遇到一棵二叉树按此顺序遍历:左子树->根节点->右子树;上图中的二叉树的中序遍历的顺序是:NULL->D->NULL->B->NULL->E->NULL->A->NULL->C->NULL,二叉树的中序遍历顺序是:DBEAC

后根遍历即遇到一棵二叉树按此顺序遍历:左子树->右子树->根节点;上图中的二叉树的后序遍历的顺序是:NULL->NULL->D->NULL->NULL->E->B->NULL->NULL->C->A,二叉树的后序遍历顺序是:DEBCA

2.6.1.2 层序遍历

除了先序遍历、中序遍历、后序遍历外,还可以对二叉树进行层序遍历。设二叉树的根节点所在层数为1,层序遍历就是从所在二叉树的根节点出发,首先访问第一层的树根节点,然后从左到右访问第2层上的节点,接着是第3层的节点,以此类推,自上而下,自左至右逐层访问二叉树的节点的过程就是层序遍历。上图中二叉树层序遍历的顺序为:ABCDE。

2.7 二叉树的链式实现

2.7.1 创建一棵简单的二叉树

我们在进行二叉树的基本操作前,需要先创建一棵二叉树。为了方便,我们先创建一棵简单的二叉树,该二叉树创建方法不是真正的二叉树的创建方法。真正的二叉树的创建方式,我会在后面的文章中继续介绍。

//二叉树的节点
typedef char BTDataType;
typedef struct BinaryTreeNode
{
	BTDataType _data;
	struct BinaryTreeNode* _left;
	struct BinaryTreeNode* _right;
}BinaryTreeNode;

//1、创建一棵二叉树
BinaryTreeNode* CreateBinaryTree(BTDataType val)
{
	BinaryTreeNode* node = (BinaryTreeNode*)malloc(sizeof(BinaryTreeNode));

	node->_data = val;
	node->_left = NULL;
	node->_right = NULL;

	return node;
}

2.7.2 前序遍历

//2、二叉树的前序遍历--将二叉树分为根-左子树-右子树
BTDataType PrevOrder(BinaryTreeNode* root)
{
	if (root == NULL)
	{
		printf("NULL ");
		return;
	}
	
	printf("%c ", root->_data);
	PrevOrder(root->_left);
	PrevOrder(root->_right);
}

前序遍历函数的递归过程如下图所示:

2.7.3 中序遍历

//3、中序遍历
void InOrder(BinaryTreeNode* root)
{
	if (root == NULL)
	{
		printf("NULL ");
		return;
	}

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

2.7.4 后序遍历

//4、后序遍历
void PostOrder(BinaryTreeNode* root)
{
	if (root == NULL)
	{
		printf("NULL ");
		return;
	}

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

2.7.5 二叉树节点个数

首先介绍几个常见的求二叉树节点个数的错误思路。

//5、求二叉树节点的个数
//错误思路1:
//这种写法求出的size==1,因为size是局部变量,
//每次递归时++size的不是同一个size。
int TreeSize(BinaryTreeNode* root)
{
	if (root == NULL)
		return 0;

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

	return size;
}

//错误思路2:
//尝试解决该问题,将size定义为全局变量,如下:
//该解决方法是错误的,因为当第一次调用该函数时,size被累加了5次,计算的结果为5;
//但是第二次调用该函数时,是在上次调用该函数的基础上累加size,计算的结果为10
int size = 0;
int TreeSize(BinaryTreeNode* root)
{
	if (root == NULL)
		return 0;

	++size;
	TreeSize(root->_left);
	TreeSize(root->_right);

	return size;
}

//错误思路3:
//将size定义为局部静态变量
//全局变量和全局静态变量在所有文件中都可见,静态变量只能在当前文件中可见;
//局部静态变量出了函数TreeSize还存在,++size是加在了同一个size上面;
//第二次调用函数TreeSize函数时计算size的结果是在第一次调用TreeSize函数的基础上累加得来的
//计算的结果与size是全局变量是一样的,这样也是不对的。
int TreeSize(BinaryTreeNode* root)
{
	if (root == NULL)
		return 0;

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

	return size;
}

求二叉树节点个数的正确解法。

//求二叉树节点个数的正确解决方案1:
//该方法在求二叉树节点个数时,需要先定义一个变量,
//然后将该变量的地址传给该函数,这种方法很不好用,而且如果只定义了一个变量,
//同时调用两次TreeSize函数时,该变量的结果依然与上述几种解决方法的结果一样。
void TreeSize(BinaryTreeNode* root, int* pSize)
{
	if (root == NULL)
		return;
	else
		(*pSize)++;

	TreeSize(root->_left, pSize);
	TreeSize(root->_right, pSize);
}

//求二叉树节点个数的正确解决方案2:
//该方法是常用的方法
int TreeSize(BinaryTreeNode* root)
{
	if (root == NULL)
		return 0;
	else
		return 1 + TreeSize(root->_left) + TreeSize(root->_right);
}

2.7.6 二叉树叶子节点的个数

//6、求二叉树的叶子节点的个数
int TreeLeafSize(BinaryTreeNode* root)
{
	if (root == NULL)
		return 0;

	if (root->_left == NULL & root->_right == NULL)
		return 1;

	return TreeLeafSize(root->_left) + TreeLeafSize(root->_right);
}

2.7.7 将二叉树的数据放入到数组中,并返回该数组

//7、创建一个数组,并将二叉树中的数据放入该数组中,并返回该数组
void _PreorderTravel(BinaryTreeNode* root, BTDataType* array, int* pi)
{
	if (root == NULL)
		return;

	array[(*pi)++] = root->_data;
	_PreorderTravel(root->_left, array, pi);
	_PreorderTravel(root->_right, array, pi);
}

BTDataType* PreorderTravel(BinaryTreeNode* root)
{
	int size = BinaryTreeSize(root);
	BTDataType* array = (BTDataType*)malloc(sizeof(BTDataType) * size);
	int i = 0;
	_PreorderTravel(root, array, &i);
	return array;
}

2.7.8 求二叉树第K层节点的个数

//8、求二叉树第K层节点的个数
//思路:求当前树的第K层节点的个数,可以转换成求该树左、右子树的节点个数之和,
//第K-1层节点的个数,层数==1时就不需要再分解。
int BinaryTreeLevelKSize(BinaryTreeNode* root, int k)
{
	if (root == NULL)
		return 0;

	if (k == 1)
		return 1;

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

2.7.9 在二叉树中查找值为val的节点,并返回该节点

//9、在二叉树中查找值为val的节点,并返回该节点
BinaryTreeNode* BinaryTreeNodeFind(BinaryTreeNode* root, BTDataType val)
{
	if (root == NULL)
		return NULL;

	if (root->_data == val)
		return root;

	BinaryTreeNode* node = BinaryTreeNodeFind(root->_left, val);
	if (node)
	{
		return node;
	}

	node = BinaryTreeNodeFind(root->_left, val);
	if (node)
	{
		return node;
	}

	return NULL;
}

2.7.10 二叉树的销毁

//10、销毁二叉树--后序销毁
void DestroyTree(BinaryTreeNode* root)
{
	if (root == NULL)
		return;

	DestroyTree(root->_left);
	DestroyTree(root->_right);
	free(root);
}

2.7.11 二叉树的层序遍历

层序遍历的分析:

//11、二叉树的层序遍历
//思路:1、根先进队列;2、持续迭代-->队列不为空,出队头数据,同时把出的节点的左右孩子节点带进去;
//3、直到队列为空,结束
//注意:队列中存的是二叉树节点的指针
void BinaryTreeLevelOrder(BinaryTreeNode* root)
{
	Queue q;
	QueueInit(&q);
	if (root == NULL)
		return;
	//根节点指针入队列
	QueuePush(&q, root);

	while (!QueueIsEmpty(&q))
	{
		//获取队头数据
		BinaryTreeNode* front = QueueFront(&q);
		QueuePop(&q);//如果队列不为空,则出队头的数据

		printf("%c ", front->_data);//打印队头数据

		//如果队头节点的左孩子节点不为空,则左孩子节点入队列
		if (front->_left)
		{
			QueuePush(&q, front->_left);
		}

		//如果队头节点的右孩子节点不为空,则右孩子节点入队列
		if (front->_right)
		{
			QueuePush(&q, front->_right);
		}
	}
	QueueDestroy(&q);
	printf("\n");
}

2.7.12 判断一棵二叉树是否是完全二叉树

//12、判断一棵二叉树是否是完全二叉树
//判断方法:利用层序的方法打印二叉树(此时,NULL节点也入队列),
//当遇到NULL节点时,程序就是break,此时检查队列中剩余的节点中除了NULL节点,是否还存在二叉树的
//节点,如果存在二叉树的节点,则不为完全二叉树;如果不存在二叉树的结点,全部是NULL节点,则为完全二叉树。
bool BinaryTreeComplete(BinaryTreeNode* root)
{
	Queue q;
	QueueInit(&q);
	if (root == NULL)
		return true;

	QueuePush(&q, root);

	while (!QueueIsEmpty(&q))
	{
		BinaryTreeNode* front = QueueFront(&q);
		QueuePop(&q);

		//队列中的节点出到NULL时break,
		//检查队列,队列非空,则不为完全二叉树;队列为空,则为完全二叉树。
		if (front == NULL)
			break;

		QueuePush(&q, front->_left);
		QueuePush(&q, front->_right);
	}
	while (!QueueIsEmpty(&q))
	{
		BinaryTreeNode* front = QueueFront(&q);
		QueuePop(&q);

		if (front)
		{
			QueueDestroy(&q);
			return false;
		}	
	}
	return true;
}

二叉树链式实现的完整代码可参考:二叉树的链式实现

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值