二叉树(第二章)

二叉树链式结构的实现

 

为什么还需要学习二叉树链式结构?不是有顺序结构了吗?
顺序结构只是适合完全二叉树,还有非完全二叉树的情况。
普通二叉树的增删查改没有价值,如果单纯是为了存储数据,不如使用线性存储。这里学习链式二叉树是给后面更复杂的搜索二叉树打基础。还有就是很多oj题是普通二叉树。搜索二叉树满足,左子树要比父亲节点小,右子树要比父亲节点要大,所有节点都满足这种情况。搜索二叉树的意义在于搜索,比如找17,17比12大,就不可能在左子树(这里存在矛盾点,但搜素二叉树还有其他性质,搜索二叉树确实是为了搜索)上,17比18小,17在18的左子树上,依次比较后,最多高度次就能找到要找的数字。极端情况下会导致搜索效率变低,然后又演变出平衡搜索二叉树。即AVL树和红黑树。
链式二叉树的第一个重点:二叉树的遍历
二叉树的定义与创建:
typedef int DinaryTreeDate;
typedef struct dinarytree
{
        struct dinarytree* left;
        struct dinarytree* right;
        DinaryTreeDate date;
}dinarytree;
dinarytree* BuyTreeNode(DinaryTreeDate i)
{
        dinarytree* node = (dinarytree*)malloc(sizeof(dinarytree));
        if (node == NULL)
        {
               printf("malloc fail\n");
               return NULL;
        }
        node->date = i;
        node->left = node->right = NULL;
        return node;
}
dinarytree* CreateDinaryTree()
{
        dinarytree* node1 = BuyTreeNode(1);
        dinarytree* node2 = BuyTreeNode(2);
        dinarytree* node3 = BuyTreeNode(3);
        dinarytree* node4 = BuyTreeNode(4);
        dinarytree* node5 = BuyTreeNode(5);
        dinarytree* node6 = BuyTreeNode(6);
        node1->left = node2;
        node1->right = node4;
        node2->left = node3;
        node4->left = node5;
        node4->right = node6;
        return node1;
}
链式二叉树要想象成递归结构,将链式二叉树分为三部分:根节点,左子树,右子树。概念图:
每个节点都要分为根节点,左子树,右子树。直到为空。
遍历结构有四种,前序遍历,中序遍历,后序遍历,层序遍历。先看前三种。
前序遍历,又称为先根遍历。即按照根,左子树,右子树的顺序依次完成遍历的,遍历的顺序为:
开始是根节点1,之后是左子树2,然后是左子树2的左子树3,3的左子树空,右子树空,左子树3结束,访问左子树2的右子树,为空,左子树2结束,访问根节点1的右子树4……
真正打印出来的顺序是1,2,3,4,5,6
中序遍历,又称为中根遍历,按照左子树,根节点,右子树的顺序遍历。遍历的顺序为:
打印出的顺序是3,2,1,5,4,6
后序遍历,又称为后根遍历,按照左子树,右子树,根的顺序遍历,遍历的顺序为:
打印出来的顺序为3,2,5,6,4,1
前三种使用递归实现遍历,层序遍历不需要递归,也最简单,按照1,2,4,3,5,6的层次顺序正常打印就可以。空可以选择打印,也可以选择不打印。
代码很简单:
void PrevOrder(dinarytree* root)
{
        if (root == NULL)
               return;
        printf("%d ", root->date);
        PrevOrder(root->left);
        PrevOrder(root->right);
}
void InOrder(dinarytree* root)
{
        if (root == NULL)
               return;
        InOrder(root->left);
        printf("%d ", root->date);
        InOrder(root->right);
}
void AfterOrder(dinarytree* root)
{
        if (root == NULL)
               return;
        AfterOrder(root->left);
        AfterOrder(root->right);
        printf("%d ", root->date);
}
二叉树的遍历是理解后面知识的基础。在后面的学习中,有不少问题利用了二叉树的思想。
如果要计数二叉树的节点个数,也需要将节点遍历一遍
计数可以选择全局变量和静态局部变量,这里推荐全局变量,因为静态全局变量的重置很麻烦。
静态变量定义在遍历函数(前序,中序,后序)中,定义语句只会执行一次,意味着不能在主函数中重置(没有在主函数中定义),而且二叉树的静态变量计数还需要返回值。如果需要对多个二叉树来进行计数,就需要重置计数变量。选择全局变量更方便。
下面是全局变量计数和局部变量计数的代码:
全局变量:
int count = 0;
void BinaryTreeSize(dinarytree* root)
{
        if (root == NULL)
               return ;
        count++;
        PrevOrder(root->left);
        PrevOrder(root->right);
}
静态变量:
int BinaryTreeSize(dinarytree* root)
{
        static int count = 0;
        if (root == NULL)
               return count;
        count++;
        PrevOrder(root->left);
        PrevOrder(root->right);
        return count;
}
但是全局变量也有线程安全问题,即多线程同时调用这个函数,会出问题,要解决这个问题就需要对函数进行改进。
最优方法:
void BinaryTreeSize(dinarytree* root, int* pCount)
{
        if (root == NULL)
               return ;
        ++(*pCount);
        BinaryTreeSize(root->left, pCount);
        BinaryTreeSize(root->right, pCount);
}
int main()
{
    binarytree tree = CreateBinaryTree();
    int count1 = 0;
    BinaryTreeSize(tree, &count1);
    printf("%d",count1);
}
还有一种思路:子问题,将问题分解成节点+左子树节点个数+右子树节点个数
int BinaryTreeSize(dinarytree* root)
{
    return root == NULL ? 0 : BinaryTreeSize(root->left) + BinaryTreeSize(root->right) + 1;
}
刚传入函数的是节点1,不为NULL,计算左子树节点个数,左子树为节点2,节点2不为空,计算节点2左子树节点个数,节点2的左子树为节点3,节点3不为空,计算节点3的左子树个数,节点3的左子树为空,返回0,节点3的右子树为空,返回0,节点3的节点个数为0+0+1,返回给节点2,节点2的左子树计算完毕,计算右子树,为空,返回0,节点2的节点个数为1+0+1,返回个节点1,节点1的左子树计算完毕。节点1的右子树同理。
这里利用了分治思想,分治:将复杂问题分解成小规模的子问题,将子问题分解成更小规模的子问题,直到不可分割,能直接得出答案。
计算叶节点个数:
int BinaryTreeLeafSize(dinarytree* root)
{
        if (root == NULL)
               return 0;
        if (root->left == NULL && root->right == NULL)
               return 1;
        return BinaryTreeLeafSize(root->left) + BinaryTreeLeafSize(root->right);
}
同样,理解不了就画递归展开图,和计算节点个数不同的原因在于计算有效数的方式不同,也就是说,返回1的条件不同。
计算第k层节点的个数:
int BinaryTreeKLevelSize(dinarytree* root, int k)
{
        assert(k >= 1);
        if (root == NULL)
               return 0;
        if (k == 1)
               return 1;
        return BinaryTreeKLevelSize(root->left, k-1) + BinaryTreeKLevelSize(root->right, k-1);
}
这里递归的思路是,根节点的第k层,即第二层节点的第k-1层,即第三层节点的第k-2层,……即第k层节点的第1层。是空就返回0,是k是1就返回1。
计算二叉树的高度:
分治思想:计算左子树的高度,计算右子树的高度,比较,大的加1(本层的高度)
int BinaryTreeheight(dinarytree* root)
{
        if (root == NULL)
               return 0;
        int leftHeight = BinaryTreeheight(root->left);
        int rightHeight = BinaryTreeheight(root->right);
        return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
}
查找值为x节点:
dinarytree* DinaryTreeFind(dinarytree* root, DinaryTreeDate x)
{
        if (root == NULL)
               return NULL;
        if (root->date == x)
               return root;
        
        dinarytree* ret1 = DinaryTreeFind(root->left, x);
        if (ret1 != NULL)
               return ret1;
        dinarytree* ret2 = DinaryTreeFind(root->right, x);
        if (ret2 != NULL)
               return ret2;
        return NULL;
}
将二叉树分为三个部分:根节点,左子树,右子树。围绕这三部分分治问题。在找到对应的值后,DinaryTreeFind函数会返回地址,地址不为空,再返回,这样能迅速返回到第一个根节点的函数,然后返回找到的地址。找到后剩下的部分就不遍历了。
代码可以化简的,但最好不要,写出的代码是需要维护的,所以逻辑越清晰越好,写的简洁可能在维护时搞不懂逻辑。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值