【数据结构】二叉树及其接口实现(递归实现) & 部分简单的二叉树OJ题(二叉树部分下篇)

本文详细介绍了二叉树的创建、销毁、遍历、节点查找等基本操作的实现,以及部分在线判题(OJ)题目如前序遍历、单值二叉树、翻转二叉树、对称二叉树和子树判断的解题思路。通过递归方法,展示了如何利用C语言处理二叉树的相关问题。
摘要由CSDN通过智能技术生成

二叉树的实现

二叉树的声明及其部分接口

typedef int BTDataType;
typedef struct BinaryTreeNode {
	BTDataType data;
	struct BinaryTreeNode* left;
	struct BinaryTreeNode* right;
}BTNode;

BTNode* CreateTree();//创建二叉树(手动)

void TreeDestroy(BTNode* root);//二叉树销毁

int TreeSize(BTNode* root);//二叉树中结点个数

int TreeLeafSize(BTNode* root);//二叉树中叶子结点的个数

int TreeKLevel(BTNode* root, int k);//二叉树中第K层的结点个数

BTNode* TreeFind(BTNode* root, BTDataType k);//找到二叉树中值为K的结点,并返回这个结点

void PreOrder(BTNode* root);//先根序遍历二叉树

void MidOrder(BTNode* root);//中根序遍历二叉树

void PostOrder(BTNode* root);后根序遍历二叉树

void LevelOrder(BTNode* root);层序遍历二叉树

int TreeComplete(BTNode* root);//判断二叉树是否为完全二叉树

int TreeHeight(BTNode* root);//求二叉树共多少层

二叉树各接口的实现代码

二叉树的创建

二叉树的创建可以用递归方法进行创建,但需要手动进行输入,空指针输入’#',其他位置输入对应数据:


void createBiTree(BTNode* root)
{
	char c;
	scanf("%c",&c);
	if('#' == c)
		T = NULL;
	else
	{
		T = (BTNode*)malloc(sizeof(BTNode));
		T->data = c;
		createBiTree(T->left);
		createBiTree(T->right);
	}
}

为了方便,如果需要用到的树规模不大,我们也可以进行手动建树:

BTNode* CreateTree()
{
	BTNode* n1 = (BTNode*)malloc(sizeof(BTNode));
	assert(n1);
	BTNode* n2 = (BTNode*)malloc(sizeof(BTNode));
	assert(n2);
	BTNode* n3 = (BTNode*)malloc(sizeof(BTNode));
	assert(n3);
	BTNode* n4 = (BTNode*)malloc(sizeof(BTNode));
	assert(n4);
	BTNode* n5 = (BTNode*)malloc(sizeof(BTNode));
	assert(n5);
	BTNode* n6 = (BTNode*)malloc(sizeof(BTNode));
	assert(n6);
	BTNode* n7 = (BTNode*)malloc(sizeof(BTNode));
	assert(n7);
	BTNode* n8 = (BTNode*)malloc(sizeof(BTNode));
	assert(n8);


	n1->data = 1;
	n2->data = 2;
	n3->data = 3;
	n4->data = 4;
	n5->data = 5;
	n6->data = 6;
	n7->data = 7;
	n8->data = 8;



	n1->left = n2;
	n1->right = n3;
	n2->left = n4;
	n2->right = n5;
	n3->left = n6;
	n3->right = n7;
	n4->left = n8;
	n4->right = NULL;
	n5->left = n5->right = NULL;
	n6->left = n6->right = NULL;
	n7->left = n7->right = NULL;
	n8->left = n8->right = NULL;


	return n1;
}

这样建树会更加直观,但是代码膨胀较为严重。

二叉树的销毁

void TreeDestroy(BTNode* root)
{
	if (root == NULL)
	{
		return;
	}
	TreeDestroy(root->left);
	TreeDestroy(root->right);
	free(root);
}

此方法为递归销毁二叉树,树根为空则直接返回,否则遍历左子树进行销毁,遇到根叶子结点时进行free,右子树同理。

二叉树求取结点个数

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

代码很简单,但是想理解深层含义还是需要琢磨一下的。这仍然是一个递归方法,先看跟是否为空,空则返回0,否则就分别求出根的左子树和右子树的结点个数。这就好比校长要知道整个大学有多少学生,他会找来所有学院的院长让他们去统计,学院院长又让各个系的主任去统计,系主任又会让各个班级的班主任去统计,班主任又会让班长去统计,班长又会让各宿舍宿舍长去统计,直到不能再向下递了,开始往回归,一层一层的加在一起,最后汇总给校长,求出总数。因此代码逻辑就是根为空,就返回null,否则就返回本身的1以及其左右子树的全部结点个数,就能求出整棵树的结点个数。

二叉树求取叶子节点个数

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);
}

代码也很简洁,代码执行应该会先递归至无法再递归时再往回返,因此第一个条件为若根为空了,那么就要返回0,第二个条件就是如果此根符合叶子结点的要求,那么返回1,完成一次计数,如果这个结点既不是空,也不是叶子结点,那么就对他的左子树和右子树进行递归求取,最后就会返回整棵树的叶子结点个数。

求二叉树第K层的结点个数

int TreeKLevel(BTNode* root, int k)
{
	assert(k > 0);
	if (root == NULL)
	{
		return 0;
	}
	if (k == 1)
	{
		return 1;
	}

	return TreeKLevel(root->left, k - 1) + TreeKLevel(root->right, k - 1);
}

assert和前两个if都比较好理解,首先层数要为正整数,其次空树无结点,一层的树只有一个节点。最后这个递归相加依然是下派任务的那种感觉,你想求第K层有多少结点,就是求第二层的结点对应的第K-1层有多少结点,一直递归到第K层,K就会减到1,该层的所有结点都会返回1,最后递归回来进行汇总,求出第K层有多少结点。

在二叉树中查找值为k的节点,并返回该结点的指针

BTNode* TreeFind(BTNode* root, BTDataType k)
{
	if (root == NULL)
		return NULL;
	if (root->data == k)
		return root;

	BTNode* lret = TreeFind(root->left, k);
	if (lret)
		return lret;
	BTNode* rret = TreeFind(root->right, k);
	if (rret)
		return rret;

	return NULL;
}

前两个if比较好理解。因为我们这里要将对应结点的地址返回,如果没有找到对应结点再返回NULL,所以在向深层次递归的时候,我们创建一个变量来接受递归左子树(右子树)的返回值,如果返回值不是空,那么一定是在这边找到了对应值(这里我们只考虑二叉树中有一个目标值),然后就会一层一层的返回上去;
也就是重复执行这三行:

	BTNode* lret = TreeFind(root->left, k);
	if (lret)
		return lret;

或者这三行:(递归return时返回上一层函数中执行过的最后一句)

	BTNode* rret = TreeFind(root->right, k);
	if (rret)
		return rret;

若返回值为空,则说明这边的子树中没有目标值,若两侧遍历结果均为空,那么说明二叉树中没有目标值,返回NULL指针。

二叉树的前中后序遍历

void PreOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("NULL ");
		return;
	}
	printf("%d ",root->data);
	PreOrder(root->left);
	PreOrder(root->right);
}

void MidOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("NULL ");
	}
	MidOrder(root->left);
	printf("%d ",root->data);
	MidOrder(root->right);
}

void PostOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("NULL ");
	}
	PostOrder(root->left);
	PostOrder(root->right);
	printf("%d ", root->data);
}

由于这部分内容相对来说比较简单,因此直接放在一起来讲。前中后序遍历的具体含义是遇到根结点时,是先访问根结点,再访问左右子树;还是先访问左子树,再访问根结点,最后访问右子树;还是先访问左子树,再访问右子树,最后访问根结点,因此还是一个递归的问题,主要是把访问这个操作放在左右子树的哪个位置。但是我们仍要了解这整个的遍历过程,一个叶子结点,如果是前序遍历,那么就是先访问其本身,然后访问左NULL,然后再访问右NULL,然后这个结点才被访问结束,归到上层,因此我们不能忽略空结点的存在,这样才能深刻理解前中后序遍历的精髓。

求二叉树树高

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

	return 1 + max(TreeHeight(root->left), TreeHeight(root->right));
}

因为我们要求取二叉树高,肯定是得到他的最大高度,因此我们要求的就是1加上根结点的左右子树高度中较大的高度,与此同时向下递归,最后求出哪边的子树更高,返回树高。

二叉树的层序遍历

二叉树的层序遍历思想如下(队列实现):
1、首先将二叉树的根节点push到队列中,判断队列不为NULL,就输出队头的元素,
2、判断出队节点是否有孩子,如果有,就将孩子push到队列中,
3、遍历过的节点出队列,
4、循环以上操作,直到Tree == NULL。
实现代码如下:

void LevelOrder(BTNode* root)
{
	Queue q;
	QueueInit(&q);

	if (root)
		QueuePush(&q, root);
	while (!QueueEmpty(&q))
	{
		BTNode* front = QueueFront(&q);
		QueuePop(&q);
		printf("%d ",front->data);

		if(front->left)
			QueuePush(&q, front->left);
		if (front->right)
			QueuePush(&q, front->right);
	}

	QueueDestroy(&q);
}

在root不为空的前提下,将其入队,然后在队列非空的条件下开始循环,先将队头元素用二叉树指针记录好,然后将其出队,然后打印备份好的原队头元素数据,最后开始判断,若原队头元素在二叉树中有左孩子,则将左孩子入队,右孩子同理。在队内元素全部出队后,将队销毁。实现动图如下:
请添加图片描述

判断二叉树是否为完全二叉树

完全二叉树的性质我们在上篇中已经提到,而如何用程序判断一个二叉树是否为完全二叉树仍然是个问题,这里我们借助层序遍历的方法来判断一棵二叉树是否为完全二叉树。
如果一棵二叉树在进行层序遍历时,队列开始向外弹出NULL的时候队列中仍然存在非NULL元素,那么说明该二叉树并不是完全二叉树,若弹出NULL时后续再没有非NULL元素弹出,则该树为完全二叉树。实现代码如下:

int TreeComplete(BTNode* root)
{
	Queue q;
	QueueInit(&q);

	if (root)
		QueuePush(&q, root);
	while (!QueueEmpty(&q))
	{
		BTNode* front = QueueFront(&q);
		QueuePop(&q);
		if (front == NULL)
		{
			break;
		}
		QueuePush(&q, front->left);
		QueuePush(&q, front->right);
	}
	while (!QueueEmpty(&q))
	{
		BTNode* front = QueueFront(&q);
		QueuePop(&q);
		if (front)
		{
			QueueDestroy(&q);
			return false;
		}
	}
	QueueDestroy(&q);
	return true;
}

代码的思想就是在层序遍历的基础上,再对队列进行遍历,如果遍历过程中找到了非空元素,那么则可以判断这棵树不是完全二叉树,否则说明该树是一棵完全二叉树。

部分简单的二叉树OJ题

二叉树的前序遍历

int TreeSize(struct TreeNode* root)
{
    if(root == NULL)
    {
        return false;
    }

    return TreeSize(root->left) + TreeSize(root->right) + 1;
}

void PreOrder(struct TreeNode* root, int* ret, int* pi)
{
    if(root == NULL)
    {
        return;
    }
    ret[(*pi)++] = root->val;
    PreOrder(root->left, ret, pi);
    PreOrder(root->right, ret, pi);
}

int* preorderTraversal(struct TreeNode* root, int* returnSize)
{
    *returnSize = TreeSize(root);
    int* ret = (int*)malloc(*returnSize * sizeof(int));
    assert(ret);
    int i = 0;
    PreOrder(root, ret, &i);

    return ret;
}

因为我们要将遍历结果存入一个数组中,因此我们要先求出整个二叉树中结点的个数,然后根据总的结点个数来申请一个相应的数组。我们在前面已经实现了TreeSize函数,所以这里选择复用一下。这里还有一个细节,因为我们遍历二叉树的方法为递归,所以想要在递归的同时能够将元素存入数组的对应位置,我们要在调用前序遍历的函数中定义一个“哨兵”,这里就是变量i,然后将其地址传入遍历函数,再每次向数组中存入变量的时候,我们解引用并使其加一,这样在遍历的同时就能够按顺序向数组中存储,最后将数组返回。

单值二叉树

这个二叉树的概念就是,如果二叉树中每个结点的值均相同,那么这个二叉树就是一个单值二叉树,具体实现如下:

bool isUnivalTree(struct TreeNode* root)
{
    if(root == NULL)
    {
        return true;
    }
    if(root->left && root->val != root->left->val)
    {
        return false;
    }
    if(root->right && root->val != root->right->val)
    {
        return false;
    }
    
    return isUnivalTree(root->left) && isUnivalTree(root->right);
}

递归的思想就是将大问题进行规约,最后将其变为一个很简单的问题。这里我们可以看到,前面的三个if语句就是对树中的单个根结点或者某个根结点的左右子树(不超过三个结点)进行判断,然后对下面的子树进行递归。若根为空,这时候并不影响是否为单值二叉树,返回true;然后分别判断根和左右两个孩子结点的值是否相等,只要不符合条件,直接返回false,若本层没有不符合条件的结点,则分别向其左子树和右子树进行递归,直至条件不符合或树被遍历结束。

翻转二叉树

要求比较简单,直接上代码:

struct TreeNode* invertTree(struct TreeNode* root) 
{
    if (root == NULL) 
    {
        return NULL;
    }
    struct TreeNode* left = invertTree(root->left);
    struct TreeNode* right = invertTree(root->right);
    root->left = right;
    root->right = left;
    return root;
}

向下递归和每层递归中执行操作的顺序主要取决于你是想自上而下操作二叉树还是自下而上操作二叉树,这里我们选择自下而上操作二叉树,因此在判定根不为空之后直接向下进行递归,先处理根的左右子树,直到递归到叶子结点,然后返回其双亲结点进行翻转,然后再一层层的返回,最后将整棵树进行翻转。

相同的树

bool isSameTree(struct TreeNode* p, struct TreeNode* q){
    if(p == NULL && q == NULL)
    {
        return true;
    }
    if((p == NULL && q != NULL) || (q == NULL && p != NULL))
    {
        return false;
    }
    if(p && q && p->val != q->val)
    {
        return false;
    }

    return isSameTree(p->left,q->left) && isSameTree(p->right,q->right);
}

读到这里我们会发现,对于这种检查类的功能我们都是自上而下的,比如求第K层的结点数,求树高,判断树是否为单值二叉树等等,这些方法要么就是从最上面一层一层的将问题简化,递归至目标层数之后开始返回,要么就是判断二叉树是否符合某些条件,一旦不符合,就会直接向上返回至某个判断符合条件后结束函数。在这个函数中的三个if中,首先空树无可厚非肯定是相同的,其次两棵树的根结点的左右子树如果不对应(也就是一个有左另一个无左或者反过来),那就可以直接判断两者不想等,最后如果两个根结点都存在,但是他们本身的值就不相同,那么也可以直接判断二者不相等,如果这些条件都跳过了,说明这一层通过了检查,要继续向左右子树递归进行验证。

对称二叉树

struct TreeNode* invertTree(struct TreeNode* root) 
{
    if (root == NULL) 
    {
        return NULL;
    }
    struct TreeNode* left = invertTree(root->left);
    struct TreeNode* right = invertTree(root->right);
    root->left = right;
    root->right = left;
    return root;
}


bool isSameTree(struct TreeNode* p, struct TreeNode* q){
    if(p == NULL && q == NULL)
    {
        return true;
    }
    if((p == NULL && q != NULL) || (q == NULL && p != NULL))
    {
        return false;
    }
    if(p && q && p->val != q->val)
    {
        return false;
    }


    return isSameTree(p->left,q->left) && isSameTree(p->right,q->right);
}


bool isSymmetric(struct TreeNode* root)
{
    if(root->left == NULL && root->right == NULL)
        return true;
    else if(isSameTree(invertTree(root->left),root->right))
        return true;
    else
        return false;
}

判断一个二叉树是否为对称二叉树借助我们前面完成的接口就能实现判断,因为对于一个对称的二叉树来说,对他的右子树进行翻转,然后判断左右子树是否相同就可以了,如果翻转后左右子树相同,那么他原来就是对称的二叉树,反之则不是。

另一棵树的子树

这里的思路也比较简单,因为我们前面已经实现了两棵树是否相同的函数,因此我们对要被判断的主树进行遍历,将每个结点都作为参数1传入判断相等函数,再将被判断的副树作为参数2传入判断相等函数,若再遍历过程中有true返回,那么说明主树中有副树存在,否则就不存在,实现代码如下:

bool isSameTree(struct TreeNode* p, struct TreeNode* q)
{
    if(p == NULL && q == NULL)
    {
        return true;
    }
    if((p == NULL && q != NULL) || (q == NULL && p != NULL))
    {
        return false;
    }
    if(p && q && p->val != q->val)
    {
        return false;
    }

    return isSameTree(p->left,q->left) && isSameTree(p->right,q->right);
}

bool isSubtree(struct TreeNode* root, struct TreeNode* subRoot)
{
    if(root == NULL)
    {
        return false;
    }
    if(isSameTree(root, subRoot))
    {
        return true;
    }

    return isSubtree(root->left, subRoot) || isSubtree(root->right, subRoot);
}

此外,如果主树为空,那么根本找不到被他包含的副树,自然也就返回false了。

判断是否为平衡二叉树

int max(int a, int b)
{
    return a > b ? a : b;
}
int TreeHeight(struct TreeNode* root)
{
	if (root == NULL)
	{
		return 0;
	}

	return 1 + max(TreeHeight(root->left), TreeHeight(root->right));
}

bool isBalanced(struct TreeNode* root)
{
    if(root == NULL)
        return true; 
    else if(abs(TreeHeight(root->left) - TreeHeight(root->right)) > 1)
        return false;
    else
        return isBalanced(root->left) && isBalanced(root->right);
}

我们可以看到判断树是否为平衡二叉树也是一个自上而下的一种递归,但凡有某个结点的判断不符合条件了,就可以直接判断它不是一个平衡二叉树。此外,这里我们还要借助前面求树高的接口,这样在根结点处判断左右子树树高差的绝对值是否大于一就可以直接进行排除了。

结束语

以上就是关于二叉树及其接口实现(递归实现) & 部分简单的二叉树OJ题,至此二叉树的基础知识部分也就告一段落了,如文章有不足或遗漏之处还请大家指正,笔者感激不尽;同时也欢迎大家在评论区进行讨论,一起学习,共同进步!

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
(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、二叉树的二叉链表存储。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值