【数据结构】二叉树的基本操作与遍历(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

目录
相关文章
|
3天前
|
C语言
【数据结构】栈和队列(c语言实现)(附源码)
本文介绍了栈和队列两种数据结构。栈是一种只能在一端进行插入和删除操作的线性表,遵循“先进后出”原则;队列则在一端插入、另一端删除,遵循“先进先出”原则。文章详细讲解了栈和队列的结构定义、方法声明及实现,并提供了完整的代码示例。栈和队列在实际应用中非常广泛,如二叉树的层序遍历和快速排序的非递归实现等。
45 9
|
2天前
|
存储 搜索推荐 算法
【数据结构】树型结构详解 + 堆的实现(c语言)(附源码)
本文介绍了树和二叉树的基本概念及结构,重点讲解了堆这一重要的数据结构。堆是一种特殊的完全二叉树,常用于实现优先队列和高效的排序算法(如堆排序)。文章详细描述了堆的性质、存储方式及其实现方法,包括插入、删除和取堆顶数据等操作的具体实现。通过这些内容,读者可以全面了解堆的原理和应用。
32 16
|
2天前
|
C语言
【数据结构】二叉树(c语言)(附源码)
本文介绍了如何使用链式结构实现二叉树的基本功能,包括前序、中序、后序和层序遍历,统计节点个数和树的高度,查找节点,判断是否为完全二叉树,以及销毁二叉树。通过手动创建一棵二叉树,详细讲解了每个功能的实现方法和代码示例,帮助读者深入理解递归和数据结构的应用。
28 8
|
4天前
|
存储 C语言
【数据结构】手把手教你单链表(c语言)(附源码)
本文介绍了单链表的基本概念、结构定义及其实现方法。单链表是一种内存地址不连续但逻辑顺序连续的数据结构,每个节点包含数据域和指针域。文章详细讲解了单链表的常见操作,如头插、尾插、头删、尾删、查找、指定位置插入和删除等,并提供了完整的C语言代码示例。通过学习单链表,可以更好地理解数据结构的底层逻辑,提高编程能力。
26 4
|
6天前
|
存储 C语言
【数据结构】顺序表(c语言实现)(附源码)
本文介绍了线性表和顺序表的基本概念及其实现。线性表是一种有限序列,常见的线性表有顺序表、链表、栈、队列等。顺序表是一种基于连续内存地址存储数据的数据结构,其底层逻辑是数组。文章详细讲解了静态顺序表和动态顺序表的区别,并重点介绍了动态顺序表的实现,包括初始化、销毁、打印、增删查改等操作。最后,文章总结了顺序表的时间复杂度和局限性,并预告了后续关于链表的内容。
24 3
|
4天前
|
C语言
【数据结构】双向带头循环链表(c语言)(附源码)
本文介绍了双向带头循环链表的概念和实现。双向带头循环链表具有三个关键点:双向、带头和循环。与单链表相比,它的头插、尾插、头删、尾删等操作的时间复杂度均为O(1),提高了运行效率。文章详细讲解了链表的结构定义、方法声明和实现,包括创建新节点、初始化、打印、判断是否为空、插入和删除节点等操作。最后提供了完整的代码示例。
20 0
|
1天前
|
存储
系统调用处理程序在内核栈中保存了哪些上下文信息?
【10月更文挑战第29天】系统调用处理程序在内核栈中保存的这些上下文信息对于保证系统调用的正确执行和用户程序的正常恢复至关重要。通过准确地保存和恢复这些信息,操作系统能够实现用户模式和内核模式之间的无缝切换,为用户程序提供稳定、可靠的系统服务。
20 4
|
25天前
|
算法 程序员 索引
数据结构与算法学习七:栈、数组模拟栈、单链表模拟栈、栈应用实例 实现 综合计算器
栈的基本概念、应用场景以及如何使用数组和单链表模拟栈,并展示了如何利用栈和中缀表达式实现一个综合计算器。
25 1
数据结构与算法学习七:栈、数组模拟栈、单链表模拟栈、栈应用实例 实现 综合计算器
|
6天前
|
算法 安全 NoSQL
2024重生之回溯数据结构与算法系列学习之栈和队列精题汇总(10)【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丢脸好嘛?】
数据结构王道第3章之IKUN和I原达人之数据结构与算法系列学习栈与队列精题详解、数据结构、C++、排序算法、java、动态规划你个小黑子;这都学不会;能不能不要给我家鸽鸽丢脸啊~除了会黑我家鸽鸽还会干嘛?!!!
|
25天前
初步认识栈和队列
初步认识栈和队列
53 10