• 二叉树链式结构的实现


    上期我们学习了二叉树的堆,这期让我们来看看二叉树的结构,以及如何实现。

    在这之前先补充上期没有说到的二叉树的链式存储:
    链式存储

    二叉树的链式存储结构是指,用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。 通常的方法是 链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所 在的链结点的存储地址 。链式结构又分为二叉链和三叉链,当前我们学习中一般都是二叉链,后面 学到高阶数据结构如红黑树等会用到三叉链。

    二叉树链式结构的实现:

    在学习二叉树的基本操作前,需先要创建一棵二叉树,然后才能学习其相关的基本操作。由于现在大家对二 叉树结构掌握还不够深入,此处手动快速创建一棵简单的二叉树,快速进入二叉树 操作学习,等二叉树结构了解的差不多时,我们反过头再来研究二叉树真正的创建方式。

    下面用代码的方式展示:假设我们要创建的是如图二叉树

    我们可以用以下代码实现:

    1. //手动创造出一颗二叉树
    2. BTNode* BinaryTreeCreate()
    3. {
    4. BTNode* n1 = (BTNode*)malloc(sizeof(BTNode));
    5. assert(n1);
    6. BTNode* n2 = (BTNode*)malloc(sizeof(BTNode));
    7. assert(n2);
    8. BTNode* n3 = (BTNode*)malloc(sizeof(BTNode));
    9. assert(n3);
    10. BTNode* n4 = (BTNode*)malloc(sizeof(BTNode));
    11. assert(n4);
    12. BTNode* n5 = (BTNode*)malloc(sizeof(BTNode));
    13. assert(n5);
    14. BTNode* n6 = (BTNode*)malloc(sizeof(BTNode));
    15. assert(n6);
    16. //给节点赋值
    17. n1->data = 1;
    18. n2->data = 2;
    19. n3->data = 3;
    20. n4->data = 4;
    21. n5->data = 5;
    22. n6->data = 6;
    23. //建立链接关系
    24. n1->left = n2;
    25. n1->right = n4;
    26. n2->left = n3;
    27. n2->right = NULL;
    28. n3->left = NULL;
    29. n3->right = NULL;
    30. n4->left = n5;
    31. n4->right = n6;
    32. n5->left = NULL;
    33. n5->right = NULL;
    34. n6->left = NULL;
    35. n6->right = NULL;
    36. return n1;
    37. }

     二叉树的遍历:(前序、中序以及后序遍历)

    学习二叉树结构,最简单的方式就是遍历。所谓二叉树遍历(Traversal)是按照某种特定的规则,依次对二叉 树中的节点进行相应的操作,并且每个节点只操作一次。访问结点所做的操作依赖于具体的应用问题。 遍历 是二叉树上最重要的运算之一,也是二叉树上进行其它运算的基础。

    按照规则,二叉树的遍历有:前序/中序/后序的递归结构遍历:

    1. 前序遍历(Preorder Traversal 亦称先序遍历)——访问根结点的操作发生在遍历其左右子树之前。

    2. 中序遍历(Inorder Traversal)——访问根结点的操作发生在遍历其左右子树之中(间)。

    3. 后序遍历(Postorder Traversal)——访问根结点的操作发生在遍历其左右子树之后。

    由于被访问的结点必是某子树的根,所以N(Node)、L(Left subtree)和R(Right subtree)又可解释为 根、根的左子树和根的右子树。NLR、LNR和LRN分别又称为先根遍历、中根遍历和后根遍历。

    我们先看看这三种遍历的结果:

     

    上图中一个矩形代表一棵树,通过其打印的前后顺序就可以看出来递归的特点。 

    二叉树的遍历我们要使用递归的方式去遍历下面是递归的代码:

    1. // 二叉树前序遍历
    2. void BinaryTreePrevOrder(BTNode* root)///根 左子树 右子树
    3. {
    4. if (root == NULL)
    5. {
    6. printf("NULL ");
    7. return;
    8. }
    9. printf("%d ", root->data);
    10. BinaryTreePrevOrder(root->left);
    11. BinaryTreePrevOrder(root->right);
    12. }
    13. // 二叉树中序遍历
    14. void BinaryTreeInOrder(BTNode* root)//左子树 根 右子树
    15. {
    16. if (root == NULL)
    17. {
    18. printf("NULL ");
    19. return;
    20. }
    21. BinaryTreeInOrder(root->left);
    22. printf("%d ", root->data);
    23. BinaryTreeInOrder(root->right);
    24. }
    25. // 二叉树后序遍历
    26. void BinaryTreePostOrder(BTNode* root)//左子树 右子树 根
    27. {
    28. if (root == NULL)
    29. {
    30. printf("NULL ");
    31. return;
    32. }
    33. BinaryTreePostOrder(root->left);
    34. BinaryTreePostOrder(root->right);
    35. printf("%d ", root->data);
    36. }

    下面我就通过画函数栈帧的图来说明这是如何遍历的:(这里以前序遍历为例)

     二叉树的其他接口:

    对于这些接口,我们的整体的思路是分而治之,就是大事化小,小事化了。

    一般我们都可以画图解决,先想好每一步该怎么走,怎么分配到子树。对于这种问题,我个人认为,从最后一层开始思考是比较好找到解决方案的,因为最后一层最接近底层,就可以不断往上走,根据所需去解决。

    1. // 二叉树节点个数
    2. int BinaryTreeSize(BTNode* root)
    3. {
    4. if (root == NULL)
    5. {
    6. return 0;
    7. }
    8. //返回左边的和右边的还有自己
    9. return BinaryTreeSize(root->left) + BinaryTreeSize(root->right) + 1;
    10. }
    11. // 二叉树叶子节点个数
    12. int BinaryTreeLeafSize(BTNode* root)
    13. {
    14. if (root == NULL)
    15. {
    16. return 0;
    17. }
    18. //判断叶子节点
    19. if (root->left == NULL && root->right == NULL)
    20. {
    21. return 1;
    22. }
    23. return BinaryTreeLeafSize(root->left) + BinaryTreeLeafSize(root->right);
    24. }
    25. // 二叉树第k层节点个数
    26. int BinaryTreeLevelKSize(BTNode* root, int k)
    27. {
    28. assert(k > 0);
    29. if (root == NULL)
    30. {
    31. return 0;
    32. }
    33. if (k == 1)
    34. {
    35. return 1;
    36. }
    37. return BinaryTreeLevelKSize(root->left, k-1) + BinaryTreeLevelKSize(root->right, k-1);
    38. }
    39. // 二叉树查找值为x的节点
    40. BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
    41. {
    42. if (root == NULL)
    43. {
    44. return NULL;
    45. }
    46. if (root->data == x)
    47. {
    48. return root;
    49. }
    50. //看根的左子树有没有
    51. BTNode* left = BinaryTreeFind(root->left, x);
    52. if (left)
    53. return left;
    54. //看根的右子树有没有
    55. BTNode* right = BinaryTreeFind(root->right, x);
    56. if (right)
    57. return right;
    58. //都没有
    59. return NULL;
    60. }
    61. //二叉树的高度
    62. int TreeHeight(BTNode* root)
    63. {
    64. //左子树和右子树的和加根自己也就是加1就是结果
    65. if (root == NULL)
    66. {
    67. return 0;
    68. }
    69. int leftHeight = TreeHeight(root->left);
    70. int rightHeight = TreeHeight(root->right);
    71. return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
    72. }
    73. // 通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树
    74. BTNode* BinaryTreeCreateByPrev(BTDataType* a, int* pi)
    75. {
    76. //遵守根左子树右子树的规则,从最后一层开始链接起来
    77. if (a[*pi] == '#')
    78. {
    79. (*pi)++;
    80. return NULL;
    81. }
    82. BTNode* root = (BTNode*)malloc(sizeof(BTNode));
    83. if (root == NULL)
    84. {
    85. perror("malloc fail");
    86. return NULL;
    87. }
    88. root->data = a[(*pi)++];
    89. //链接根的左右
    90. root->left = BinaryTreeCreateByPrev(a, pi);
    91. root->right = BinaryTreeCreateByPrev(a, pi);
    92. return root;
    93. }
    94. //二叉树的销毁
    95. void BinaryTreeDestory(BTNode* root)
    96. {
    97. //递归到最后一个再不断向上销毁
    98. if (root == NULL)
    99. {
    100. return;
    101. }
    102. BinaryTreeDestory(root->left);
    103. BinaryTreeDestory(root->right);
    104. free(root);
    105. }

    不用递归的两个接口:

    一个是层序遍历,另一个是判断一棵树是否为满二叉树。

    首先先介绍什么是层序遍历:
    层序遍历:除了先序遍历、中序遍历、后序遍历外,还可以对二叉树进行层序遍历。设二叉树的根节点所在 层数为1,层序遍历就是从所在二叉树的根节点出发,首先访问第一层的树根节点,然后从左到右访问第2层 上的节点,接着是第三层的节点,以此类推,自上而下,自左至右逐层访问树的结点的过程就是层序遍历。

    通过以上方式打印出来的就是层序遍历,那我们该如何实现呢?

    很容易看出来这里用递归的方式不合适,因为递归是要递归到最后的,而这里的打印就不能递归到最后解决,这里可以利用队列的性质,根入队列后就记录打印一下,出队列的时候如该根的左右子树,这样最后的顺序就是正确的,让我们看看代码的实现方式:

    1. // 层序遍历
    2. void BinaryTreeLevelOrder(BTNode* root)
    3. {
    4. //利用队列的性质,先将根的左右子树进队,然后每次出队的时候都进出队的那个元素的两个子树
    5. Qe q;
    6. QueueInit(&q);
    7. //先讲根进队列
    8. QueuePush(&q,root);
    9. while (!QueueEmpty(&q))
    10. {
    11. BTNode* front = QueueFront(&q);
    12. printf("%d ", front->data);
    13. QueuePop(&q);
    14. //出一个根进根的左右子树
    15. if (front->left)
    16. {
    17. QueuePush(&q, front->left);
    18. }
    19. if (front->right)
    20. {
    21. QueuePush(&q, front->right);
    22. }
    23. }
    24. }

     接下来就是和层序遍历的思想很像的判断一棵树是否为完全二叉树:主要思路也是利用队列来实现,和层序遍历相似,把全部元素入队列,如果遇到空指针,并且空指针的后面还有其他元素则说明这不是完全二叉树,否则就是完全二叉树,下面我们用代码来实现:

    1. // 判断二叉树是否是完全二叉树
    2. int BinaryTreeComplete(BTNode* root)
    3. {
    4. //利用层序遍历的思想,如果最后出到空之后的元素中还存在非空元素,就不是完全二叉树
    5. Qe q;
    6. QueueInit(&q);
    7. //先讲根进队列
    8. QueuePush(&q, root);
    9. while (!QueueEmpty(&q))
    10. {
    11. BTNode* front = QueueFront(&q);
    12. QueuePop(&q);
    13. //出到空指针就跳出去
    14. if (front == NULL)
    15. {
    16. break;
    17. }
    18. //出一个根进根的左右子树,这里空也要进去,因为后面要判断
    19. QueuePush(&q, front->left);
    20. QueuePush(&q, front->right);
    21. }
    22. //到这里所有元素都进入了并且如果是完全二叉树的话就只剩空
    23. while (!QueueEmpty(&q))
    24. {
    25. BTNode* front = QueueFront(&q);
    26. QueuePop(&q);
    27. if (front != NULL)
    28. {
    29. QueueDestroy(&q);
    30. return false;
    31. }
    32. }
    33. QueueDestroy(&q);
    34. return true;
    35. }

    最后在出函数前不要忘记销毁队列,防止内存泄漏。

    到这里二叉树的学习就暂时到一段落了,在学习二叉树的时候,尤其是递归,如果不会一定要画图分析,因为不可能有正确的代码给你画函数栈帧。

  • 相关阅读:
    使用Hadoop和Nutch构建音频爬虫:实现数据收集与分析
    【MySQL索引学习】MySQL索引详细学习
    MaskRCNN(matterport)模型搭建与实验
    List类使用
    LeetCode 0228. 汇总区间
    5种典型 API 攻击及预防建议
    C++并发编程实战 第二版 第二章
    中国人民大学与加拿大女王大学金融硕士——人生总要逼自己一把
    k8s--基础--12.5--pod--名称空间,标签,节点名称
    SpringCloud 学习笔记总结 (五)
  • 原文地址:https://blog.csdn.net/weixin_72068014/article/details/126262350