• 初阶数据结构学习记录——여덟 二叉树


    目录 

    一、树

    概念 

    二、二叉树

    二叉树的特点

    三、这篇重点在于堆的实现

    该用数组还是链表体现二叉树?

    假设是链表

    假设是数组

    头文件里

    Heap.c(包括push和pop,向上/下比较法)

    Heap.h

    Heap.c

    Test.c


    一、树

    顾名思义,结构即为树,由一个根节点分出多个节点,这几个节点再继续往下连接其他节点形成一个个子树。不过这棵树是根朝上,叶朝下的。一个根不限制连接多少个节点,把第二层的几个节点也看成根节点,最终形成一整个树结构。形象的图可以搜到,这里就不写了。

    来点形式主义

    概念 

    树是一种非线性的数据结构,它是由n(n >= 0)个有限节点组成一个具有层次关系的集合。把它叫做树是因为它看起来像一颗倒挂的数,也就是说它是根朝上,而叶朝下的。

    ~ 有一个特殊的节点,称为根节点,根节点没有前驱节点。

    ~ 除根节点外,其余节点被分成M(M > 0)个互不相交的集合T1,T2....TM, 其中每一个集合Ti(1 <= i <= M)又是一棵结构与树类似的子树。每棵子树的根节点有且只有一个前驱,可以有0个或多个后继节点。

    ~ 因此,树是递归定义的。

    接下来要看一下树的一些定义

     

    节点的度:一个节点含有的子树的个数称为该节点的度,如上图:A的为6

    叶节点或终端节点:度为0的节点称为叶节点,如上图:B、C、H、I.....等节点为叶节点

    非终端节点或分支节点:度不为0的节点;如上图:D、E、F、G....等节点为分支节点

    双亲节点或父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点。如上图, A是B的节点

    孩子节点或者子节点:一个节点含有的子树的根节点称为该节点的子节点。如上图:B是A的子节点

    兄弟节点:具有相同父节点的节点互称为兄弟节点。如上图:B、C是兄弟节点

    树的度:一棵树中,最大的节点的度称为树的度。如上图:树的度为6

    节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推。

    树的高度或深度:树中节点的最大层次。如上图,树的高度为4

    堂兄弟节点:双亲在同一层的节点互为堂兄弟。如上图:H、I互为堂兄弟节点

    节点的祖先:从根到该节点所经分支上的所有节点。如上图:A是所有节点的祖先

    子孙:以某节点为根的子树中任一节点都称为该节点的子孙。如上图:所有节点都是A的子孙

    森林:由m(m > 0)棵互不相交的树的集合称为森林

    这些概念是树中常用名词,无论是为了做题还是为了工作,都应当记住。

    二、二叉树

    二叉树的每个节点最多有两个子节点,分为左子树和右子树。二叉树中又有满二叉树和完全二叉树。

    满二叉树是每一层的节点数都达到最大。所以如果高度为h的满二叉树,它的节点数应当是2 ^ h - 1。

    完全二叉树:完全二叉树是一个效率比较高的结构,由满二叉树变种而来。要求除去最后一层外其他层节点数达到最大,最后一层节点数至少为1,且节点必须连续,比如A为根节点,BC作为第二层节点,B和C下各一个左子树或者右子树就是不连续,就不是完全二叉树。

    二叉树的特点

    1、若规定根节点的层数为1,则一棵非空二叉树的第i层上最多有2 ^ (i - 1)个节点。

    2、若规定根节点的层数为1,则深度为h的二叉树的最大节点数是2 ^ h - 1。

    3、对任何一棵二叉树,如果度为0,其叶节点个数为n0,度为2的分支节点个数为n2,则有n0 = n2 + 1。(度为0总比度为2的多一个

    4、若规定根节点的层数为1,具有n个节点的满二叉树的深度,h = log2(n + 1)。

    具体推算过程就不写了。

    三、这篇重点在于堆的实现

    二叉树有大堆和小堆之分,大堆就是父节点大于等于子节点,小堆就是父节点小于等于子节点。这篇写大堆。

    该用数组还是链表体现二叉树?

    假设是链表

    插入第一个后,需要用一个指针指向第二个。根节点如果有好几个子节点,那就得需要多个指针,可以在创建结构体时,里面放入多个指针,也可以简单点,建立一个指针数组,大小就是树的度。不过这样或许还是不够好,另有一个方法,创建结构体后,里面放入一个child和brother指针,child指向自己的子节点,brother则指向兄弟节点。比如A为祖先,下有3个子节点,child指向节点B,B的brother指向C,C的brother指向d。BCD三个节点如果有子节点,那就用child指向子节点,这样就方便了。

    现在想一下push功能,这是不是相当于尾插功能?所以我们需要第三个指针来指向尾部。插入后新数字需要和之前的数字比较。可是他该如何和其他元素比较?插入的新元素应当是brother或者child指向的数字,想访问指向新数字的节点,这里就得需要一个prev指针指向前面了吧?

    所以可以发现一个问题,我没办法随意的访问其他节点元素。push或者pop时就总会有些麻烦,那么现在转向数组。

    假设是数组

    数组可以随意访问其他元素,只要找到下标之间的规律即可。不过数组又该如何体现二叉树结构?这个并不是难事,我们需要脱离树状结构这个臆想图,真正在电脑里面存储时都是一块块空间,我们只需要使空间里的数字符合规律,可以简单地访问其他元素即可。现在以数组来完成二叉树。

    调用整个树之前先创建好空间,所以初始化函数里就不写malloc了。

    头文件里
    1. typedef int HPDataType;
    2. typedef struct Heap
    3. {
    4. HPDataType* a;
    5. HPDataType size;
    6. HPDataType capacity;
    7. }HP;
    Heap.c(包括push和pop,向上/下比较法)
    1. void HeapInit(HP* php)
    2. {
    3. assert(php);
    4. php->a = NULL;
    5. php->size = php->capacity = 0;
    6. }
    7. void HeapDestroy(HP* php)
    8. {
    9. assert(php);
    10. free(php->a);
    11. php->a = NULL;
    12. php->size = php->capacity = 0;
    13. }

    先放上这两个函数。然后开始push和pop的代码。

    现在的情况是已经存入了一些数据,要继续存入。

    该怎么体现树状结构?我们确定了要使用数组,数组为空的时候,push一个就是放在下标0位置处,push第二个就需要和数组里的元素进行比较来确定谁当祖先。之后一个个存入,我们都需要比较,放好位置,所以搞清楚这个做法也就完成了push函数的思路。进行比较的时候,应该跟谁比较?怎么去访问?如果是链表,我们可以通过指针,不过数组就需要找规律。先前已经说过,抛掉两个树状结构的臆想图,一个根节点只能访问两个子节点,那么就把两个子节点放在根节点之后,通过下标转换来找到子节点。比如根节点下标为0,两个子节点下标分别是1和2,1这个节点继续分支,占据了3和4下标,2这个节点继续分支,占据5和6下标,这样找到规律即可随机访问。那么我们开始写push和pop函数。

    插入之前先判断为不空

    1. void HeapPush(HP* php, HPDataType x)
    2. {
    3. assert(php);
    4. if (php->size == php->capacity)
    5. {
    6. int newCapacity = php->capacity == 0 ? 4 : php->capacity * 2;
    7. HPDataType* tmp = (HPDataType*)realloc(php->a, sizeof(HPDataType) * newCapacity);
    8. if (tmp == NULL)
    9. {
    10. perror("realloc fail");
    11. exit(-1);
    12. }
    13. php->a = tmp;
    14. php->capacity = newCapacity;
    15. }
    16. php->a[php->size] = x;
    17. php->size++;
    18. }

    先写出来这些代码。现在已经在尾部插入一个数据了,那么开始判断大小吧。兄弟节点没必要比较,如果新数字比父节点大,那么自然也就比兄弟节点大;如果小,那就不需要动位置。之后一步步往上挪,整个过程都不需要跟兄弟节点比较,只跟父节点比较,大于就交换位置,直到祖先节点,如果比根节点大,那么新插入的数据就称为新的祖先。和父节点交换位置,父节点也不需要再跟其他节点进行比较。这是向上比较法。两个比较的对象,一个下标是新插入的位置,另一个则是通过规律来寻找,规律就是(i - 1) / 2,就能找到它的父节点,交换完后继续向上找父节点。

    1. void Swap(HPDataType* s1, HPDataType* s2)
    2. {
    3. HPDataType tmp = *s1;
    4. *s1 = *s2;
    5. *s2 = tmp;
    6. }
    7. void AdjustUp(HPDataType* a, int child)
    8. {
    9. int parent = (child - 1) / 2;
    10. while (child)
    11. {
    12. if (a[child] > a[parent])
    13. {
    14. Swap(&a[child], &a[parent]);
    15. child = parent;
    16. parent = (child - 1) / 2;
    17. }
    18. else
    19. {
    20. break;
    21. }
    22. }
    23. }
    24. void HeapPush(HP* php, HPDataType x)
    25. {
    26. assert(php);
    27. if (php->size == php->capacity)
    28. {
    29. int newCapacity = php->capacity == 0 ? 4 : php->capacity * 2;
    30. HPDataType* tmp = (HPDataType*)realloc(php->a, sizeof(HPDataType) * newCapacity);
    31. if (tmp == NULL)
    32. {
    33. perror("realloc fail");
    34. exit(-1);
    35. }
    36. php->a = tmp;
    37. php->capacity = newCapacity;
    38. }
    39. php->a[php->size] = x;
    40. php->size++;
    41. AdjustUp(php->a, php->size - 1);
    42. }

    向上比较函数里,判断条件是child > 0。为什么不能是 >= 0?当最后和祖先节点比较时,如果还是更大,那么child就变成下标0位置了,这时候整个过程应当结束,如果是>= 0,while还会继续,就会越界访问了。

    那么数组为空时,这个push函数有没有效?还是可以的,因为child为0,循环进不去,UP函数就会break了。再插入一个数据,进入函数,就进行比较,排好位置,会发现整个结构会按照大堆方向排列。写一个测试代码

    1. void HeapPrint(HP* php)
    2. {
    3. assert(php);
    4. for (int i = 0; i < php->size; i++)
    5. {
    6. printf("%d ", php->a[i]);
    7. }
    8. printf("\n");
    9. }
    10. void TestHeap1()
    11. {
    12. int arr[] = { 27, 15, 19, 18, 28, 34, 65, 49, 25, 37 };
    13. HP hp;
    14. HeapInit(&hp);
    15. for (int i = 0; i < sizeof(arr) / sizeof(int); i++)
    16. {
    17. HeapPush(&hp, arr[i]);
    18. }
    19. HeapPrint(&hp);
    20. HeapDestroy(&hp);
    21. }
    22. int main()
    23. {
    24. TestHeap1();
    25. return 0;
    26. }

    结果就是


    所以没问题

    接下来看pop函数。

    删除根部元素如何删除?尾部元素很好删除,就是尾删功能,下标最后一个删除,而且也没有改变节点之间的关系,因为它是一个子节点,但是根部元素的删除确实有点麻烦。因为这相当于删除祖先,我们就需要再找一个节点当祖先。这里能不能挪动覆盖?把49及之后的数据往前挪一次,这样头部就删除了,但是挪动数据时间复杂度为O(N),,且这样挪动的话,节点之间的关系就乱了,很有可能子节点比父节点数值大,所以不能挪动。那如果是把49覆盖到65上,然后再往后找数值放到49这个位置呢?其实也不好做,画图仔细想想,会发现挪动的每个数据的下标不好找规律。

    现在有另一个方法。把最后一个数字和第一个数字调换一下,这两个下标很好找,尾删一下,也就是size--,此时根部位置的数字变成了尾部数字,这时根部位置的数字一定比它现在的子节点的数要小。要改成正确的顺序其实就可以参照push函数的做法,不过这里是向下比较法。换过来后,和第二层的数字比较,找出大的那个,让它来做新的祖先,然后一层层向下探索。

    1. void AdjustDown(HPDataType* a, int n, int parent)
    2. {
    3. int child = parent * 2 + 1;
    4. while (child < n)
    5. {
    6. //确认child指向大的那个孩子并且child要小于size
    7. if (child + 1 < n && a[child + 1] > a[child])
    8. {
    9. ++child;
    10. }
    11. if (a[child] > a[parent])
    12. {
    13. Swap(&a[child], &a[parent]);
    14. parent = child;
    15. child = parent * 2 + 1;
    16. }
    17. else
    18. {
    19. break;
    20. }
    21. }
    22. }
    23. void HeapPop(HP* php)
    24. {
    25. assert(php);
    26. assert(php->size > 0);
    27. Swap(&php->a[0], &php->a[php->size - 1]);
    28. php->size--;
    29. AdjustDown(php->a, php->size, 0);
    30. }

    在之前的测试代码再加上几行

    1. void TestHeap1()
    2. {
    3. int arr[] = { 27, 15, 19, 18, 28, 34, 65, 49, 25, 37 };
    4. HP hp;
    5. HeapInit(&hp);
    6. for (int i = 0; i < sizeof(arr) / sizeof(int); i++)
    7. {
    8. HeapPush(&hp, arr[i]);
    9. }
    10. HeapPrint(&hp);
    11. int k = 5;
    12. while (k--)
    13. {
    14. printf("%d ", HeapTop(&hp));
    15. HeapPop(&hp);
    16. }
    17. HeapDestroy(&hp);
    18. }

    难题攻克了,我们把剩下的点一一写完

    1. HPDataType HeapTop(HP* php)
    2. {
    3. assert(php);
    4. assert(php->size > 0);
    5. return php->a[0];
    6. }
    7. HPDataType HeapSize(HP* php)
    8. {
    9. assert(php);
    10. return php->size;
    11. }
    12. bool HeapEmpty(HP* php)
    13. {
    14. assert(php);
    15. return php->size == 0;
    16. }

    最后,还有一个问题,有的时候给的接口里面会有创建堆这个函数,关于这个之后再写。先放上所有的代码

    Heap.h

    1. #pragma once
    2. #define _CRT_SECURE_NO_WARNINGS 1
    3. #include
    4. #include
    5. #include
    6. #include
    7. typedef int HPDataType;
    8. typedef struct Heap
    9. {
    10. HPDataType* a;
    11. HPDataType size;
    12. HPDataType capacity;
    13. }HP;
    14. void HeapInit(HP* php);
    15. void HeapDestroy(HP* php);
    16. void HeapPrint(HP* php);
    17. void HeapPush(HP* php, HPDataType x);
    18. void HeapPop(HP* php);
    19. HPDataType HeapTop(HP* php);
    20. HPDataType HeapSize(HP* hp);
    21. bool HeapEmpty(HP* hp);

    Heap.c

    1. void HeapInit(HP* php)
    2. {
    3. assert(php);
    4. php->a = NULL;
    5. php->size = php->capacity = 0;
    6. }
    7. void HeapDestroy(HP* php)
    8. {
    9. assert(php);
    10. free(php->a);
    11. php->a = NULL;
    12. php->size = php->capacity = 0;
    13. }
    14. void HeapPrint(HP* php)
    15. {
    16. assert(php);
    17. for (int i = 0; i < php->size; i++)
    18. {
    19. printf("%d ", php->a[i]);
    20. }
    21. printf("\n");
    22. }
    23. void Swap(HPDataType* s1, HPDataType* s2)
    24. {
    25. HPDataType tmp = *s1;
    26. *s1 = *s2;
    27. *s2 = tmp;
    28. }
    29. void AdjustUp(HPDataType* a, int child)
    30. {
    31. int parent = (child - 1) / 2;
    32. while (child)
    33. {
    34. if (a[child] < a[parent])
    35. {
    36. Swap(&a[child], &a[parent]);
    37. child = parent;
    38. parent = (child - 1) / 2;
    39. }
    40. else
    41. {
    42. break;
    43. }
    44. }
    45. }
    46. void HeapPush(HP* php, HPDataType x)
    47. {
    48. assert(php);
    49. if (php->size == php->capacity)
    50. {
    51. int newCapacity = php->capacity == 0 ? 4 : php->capacity * 2;
    52. HPDataType* tmp = (HPDataType*)realloc(php->a, sizeof(HPDataType) * newCapacity);
    53. if (tmp == NULL)
    54. {
    55. perror("realloc fail");
    56. exit(-1);
    57. }
    58. php->a = tmp;
    59. php->capacity = newCapacity;
    60. }
    61. php->a[php->size] = x;
    62. php->size++;
    63. AdjustUp(php->a, php->size - 1);
    64. }
    65. void AdjustDown(HPDataType* a, int n, int parent)
    66. {
    67. int child = parent * 2 + 1;
    68. while (child < n)
    69. {
    70. //确认child指向大的那个孩子并且child要小于size
    71. if (child + 1 < n && a[child + 1] < a[child])
    72. {
    73. ++child;
    74. }
    75. //1、孩子大于父亲,交换,继续向下调整
    76. //2、孩子小于父亲,则调整结束
    77. if (a[child] < a[parent])
    78. {
    79. Swap(&a[child], &a[parent]);
    80. parent = child;
    81. child = parent * 2 + 1;
    82. }
    83. else
    84. {
    85. break;
    86. }
    87. }
    88. }
    89. void HeapPop(HP* php)
    90. {
    91. assert(php);
    92. assert(php->size > 0);
    93. Swap(&php->a[0], &php->a[php->size - 1]);
    94. php->size--;
    95. AdjustDown(php->a, php->size, 0);
    96. }
    97. HPDataType HeapTop(HP* php)
    98. {
    99. assert(php);
    100. assert(php->size > 0);
    101. return php->a[0];
    102. }
    103. HPDataType HeapSize(HP* php)
    104. {
    105. assert(php);
    106. return php->size;
    107. }
    108. bool HeapEmpty(HP* php)
    109. {
    110. assert(php);
    111. return php->size == 0;
    112. }


     

    Test.c

    1. #include "Heap.h"
    2. /*void TestHeap1()
    3. {
    4.     int arr[] = { 27, 15, 19, 18, 28, 34, 65, 49, 25, 37 };
    5.     HP hp;
    6.     HeapInit(&hp);
    7.     for (int i = 0; i < sizeof(arr) / sizeof(int); i++)
    8.     {
    9.         HeapPush(&hp, arr[i]);
    10.     }
    11.     HeapPrint(&hp);
    12.     int k = 5;
    13.     while (k--)
    14.     {
    15.         printf("%d ", HeapTop(&hp));
    16.         HeapPop(&hp);
    17.     }
    18.     HeapDestroy(&hp);
    19. }*/
    20. void TestHeap2()
    21. {
    22. int arr[] = { 27, 15, 19, 18, 28, 34, 65, 49, 25, 37 };
    23. HP hp;
    24. HeapInit(&hp);
    25. for (int i = 0; i < sizeof(arr) / sizeof(int); i++)
    26. {
    27. HeapPush(&hp, arr[i]);
    28. }
    29. HeapPrint(&hp);
    30. while (!HeapEmpty(&hp))
    31. {
    32. printf("%d ", HeapTop(&hp));
    33. HeapPop(&hp);
    34. }
    35. HeapDestroy(&hp);
    36. }
    37. int main()
    38. {
    39. //TestHeap1();
    40. TestHeap2();
    41. return 0;
    42. }

    结束。下一篇再补上创建堆的接口。

  • 相关阅读:
    Vue3+vite中引入Echarts图表
    基于Bootstrap+Django+Python的点菜信息管理系统
    【数据结构】优先级队列(堆)
    vue 项目 前端 模拟后端接口数据(vue2,vue3)
    第2-1-4章 SpringBoot整合FastDFS文件存储服务
    欧科云链做客Google Cloud与WhalerDAO专题论坛,畅谈Web3数据机遇
    RabbitMQ------交换机(fanout(扇出)、direct(直接)、topic(主题))(五)
    林乐博士走进中国人民大学,讲述区块链的理解与实践
    英码边缘计算盒子IVP03X——32T超强算力,搭载BM1684X算能TPU处理器
    Git学习笔记
  • 原文地址:https://blog.csdn.net/kongqizyd146/article/details/127920002