【数据结构】二叉树的基本操作与遍历(C语言)

简介: 【数据结构】二叉树的基本操作与遍历(C语言)

定义


🦄二叉树是由树发展过来的,即度最大为2的树,且子树有左右之分,可以这么理解,二叉树是空结点跟左右子树的结合体。

image.png

🦄下面这张图可能更好理解一点,任何二叉树都是下列几种情况复合而成的。因此只要这个树的度超过 2 ,那么它就不是二叉树。

image.png

满二叉树


🦄满二叉树是一种特殊的二叉树,即每一层结点都到达最大值。举个简单的例子,假设这个二叉树根结点在 1 层且一共有 i 层,若结点总数为(2^i) -1 个那么这个二叉树就是满二叉树。

image.png

完全二叉树


🦄可以说满二叉树也是一种特殊的完全二叉树,完全二叉树的底层的结点的排序从左到右都是连续的。若假设下面这张图里的F结点还有一个左结点,那么这个二叉树就不是完全二叉树了。

image.png

性质


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

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

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

4. 若规定根节点的层数为 1 ,具有 n 个结点的满二叉树的深度,h = log (n + 1) 。(ps: 是 log 以2为底,n+1 为对数)

5. 对于具有 n 个结点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有节点从 0 开始编号,则对于序号为 i 的结点有:

1. 若 i>0 ,i 位置节点的双亲为:( i - 1 ) / 2 。

2. 若 2i + 1 < n ,左孩子为:2i+1 ,2i + 1 >= n否则无左孩子。

3. 若2i + 2 < n,右孩子为:2i + 2,2i + 2 >= n否则无右孩子。

应用


🦄与堆不同,二叉树是多个结点链接起来的链式结构,因此对应其特性,每一个根节点都指向左右两个结点。

typedef int BTdatatype;
typedef struct BinaryTreeNode 
{
  BTdatatype data;                 //数据
  struct BinaryTreeNode* left;     //左结点
  struct BinaryTreeNode* right;    //右结点
}BTNode;

🦄而这里二叉树的实现主要依靠的是递归,为的便是能够在访问完一个子树后还能返回到它原来的根结点。

计算二叉树结点个数


🦄这里开始递归就初见雏形,需要一次次递归遍历整个二叉树来计算结点个数,通过返回值的方式将数值统计起来。(若使用全局变量,在多次调用该函数的时候就会出现全局变量未初始化的情况而导致结果出错)

int BinaryTreeSize(BTNode* root)
{
  if (root == NULL)
  {
    return 0;         //遇到空结点就返回0(代表0个结点)
  }
  return BinaryTreeSize(root->left) + BinaryTreeSize(root->right) + 1;  //左右子树结点加上自己的总个数
}

计算叶子结点的个数


🦄这题跟上一题类似,但还需要加上一个条件筛选叶子结点。

// 二叉树叶子节点个数
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); //两边叶子结点的个数
}

第 k 层结点的个数


🦄这题的关键在于对 k 层的检索,既要找到 k 层的结点又要在 k 层前遇到空返回,则需要两个判断。举个例子而言,根节点距离 k 层的长度为 k-1 ,即第 2 层的结点距离 k 层的长度为 k-2 ,因此每次进一层的时候就把 k-- 当 k==1 的时候该结点就是 k 层的结点,因此只需要统计这些结点的个数就可以了。

// 二叉树第k层节点个数
int BinaryTreeLevelKSize(BTNode* root, int k)
{
  if (root == NULL)                  //空结点不计数
  {
    return 0;
  }
  if (k == 1 && root != NULL)        //到k层且结点不为空则计数
  {
    return 1;
  }
  return BinaryTreeLevelKSize(root->left, k - 1) + BinaryTreeLevelKSize(root->right, k - 1);  //统合数量
}

查找值为x的节点


🦄这题要注意的是对结点的值的返回,我们都知道一次的结果并不一定会影响到最终返回的结果,因此处理好一找到点就立刻返回的方法非常重要。这里就通过对返回值的判断,只有找到目标结点时才会直接层层返回。

// 二叉树查找值为x的节点
BTNode* BinaryTreeFind(BTNode* root, BTdatatype x)
{
  if (root == NULL)
  {
    return 0;                      
  }
  if (root->data == x)               //找到值为x的结点返回
  {
    return root;
  }
  BTNode* ret1 = BinaryTreeFind(root->left, x);    //往左找x
  if (ret1)                                        //有返回值则继续返回
  { 
    return ret1;
  }
  BTNode* ret2 = BinaryTreeFind(root->right, x);    //往右找x
  if(ret2)                                          //有返回值继续返回
  {
    return ret2;
  }
}

遍历


🦄遍历是二叉树上最重要的运算之一,也是二叉树上进行其它运算的基础。根据规则的不同,二叉树的遍历可以分作四种,分别是前序、中序、后序以及层序遍历。

前序遍历


传送门:144.二叉树的前序遍历

🦄口诀:根 左 右,即先访问根结点后再依次访问左右结点,倒也像一个人从根结点绕着外围跑遍了整个二叉树。

image.png

// 二叉树前序遍历 
void BinaryTreePrevOrder(BTNode* root)
{
  if (root == NULL)                //为空返回
  {
    printf("NULL ");
    return 0;
  }
  printf("%d ", root->data);         //先打印根节点的数据
  BinaryTreePrevOrder(root->left);   //访问左子树
  BinaryTreePrevOrder(root->right);  //访问右子树
}

中序遍历


传送门:94.二叉树的中序遍历

🦄口诀:左 根 右,中序遍历则需要先访问左子树之后再访问根结点,最后才是右子树。简单地看就是从左到右依次把结点拿下来并访问。

image.png

// 二叉树中序遍历
void BinaryTreeInOrder(BTNode* root)
{
  if (root == NULL)                   //为空返回
  {
    printf("NULL ");
    return 0;
  }
  BinaryTreeInOrder(root->left);   //先访问左子树
  printf("%d ", root->data);       //打印根结点
  BinaryTreeInOrder(root->right);  //再访问右子树
}

后序遍历


传送门:145.二叉树的后序遍历

🦄口诀:左 右 根 ,后序遍历则是先遍历两个子树之后才是访问根结点。即要访问这个结点它的左右子树都必须先遍历一遍。可以看成一个结点必须是叶结点才能对其访问,若非叶结点就先访问并“删除”左右结点后让原结点变成叶结点之后再进行访问。

image.png

// 二叉树后序遍历
void BinaryTreePostOrder(BTNode* root)
{
  if (root == NULL)                  //为空返回
  {
    printf("NULL ");
    return 0;
  }
  BinaryTreePostOrder(root->left);   //先遍历左子树
  BinaryTreePostOrder(root->right);  //再遍历右子树
  printf("%d ", root->data);         //最后打印根结点
}

层序遍历


传送门:102. 二叉树的层序遍历

🦄正如其名,层序遍历就是以层为单位进行遍历。若要实现层序遍历的话,所需的思想与上面的遍历就完全不同。因为之前的遍历是以深度优先进行的,而层序遍历需要的是广度优先的遍历。

image.png

🦄为了实现依层遍历的功能,我们需要使用队列来辅助实现。第一次传入的是根结点,每次访问完根结点之后把队头删除,再把队头对应的左右子树对应的指针传入队列之中,根据队列先进先出的性质,所以后传入的结点就会较后地被访问。

ab157a7f629d42b6ad903d6bec641a02.png

typedef BTNode* Qdatatype;
typedef struct Qnode
{
  Qdatatype data;          //数据
  struct Queue* next;      //指向下个结点
}Qnode;
typedef struct Queue
{
  Qnode* head;             //队列的头
  Qnode* tail;             //队列的尾
  int size;                //大小
}Queue;
void Queueinit(Queue* p)
{
  p->head = NULL;           //头尾结点制空
  p->tail = NULL;
  p->size = 0;              //数据量为0
}
bool QueueEmpty(Queue* p)
{
  assert(p);
  return p->head == NULL || p->tail == NULL;    //二者一个为空则队列为空
}
void Queuepush(Queue* p, Qdatatype x)
{
  assert(p);                //断言
  Qnode* newnode = (Qnode*)malloc(sizeof(Qnode));   //开辟新结点
  if (newnode == NULL)              //开辟失败返回
  {
    perror(malloc);
    exit(-1);
  }
  newnode->data = x;                //赋值
  newnode->next = NULL;
  if (p->head == NULL)              //队列为空的情况
  {
    p->head = p->tail = newnode;
  }
  else
  {
    p->tail->next = newnode;      //链接
    p->tail = newnode;
  }
  p->size++;                        //对size进行处理
}
void Queuepop(Queue* p)
{
  assert(p);                      //断言p不为空
  assert(!QueueEmpty(p));         //断言队列不为空
  Qnode* next = p->head->next;    //存储下一结点
  free(p->head);                  //释放当先头结点
  p->head = next;                 //下一结点变成头结点
  p->size--;                      //对size进行处理
}
Qdatatype Queuefront(Queue* p)
{
  assert(p);
  assert(!QueueEmpty(p));         //断言不为空
  return p->head->data;           //头结点存储的就是队头的数据
}
void QueueDestroy(Queue* p)
{
  assert(p);
  Qnode* cur = p->head;
  while (cur)
  {
    Qnode* next = cur->next;
    free(cur);
    cur = next;
  }
  p->head = p->tail = NULL;       //头尾结点制空
  p->size = 0;                    //大小归0
}
// 层序遍历
void BinaryTreeLevelOrder(BTNode* root)
{
  assert(root);
  Queue q;
  Queueinit(&q);                    //构建队列
  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); //插入队头的右结点
    }
  }
  printf("\n");
  QueueDestroy(&q);                    //销毁队列
}

判断是否为完全二叉树


🦄这个问题是在层序遍历的基础之上实现的,我们知道判断是否为完全二叉树的关键在于最后一层前都为满,最后一层的结点必须连续。因此我们可以这样想,只要层序遍历直到遇到空指针就停止,之后判断队列里面还有没有非空结点。若队列里都是空指针则说明这棵树为完全二叉树。

1b965a84d3834c5085b6586d12678eae.png

// 判断二叉树是否是完全二叉树
int BinaryTreeComplete(BTNode* root)
{
  assert(root);
  Queue q;
  Queueinit(&q);
  Queuepush(&q, root);
  while (!QueueEmpty(&q))          
  {
    BTNode* front = Queuefront(&q);     //取队头
    if (front == NULL)                  //队头不为空则继续循环
    {
      break;
    }
    Queuepop(&q);
    Queuepush(&q, front->left);         //插入左右结点
    Queuepush(&q, front->right);
  }
  while (!QueueEmpty(&q))                 
  {
    BTNode* front = Queuefront(&q);      
    if (front != NULL)                  //只要判断队列之后还有没有非空结点
    {
      QueueDestroy(&q);
      return -1;
    }
    Queuepop(&q);
  }
  QueueDestroy(&q);
  return 1;
}

🦙那么,今天二叉树的基本操作与遍历就告一段落,如果这篇文章对你有帮助的话,不妨留下三连支持以下博主,谢谢大家!!!

image.png

目录
相关文章
|
17天前
|
算法 数据处理 C语言
C语言中的位运算技巧,涵盖基本概念、应用场景、实用技巧及示例代码,并讨论了位运算的性能优势及其与其他数据结构和算法的结合
本文深入解析了C语言中的位运算技巧,涵盖基本概念、应用场景、实用技巧及示例代码,并讨论了位运算的性能优势及其与其他数据结构和算法的结合,旨在帮助读者掌握这一高效的数据处理方法。
28 1
|
25天前
|
存储 算法 搜索推荐
【趣学C语言和数据结构100例】91-95
本文涵盖多个经典算法问题的C语言实现,包括堆排序、归并排序、从长整型变量中提取偶数位数、工人信息排序及无向图是否为树的判断。通过这些问题,读者可以深入了解排序算法、数据处理方法和图论基础知识,提升编程能力和算法理解。
41 4
|
25天前
|
存储 机器学习/深度学习 搜索推荐
【趣学C语言和数据结构100例】86-90
本文介绍并用C语言实现了五种经典排序算法:直接插入排序、折半插入排序、冒泡排序、快速排序和简单选择排序。每种算法都有其特点和适用场景,如直接插入排序适合小规模或基本有序的数据,快速排序则适用于大规模数据集,具有较高的效率。通过学习这些算法,读者可以加深对数据结构和算法设计的理解,提升解决实际问题的能力。
38 4
|
25天前
|
存储 算法 数据处理
【趣学C语言和数据结构100例】81-85
本文介绍了五个经典算法问题及其C语言实现,涵盖图论与树结构的基础知识。包括使用BFS求解单源最短路径、统计有向图中入度或出度为0的点数、统计无向无权图各顶点的度、折半查找及二叉排序树的查找。这些算法不仅理论意义重大,且在实际应用中极为广泛,有助于提升编程能力和数据结构理解。
35 4
|
25天前
|
算法 数据可视化 数据建模
【趣学C语言和数据结构100例】76-80
本文介绍了五种图论算法的C语言实现,涵盖二叉树的层次遍历及广度优先搜索(BFS)和深度优先搜索(DFS)的邻接表与邻接矩阵实现。层次遍历使用队列按层访问二叉树节点;BFS利用队列从源节点逐层遍历图节点,适用于最短路径等问题;DFS通过递归或栈深入图的分支,适合拓扑排序等场景。这些算法是数据结构和算法学习的基础,对提升编程能力和解决实际问题至关重要。
40 4
|
25天前
|
存储 算法 vr&ar
【趣学C语言和数据结构100例】71-75
本文介绍了五个C语言数据结构问题及其实现,涵盖链表与二叉树操作,包括按奇偶分解链表、交换二叉树左右子树、查找节点的双亲节点、计算二叉树深度及求最大关键值。通过递归和遍历等方法,解决了理论与实际应用中的常见问题,有助于提升编程能力和数据结构理解。
34 4
|
25天前
|
存储 算法 C语言
【趣学C语言和数据结构100例】66-70
本书《趣学C语言和数据结构100例》精选了5个典型的数据结构问题及C语言实现,涵盖链表与数组操作,如有序集合的集合运算、有序序列表的合并、数组中两顺序表位置互换、三递增序列公共元素查找及奇偶数重排。通过详细解析与代码示例,帮助读者深入理解数据结构与算法设计的核心思想,提升编程技能。
32 4
|
18天前
|
存储 缓存 算法
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式,强调了合理选择数据结构的重要性,并通过案例分析展示了其在实际项目中的应用,旨在帮助读者提升编程能力。
40 5
|
18天前
|
存储 算法 C语言
C语言中常见的字符串处理技巧,包括字符串的定义、初始化、输入输出、长度计算、比较、查找与替换、拼接、截取、转换、遍历及注意事项
本文深入探讨了C语言中常见的字符串处理技巧,包括字符串的定义、初始化、输入输出、长度计算、比较、查找与替换、拼接、截取、转换、遍历及注意事项,并通过案例分析展示了实际应用,旨在帮助读者提高编程效率和代码质量。
54 4
|
17天前
|
并行计算 算法 测试技术
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面,旨在通过综合策略提升程序性能,满足实际需求。
43 1

热门文章

最新文章