文章的主体内容 1.链式二叉树链式二叉树的表示法 2.二叉树的前中后序遍历思想 3.求二叉树节点的个数及叶子节点的个数及第k层节点数 5.查找二叉树值为x的节点 6.二叉树的层序遍历 7.二叉树的深度 8…二叉树的销毁 **正文开始: ** ??在上一篇博客里,我们已经接触过了二叉树的顺序存储结构以及堆的实现和TOK问题。那么今天我们介绍的是二叉树的链式存储结构,相对于顺序结构而言,在二叉树的OJ题里更多是链式结构形式更多!所有二叉树的链式存储结构我们同样也要掌握!另外,从现在开始,我们看一棵树的角度就要发生转变:即我们看一棵树都要看成:根节点---->左子树---->右子树,这点对于我们后面的一些思想和OJ题目里非常重要! 一.链式二叉树及表示法 ??从名字不难看出,这是用链表的结构来实现二叉树,通过指针域来连接父节点和左右孩子节点.那么具体的结构定义如下:
typedef int BTDataType; typedef struct BinaryTreeNode { BTDataType data;//值域 struct BinaryTreeNode* left; //左孩子 struct BinaryTreeNode* right;//右孩子 }BTNode;
使用的时候根据实际的二叉树的图链接指针域即可. 二叉树的前序,中序,后序遍历以及分治算法 ??和线性表不一样,树不支持顺序遍历,而是提供了三种遍历节点的方式:前序,中序,后序
前序:先访问根节点,再访问左子树,最后访问右子树 中序:先访问左子树,再访问根节点,最后访问右子树 后序:先访问左子树,再访问右子树,最后访问根节点
??这三种遍历的算法的核心思想就是分治算法,所谓的分治算法的核心就是大问题化解成小的子问题,小的子问题再分解成更小的子问题,一直知道子问题可以拿到结果的一种算法思想,这种算法思想在二叉树的身上更是体现的淋漓尽致!就拿前序遍历来说,把遍历整棵树分解成访问根节点和左子树和右子树,接着又可以再对左子树进行分解,直到分解成空树就得到了结果!由于,递归的思想就是大问题化成小问题,所以在分治算法中会经常见到递归的身影!对于递归算法,没有更好的方法去理解,只有老老实实地化递归展开图来帮助理解。 给定一棵如下的二叉树:要求写出前序遍历的代码实现: 在这里我先给出代码,然后接下来结合代码画递归展开图来帮助大家分析和理解是怎么得到结果的:
/ 二叉树前序遍历
void BinaryTreePrevOrder(BTNode* root)
{
if (root == NULL)
return;
printf("%d ", root->data);
BinaryTreePrevOrder(root->left);
BinaryTreePrevOrder(root->right);
}
因为篇幅的原因,这里我把A的左子树的递归展开图花了出来,右子树的递归展开图类似,我这里就不画出来了。 最后前序遍历的结果是ABDCEF 中序遍历和后序遍历的流程也是类似的,不理解的读者可以自行去画递归展开图帮助理解。 求二叉树节点的个数及叶子节点的个数及第k层节点数: 还是以前面的二叉树为例: 那么还是利用分治的思想:如果当前的树是空树,那么节点个数就是0,否则就是求左子树的节点个数和右子树个数相加再加一就是总的个数 具体实现代码如下:
int BinaryTreeSize(BTNode* root)
{
return root == NULL ? 0 : BinaryTreeSize(root->left) + BinaryTreeSize(root->right) + 1;
}
我们同样画出递归展开图来帮助理解代码: 求叶子节点的个数也是类似的思想,具体的思想是: 1.如果是空树,返回 0 2.如果当前节点非空并且左子树为空,右子树为空,返回1 3.如果不是1,2两种情况,递归左子树和右子树 具体代码如下:
int BinaryTreeLeafSize(BTNode* root)
{
if (root == NULL)
return 0;
if (root->left == NULL && root->right == NULL)
return 1;
return BinaryTreeLeafSize(root->left) + BinaryTreeLeafSize(root->right);
}
递归展开图和求节点类似,这里就不做赘述了。 3.求二叉树第K层的节点个数: 思想和前面的求节点数类似,但是在实际的执行过程中会相对难以理解,这里我会非常仔细的画出递归展开图来帮助理解这个流程
int BinaryTreeLevelKSize(BTNode* root, int k)
{
assert(k >= 1);
if (NULL == root)
return 0;
if (k == 1)
return 1;
return BinaryTreeLevelKSize(root->left, k - 1) + BinaryTreeLevelKSize(root->right, k - 1);
}
这里我们用一颗层数稍微多一点的二叉树来分析: 3.查找二叉树中值为x的节点 查找有三种情况:
1.空树返回NULL 2.如果当前节点的值是x,直接返回当前节点, 3.如果不是,递归左子树,如果找到了就返回找到的人节点值,如果没有,递归右子树 4.右子树也没有找到,返回NULL
本质上和前序的思想是一样的,不同的是查找是需要记录相应的返回值,找到就可以不用继续递归查找了
/ 二叉树查找值为x的节点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{
if (root == NULL)
return NULL;
if (root->data == x)
return root;
BTNode* ret1 = BinaryTreeFind(root->left, x);
if (ret1)
return ret1;
BTNode* ret2 = BinaryTreeFind(root->right, x);
if (ret2)
return ret2;
return NULL;
二叉树的层序遍历 ??除了前面的前中后序遍历方式,二叉树还有一种遍历方式叫做层序遍历。,那么层序遍历的就是利用队列的先进先出性质,一层带一层地把节点带出,我们来看一下整个层序遍历的流程。 ??注意:队列里面存储的不是树的节点,而是树节点的指针,如果取了节点,那么接下来就拿不到节点的左孩子和有孩子,这点要特别注意。 ??由于C语言缺乏相应得数据结构,所以我们把先前写好的队列的代码拷贝到当前的项目里,这样队列就可以为我所用。 如果拷贝到当前目录下以后,编译报了一堆错误。不要惊慌失措,大概率是因为头文件展开的时候,编译器向上扫描找不到对应的定义导致,这时候在Queue.h里面加上一个前置声明就可以解决!
struct BinaryTreeNode;//这句话的作用是告诉编译器这是一个结构体。注意一定要用原生的结构体类型,不能是typedef的类型!!!
层序遍历的具体代码如下:
void BinaryTreeLevelOrder(BTNode* root)
{
Queue q;
QueueInit(&q);
if (root)
{
QueuePush(&q, root);
}
while (!QueueEmpty(&q))
{
BTNode* front = QueueFront(&q);
printf("%d ", front->data);
QueuePop(&q);
if (front->left)
{
QueuePush(&q, front->left);
}
if (front->right)
{
QueuePush(&q, front->right);
}
}
QueueDestroy(&q);
}
二叉树的深度 求二叉树的深度是一种后序遍历的思想,当前节点非空,那么二叉树的深度就是左右孩子深度比较深的+1。 那么对应的代码如下:
int TreeDepth(BTNode* root)
{
if (root == NULL)
return 0;
int leftDepth = TreeDepth(root->left);
int rightDepth = TreeDepth(root->right);
return leftDepth > rightDepth ? leftDepth + 1 : rightDepth + 1;
}
可能你会好奇为什么不是这样写:
return TreeDepth(root->left) > TreeDepth(root->right) ? TreeDepth(root->left) + 1 : TreeDepth(root->right) + 1;
??这样写得到逻辑上来是正确的,但是第一次递归计算完的结果没有得到保存就又重复递归下去,做了冗余的计算,所以我们选择第一种写法。 二叉树的销毁 销毁一颗二叉树也应用了后序的思想,不能把根节点先释放,否则左右子树就找不到了,所以应该递归销毁左子树,递归销毁右子树,最后再销毁根节点
void BinaryTreeDestory(BTNode* root)
{
if (root == NULL)
return;
BinaryTreeDestory(root->left);
BinaryTreeDestory(root->right);
free(root);
}
这里free完没有对root置空的原因是,root是一个局部变量,即使这里置空了,对外界的root也没有实际性的影响,所以再调用这个函数以后,需要用户自动把外界的root置空! 总结: ??对于链式二叉树,每一个节点我们用分治的思想分解成根,左子树,右子树,用分治的眼光看待前中后序遍历,还有层序遍历一层带一层的思想,这些在下一篇的二叉树的常见OJ里面都有体现。 文章如有错误或者不足之处还望指出,希望能和大家共同进步。
|