二叉树的各种操作

一、二叉树的遍历

前序遍历:访问根节点的操作发生在遍历左右子树之前

中序遍历:访问根节点的操作发生在遍历其左右子树中间。

后序遍历:访问根节点的操作发生在遍历其左右子树之后。

这三种遍历方式都属于递归遍历。还有一种遍历方式叫做层序遍历,属于非递归遍历。

BFS是广度优先的遍历,DFS是深度优先的遍历。

接下来我们需要讲这些遍历方式用代码实现出来。为了方便,我们先手搓一棵如图的二叉树出来。

cpp 复制代码
//自己搓一棵树出来
TreeNode* BuyNode(BTDataType x)
{
	TreeNode* newnode = (TreeNode*)malloc(sizeof(BTDataType));
	if (newnode == NULL)
	{
		perror("malloc");
		return NULL;
	}
	newnode->data = x;
	newnode->left = NULL;
	newnode->right = NULL;
	return newnode;
}

TreeNode* CreateBinaryTree()
{
	TreeNode* node1 = BuyNode(1);
	TreeNode* node2 = BuyNode(2);
	TreeNode* node3 = BuyNode(3);
	TreeNode* node4 = BuyNode(4);
	TreeNode* node5 = BuyNode(5);
	TreeNode* node6 = BuyNode(6);

	node1->left = node2;
	node1->right = node4;
	node2->left = node3;
	node4->left = node5;
	node4->right = node6;

	return node1;
}

然后,我们实现前序遍历的代码:

cpp 复制代码
void PrevOrder(TreeNode* root)//前序遍历
{
	if (root == NULL)
	{
		printf("N ");//空树不可再分解,打印一个N
		return;
	}

	printf("%d ",root->data);//先访问根节点
	PrevOrder(root->left);
	PrevOrder(root->right);//递归方式访问左右子树

}

int main()
{
	TreeNode* root = CreateBinaryTree();
	PrevOrder(root);
	printf("\n");

	return 0;
}

运行结果如图:

中序遍历的代码实现也差不多,只不过是调换了顺序:

cpp 复制代码
void InOrder(TreeNode* root)
{
	if (root == NULL)
	{
		printf("N ");
		return;
	}
	
	InOrder(root->left);
	printf("%d ", root->data);
	InOrder(root->right);
}

int main()
{
	TreeNode* root = CreateBinaryTree();
	InOrder(root);
	printf("\n");

	return 0;
}

后序遍历也一样。

做一道前序遍历的算法题:

题干是这样的:

cpp 复制代码
int* preorderTraversal(struct TreeNode* root, int* returnSize)

这里传递的参数returnSize是用来记录返回的数组的大小的。但凡要返回数组的函数,都应该有这样一个参数。

我们先计算一下二叉树的节点个数作为返回的returnSize。求节点个数的方法就在下面,这篇文章的第二部分↓

cpp 复制代码
int TreeSize(struct TreeNode* root)
{
    if(root == NULL)
        return 0;
    return TreeSize(root->left) + TreeSize(root->right) + 1;
}

现在我们就知道要开辟一个多大的数组来存放遍历的结果了。

然后我们再写一个函数用于前序遍历并且把遍历结果放进数组中。此处我们需要传递一个指针来记录当前的下标,否则整型变量的作用域只在当前的函数中,没有办法一直++。

然后返回这个数组就好了。

cpp 复制代码
void preOrder(struct TreeNode* root, int* a,int* pi)
{
    if(root == NULL)
        return;
    a[(*pi)++] = root->val;
    preOrder(root->left,a,pi);
    preOrder(root->right,a,pi);
}

int* preorderTraversal(struct TreeNode* root, int* returnSize) {
    *returnSize = TreeSize(root);
    //returnSize 是为了记录数组的大小
    int* a = (int*)malloc(sizeof(int)*(*returnSize));
    int i = 0;
    preOrder(root, a, &i);
    return a;
}

二、求二叉树节点的个数

最先想到的肯定是遍历的时候用一个size变量记录节点个数,但是由于是递归调用,每次调用的时候size都会归零。

我们考虑用静态变量来记录,但是这就产生了另一个问题:这个函数只能用一次,否则第二次用的时候就会从上一次算完的地方累加。

那我们再考虑用外部变量来记录,传这个外部变量的指针,写出来的代码是这样的:

cpp 复制代码
void TreeSize(TreeNode* root, int* psize)
{
	if (root == NULL)
		return 0;
	else
		++(*psize);
	TreeSize(root->left, psize);
	TreeSize(root->right, psize);
}

int main()
{
	TreeSize(root, &size);
	printf("Treesize:%d ", size);
	return 0;
}

但是这样还是麻烦。更好的思路是分治递归:

如果根节点为空,就是零个节点(最小子问题)

如果根节点不为空,那么可以分为左子树节点数+右子树节点数+1(1表示这个根节点自己)。

代码实现非常简单,只需要一行:

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

三、求二叉树叶子节点的个数

首先想到的是这样写:(注意以下为错误示范)

如果一个节点左右子树都为空,那么这个节点就是叶子节点。某一个节点下的所有叶子节点等于它的左子树中的所有叶子节点和右子树的所有叶子节点的数量之和。

cpp 复制代码
int TreeLeafSize(TreeNode* root)
{
	if (root->left == NULL && root->right == NULL)
	{
		return 1;
	}
	else
	{
		return (TreeLeafSize(root->left) + TreeLeafSize(root->right));
	}
}

这样写有问题,因为无法处理空树或者某节点子树为空的情况。

正确的写法要加上对于空树的处理:

cpp 复制代码
int TreeLeafSize(TreeNode* root)
{
	if (root == NULL)
	{
		return 0;
	}

	if (root->left == NULL && root->right == NULL)
	{
		return 1;
	}
	else
	{
		return (TreeLeafSize(root->left) + TreeLeafSize(root->right));
	}
}

四、求二叉树的高度

依然要用递归的思路。一棵二叉树的高度等于它的左子树和右子树中高的那个子树的高度再加一。

cpp 复制代码
int TreeHeight(TreeNode* root)
{
	if (root == NULL)
		return 0;
	int heightLeft = TreeHeight(root->left);
	int heightRight = TreeHeight(root->right);
	int higher = heightLeft;
	if (heightLeft < heightRight)
		higher = heightRight;
	return higher + 1;
}

五、单值二叉树判断

仍然用递归的思路进行判断。

首先,边界终止条件是遇到空节点直接返回true。

然后判断左右孩子是否冲突,如果左/右孩子存在且与根节点的值不同,则返回false。

最后,递归遍历左右子树,左右子树都返回true的时候返回true。第二种思路是遍历二叉树,把所有节点的值和固定值进行比较。

六、求二叉树第k层的节点个数

从根节点开始的第k层,相当于从第二层开始的第(k-1)层,相当于从第三层开始的第(k-

2)层......向下递归遍历,遇到空节点返回0,k=1时返回1。

cpp 复制代码
int TreeLevelKSize(TreeNode* root, int k)
{
	if (root == NULL)
		return 0;
	if (k == 1)
		return 1;
	//子问题
	return TreeLevelKSize(root->left, k - 1) + TreeLevelKSize(root->right, k - 1);
}

七、查找二叉树值为x的节点

仍然递归遍历。找到了就把这个节点的指针返回给上一层,没找到就把空指针返回给上一层。

cpp 复制代码
TreeNode* TreeFind(TreeNode* root, BTDataType x)
{
	if (root == NULL)
		return NULL;
	if (root->data == x)
		return root;
	TreeNode* left = TreeFind(root->left, x);
	if (left)
		return left;
	return TreeFind(root->right, x);
}

八、判断两棵树是否相同

同时遍历两棵树来比较对应位置的节点:都是空节点返回true,一个为空一个不为空返回false,两者不为空但是值不相等返回false。

然后就递归遍历两棵树即可。

cpp 复制代码
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);
}

判断对称二叉树的逻辑和半段二叉树相同的逻辑几乎完全一样,其实就是把根节点的左右子树看成两棵树,比较他们是否对称。

cpp 复制代码
bool _isSymmetric(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 _isSymmetric(p->left,q->right) && _isSymmetric(p->right,q->left);
}

bool isSymmetric(struct TreeNode* root) {
    return _isSymmetric(root->left,root->right);
}

九、另一棵树的子树

思路是这样的:我们已经写过判断两棵树是否相等的算法,那么我们只需找出root的所有子树,和subroot比较一下是否相等。

递归的终止条件是遇到空节点(返回false)或者找到了对应的子树(返回true),然后递归遍历所有的节点就可以了。判断两棵树是否相等的代码直接复制粘贴。

cpp 复制代码
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(isSameTree(root,subRoot))
        return true;
    return(isSubtree(root->left, subRoot) || isSubtree(root->right,subRoot));
}

十、前序构建一棵树并按中序输出

前序构建二叉树时,依旧传一个指针来记录当前的数组下标。按照前序方式遍历即可。

中序遍历的代码可以直接复制粘贴,稍微修改一下。

cpp 复制代码
typedef struct BinTreeNode
{
    struct BinTreeNode* left;
    struct BinTreeNode* right;
    char val;
}TreeNode;


TreeNode* CreateTree(char* a,int* pi)
{
    if(a[*pi] == '#')
    {
        (*pi)++;
        return NULL;
    }


    TreeNode* root = (TreeNode*)malloc(sizeof(TreeNode));
    root->val = a[(*pi)++];
    root->left = CreateTree(a,pi);
    root->right = CreateTree(a,pi);

    return root;
}

void InOrder(TreeNode* root)
{
    if(root == NULL)
    {
        return;
    }
   
   InOrder(root->left);
   printf("%c ",root->val);
   InOrder(root->right);
}

int main() {
    char a[100];
    scanf("%s",a);
    int i = 0;
    TreeNode* root = CreateTree(a,&i);
    InOrder(root);
    return 0;
}

十一、二叉树的销毁

要用后序遍历,因为如果先销毁根节点,就找不到左右孩子了。

cpp 复制代码
void TreeDestroy(TreeNode* root)
{
	if (root == NULL)
		return;
	TreeDestroy(root->left);
	TreeDestroy(root->right);
	free(root);
}

十二、层序遍历

层序遍历是广度优先遍历。

此处要用到之前学过的队列这种数据结构。根节点出队列,就带自己的两个子节点进入队列。这样恰好可以让所有节点按照层序出队列。

队列的实现在这篇文章里:队列的概念、C语言实现及应用-CSDN博客

cpp 复制代码
void TreeLevelOrder(TreeNode* root)
{
	Queue q;
	QueueInit(&q);
	if (root)
	{
		QueuePush(&q, root);
	}
	while (!QueueEmpty(&q))
	{
		TreeNode* 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);
}

此处可能会有疑问:为什么QueuePop之后还可以对front解引用?因为队列里存的是节点的地址而不是节点本身,pop之后节点依然存在。

十三、判断是否完全二叉树

这里要用层序遍历的思路。按照层序来遍历二叉树,空节点也进队列。当第一个空节点出队列时,如果队列中还有非空的节点,说明不是完全二叉树。如果没此时队列中没有非空节点了,那么一定是完全二叉树。

会不会出现这种情况:空节点出队列时,队列中全是空节点,但还有非空节点没有进队列?不会,因为后面的非空节点一定是前面的非空节点的孩子。

cpp 复制代码
bool isCompleteTree(TreeNode* root)
{
	Queue q;
	QueueInit(&q);
	QueuePush(&q, root);
	while (!QueueEmpty(&q))
	{
		TreeNode* front = QueueFront(&q);
		QueuePop(&q);
		//第一个空节点出队列,开始判断
		if (front == NULL)
		{
			break;
		}
		QueuePush(&q, front->left);
		QueuePush(&q, front->right);
	}

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

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

十四、二叉树的性质

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

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

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

第三条性质这样理解:假设现在有这样一棵树,只有两个节点,n0为1,n2为0。

在此基础上开始增加节点。度为1的节点一定是由度为0的节点再连接一个节点变来的。所以,每增加一个度为1的节点,度为0的节点个数不变。然而,度为2的节点是由度为1的节点连接一个节点变来的。所以,增加一个度为2的节点,就一定会增加一个度为0的节点。n0和n2同步增长,保持n0=n2+1的关系。

4.若规定根节点的层数为1,具有n个结点的满二叉树的深度,h=log2 (n+1)。(ps:log2 (n+1)是log以2为底,n+1为对数)

另外,有一种题目会给一棵二叉树的前序和中序,要求推出树的结构。这种题这样做:前序确定根,中序分隔左子树和右子树。

End