【数据结构】二叉树接口实现指南:递归方法的高效运用 (附经典算法OJ)

文章目录

1、前置说明

1、定义二叉树结点结构

2、创建新节点

3、手动创建二叉树

2、二叉树的遍历

1、前序,中序和后序遍历

1.1、二叉树前序遍历

1.2、二叉树中序遍历

1.3、二叉树后序遍历

2、二叉树层序遍历

3、二叉树的基础操作

1、二叉树节点总数

2、叶子节点个数

3、二叉树高度

4、二叉树第k层节点数

5、查找目标节点

6、通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树

7、销毁

4、经典算法OJ

1、单值二叉树

2、检查两颗树是否相同

3、另一棵树的子树


1、前置说明

前面的文章我们根据完全二叉树的结构特点,用顺序存储的方式实现了堆相关的接口以及堆排序。下面我们用链式结构来实现普通二叉树的基本操作,但在实现之前,我们需要先创建一棵二叉树,然后才能学习相关的操作,由于我们对二叉树的结构掌握还不够深入,我们就先手动地创建一棵二叉树,等我们对二叉树的结构有了进一步的理解,再反过来实现二叉树真正的创建。

二叉树是: 1、空树

**2、非空:**根结点,根结点的左子树、根结点的右子树组成的。

从概念中可以看出,二叉树定义是递归式的,因此后序基本操作中基本都是按照该概念实现的。

1、定义二叉树结点结构

用链式结构实现二叉树,节点结构就类似于链表的节点。同时,我们还需要两个结构体类型的指针来连接孩子节点。

复制代码
//定义二叉树存储的数据类型
typedef int BTDateType;
//定义节点结构
typedef struct BinaryTree
{
	BTDateType val;
	struct BinaryTree* left;
	struct BinaryTree* right;
}BTNode;

2、创建新节点

复制代码
BTNode* BuyBTNode(BTDateType x)
{
	BTNode* pst = (BTNode*)malloc(sizeof(BTNode));
	if (pst == NULL)
	{
		perror("malloc");
		exit(1);
	}
	pst->val = x;
	pst->left = NULL;
	pst->right = NULL;
}

3、手动创建二叉树

复制代码
BTNode* BinaryTreeCreat()
{
	//创建二叉树的节点
	BTNode* s1 = BuyBTNode(1);
	BTNode* s2 = BuyBTNode(2);
	BTNode* s3 = BuyBTNode(3);
	BTNode* s4 = BuyBTNode(4);
	BTNode* s5 = BuyBTNode(5);
	BTNode* s6 = BuyBTNode(6);

    //连接节点成为二叉树
	s1->left = s2;
	s2->left = s3;
	s2->right = s7;
	s1->right = s4;
	s4->left = s5;
	s4->right = s6;
	return s1;
}

2、二叉树的遍历

1、前序,中序和后序遍历

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

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

  1. 前序遍历(先序遍历)------访问根结点的操作发生在遍历其左右子树之前,根->左子树->右子树。

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

  3. 后序遍历------访问根结点的操作发生在遍历其左右子树之后,左子树->右子树->根。

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

复制代码
// 二叉树前序遍历 
void prevOrder(BTNode* root);
// 二叉树中序遍历
void midOrder(BTNode* root);
// 二叉树后序遍历
void afterOrder(BTNode* root);

1.1、二叉树前序遍历

前序遍历递归图解:

复制代码
//前序遍历:根  左子树  右子树
void prevOrder(BTNode* root)
{
    //打印空节点
	if (root == NULL)
	{
		printf("N ");
		return;
	}
    //打印根节点的值
	printf("%d ", root->val);
    //递归遍历左子树
	prevOrder(root->left);
    //递归遍历右子树
	prevOrder(root->right);
}

输出结果:

复制代码
1 2 3 N N N 4 5 N N 6 N N

为了能够更加直观的理解递归调用的过程,我们画图来感受一下,后面用递归实现接口的过程中,我们也可以用类似的方法来画图,帮助我们理解!!!

1.2、二叉树中序遍历

复制代码
//中序遍历:左子树  根  右子树
void midOreder(BTNode* root)
{   
    //打印空节点
	if (root == NULL)
	{
		printf("N ");
		return;
	}
    //递归遍历左子树
	midOreder(root->left);
    //打印根节点的值
	printf("%d ", root->val);
    //递归遍历右子树
	midOreder(root->right);
}

输出结果

复制代码
N 3 N 2 N 1 N 5 N 4 N 6 N

1.3、二叉树后序遍历

复制代码
//后序遍历:左子树  右子树  根
void afterOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("N ");
		return;
	}
    //递归遍历左子树
	afterOrder(root->left);
    //递归遍历右子树
	afterOrder(root->right);
    //打印根节点的值
	printf("%d ", root->val);
}

输出结果

复制代码
N N 3 N 2 N N 5 N N 6 4 1

2、二叉树层序遍历

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

3、二叉树的基础操作

1、二叉树节点总数

思路1:遍历二叉树,创建一个变量来记录节点个数,但是,每次函数调用都是在栈区开辟空间,数据在函数调用结束后就会销毁,所以要创建全局变量,或者用static修饰的全局变量(但是在调用之前需要初始化为0)。当遇到某个节点的左孩子或者右孩子为空时,递归结束,开始返回。

复制代码
int size = 0;//全局变量
//static int size = 0;

int TreeSize(BTNode* root)
{
    //递归结束,遇到空树,返回
	if (root == NULL)
		return 0;
	else
		++size;
    //遍历二叉树
	TreeSize(root->left);
	TreeSize(root->right);

	return size;
}

还有一种简单的写法,用三目操作符来判断,但是不容易理解

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

递归调用图解:

思路2:将size作为实参,但是为了让形参改变的同时改变实参的值,我们需要传实参的地址。

复制代码
//将size作为参数
void TreeSize(BTNode* root, int* psize)
{
	if (root == NULL)
		return 0;
	else
		++(*psize);

	TreeSize(root->left, psize);
	TreeSize(root->right, psize);
}

2、叶子节点个数

思路:遍历二叉树,将二叉树不断分为左子树与右子树,找左孩子与右孩子都不存在的节点,即叶子节点,然后将所有左子树与右子树的叶子节点加起来。

复制代码
//计算叶子节点个数
int TreeLeafCount(BTNode* root)
{
    //遇到空节点,递归结束条件
	if (root == NULL)
	{
		return 0;
	}
    //当左子树与右子树同时为空时为叶子节点
	if ((root->left == NULL) && (root->right == NULL))
	{
		return 1;
	}
    //叶子节点数等于左子树+右子树
	return TreeLeafCount(root->left) + TreeLeafCount(root->right);
}

3、二叉树高度

思路:递归遍历二叉树的左子树与右子树的同时,记下左子树与右子树的高度,然后比较左子树与右子树,二叉树的高度就是较高的一个加上根节点的高度。

复制代码
//计算二叉树高度
int TreeHeight(BTNode* root)
{
    //递归结束条件
	if (root == NULL)
	{
		return 0;
	}

	//记下返回值,防止造成时间效率低下
    //左子树高度
	int leftheight = TreeHeight(root->left);
    //右子树高度
	int rightheight = TreeHeight(root->right);
	//高度为较大的高度+根节点的高度
	if (leftheight > rightheight)
		return leftheight + 1;
	else
		return rightheight + 1;
	//三目操作符
	//return leftheight > rightheight ? leftheight + 1 : rightheight + 1;
}

4、二叉树第k层节点数

思路:计算二叉树第k层节点数,递归遍历二叉树的左子树与右子树,计算第k-1层的节点的不为空的左孩子和右孩子的个数,返回左子树与右子树k-1层的节点的不为空的左孩子和右孩子的个数的和。

复制代码
//计算第k层有多少个节点
int TreeNodecount_k(BTNode* root, int k)
{
    //递归结束条件
	if (root == NULL)
		return 0;
    //处理特殊情况
	if (k == 1)
		return 1;

	//注意不能使用k--或者--k,这样递归一次,后一次调用相当于k-1-1
	return TreeNodecount_k(root->left, k - 1) + TreeNodecount_k(root->right, k - 1);
}

5、查找目标节点

递归遍历左子树与右子树

复制代码
//查找目标节点
BTNode* TreeFind(BTNode* root, BTDateType x)
{
	if (root == NULL)
		return NULL;
	if (root->val == x)
	{
		return root;
	}
	//记下返回值
	BTNode* ret1 = TreeFind(root->left, x);
	//找到了,就返回
	if (ret1)
		return ret1;
	BTNode* ret2 = TreeFind(root->right, x);
	if (ret2)
		return ret2;
	return NULL;
}

6、通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树

复制代码
//定义二叉树存储的数据类型
typedef char BTDateType;

ABC##DE#G##F### 其中"#"表示的是空格,空格字符代表空树。

复制代码
//创建一颗二叉树
BTNode* CreatTree(char* a, int* pi)
{
    //递归结束条件,同时处理空树
    if (a[*pi] == '#')
    {
        (*pi)++;
        return NULL;
    }
    //不是'#'就为节点开辟空间
    BTNode* newnode = (BTNode*)malloc(sizeof(BTNode));
    
    newnode->val = a[(*pi)++];
    //递归遍历数组
    newnode->left = CreatTree(a, pi);
    newnode->right = CreatTree(a, pi);
    return newnode;
}

int main()
{
    char arr[] = { "ABD##E#H##CF##G##" };
    int i = 0;
    BTNode* root = CreatTree(arr, &i);
    return 0;
}

7、销毁

思路:递归遍历二叉树,用后序遍历,防止将根释放而找不到左子树与右子树。

复制代码
//销毁二叉树
//后序遍历:左子树->右子树->根,防止将根释放而找不到左子树与右子树
void TreeDestry(BTNode* root)
{
	if (root == NULL)
	{
		return;
	}
    //递归遍历左子树
	TreeDestry(root->left);
    //递归遍历右子树
	TreeDestry(root->right);
	free(root);
}

4、完整源码

复制代码
#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
#include<math.h>
#include"Queue.h"

//定义二叉树的节点
typedef int BTDateType;
typedef struct BinaryTree
{
	BTDateType val;
	struct BinaryTree* left;
	struct BinaryTree* right;
}BTNode;

BTNode* BuyBTNode(BTDateType x)
{
	BTNode* pst = (BTNode*)malloc(sizeof(BTNode));
	if (pst == NULL)
	{
		perror("malloc");
		exit(1);
	}
	pst->val = x;
	pst->left = NULL;
	pst->right = NULL;
}
BTNode* BinaryTreeCreat()
{
	//创建一棵树
	BTNode* s1 = BuyBTNode(1);
	BTNode* s2 = BuyBTNode(2);
	BTNode* s3 = BuyBTNode(3);
	BTNode* s4 = BuyBTNode(4);
	BTNode* s5 = BuyBTNode(5);
	BTNode* s6 = BuyBTNode(6);
	BTNode* s7 = BuyBTNode(7);

	s1->left = s2;
	s2->left = s3;
	s2->right = s7;
	s1->right = s4;
	s4->left = s5;
	s4->right = s6;
	//s5->right = s7;
	return s1;
}
//前序遍历:根  左子树  右子树
void prevOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("N ");
		return;
	}
	printf("%d ", root->val);
	prevOrder(root->left);
	prevOrder(root->right);
}

//中序遍历:左子树  根  右子树
void midOreder(BTNode* root)
{
	if (root == NULL)
	{
		printf("N ");
		return;
	}
	midOreder(root->left);
	printf("%d ", root->val);
	midOreder(root->right);
}
//后序遍历:左子树  右子树  根
void afterOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("N ");
		return;
	}
	afterOrder(root->left);
	afterOrder(root->right);
	printf("%d ", root->val);
}

//计算节点总数
//每次函数调用都是在栈区开辟空间,数据在函数调用结束后就会销毁,所以要创建全局变量,或者用static修饰的全局变量(但是在调用之前需要初始化为0)

//错误示范:static修饰的变量在函数作用域内,又static修饰的变量不会销毁,所以在main函数中反复调用会导致结果不断增加
//int TreeNodeCount(BTNode* root)
//{
//	static int size = 0;
//	if (root == NULL)
//	{
//		return 0;
//	}
//	size++;
//	TreeNodeCount(root->left);
//	TreeNodeCount(root->right);
//	return size;
//}

int size = 0;//全局变量
//static int size = 0;

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

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

	return size;
}

////将size作为参数
//void TreeSize(BTNode* root, int* psize)
//{
//	if (root == NULL)
//		return 0;
//	else
//		++(*psize);
//
//	TreeSize(root->left, psize);
//	TreeSize(root->right, psize);
//}

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

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


//计算二叉树高度
int TreeHeight(BTNode* root)
{
	if (root == NULL)
	{
		return 0;
	}

	//记下返回值,防止造成时间效率低下
	int leftheight = TreeHeight(root->left);
	int rightheight = TreeHeight(root->right);
	//高度为较大的高度+根节点的高度
	if (leftheight > rightheight)
		return leftheight + 1;
	else
		return rightheight + 1;
	//三目操作符
	//return leftheight > rightheight ? leftheight + 1 : rightheight + 1;
}

////使用fmax()函数求较大的值
//int TreeHeight(BTNode* root)
//{
//	if (root == NULL)
//		return 0;
//
//	return fmax(TreeHeight(root->left), TreeHeight(root->right)) + 1;
//}

//计算第k层有多少个节点
int TreeNodecount_k(BTNode* root, int k)
{
	if (root == NULL)
		return 0;
	if (k == 1)
		return 1;

	//注意不能使用k--或者--k,这样递归一次,后一次调用相当于k-1-1
	return TreeNodecount_k(root->left, k - 1) + TreeNodecount_k(root->right, k - 1);
}

//查找目标节点
BTNode* TreeFind(BTNode* root, BTDateType x)
{
	if (root == NULL)
		return NULL;
	if (root->val == x)
	{
		return root;
	}
	//记下返回值
	BTNode* ret1 = TreeFind(root->left, x);
	//找到了,就返回
	if (ret1)
		return ret1;
	BTNode* ret2 = TreeFind(root->right, x);
	if (ret2)
		return ret2;
	return NULL;
}

//销毁二叉树
//后序遍历:左子树->右子树->根,防止将根释放而找不到左子树与右子树
void TreeDestry(BTNode* root)
{
	if (root == NULL)
	{
		return;
	}
	TreeDestry(root->left);
	TreeDestry(root->right);
	free(root);
}

//测试
int main()
{
	BTNode* root = BinaryTreeCreat();
	//前序遍历
	prevOrder(root);
	printf("\n");

	//中序遍历
	midOreder(root);
	printf("\n");
	//后序遍历
	afterOrder(root);
	printf("\n");

	//计算二叉树节点总个数

	//size = 0;
	//printf("TreeSize:%d\n", TreeSize(root));
	//size = 0;
	//printf("TreeSize:%d\n", TreeSize(root));
	//size = 0;
	//printf("TreeSize:%d\n", TreeSize(root));

	//int size = 0;
	//TreeSize(root, &size);
	//printf("TreeSize:%d\n", size);

	//size = 0;
	//TreeSize(root, &size);
	//printf("TreeSize:%d\n", size);

	//printf("%d ", TreeSize(root));
	//printf("%d ", TreeSize(root));

	//求叶子节点的个数
	//printf("%d\n", TreeLeafCount(root));

	////求二叉树的高度
	//printf("%d\n", TreeHeight(root));

	////求二叉树第k层有多少个节点
	//printf("%d\n",TreeNodecount_k(root, 3));

	////查找目标节点
	//BTNode* ret = TreeFind(root, 5);
	//printf("%d\n", ret->val);

	return 0;
}

5、经典算法OJ

1、单值二叉树

965. 单值二叉树 - 力扣(LeetCode)

题目描述:

示例:

复制代码
bool _isUnivalTree(struct TreeNode* root,int x) 
{  
   //处理空树情况,同时为递归结束条件
   if(root==NULL)
   {
    return true;
   }

   //排除空树,且只要不等于根结点的值,就返回false
   if(root->left&&root->left->val!=x)
   {
    return false;
   }
   if(root->right&&root->right->val!=x)
   {
    return false;
   }

   //递归遍历左子树与右子树
   return _isUnivalTree(root->left, x)&&_isUnivalTree(root->right,x);
}

bool isUnivalTree(struct TreeNode* root) 
{
   return _isUnivalTree(root,root->val);
}

2、检查两颗树是否相同

100. 相同的树 - 力扣(LeetCode)

题目描述:

示例:

首先判断如果都是空树时返回真;一个为空一个不为空时返回false;值不相等,返回false。然后递归遍历左子树和右子树,左子树与左子树比较,右子树与右子树比较,当递归到都是空时返回true(与刚开始的判空对应),递归到一个为空一个不为空时返回false(与刚开始的判断一个为空一个不为空对应),最后左子树与左子树相等且右子树与右子树相等则返回true;反之,返回false。

复制代码
bool isSameTree(struct TreeNode* p, struct TreeNode* q) 
{
    //都是空树或递归到都是空时返回真
    if(p==NULL&&q==NULL)
    {
        return true;
    }
    //一个为空一个不为空或递归到一个为空一个不为空时返回false
    if(p==NULL||q==NULL)
    {
        return false;
    }
    //值不相等,返回false
    if(p->val!=q->val)
    {
        return false;
    }
    //递归遍历左子树和右子树,左子树与左子树比较,右子树与右子树比较
    return isSameTree(p->left,q->left)&&isSameTree(p->right,q->right);
}

3、另一棵树的子树

572. 另一棵树的子树 - 力扣(LeetCode)

题目描述:

不断递归遍历一棵树的左子树和右子树,然后判断这棵树的左子树与右子树与另一棵树是否相等。可以借助前面实现的判断两棵树是否相等的函数。

复制代码
bool isSameTree(struct TreeNode*p,struct TreeNode*q)
{
    //都为空
    if(p==NULL&&q==NULL)
    {
        return true;
    }
    //一个为空,一个不为空
    if(p==NULL||q==NULL)
    {
        return false;
    }
    if(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((root->val==subRoot->val) && (isSameTree(root,subRoot)))
    { 
        return true;
    }
    //将root拆分为左子树和右子树与subRoot比较
    return isSubtree(root->left,subRoot)||isSubtree(root->right,subRoot);
}
相关推荐
DASXSDW9 分钟前
NET性能优化-使用RecyclableBuffer取代RecyclableMemoryStream
java·算法·性能优化
kfepiza15 分钟前
CAS (Compare and Swap) 笔记251007
java·算法
墨染点香44 分钟前
LeetCode 刷题【103. 二叉树的锯齿形层序遍历、104. 二叉树的最大深度、105. 从前序与中序遍历序列构造二叉树】
算法·leetcode·职场和发展
啊我不会诶1 小时前
23ICPC澳门站补题
算法·深度优先·图论
Brookty2 小时前
【算法】二分查找(一)朴素二分
java·学习·算法·leetcode·二分查找
黑色的山岗在沉睡3 小时前
LeetCode 2761. 和等于目标值的质数对
算法·leetcode·职场和发展
bawangtianzun3 小时前
重链剖分 学习记录
数据结构·c++·学习·算法
ChoSeitaku7 小时前
NO.14数据结构红黑树|树高|转化4阶B树|插入操作|删除操作
数据结构·b树
T1an-17 小时前
力扣70.爬楼梯
算法·leetcode·职场和发展
T1an-17 小时前
力扣169.多数元素
数据结构·算法·leetcode