数据结构笔记9:二叉树

目录

二叉树的表示形式:

二叉树的遍历:

先来看看前序(先序)遍历方法:

中序和后序遍历:

二叉树的前序构建:

看完二叉树的构建,来看看二叉树的销毁:

前序遍历销毁:

二叉树中的一些基本方法:

计算二叉树的节点个数:

计算二叉树的叶子节点个数:

计算第k层的节点个数:

查找值为x的节点:

二叉树的层序遍历:

判断一个树是否是完全二叉树:

OJ:

单值二叉树:

二叉树的前序遍历(OJ版):

相同二叉树:

对称二叉树:

判断树中是否有另一个子树:

二叉树的遍历和构建(牛客OJ版):

反转二叉树:

判断二叉树是否是平衡二叉树:

二叉树的一些重要结论:

长文预告

二叉树的表示形式:

 这是二叉树最常见的表示形式:二叉链形式

一个节点包含三个值,数据,左子节点,右子节点

typedef struct BinaryTreeNode
{
	BTDataType _data;
	struct BinaryTreeNode* _left;
	struct BinaryTreeNode* _right;
}BTNode;

这是二叉树的三叉链表示形式

在二叉链的基础上增加了一个指向父节点的值,类似双向链表指向前一个节点

typedef struct TriTNode {
    ElemType data;                     // 数据域
    struct TriTNode *lchild, *rchild;  // 左右孩子指针
    struct TriTNode *parent;           // 父结点指针
} TriTNode, *TriTree;

二叉树的遍历:

二叉树的遍历有:前序、后序、中序的递归遍历

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

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

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

也就是

前序:根、左、右

中序:左、根、右

后序:左、右、根

许多二叉树的方法都围绕着这三种遍历方式展开

二叉树的一些方法:

包括通过数组构建二叉树、销毁二叉树、计算节点个数等等。

// 通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树
BTNode* BinaryTreeCreate(BTDataType* a, int n, int* pi);
// 二叉树销毁
void BinaryTreeDestory(BTNode** root);
// 二叉树节点个数
int BinaryTreeSize(BTNode* root);
// 二叉树叶子节点个数
int BinaryTreeLeafSize(BTNode* root);
// 二叉树第k层节点个数
int BinaryTreeLevelKSize(BTNode* root, int k);
// 二叉树查找值为x的节点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x);
// 二叉树前序遍历 
void BinaryTreePrevOrder(BTNode* root);
// 二叉树中序遍历
void BinaryTreeInOrder(BTNode* root);
// 二叉树后序遍历
void BinaryTreePostOrder(BTNode* root);
// 层序遍历
void BinaryTreeLevelOrder(BTNode* root);
// 判断二叉树是否是完全二叉树
int BinaryTreeComplete(BTNode* root);

先来看看前序(先序)遍历方法:

// 二叉树前序遍历 
void BinaryTreePrevOrder(BTNode* root)
{
	if (root == NULL)
	{
		return;
	}
	printf("%c ", root->_data);
	BinaryTreePrevOrder(root->_left);
	BinaryTreePrevOrder(root->_right);
}

打印当前根节点的值,然后遍历左子树,再遍历右子树。根节点为空作为递归的返回条件。

然后中序遍历和后序遍历都是差不多,只不过中序遍历发生在遍历左右子树中间,而后序遍历发生在遍历左右子树之后。

中序和后序遍历:

// 二叉树中序遍历
void BinaryTreeInOrder(BTNode* root)
{
	if (root == NULL)
		return;
	BinaryTreeInOrder(root->_left);
	printf("%c ", root->_data);
	BinaryTreeInOrder(root->_right);

}
// 二叉树后序遍历
void BinaryTreePostOrder(BTNode* root)
{
	if (root == NULL)
		return;
	BinaryTreePostOrder(root->_left);
	BinaryTreePostOrder(root->_right);
	printf("%c ", root->_data);
}

现在已经知道了二叉树的递归方法,尝试实现遍历一个数组,并使用前序构建二叉树。

二叉树的前序构建:

这里如果能够保证传入的数组是正确的前序遍历顺序,可以不需要对*pi的值进行判断。

// 通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树
BTNode* BinaryTreeCreate(BTDataType* a, int n, int* pi)
{
	if (*pi >= n || a[*pi] == '#')
	{
		(*pi)++;//越界或者#号就代表空节点
		return NULL;
	}
	BTNode* root = malloc(sizeof(BTNode));
	root->_data = a[*pi++];
	root->_left = BinaryTreeCreate(a, n, pi);
	root->_right = BinaryTreeCreate(a, n, pi);
	return root;
}

这里也是使用了前序遍历,只不过对根节点的操作从之前的打印根节点的值变成了给根节点赋值,并更新*pi的值,使它指向数组的下一个值。

在进行不同的操作时应该使用不同的遍历方式:

比如说在这个前序构建二叉树的方法中,由于我们知道了一个完整的二叉树的前序结构(包括空节点的位置),所以我们可以使用前序遍历来构建。但也不是只能使用前序遍历来构建二叉树:

当我们知道二叉树的中序、+前序/后序的遍历结果,可以通过中序结果确定左右子树,通过前序/后序知道根节点的位置。

当我们知道二叉树是个完全二叉树,并且知道它的层序遍历顺序,可以使用层序遍历来构建二叉树。

看完二叉树的构建,来看看二叉树的销毁:

二叉树的销毁很自然的就使用了后序的方法,因为我们要先销毁左右子树再销毁根节点。

// 二叉树销毁
void BinaryTreeDestory(BTNode** root)
{
	if ((*root) == NULL)
		return;
	BinaryTreeDestory((*root)->_left);
	BinaryTreeDestory((*root)->_right);
	free((*root));
	*root = NULL;
}

但我们同样可以不使用后序遍历:

层序遍历销毁:

先存储左右子节点(入队),再销毁当前节点。

void DestroyTreeLevelOrder(BiTree root) {
    if (root == NULL) return;
    
    Queue q;
    InitQueue(&q);
    EnQueue(&q, root);
    
    while (!IsEmptyQueue(q)) {
        BiTree node = DeQueue(&q);
        
        if (node->left) EnQueue(&q, node->left);
        if (node->right) EnQueue(&q, node->right);
        
        free(node); // 释放当前节点
    }
}

前序遍历销毁:

先存储左右子节点,再销毁当前节点,再销毁左树,再销毁右树:

void DestroyTreePreorder(BiTree root) {
    if (root == NULL) return;
    
    BiTree left = root->left;   // 临时保存
    BiTree right = root->right; // 临时保存
    
    free(root);                // 先释放当前节点
    DestroyTreePreorder(left);  // 再释放左子树
    DestroyTreePreorder(right); // 最后释放右子树
}

二叉树中的一些基本方法:

看完二叉树销毁来看一些比较简单但实用的二叉树的方法:

计算二叉树的节点个数:

// 二叉树节点个数
int BinaryTreeSize(BTNode* root)
{
	if (root == NULL)
		return 0;
	return BinaryTreeSize(root->_left) + BinaryTreeSize(root->_right) + 1;
}

从拆分问题的逻辑出发:计算二叉树的节点,就是计算左子树的节点+右子树的节点数+当前节点的节点数。

计算二叉树的叶子节点个数:

计算叶子节点个数就是计算左树叶子节点+右树叶子结点个数。

// 二叉树叶子节点个数
int BinaryTreeLeafSize(BTNode* root)
{
    if(root == NULL)
    return 0;
	if (root->_left == NULL && root->_right == NULL)
	{
		return 1;//是叶子就返回1
	}
    int left = BinaryTreeLeafSize(root->_left);
	int right = BinaryTreeLeafSize(root->_right);
	return left + right;
}

计算第k层的节点个数:

这个稍微难想到一些,计算第k层的节点数,就是计算以子节点为根的第k-1层的节点个数。

// 二叉树第k层节点个数
int BinaryTreeLevelKSize(BTNode* root, int k)
{
	if (root == NULL)
		return 0;
	if (k == 1)
	{
		return 1;
	}
	return BinaryTreeLevelKSize(root->_left, k - 1) + BinaryTreeLevelKSize(root->_right, k - 1);
}

查找值为x的节点:

这个类似前序的遍历,先看根节点是否是要找的节点,否则遍历左右子树,返回不为空的值。

// 二叉树查找值为x的节点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{
	if (root == NULL)//这个对于空节点的判断总是有两种效果,第一它是作为递归的结束判断条件,第二它是作为传入参数的判空。
	{
		return NULL;
	}
	if (root->_data == x)
	{
		return root;
	}
	BTNode* left = BinaryTreeFind(root->_left, x);
	BTNode* right = BinaryTreeFind(root->_right, x);
	if (left != NULL)
	{
		return left;
	}
	else {
		return right;
	}
}

二叉树的层序遍历:

层序遍历不同以上的遍历,这是使用循环的方式来实现了对二叉树的遍历。

实现二叉树的层序遍历稍微复杂些,这里需要用到队列:

首先创建一个队列,初始化好后,将根节点传入队列中,然后以队列是否为空作为循环进行的条件。节点出队列并进行操作,将其左右节点分别入队列,由于队列的先进先出原则,最后出队列的顺序就是层序遍历的结果。

// 层序遍历
void BinaryTreeLevelOrder(BTNode* root)
{
	if (root == NULL)
		return;
	Queue* q1 = malloc(sizeof(Queue));
	QueueInit(q1);
	QueuePush(q1, root);
	while (!QueueEmpty(q1))//层序遍历使用了前序遍历,根节点出队列,左子树入队列,右子树入队列
	{
		BTNode* tmp = QueueFront(q1);
		printf("%c ", tmp->_data);
		QueuePop(q1);
		if (tmp->_left != NULL)
			QueuePush(q1,tmp->_left);
		if (tmp->_right != NULL)
			QueuePush(q1, tmp->_right);

	}
}

看完了层序遍历可以来看看层序遍历的实际使用方法:

判断一个树是否是完全二叉树:

 完全二叉树的特征:

1、一个完全二叉树的任意节点只要有右节点,就必须有左节点

2、如果只有左节点的话,它之后的节点都只能是叶子节点

因为层序遍历完全二叉树时,只要遇到第一个叶子结点(或者第一个叶子节点前的节点),那么接下来遇到的只能是叶子节点。

当队列为空的时候,如果遍历的每个节点都能满足这两个条件,那么它就是完全二叉树。

// 判断二叉树是否是完全二叉树
int BinaryTreeComplete(BTNode* root)
{
	//完全二叉树的特征就是,任意一个节点都必须满足如果存在右子节点就必须存在左子节点
	//使用层序遍历遇到第一个叶子节点之后,接下来就不能存在非叶子节点
	Queue q1;
	QueueInit(&q1);
	QueuePush(&q1, root);
	while (!QueueEmpty(&q1))
	{
		int flag = 0;
		BTNode* cur = QueueFront(&q1);
		QueuePop(&q1);
		if (cur->_right && !cur->_left)
		{
			return 0;
		}
		if (flag && (cur->_left || cur->_right))
		{
			return 0;
		}
		if (cur->_left != NULL)
		{
			QueuePush(&q1, cur->_left);
		}
		if (cur->_right != NULL)
		{
			QueuePush(&q1, cur->_right);
		}
		else {
			flag = 1;//这个节点或者这个节点之后就是叶子节点
		}
	}
	return 1;
}

除了层序遍历,二叉树中大部分方法都可以用递归的方式完成,而且很多都是前序、中序、后序遍历的变形,区别在于每次访问根节点做的事情不同,以及递归返回条件不同和返回值不同。

OJ:

单值二叉树:

一个单值二叉树就是所有值都一样的二叉树,给一个根节点该怎么判断它是否是单值二叉树呢?

先判断左右节点的值是否和根节点一致,一旦有不一致的就不是单值二叉树,然后再判断左右子树是否是单值二叉树。

bool isUnivalTree(struct TreeNode* root) {
    if(root == NULL)
    return true;
    int x = root->val;
    if(root->left != NULL && root->left->val != x)//判断左值,我把这个和判断相同树的逻辑弄混了
    return false;
    if(root->right != NULL && root->right->val != x)//判断右值
    return false;
    return isUnivalTree(root->left) && isUnivalTree(root->right);
}

这里对单值二叉树判断的思想属于前序遍历(判断根、左、右子树),但是和之前不一样的是,由于这个方法有返回值,并且进行的是单值二叉树的判断,所以递归的返回条件不同。

计算二叉树的最大深度:

选取左右子树中深度较大的那个,返回较大深度+1。

int maxDepth(struct TreeNode* root) {
    if(root == NULL)
    return 0;
    int left = maxDepth(root->left);
    int right = maxDepth(root->right);
    int max = left;
    if(left < right)
    max = right;
    return max+1;
}

二叉树的前序遍历(OJ版):

OJ提供的接口,有一个根节点,和一个数组大小,要求我们返回一个前序遍历结果的数组。

要数组就要创建数组,创建数组就要知道数组的大小,先通过我们之前写的计算二叉树节点个数方法求出数组大小。然后新写一个前序遍历方法,在访问根节点的时候先将根节点的值传入数组,再依次传入左右子树。

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

void preorder(struct TreeNode* root,int *a,int* pi)//这个pi必须是指针,为了递归的时候用
{
    if(root == NULL)
    return;
    a[(*pi)++] = root->val;//虽然最后pi会越界,但是不会使用
    preorder(root->left,a,pi);
    preorder(root->right,a,pi);
}
int* preorderTraversal(struct TreeNode* root, int* returnSize) {
    *returnSize = TreeSize(root);//这是需要返回的大小,方便系统测试
    int * a = malloc(sizeof(int)*(*returnSize));
    int i = 0;
    preorder(root,a,&i);//因为前序遍历必须要递归
    return a;
}

相同二叉树:

判断两个二叉树是否相同

首先判断当前两个节点是否存在空节点,然后判断它们的值是否相同(空指针不能引用),如果相同再判断左右子树是否相同。

bool isSameTree(struct TreeNode* p, struct TreeNode* q) {
    // if((p == NULL && q != NULL) || (p != NULL && q == NULL))
    // return false;
    // if(p == NULL && q == NULL)
    // return true;
    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);
}

对称二叉树:

OJ的接口只给出根节点,判断它是否是对称二叉树,就要判断它的左右子树是不是对称二叉树。

这里需要新写一个方法,这样才能完成递归。判断左右子树是否是对称二叉树的逻辑和相同二叉树有点像,先判断左右节点是否存在空节点(空节点不能访问),然后判断左右节点值是否相同,如果相同就继续判断左节点的左节点和右节点的右节点是否相同,以及左节点的右节点和右节点的左节点是否相同。

bool _isSymmetric(struct TreeNode* left,struct TreeNode* right)
 {
    // if((left == NULL && right != NULL) || (left != NULL && right == NULL))
    // return false;
    // if(left == NULL && right == NULL)
    // return true;
    //更简单的写法
    if(left == NULL && right == NULL)
    return true;
    if(left == NULL || right == NULL)
    return false;
    if(left->val != right->val)//判断左右节点
    return false;
    return _isSymmetric(left->left,right->right) && _isSymmetric(left->right,right->left);//判断左节点的左树和右节点的右树
    //判断左节点的右树和右节点的左树,这是它和相同二叉树的区别
 }
bool isSymmetric(struct TreeNode* root) {
    if(root == NULL)
    return true;
    return _isSymmetric(root->left,root->right);//判断是否对称需要判断左右子树是否对称,因为要递归所以要一个新的方法
}

判断树中是否有另一个子树:

这个可以使用前序遍历判断每一个根节点是否和该子树相同。

由于要判断两棵树是否相同,就要用到之前写的相同二叉树的方法。

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

二叉树的遍历和构建(牛客OJ版):

要求使用一个传入的数组,通过前序来构建,中序来遍历输出。

这个本身不难写,难在它是一道OJ题,需要写的方法比较多

首先要定义一个树的结构(这里使用二叉链的形式)

然后写一个前序的构建方法,由于需要遍历数组,传入参数就是数组和pi指针(表示遍历到什么位置)。

再写一个中序的输出方法,这里只需要传入一个二叉树的根节点,通过中序遍历来打印。

typedef struct TreeNode
{
    struct TreeNode * left;
    struct TreeNode * right;
    char data;
}TreeNode;

TreeNode* CreatTree(char* a,int * pi)
{
    if(a[*pi] == '#')
    {
        (*pi)++;
        return NULL;
    }
    TreeNode* root = malloc(sizeof(TreeNode));
    root->data = a[(*pi)++];
    root->left = CreatTree(a, pi);
    root->right = CreatTree(a, pi);
    return root;
}
void inorder(TreeNode* root)
{
    if(root == NULL)
    return;
    inorder(root->left);
    printf("%c ",root->data);
    inorder(root->right);
}
int main() {
    char ch[100] = {0};
    while(scanf("%s",ch) != EOF)
    {
        int i = 0;
        TreeNode * root = CreatTree(ch,&i);
        inorder(root);
    }
}

反转二叉树:

翻转一颗二叉树,这里需要注意的是不是对值进行更改,而是要互换节点的左右节点的值。

由于要对根节点进行无返回值前序遍历,所以要重新写一个方法用于递归反转。

交换左右节点,再递归交换左右子树。

void _invertTree(struct TreeNode* root)//究竟是根节点还是左右节点,如果是左右节点的话,无法改变根节点的情况,所以要传的是根节点
 {
    if(root == NULL)
    return;
    struct TreeNode* tmp = root->left;
    root->left = root->right;
    root->right = tmp;
    _invertTree(root->left);
    _invertTree(root->right);//反转二叉树就是先反转根节点再反转左右节点。
 }
struct TreeNode* invertTree(struct TreeNode* root) {
    if(root == NULL)
    return root;
    _invertTree(root);
    return root;
}

判断二叉树是否是平衡二叉树:

平衡二叉树:是指该树所有节点的左右子树的高度相差不超过 1。

先判断根节点是不是平衡二叉树,再判断左右子树是不是平衡二叉树。判断平衡二叉树需要求左右子树的高度,所以需要一个求二叉树的高度的方法。然后前序遍历判断。

int TreeHigh(struct TreeNode* root)
 {
    if(root == NULL)
    return 0;
    int left = TreeHigh(root->left);
    int right = TreeHigh(root->right);
    int max = left;
    if(left < right)
    max = right;
    return max + 1;
 }
bool isBalanced(struct TreeNode* root) {
    if(root == NULL)
    return true;

    int left = TreeHigh(root->left);
    int right = TreeHigh(root->right);
    if(abs(left - right) > 1)
    return false;
    return isBalanced(root->left) && isBalanced(root->right);
}

这些二叉树的OJ题如果要使用递归来完成,很多都需要创建一个新的接口,才能实现函数的递归,并且一些简单的二叉树方法常常在其他较为复杂的二叉树的方法中使用。

二叉树的一些重要结论:

1、所有二叉树中,度为0的节点(叶子节点)个数比度为2的节点永远多一个。

这篇文章有证明过程:数据结构笔记8:堆-CSDN博客

2、高度为h的满二叉树,节点个数为2^h - 1。

3、满二叉树的第h层,有2^(h-1)个节点

4、前序遍历是深度优先遍历(DFS: Depth-First Search),而后序不是,因为前序遍历是先遍历完一整条路线再返回,而后序遍历是从一条路线底部从下往上折返。层序遍历属于广度优先遍历(BFS :Breadth-First Search)

5、二叉树的节点个数和边的个数的关系是:边的数量+1 = 节点个数(这是因为除了根节点外每个节点都贡献一条边)

6、单边二叉树的前序后序恰好相反

7、知道二叉树的中序、+前序/后序的遍历结果,可以通过中序结果确定左右子树,通过前序/后序知道根节点的位置。

二叉树天然适合使用递归来解决问题,但也可以借助其他数据结构来实现循环解决(比如层序遍历,判断是否是完全二叉树),在遍历二叉树的时候,根据要做的事情不同,选择适合的遍历方式(前序、中序、后序、和层序)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值