二叉树详解(2)

2024-09-06 12:09:43 浏览数 (1)

4. 二叉树链式结构的实现

首先,我们先要了解一下二叉树的遍历顺序有哪些:

二叉树的遍历顺序二叉树的遍历顺序

通过了解二叉树的遍历顺序,我们不难看出要实现二叉树的遍历需要用到递归,而使用递归我们就要思考以下两点:

  1. 子问题
  2. 结束条件(最小子问题)

在这里,空树就是不可再分割的子问题。


我们先手搓一棵树,方便我们一会进行验证:

代码语言:javascript复制
#include <stdio.h>
#include <stdlib.h>

typedef struct BinTreeNode
{
	struct BinTreeNode* left;
	struct BinTreeNode* right;
	int val;
}BTNode;

BTNode* BuyBTNode(int val)
{
	BTNode* newnode = (BTNode*)malloc(sizeof(BTNode));

	if (NULL == newnode)
	{
		perror("malloc fail");
		return NULL;
	}

	newnode->val = val;
	newnode->left = NULL;
	newnode->right = NULL;

	return newnode;
}

//手搓一棵树
BTNode* CreateTree()
{
	BTNode* n1 = BuyBTNode(1);
	BTNode* n2 = BuyBTNode(2);
	BTNode* n3 = BuyBTNode(3);
	BTNode* n4 = BuyBTNode(4);
	BTNode* n5 = BuyBTNode(5);
	BTNode* n6 = BuyBTNode(6);

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

	return n1;
}

int main()
{
	BTNode* root = CreateTree();
	
	return 0;
}
  1. 前序
代码语言:javascript复制
void PreOrder(BTNode* root)
{
	if (NULL == root)
	{
		printf("N ");
		return;
	}

	printf("%d ", root->val);
	PreOrder(root->left);
	PreOrder(root->right);
}
前序的递归展开图前序的递归展开图
  1. 中序
代码语言:javascript复制
void InOrder(BTNode* root)
{
	if (NULL == root)
	{
		printf("N ");
		return;
	}

	InOrder(root->left);
	printf("%d ", root->val);
	InOrder(root->right);
}
  1. 后序
代码语言:javascript复制
void PostOrder(BTNode* root)
{
	if (NULL == root)
	{
		printf("N ");
		return;
	}

	PostOrder(root->left);
	PostOrder(root->right);
	printf("%d ", root->val);
}
  1. 节点个数 最容易想到的就是用计数的方法来实现:
代码语言:javascript复制
int TreeSize(BTNode* root)
{
	static int size = 0;

	if (NULL == root)
	{
		return 0;
	}
	else
	{
		  size;
	}

	TreeSize(root->left);
	TreeSize(root->right);
	return size;
}

但是这样写会出现问题:

代码语言:javascript复制
int main()
{
	BTNode* root = CreateTree();

	printf("%dn", TreeSize(root));
	printf("%dn", TreeSize(root));
	printf("%dn", TreeSize(root));

	return 0;
}

多次调用时它的个数就会出错,这是因为第一次调用完之后第二次再调用时size的值并不为0,但是size又是在函数里面的,在函数外又不能把它变为0,所以我们可以这样修改:

代码语言:javascript复制
//static int size = 0;
int size = 0;

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

	TreeSize(root->left);
	TreeSize(root->right);
	return size;
}

int main()
{
	BTNode* root = CreateTree();

	printf("%dn", TreeSize(root));
	size = 0;
	printf("%dn", TreeSize(root));
	size = 0;
	printf("%dn", TreeSize(root));

	return 0;
}

但是因为创建的是全局变量,这样写会有线程安全的风险,比如说这个函数给多个线程用,它们之间互相会影响(这里先了解一下,之后学了多线程就可以理解了)

如果一定要用计数的方式来实现,可以这样写:

代码语言:javascript复制
void TreeSize(BTNode* root, int* psize)
{
	if (NULL == root)
	{
		return;
	}
	else
	{
		  (*psize);
	}

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

那么还有没有其他更好的方法呢?

还是递归,递归本质上就是分而治之。

我们举一个形象的例子:

二叉树节点个数的举例二叉树节点个数的举例
代码语言:javascript复制
int TreeSize(BTNode* root)
{
	return NULL == root ? 0 : TreeSize(root->left)   TreeSize(root->right)   1;
}

int main()
{
	BTNode* root = CreateTree();
	
	printf("%dn", TreeSize(root));
	printf("%dn", TreeSize(root));
	printf("%dn", TreeSize(root));

	return 0;
}

这种写法可以看作是后序,因为先算出左子树的节点个数,再算出右子树的节点个数,最后加上就可以算出根所在的这棵树的节点个数。(代码中的 1 不管放在最前面,还是中间还是最后面都不会影响它是后序,因为我们必须要先算出左右子树的个数才能算出总的节点个数;如果把 1 放在中间就认为是中序,那么就变成了只要算出左子树的节点个数,再加上根就可以算出总的节点个数了,这显然是不对的;所以判断前中后序不能简单的看代码,而是要了解它的核心逻辑,它是如何达到这个结果的)

  1. 树的高度
代码语言:javascript复制
int TreeHeight(BTNode* root)
{
	if (NULL == root)
	{
		return 0;
	}

	int leftHeight = TreeHeight(root->left);
	int rightHeight = TreeHeight(root->right);

	return leftHeight > rightHeight ? leftHeight   1 : rightHeight   1;
}

如果我们不记录左右子树的高度,而是直接把递归写到return里,也是对的,但是它的时间复杂度会变得很大:

求树的高度的时间复杂度求树的高度的时间复杂度
  1. 第k层的节点个数
第k层的节点个数第k层的节点个数
代码语言:javascript复制
int TreeKLevel(BTNode* root, int k)
{
	assert(k > 0);

	if (NULL == root)
	{
		return 0;
	}

	if (1 == k)
	{
		return 1;
	}

	//不等于空,且k > 1说明第k层的节点在子树里面,转换成子问题求解
	return TreeKLevel(root->left, k - 1)   TreeKLevel(root->right, k - 1);
}
  1. 查找x所在的节点
查找x所在的节点查找x所在的节点

用递归写很容易会写出这样错误的代码:

代码语言:javascript复制
BTNode* TreeFind(BTNode* root, int x)
{
	if (NULL == root)
	{
		return NULL;
	}

	if (root->val == x)
	{
		return root;
	}

	TreeFind(root->left, x);
	TreeFind(root->right, x);
}

通过画递归展开图我们就可以很容易发现问题:有一些递归函数是没有返回值的。

那我们这样修改:

代码语言:javascript复制
BTNode* TreeFind(BTNode* root, int x)
{
	if (NULL == root)
	{
		return NULL;
	}

	if (root->val == x)
	{
		return root;
	}

	return TreeFind(root->left, x) || TreeFind(root->right, x);
}

这样写也是不对的,如果是实现查找x在不在二叉树中,那么可以用这种写法:

代码语言:javascript复制
bool TreeFind(BTNode* root, int x)
{
	if (NULL == root)
	{
		return false;
	}

	if (root->val == x)
	{
		return true;
	}

	return TreeFind(root->left, x) || TreeFind(root->right, x);
}

正确写法:

代码语言:javascript复制
BTNode* TreeFind(BTNode* root, int x)
{
	if (NULL == root)
	{
		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;
}

当然,也可以简化一下:

代码语言:javascript复制
BTNode* TreeFind(BTNode* root, int x)
{
	if (NULL == root)
	{
		return NULL;
	}

	if (root->val == x)
	{
		return root;
	}

	BTNode* ret1 = TreeFind(root->left, x);

	if (ret1)
	{
		return ret1;
	}

	return TreeFind(root->right, x);
}

第一种写法:为空树就返回空;找到了就返回当前节点;找不到就先找左子树,找到了返回节点;找不到就再找右子树,找到了返回节点;左右子树都找不到就返回空

第二种写法:为空树就返回空;找到了就返回当前节点;找不到就先找左子树,找到了返回节点;找不到就直接返回右子树(因为右子树中如果找到了就返回节点,找不到就返回空,而左右子树都没找到的时候确实应该返回空,所以这样写没有问题 )

5. 二叉树基础oj练习

  1. 检查两颗树是否相同
代码语言:javascript复制
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);
}
  1. 单值二叉树

子问题:根(根的值和它的孩子的值进行比较)、左子树、右子树

结束条件(最小子问题):空树

代码语言:javascript复制
bool isUnivalTree(struct TreeNode* root)
{
	if (NULL == root)
	{
		return true;
	}

	if (root->left && root->left->val != root->val)
	{
		return false;
	}

	if (root->right && root->right->val != root->val)
	{
		return false;
	}

	return isUnivalTree(root->left) && isUnivalTree(root->right);
}
  1. 对称二叉树

这道题其实就是检查两棵树是否相同的变形:将左子树和左子树比,右子树和右子树比改成了左子树和右子树比,右子树和左子树比。

代码语言:javascript复制
bool _isSymmetric(struct TreeNode* root1, struct TreeNode* root2)
{
	//根比较
	//左子树和右子树比较
	//右子树和左子树比较

	//都为空
	if (root1 == NULL && root2 == NULL)
	{
		return true;
	}

	//一个为空 另一个不为空
	if (root1 == NULL || root2 == NULL)
	{
		return false;
	}

	if (root1->val != root2->val)
	{
		return false;
	}

	return _isSymmetric(root1->left, root2->right) && _isSymmetric(root1->right, root2->left);
}

bool isSymmetric(struct TreeNode* root)
{
	return _isSymmetric(root->left, root->right);
}
  1. 二叉树的前序遍历

这道题的第二个参数有些小伙伴可能会觉得有些奇怪,其实这个参数传的是数组大小的一个指针;因为我们最后要返回一个数组,后台为了方便测试,就统一规定了返回数组的时候就要返回数组的大小。

法一:像顺序表一样,空间不够了就扩容

法二:先把树的节点个数算出来,再malloc数组

第一次写的时候可能会出现这样的问题:

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

void preorder(struct TreeNode* root, int* a, int i)
{
    if (NULL == root)
    {
        return;
    }

    a[i  ] = root->val;
    preorder(root->left, a, i);
    preorder(root->right, a, i);
}

int* preorderTraversal(struct TreeNode* root, int* returnSize)
{
    *returnSize = TreeSize(root);
    int* a = (int*)malloc(sizeof(int) * (*returnSize));
    preorder(root, a, 0);

    return a;
}

这样写只能过一部分测试用例,原因在于:函数在递归的时候开辟了很多块栈帧,一个栈帧中的i 是不会影响另一个栈帧中的i的,所以当递归返回的时候上一个递归函数中的i并没有被 ,就会导致数组中存的数据被覆盖掉(画递归展开图可以很直观的看出来)

正确代码:

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

void preorder(struct TreeNode* root, int* a, int* pi)
{
	if (NULL == root)
	{
		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);
	int* a = (int*)malloc(sizeof(int) * (*returnSize));
	int i = 0;
	preorder(root, a, &i);

	return a;
}
  1. 另一颗树的子树

思路:跟原树的每一棵子树进行比较,看是不是相同(比较两棵树是不是相同前面的题已经写过,可以直接复用前面的代码)

代码语言:javascript复制
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 (NULL == root)
	{
		return false;
	}

	if (root->val == subRoot->val && isSameTree(root, subRoot))
	{
		return true;
	}

	return isSubtree(root->left, subRoot) || isSubtree(root->right, subRoot);
}
  1. 二叉树的构建及遍历
二叉树的构建二叉树的构建
代码语言:javascript复制
#include <stdio.h>
#include <stdlib.h>

typedef struct BinTreeNode
{
	struct BinTreeNode* left;
	struct BinTreeNode* right;
	char val;
}BTNode;

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

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

	return root;
}

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

	InOrder(root->left);
	printf("%c ", root->val);
	InOrder(root->right);
}

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

	return 0;
}

补充:

  1. 二叉树的销毁:

后序比较好(前序也可以,但是比较麻烦,要在销毁根之前把它保存起来)

代码语言:javascript复制
void TreeDestroy(BTNode* root)
{
	if (NULL == root)
	{
		return;
	}

	TreeDestroy(root->left);
	TreeDestroy(root->right);
	free(root);
}
  1. 层序遍历
层序遍历层序遍历
代码语言:javascript复制
//Queue.h

#include <stdio.h>
#include <stdbool.h>
#include <assert.h>
#include <stdlib.h>

typedef struct BinTreeNode* QDataType;

typedef struct QueueNode
{
	QDataType val;
	struct QueueNode* next;
}QNode;

typedef struct Queue
{
	QNode* phead;
	QNode* ptail;
	int size;
}Queue;

void QueueInit(Queue* pq);
void QueueDestroy(Queue* pq);

//入队列
void QueuePush(Queue* pq, QDataType x);
//出队列
void QueuePop(Queue* pq);

QDataType QueueFront(Queue* pq);
QDataType QueueBack(Queue* pq);
bool QueueEmpty(Queue* pq);
int QueueSize(Queue* pq);
代码语言:javascript复制
//Test.c

typedef struct BinTreeNode
{
	struct BinTreeNode* left;
	struct BinTreeNode* right;
	int val;
}BTNode;

#include "Queue.h"

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

	if (root)
	{
		QueuePush(&q, root);
	}

	while (!QueueEmpty(&q))
	{
		BTNode* front = QueueFront(&q);
		QueuePop(&q);
		printf("%d ", front->val);
		
		//带入下一层
		if (front->left)
		{
			QueuePush(&q, front->left);
		}

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

	printf("n");
	QueueDestroy(&q);
}

注: 队列具体的实现由于之前讲过,所以这里就不展示出来了

如果想要把空也打印出来,可以这样写:

代码语言:javascript复制
void TreeLevelOrder(BTNode* root)
{
	Queue q;
	QueueInit(&q);

	if (root)
	{
		QueuePush(&q, root);
	}

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

		if (front)
		{
			printf("%d ", front->val);

			//带入下一层
			QueuePush(&q, front->left);
			QueuePush(&q, front->right);
		}
		else
		{
			printf("N ");
		}
	}

	printf("n");
	QueueDestroy(&q);
}

  1. 判断二叉树是否是完全二叉树
判断二叉树是否是完全二叉树判断二叉树是否是完全二叉树
代码语言:javascript复制
bool TreeIsComplete(BTNode* root)
{
	Queue q;
	QueueInit(&q);

	if (root)
	{
		QueuePush(&q, root);
	}

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

		//遇到空以后就跳出后续判断
		if (NULL == front)
		{
			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;
}

补充:

二叉树的性质:

  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的结点有:
  • 若i>0,i位置节点的双亲序号:(i-1)/2;i=0,i为根节点编号,无双亲节点
  • 若2i 1<n,左孩子序号:2i 1;2i 1>=n,无左孩子
  • 若2i 2<n,右孩子序号:2i 2;2i 2>=n,无右孩子
二叉树的性质二叉树的性质

选择题:

  1. 某二叉树共有 399 个结点,其中有 199 个度为 2 的结点,则该二叉树中的叶子结点数为(B) A 不存在这样的二叉树 B 200 C 198 D 199
  2. 下列数据结构中,不适合采用顺序存储结构的是(A) A 非完全二叉树 B 堆 C 队列 D 栈
  3. 在具有 2n 个结点的完全二叉树中,叶子结点个数为(A) A n B n 1 C n-1 D n/2
  4. 一个具有767个节点的完全二叉树,其叶子节点个数为(B) A 383 B 384 C 385 D 386
选择题分析选择题分析
  1. .一棵完全二叉树的节点数位为531个,那么这棵树的高度为(B) A 11 B 10 C 8 D 12

0 人点赞