二叉树链式结构的实现
上篇文章我们介绍了二叉树的顺序存储,但在现实生活中,我们经常碰到缺胳膊少腿的二叉树,也就是非完全二叉树,这时候再用顺序存储就很浪费空间,而且数组插入删除需要挪动数据,非常不方便,所以我们需要链式存储
我们学这么多,最终的目的都是为了我们的终极目标:搜索二叉树
最多查找高度次
但是搜索二叉树也不是我们的终极目标,因为再极端情况下,搜索二叉树会有一定的缺陷,可能会变成一个单支
操作系统内核、数据库一般都会用这些结构,是非常重要的结构。
学习步骤:
- 目标一:普通二叉树的结构(遍历求高度、求节点个数、销毁节点……)
本篇文章将介绍二叉树的遍历:
二叉树的前序、中序以及后序遍历
所谓**二叉树遍历****(Traversal)**是按照某种特定的规则,依次对二叉树中的结点进行相应的操作,并且每个结点只操作一次。访问结点所做的操作依赖于具体的应用问题。 遍历
是二叉树上最重要的运算之一,也是二叉树上进行其它运算的基础。
任何一棵子树的访问,都要符合前序,拆成根、左子树、右子树,这样在程序上,我们就可以用递归实现
非递归遍历:层序遍历
上面的前中后序遍历都是递归遍历,层序遍历是一层一层走,直到遇到空
实际应用中,一般用不到NULL,我们这里写是为了方便理解,实际中把NULL去掉就行
补充:BFS和DSF
广度优先遍历和深度优先遍历,深度优先遍历就是先往深走,前序遍历就是一个深度优先遍历的典型例子
代码实现:
注意,我们这里实现遍历是在树的基础上实现的,但是我们现阶段写不出来树,所以我们需要手搓一棵树出来,也就是malloc 6个节点,把他们链接在一起,临时充当我们的树来进行测试
我们就搓这棵树。
注意return不会全部结束!!!它会回到上层调用的地方
前序遍历:
//手搓一棵树:
BTNode* BuyNode(BTDataType x)
{
BTNode* node = (BTNode*)malloc(sizeof(BTNode));
node->data = x;
node->left = node->right = NULL;
return node;
}
BTNode* CreateBinaryTree()
{
BTNode* node1 = BuyNode(1);
BTNode* node2 = BuyNode(2);
BTNode* node3 = BuyNode(4);
BTNode* node4 = BuyNode(3);
BTNode* node5 = BuyNode(5);
BTNode* node6 = BuyNode(6);
node1->left = node2;
node1->right = node3;
node2->left = node4;
node3->left = node5;
node3->right = node6;
return node1;
}
//前序遍历:
void PrevOrder(BTNode* root)
{
if (root == NULL)
{
printf("N ");
return;
}
printf("%d ", root->data);
PrevOrder(root->left);
PrevOrder(root->right);
}
中序:
//中序遍历:
void InOrder(BTNode* root)
{
if (root == NULL)
{
printf("N ");
return;
}
InOrder(root->left);
printf("%d ",root->data);
InOrder(root->right);
}
后序:
//后序遍历:
void BackOrder(BTNode* root)
{
if (root == NULL)
{
printf("N ");
return;
}
BackOrder(root->left);
BackOrder(root->right);
printf("%d ", root->data);
}
本来很复杂的结构,但是代码实现就很简单,这里依赖于我们的递归,递归的本质就是把当前问题拆成子问题,但注意,递归一定要有返回条件,也就是最小的子问题。
二叉树的节点个数、叶子数、高度(深度)
节点个数
int TreeSize(BTNode* root)
{
static int size = 0;//放在静态区
if (root == NULL)
return 0;
else
++size;
TreeSize(root->left);
TreeSize(root->right);
return size;
}
看着没什么问题对吧?
但是如果我调用两次呢?调用三次呢?你这个静态变量是一定会累加的,所以,我们这里不能用静态变量的方式解决问题!静态变量不论你调用多少次,只会在第一次调用的时候及逆行初始化,也就是只初始化一次!
所以,正确的做法是把它放到全局,全局还是全局静态都可以,不影响,但是每次使用之前,需要把这个全局变量置为0
int size = 0;
int TreeSize(BTNode* root)
{
if (root == NULL)
return;
else
++size;
TreeSize(root->left);
TreeSize(root->right);
return size;
}
int main()
{
BTNode* root = CreateBinaryTree();
size = 0;
int ret = TreeSize(root);
printf("%d\n", ret);
size = 0;
ret = TreeSize(root);
printf("%d\n", ret);
size = 0;
ret = TreeSize(root);
printf("%d\n", ret);
return 0;
}
还有一种做法就是传址,我们把一个记录size的变量的地址传进去:
int TreeSize(BTNode* root,int* psize)
{
if (root == NULL)
return;
else
++ * psize;
TreeSize(root->left,psize);
TreeSize(root->right,psize);
return *psize;//可以不要返回值,只是方便我们操作而已
}
int main()
{
int size = 0;
BTNode* root = CreateBinaryTree();
int ret = TreeSize(root,&size);
printf("%d\n", ret);
size = 0;
ret = TreeSize(root, &size);
printf("%d\n", ret);
size = 0;
ret = TreeSize(root, &size);
printf("%d\n", ret);
return 0;
}
其实本质一样的,还是需要置0。
更好的思路是:分治递归(运用递归的思想)
int TreeSize(BTNode* root)
{
if (root == NULL)
return 0;
else
return TreeSize(root->left) + TreeSize(root->right) + 1;
}
这代码不爽死?
还有更简单的写法:
return root == NULL ? 0 : TreeSize(root->left) + TreeSize(root->right) + 1;
测一下:
int main()
{
BTNode* root = CreateBinaryTree();
int ret = TreeSize(root);
printf("%d\n", ret);
ret = TreeSize(root);
printf("%d\n", ret);
ret = TreeSize(root);
printf("%d\n", ret);
return 0;
}
求叶子数:
int TreeLeafSize(BTNode* root)
{
if (root == NULL)
return 0;
else if(root->right == NULL && root->left == NULL)
return 1;
else
return TreeLeafSize(root->left) + TreeLeafSize(root->right);
}
求高度:
这样写可以吗?可以是可以但是性能不行(有效率)
这个算法会重复算很多很多次,当树特别大的时候,会崩,优化代码就是把这两个记录下来:
int TreeHeight(BTNode* root)
{
if (root == NULL)
return 0;
int lefthight = TreeHeight(root->left);
int righthight = TreeHeight(root->right);
return lefthight > righthight ? lefthight + 1 : righthight + 1;
}
或者用一个取大函数,效果是一样的:
int Max(int x, int y)
{
return x > y ? x : y;
}
int TreeHeight(BTNode* root)
{
if (root == NULL)
return 0;
return Max(TreeHeight(root->left), TreeHeight(root->right)) + 1;
}
求第K层节点的个数:
代码:
//求第k层的节点个数:
int TreeLevelSize(BTNode* root, int k)
{
if (root == NULL)
return 0;
if (root != NULL && k == 1)
return 1;
else
return TreeLevelSize(root->left, k - 1) + TreeLevelSize(root->right, k - 1);
}
查找值为k的节点
经典错误代码:
// 二叉树查找值为x的节点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{
if (root == NULL)
{
return NULL;
}
if (root->_data == x)
return root;
BinaryTreeFind(root->_left, x);
BinaryTreeFind(root->_right, x);
}
我们return root,是给上一层的,而不是直接返回去
正确代码:
// 二叉树查找值为x的节点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{
if (root == NULL)
return NULL;
if (root->_data == x)
return root;
BTNode* left = BinaryTreeFind(root->_left, x);
if (left)
return left;
//说明左树没找到,到右树去找
BTNode* right = BinaryTreeFind(root->_right, x);
if (right)
return right;
return NULL;
}
简化代码:
// 二叉树查找值为x的节点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{
if (root == NULL)
return NULL;
if (root->_data == x)
return root;
BTNode* left = BinaryTreeFind(root->_left, x);
if (left)
return left;
//说明左树没找到,到右树去找
return BinaryTreeFind(root->_right, x);
}
但是还是建议写上面这个,逻辑更清晰
二叉树练习题:
二叉树前序遍历
这里的前序遍历和之前的不太一样,这一是要插值的,不是简单的遍历就完了:
OJ链接:https://leetcode.cn/problems/binary-tree-postorder-traversal/
代码:
void ProOrder(int* a,struct TreeNode* root,int* i)
{
if(root == NULL)
return;
a[(*i)++] = root->val;
ProOrder(a,root->left,i);
ProOrder(a,root->right,i);
}
int TreeSize(struct TreeNode* root)//先计算好树的节点个数,方便知道我们的数组需要开多大
{
if(root == NULL)
return 0;
return TreeSize(root->left) + TreeSize(root->right) + 1;
}
int* preorderTraversal(struct TreeNode* root, int* returnSize) {
*returnSize = TreeSize(root);//这里的reutrnSize是返输出型参数,就比如我在main函数里面调用了这个函数,传的第三个参数需要被修改就需要传地址。
int* a = (int*)malloc(sizeof(int) * (*returnSize));
int i = 0;//传i的地址,确保数组的下标是能修改的
ProOrder(a,root,&i);//这个函数我们不能递归,因为我们不能每调一次函数就Malloc一个a数组吧,所以我们需要用自己的函数实现
return a;
}
二叉树后序遍历
有了前序遍历的经验,这后序遍历就是小菜一碟
OJ链接:https://leetcode.cn/problems/binary-tree-postorder-traversal/description/
代码:
void ProOrder(int* a,struct TreeNode* root,int* i)//后序遍历的数组里面存储的是左子树 右子树 根,我们这里倒过去,就应该是根 右子树 左子树
{
if(root == NULL)
return;
a[(*i)--] = root->val;
ProOrder(a,root->right,i);//注意顺序哟~
ProOrder(a,root->left,i);
}
int TreeSize(struct TreeNode* root)
{
if(root == NULL)
return 0;
return TreeSize(root->left) + TreeSize(root->right) + 1;
}
//输出型参数,returnSize是用来记录数的节点个数的
int* postorderTraversal(struct TreeNode* root, int* returnSize) {
*returnSize = TreeSize(root);
int* a = (int*)malloc(sizeof(int) * (*returnSize));
int i = *returnSize - 1;
ProOrder(a,root,&i);
return a;
}
另一棵树的子树
OJ链接:https://leetcode.cn/problems/subtree-of-another-tree/description/
思路:
我们想找树里面有没有这个子树存在,那就先把所有子树找出来,再和子树比较不就行了?
子树怎么找?每一个节点不就对应一棵子树的根吗?所以,每一个节点都是子树,我们只需要用前序遍历找到每一棵子树就行。
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(root->val == subRoot->val && isSameTree(root, subRoot))
return true;
return isSubtree(root->left, subRoot) || isSubtree(root->right, subRoot);//只要能找到一棵一样就行,所以是||,同时这里的递归操作就是在前序遍历得到所有子树的根节点
}