二叉树万字详解(遍历+oj)

目录

一、二叉树的三种遍历

1.1前序遍历

1.2中序遍历

1.3后序遍历

1.4遍历推倒遍历

 二、二叉树的一般接口

2.1二叉树的层序遍历

2.2计算叶子节点的个数

 2.3二叉树的高度

2.4计算二叉树第K层节点个数

2.5二叉树查找值为x的节点

三、二叉树基础oj练习

3.1单值二叉树

3.2相同的树

3.3对称二叉树

3.4另一个棵树的子树

3.5平衡二叉树

3.6二叉树遍历

 

一、二叉树的三种遍历

1.1前序遍历

对于二叉树的链式结构,常用的三种遍历方式是

1、前序遍历(PreOrder)--访问根节点的操作发生在遍历其左右子树之前。

2、中序遍历(InOrder)--访问根节点的操作发生在遍历其左右子树之间。

3、后序遍历(PostOrder)--访问根节点的操作发生在遍历其左右子树之后。

前序遍历,简单来说就是以根-->左子树-->右子树的形式访问遍历。也就是说:在访问任何一个节点时,都是根的优先级最高,优先访问根,然后是左子树,右子树。以递归的形式访问遍历。

注意:

可能我说的还是优点抽象,以图为例:

 比如这样一个二叉树,在进行前序遍历的时候,可能有读者误解为这样遍历:

先访问1,然后访问它的左子树2,然后再访问1右子树4,然后再访问3,以此类推。这里特别提醒,这样是错误的,原因有二:

1、虽然好像这样满足我说的前序遍历--根-->左子树-->右子树,但是这是不符合递归的核心思想的,虽然2作为1的左子树,但是同时它还是3的根,以根-->左子树-->右子树访问,根的优先级最高的话,应是先访问本身作为根的2的左子树3,然后3又作为根去访问它的左子树,直到遇到NULL返回。

2、还有一个致命的问题,就是访问完4之后如何找到2的左子树3呢?因此这样是不合理的。

对于上面这个二叉树,正确的遍历应该是:

 通过这个相对潦草的图解,我们可以发现这样访问:

无论从整体还是局部上满足根-->左子树-->右子树的访问。

我们要写它的递归的话,满足这样一个规律:当从根访问到左子树,左子树本身又作为它的左右子树的根,它再访问它的左子树,当访问到NULL,就去访问右子树,而右子树本身也是作为它的左右子树的根,去访问它的左子树,以此类推,而这,就是前序遍历递归的本质中序遍历后序遍历与之类似。

代码实现:

我先把构造上面的树的代码放在下面,以便使用:

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

BTNode* CreateBinaryTree()
{
	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);

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


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

前序遍历代码:

void PreOrder(BTNode* root)//前序遍历
{
	if (root == NULL)
	{
		printf("NULL ");
		return;
	}
	printf("%d ", root->data);
	PreOrder(root->left);
	PreOrder(root->right);
}
int main()
{
	BTNode* root= CreateBinaryTree();
	PreOrder(root);//前序遍历
    printf("\n");
    return 0;
}

遍历运行的结果也是和我们所推测的一样。同时也可以看到,这个递归写起来也是比较简单的。

1.2中序遍历

中序遍历,就是以左子树-->根-->右子树的形式访问遍历。也就是说:在访问任何一个节点时,都是左子树的优先级最高,优先访问左子树,然后是根,右子树。以递归的形式访问遍历。

相信读者通过我对前序遍历介绍已经可以自己轻松写出中序遍历了,所以对于中序遍历和后序遍历,博主也是简单介绍,不多赘述了。

void InOrder(BTNode* root)//中序遍历
{
	if (root == NULL)
	{
		printf("NULL ");
		return;
	}
	InOrder(root->left);
	printf("%d ", root->data);
	InOrder(root->right);
}
int main()
{
	BTNode* root= CreateBinaryTree();
	InOrder(root);
	printf("\n");
	return 0;
}

 

 也是和推断的相吻合的。

1.3后序遍历

后序遍历,就是以左子树-->右子树-->根的形式访问遍历。也就是说:在访问任何一个节点时,都是左子树的优先级最高,优先访问左子树,然后是右子树,根。以递归的形式访问遍历。

 代码:

void PostOrder(BTNode* root)//后序遍历
{
	if (root == NULL)
	{
		printf("NULL ");
		return;
	}
	PostOrder(root->left);
	PostOrder(root->right);
	printf("%d ", root->data);
}
int main()
{
	BTNode* root= CreateBinaryTree();
	PostOrder(root);//后序遍历
	printf("\n");
	return 0;
}

验证: 

1.4遍历推倒遍历

 在一些习题里,我们经常会遇到题目给出前序遍历中序遍历来求后序遍历,或者由中序遍历后序遍历来求前序遍历。初学者可能比较头疼,尽管我们由它的访问原则很容易就得出根,但是接下来就无从下手了。

今天博主就分享比较简单的方式去拆解,相信看完后再麻烦的遍历都难不倒你!

首先声明:仅有两种方式可以唯一确定二叉树。

1、已知前序遍历和中序遍历;

2、已知中序遍历和后序遍历.

对于第三种已知前序遍历和后序遍历是无法唯一确定二叉树的。

例题:已知某二叉树的中序遍历序列为J G D H K B A E L I M C F,后序遍历序列为 J G K H D B L M I E F C A,则其前序遍历为:?

其实这种题谨记一点:我们在遍历二叉树的时候使用了什么规则,就按这个规则去拆解

 通过后序遍历,我们确定最后一个节点一定是根,通过中序遍历,我们确定了根的左子树有哪些元素,根的右子树有哪些元素。然后对左子树进行这样的分析,对右子树也是如此。

左子树元素有:J G D H K B(中序遍历)

                         J G K H D B(后序遍历)

我们再次确认B是根,B的左子树为:J G D H K (中序遍历)

                                                           J G K H D (后序遍历)

B的右子树为NULL,再次分析根为DD的左子树为:J G(中序遍历) J G(后序遍历)

                                                             D的右子树为: H K(中序遍历) K H(后序遍历)

至此,左子树可以画出来了:

右子树元素有:E L I M C F(中序遍历)

                         L M I E F C(后序遍历)

现在确定C是根,C的左子树为:E L I M (中序遍历)

                                                     L M I E (后序遍历)

C的右子树为F,再次分析根为EE的左子树为:NULL

                                                      E的右子树为:L I M(中序遍历)  L M I(后序遍历)

现在,右子树可以画出来了:

合并一下:整体的树:

前序遍历:A B D G J H K C E I L M F.

最后;为什么前序和后序无法唯一确定呢?很简单,因为前序遍历第一个是根,后序遍历最后一个也是根,那么,哪一个是根呢?

 二、二叉树的一般接口

2.1二叉树的层序遍历

层序遍历就是从所在二叉树的根节点出发,首先访问第一层的树根节点,然后从左到右访问第2层上的节点,接着是第三层的节点,以此类推,自上而下,自左至右逐层访问树的结点的过程就是层序遍历。 

 像这样,就是层序遍历。这个接口在于计算节点的个数,我们如何通过一层一层的遍历实现统计节点的个数呢?

很明显,层序遍历与上文的前中后序遍历有着殊途同归的意味,我们也是通过递归来实现的。我们把这个问题缩小到一个根和它的左右子树这样一个范围内会有助于我们思考:

        如果根不是空,那么算上根的个数之后我们就要去计算它的左右子树的个数,而左右子树又是作为它的左右子树的根,类似于套娃。

       打个比方:这个问题就类似于,老师让班长去统计人数,然后班长又去让各科课代表去统计人数,各科课代表呢,又让小组长去统计人数,无限细分,当就剩一个人的时候,就返回。

int TreeSize(BTNode* root)
{
	return root == NULL ? 0 :
		TreeSize(root->left) + TreeSize(root->right) + 1;//加一是要算上根的个数
}
int main()
{
	BTNode* root= CreateBinaryTree();
	int size = TreeSize(root);
	printf("%d ", size);
	printf("\n");
	return 0;
}

以递归的方式来写是很简单的。我们可以画图来印证一下:

2.2计算叶子节点的个数

设计一个递归函数,计算一个二叉树叶子节点的个数。

我们还是借用之前的思路,将问题先缩小到一个小范围--一个根和它的左右子树,然后思考叶子节点的特点,很显然,就是叶子结点的左右子树都为NULL。明白了这一点,我们就着手写这个函数。

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);
}
int main()
{
	BTNode* root= CreateBinaryTree();
	int leafsize = TreeLeafSize(root);
	printf("%d ", leafsize);
	printf("\n");
	return 0;
}

 2.3二叉树的高度

递归实现并返回二叉树的高度。

这个问题有点意思,我们还是要利用二叉树的结构特点来解这个问题,想一下,如果对于一个根和它的左右子树,我们一定是算左右子树高度,然后取较大的那个再加上根那一层。没错,这个递归也是基于这一理念。

int TreeHeight(BTNode* root)
{
	if (root == NULL)
		return 0;
	int lh = TreeHeight(root->left);
	int rh = TreeHeight(root->right);
	return lh > rh ? lh + 1 : rh + 1;//加一是加上根
}
int main()
{
	BTNode* root= CreateBinaryTree();
	int height = TreeHeight(root);
	printf("%d ", height);
	printf("\n");
	return 0;
}

2.4计算二叉树第K层节点个数

递归计算二叉树第K层节点的个数。

这个问题本质就是对第K层进行层序遍历,那么我们怎么找到第K层对它进行层序遍历呢?

我们假设一种极端情况,如果只有一个根,那么遍历很明显第1层就1个,于是就设计出这样一种思想:将第K层转换成左右子树的第K-1层,那么当K==1时,不就是只有一个根那种情况计算了

int TreeKLevel(BTNode* root, int k)
{
	if (root == NULL)
		return 0;
	if (k == 1)
		return 1;
	//转化为左右子树的第k-1层
	return TreeKLevel(root->left, k - 1) +
		TreeKLevel(root->right, k - 1);
}
int main()
{
	BTNode* root= CreateBinaryTree();
	int Klevel = TreeKLevel(root, 4);
	printf("%d ", Klevel);
	printf("\n");
	return 0;
}

2.5二叉树查找值为x的节点

递归查找二叉树中值为x的节点。

还是那一招,借助二叉树的结构查找,很简单的设计思想,先去左子树查找,如果没有找到,那么就去右子树查找。

BTNode* TreeFind(BTNode* root, int x)
{
	BTNode* lret, *rret;
	if (root == NULL)
		return NULL;
	if (root->data == x)
		return root;
	lret = TreeFind(root->left, x);
	if (lret)
		return lret;
	rret = TreeFind(root->right, x);
	if (rret)
		return rret;
}
int main()
{
	BTNode* root= CreateBinaryTree();
	BTNode* find = TreeFind(root, 7);
	if (find)
		printf("找到了,%d\n", find->data);
	else
		printf("没找到\n");
	return 0;
}

三、二叉树基础oj练习

3.1单值二叉树

来源:leetcode:965、单值二叉树

如果二叉树每个节点都具有相同的值,那么该二叉树就是单值二叉树。

只有给定的树是单值二叉树时,才返回 true;否则返回 false

示例:

 缩小到一个根和它的左右子树。

1、如果遇到空,它被视为是正确的。

2、只有它的非空左右子树都等于根时才是true,但是正着写代码,比较麻烦,我们正难则反,如果不相等就为false。

bool isUnivalTree(struct TreeNode* root)
{
    //每一个双亲和它孩子的值相等,
    //如果遇到空,被视为正确的
    //双亲的左孩子和右孩子都应该保证它和它的左右孩子相等
    if(root==NULL)
        return true;
    if(root->left&&root->left->val!=root->val)//左右子树不为NULL才有比较的意义
        return false;
    if(root->right&&root->right->val!=root->val)
        return false;
    return isUnivalTree(root->left)&&isUnivalTree(root->right);

}

3.2相同的树

来源:leetcode:100、相同的树

给你两棵二叉树的根节点 p 和 q ,编写一个函数来检验这两棵树是否相同。

如果两个树在结构上相同,并且节点具有相同的值,则认为它们是相同的。

示例:

 对于这个题目,还是正难则反的思想,因为判断true所需的条件过多,而判断为false则很简单,因此要反着写。

bool isSameTree(struct TreeNode* p, struct TreeNode* q)
{
    //先考虑特殊情况
    if(p==NULL&&q==NULL)//都为空的情况,显然是true
        return true;
    if(p==NULL||q==NULL)//一个为空,一个不为空的情况,为false
        return false;
    if(p->val!=q->val)
        return false;
    else
    {
        return isSameTree(p->left,q->left)&&
                isSameTree(p->right,q->right);
    }
}

3.3对称二叉树

来源:leetcode:101、对称二叉树

给你一个二叉树的根节点 root , 检查它是否轴对称。

 如果理解了相同的树这道题目,这道题目应是不难的,只是换汤不换药。唯一的区别在于,相同,改为判断是否对称,那我们只需由原来的判断左子树是否等于左子树,和右子树是否等于右子树变为,左子树是否等于右子树,右子树是否等于左子树不就解决了?我们借用上面的函数,稍微更改一下,就解决了这道题目。

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->right)&&isSameTree(p->right,q->left);

    //其中一个为NULL,另一个不为NULL
}

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

}

3.4另一个棵树的子树

来源:leetcode:572、另一棵树的子树

给你两棵二叉树 root 和 subRoot 。检验 root 中是否包含和 subRoot 具有相同结构和节点值的子树。如果存在,返回 true ;否则,返回 false 。

二叉树 tree 的一棵子树包括 tree 的某个节点和这个节点的所有后代节点。tree 也可以看做它自身的一棵子树。 

 示例:

 这道题目,额,,,不还是相同的树的一种演变吗?我们还是可以直接拷过来用。只不过我们需判断,从根开始是否和另一个树相同,从左子树开始是否和另一个树相同,从右子树开始是否和另一个树相同

不同的是要注意,如果根为空,那么另一个树不可能为它的子树。

bool isSameTree(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;
    else
    {
        return isSameTree(root1->left,root2->left)
        &&isSameTree(root1->right,root2->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);
    //因为左右子树只要有一个相同就行
}

3.5平衡二叉树

来源:leetcode:110、平衡二叉树

给定一个二叉树,判断它是否是高度平衡的二叉树。

本题中,一棵高度平衡二叉树定义为:

一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1 。

 这道题目,如果前面计算二叉树的高度掌握的话,相信也是小菜一碟。我们只需要计算根的左右子树的高度,然后计算差距高度,要求不超过1并且每个根都要满足这样的条件,所谓正难则反,我们反过来写超过1就给false,然后递归。

注意,根为空是满足条件的。

int TreeHeight(struct TreeNode* root)
{
    if(root==NULL)
    return 0;
    int lh=TreeHeight(root->left);
    int rh=TreeHeight(root->right);
    return lh>rh?lh+1:rh+1;
}

bool isBalanced(struct TreeNode* root)
{
    //直接计算高度
    if(root==NULL)
    return true;

    int lh= TreeHeight(root->left);
    int rh=TreeHeight(root->right);
    //正难则反
    int gap=lh-rh;
    if(gap<0)
        gap=-gap;
    if(gap>1)
    return false;
    else
    {
        return isBalanced(root->left)&&isBalanced(root->right);
    } 

}

3.6二叉树遍历

来源:牛客网:KY11 二叉树遍历

编一个程序,读入用户输入的一串先序遍历字符串,根据此字符串建立一个二叉树(以指针方式存储)。 例如如下的先序遍历字符串: ABC##DE#G##F### 其中“#”表示的是空格,空格字符代表空树。建立起此二叉树以后,再对二叉树进行中序遍历,输出遍历结果。

示例: 

输入:

abc##de#g##f###

输出:

c b e g d f a 

这道题目给出我们先序遍历的结果,需要我们根据先序遍历构建二叉树,然后中序遍历一次我们构建的二叉树,输出即可。所以我们需要解决的问题主要有两个

1、根据先序遍历构建二叉树

2、对二叉树进行中序遍历

①由先序遍历构建二叉树

单纯来想是不好想的,我们要反其道而行之,先序遍历的结果是如何得来的?

不就是对二叉树进行根-->左子树-->右子树的遍历方式得到的吗?所以我们对遍历的结果再进行同样的遍历去构建链接二叉树不就行了吗?

博主先把typedef的一些内容放在前面。

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


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

先序遍历重建二叉树代码:

BTNode* BTCreatePreOrder(char* a, int* pi)
{
	if (a[*pi]=='#')
	{
		(*pi)++;
		return NULL;
	}
	BTNode* root = (BTNode*)malloc(sizeof(BTNode));
	if (root == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	root->data = a[*pi];
	(*pi)++;
	root->left = BTCreatePreOrder(a, pi);
	root->right = BTCreatePreOrder(a, pi);
	return root;
}
int main()
{
	char TData[100] = { 0 };
	while (scanf("%s", TData) != EOF)
	{
		int i = 0;
		BTNode* root=BTCreatePreOrder(TData, &i);
		InOrder(root);
		BTDestory(root);
	}
}

IO型题目需要我们自己写main函数,不只是接口,这里有些细节还需要我们注意。

1、首先是为什么传i的地址?

这是因为,我们在递归重建二叉树的时候是会建立函数栈帧的,而如果我们传值,在函数返回的时候,变量销毁,是无法影响到外面的值,导致无法遍历字符数组的下一个

2、不要忘记当我们遇到'#'的时候,字符数组需要++.

中序遍历二叉树以及释放空间

中序遍历前面博主有写过,这里不再赘述,需要注意我们重建二叉树,向堆申请了空间,所以我们需要遍历释放空间的。

void InOrder(BTNode* root)
{
	if (root == NULL)
	{
		return;
	}
	InOrder(root->left);
	printf("%c ", root->data);
	InOrder(root->right);
}
void BTDestory(BTNode* root)
{
	if (root == NULL)
		return;
	BTDestory(root->left);
	BTDestory(root->right);
	free(root);
}

整体代码:

#include <stdio.h>
#include<stdlib.h>
typedef char BTDataType;
typedef struct BinaryTreeNode
{
    BTDataType data;
    struct BinaryTreeNode* left;
    struct BinaryTreeNode* right;
}BTNode;
typedef char BTDataType;

BTNode* BTCreatePreOrder(char* a, int* pi)
{
	if (a[*pi]=='#')
	{
		(*pi)++;
		return NULL;
	}
	BTNode* root = (BTNode*)malloc(sizeof(BTNode));
	if (root == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	root->data = a[*pi];
	(*pi)++;
	root->left = BTCreatePreOrder(a, pi);
	root->right = BTCreatePreOrder(a, pi);
	return root;
}
void InOrder(BTNode* root)
{
	if (root == NULL)
	{
		return;
	}
	InOrder(root->left);
	printf("%c ", root->data);
	InOrder(root->right);
}
void BTDestory(BTNode* root)
{
	if (root == NULL)
		return;
	BTDestory(root->left);
	BTDestory(root->right);
	free(root);
}
int main()
{
	char TData[100] = { 0 };
	while (scanf("%s", TData) != EOF)
	{
		int i = 0;
		BTNode* root=BTCreatePreOrder(TData, &i);
		InOrder(root);
		BTDestory(root);
	}
}

还原的二叉树:(方便读者印证)

 对于之前博主提到的,前序+中序重建二叉树以及中序+后序重建二叉树的代码实现,由于需要用到一些高阶数据结构(哈希表等)以及C++的知识,因为博主目前实力有限,是个菜鸡,所以无法实现,日后补上。码文不易,希望各位支持哦!

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值