数据结构---手撕图解二叉树(含大量递归图解)

文章目录

  • 写在前面
  • 二叉树的创建
  • 二叉树的遍历
    • 前序遍历
    • 中序遍历
    • 后序遍历
    • 层序遍历
  • 二叉树的销毁
  • 二叉树节点个数
  • 二叉树叶子节点的个数
  • 二叉树查找值为x的节点
  • 二叉树是否为完全二叉树

写在前面

二叉树的几乎所有实现都是依靠递归实现,递归的核心思路是把任何一个二叉树看成根和左右子树,而二叉树递归的核心玩法就是把二叉树的左右子树再看成根,再找左右子树,再看成根…

因此,解决二叉树问题实际上要把二叉树转换成一个一个子树的过程,找到一个一个的子树再组装起来就形成了二叉树

二叉树的创建

二叉树建立的正统方法是利用递归,这里展示递归的一种写法

BTNode* BuyNode(BTDataType a)
{BTNode* newnode = (BTNode*)malloc(sizeof(BTNode));newnode->data = a;newnode->left = NULL;newnode->left = NULL;return newnode;
}BTNode* BinaryTreeCreate(BTDataType* a, int* pi)
{if (a[*pi] == '#'){(*pi)++;return NULL;}BTNode* root = BuyNode(a[*pi]);(*pi)++;root->left = BinaryTreeCreate(a, pi);root->right = BinaryTreeCreate(a, pi);return root;
}

如果你对递归的认识并不熟悉,下面我画了一幅递归展开图,更好解释这其中的原理

在这里插入图片描述
(右子树的部分过程由于空间原因不够完全展开,但如果看懂左子树的构建过程,右子树不难自己画出)

这里注意的是,函数栈帧的创建并不是每一个都不一样的位置,当一个栈帧被销毁后,另外一个栈帧创建就会在原来的位置,因此在计算空间复杂度的时候需要考虑这个问题:
空间是可以重复利用的,时间是一去不复返的

从这幅图中,相信可以理解这段代码的含义了

首先创建一个根节点,根节点去寻找左子树进入递归,而进入递归后继续寻找左子树…直到遇到NULL截止,而遇到空后就寻找右子树,右子树也找到空后返回,这样就构建出了二叉树中最小的一棵树,而这棵树会返回,作为根的左子树,然后继续进入递归寻找根的右子树…

由于是借助链表进行实现的,因此我们可以理解为首先创建好根,而根的左右子树用函数创建好后再连接起来…当遇到#就返回,形成了一个一个的小树,小树最后组成大树,就形成了二叉树

二叉树的遍历

二叉树的遍历主要有前序遍历,中序遍历,后序遍历,层序遍历

前序遍历

前序遍历是指遍历时先访问根,再访问左子树,再访问右子树

代码实现如下

void BinaryTreePrevOrder(BTNode* root)
{if (root == NULL){printf("N ");return;}printf("%c ", root->data);BinaryTreePrevOrder(root->left);BinaryTreePrevOrder(root->right);
}

下面依旧画出它的递归展开图

在这里插入图片描述

中序遍历

中序遍历是先访问左子树,再访问根,最后访问右子树

void BinaryTreeInOrder(BTNode* root)
{if (root == NULL){printf("N ");return;}BinaryTreeInOrder(root->left);printf("%c ", root->data);BinaryTreeInOrder(root->right);
}

递归图和上面基本类似,就不做画出了

后序遍历

后序遍历是先访问左子树,再访问右子树,最后访问根

void BinaryTreePostOrder(BTNode* root)
{if (root == NULL){printf("N ");return;}BinaryTreePostOrder(root->left);BinaryTreePostOrder(root->right);printf("%c ", root->data);
}

递归图和上面基本类似,就不做画出了

层序遍历

层序遍历指的是对二叉树进行一层一层的遍历,每一层分别遍历,这里借助队列进行遍历,基本思路把根放到队列中,当某一个根要出队列时,就令该根的左子树和右子树进队列,这样就能实现一层一层遍历,画法如下:

在这里插入图片描述
代码实现相较于前面来说简单一些,但需要引入队列的,关于队列的介绍:

数据结构—手撕队列栈并相互实现

下面展示要使用的队列的相关函数实现

// queue.h
typedef struct BinaryTreeNode* QDataType;typedef struct QNode
{QDataType data;struct QNode* next;
}QNode;typedef struct Queue
{QNode* phead;QNode* ptail;int size;
}Queue;// queue.c
void QueueInit(Queue* pq)
{assert(pq);pq->phead = pq->ptail = NULL;pq->size = 0;
}void QueueDestroy(Queue* pq)
{assert(pq);QNode* cur = pq->phead;while (cur){QNode* next = cur->next;free(cur);cur = next;}pq->phead = pq->ptail = NULL;pq->size = 0;
}QNode* BuyQnode(QDataType x)
{QNode* newnode = (QNode*)malloc(sizeof(QNode));if (newnode == NULL){perror("malloc fail");return NULL;}newnode->data = x;newnode->next = NULL;return newnode;
}void QueuePush(Queue* pq, QDataType x)
{assert(pq);QNode* newnode = BuyQnode(x);if (pq->ptail == NULL){assert(pq->phead == NULL);pq->phead = pq->ptail = newnode;}else{pq->ptail->next = newnode;pq->ptail = newnode;}pq->size++;
}bool QueueEmpty(Queue* pq)
{if (pq->size == 0){return true;}return false;
}void QueuePop(Queue* pq)
{assert(pq);assert(!QueueEmpty(pq));if (pq->phead->next == NULL){free(pq->phead);pq->phead = pq->ptail = NULL;}else{QNode* newhead = pq->phead->next;free(pq->phead);pq->phead = newhead;}pq->size--;
}QDataType QueueFront(Queue* pq)
{assert(pq);assert(!QueueEmpty(pq));return pq->phead->data;
}QDataType QueueBack(Queue* pq)
{assert(pq);return pq->ptail->data;
}int QueueSize(Queue* pq)
{assert(pq);return pq->size;
}

那么借助队列我们来实现刚才的层序遍历

void BinaryTreeLevelOrder(BTNode* root)
{Queue q;QueueInit(&q);if (root){QueuePush(&q, root);}while (!QueueEmpty(&q)){BTNode* front = QueueFront(&q);printf("%c ", front->data);QueuePop(&q);if (front->left)QueuePush(&q, front->left);if(front->right)QueuePush(&q, front->right);}printf("\n");//BinaryTreeDestory(&q);
}

二叉树的销毁

在知道了遍历的多种途径后,二叉树的销毁就很简单了,在确定代码如何实现前,先思考问题:应该选用哪种遍历来进行二叉树的销毁?

结果是很明显的,选用后序遍历是最方便的,原因在于销毁是需要进行节点的释放的,如果使用前序或者中序遍历,那么在遍历的过程中根节点会被先销毁,那么找左子树或者右子树就带来了不便(可以定义一个变量保存位置),因此最好用后序遍历,把子树都销毁了最后销毁根

void BinaryTreeDestory(BTNode* root)
{if (root == NULL){return;}BinaryTreeDestory(root->left);BinaryTreeDestory(root->right);free(root);
}

二叉树节点个数

二叉树节点个数也是转换成子树来解决,如果为NULL就返回0,其他情况返回左右相加再加上节点本身

int BinaryTreeSize(BTNode* root)
{if (root == NULL){return 0;}return 1 + BinaryTreeSize(root->left) + BinaryTreeSize(root->right);
}

二叉树叶子节点的个数

叶子节点个数求解和上述方法相似,也是找最小的子树,但不同的地方是,叶子节点找到的条件是叶子作为根,它的左右子树都为空,符合这样的就是叶子节点,那么根据这个想法求得代码不难得到:

int BinaryTreeLeafSize(BTNode* root)
{if (root == NULL){return 0;}if (root->left == NULL && root->right == NULL){return 1;}int leftleave = BinaryTreeSize(root->left);int rightleave = BinaryTreeSize(root->right);return leftleave + rightleave;
}

二叉树查找值为x的节点

这个相对较麻烦一点,先上展开图和代码

BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{if (root == NULL){return NULL;}if (root->data == x){return root;}BTNode* ret1 = BinaryTreeFind(root->left, x);if (ret1)return ret1;BTNode* ret2 = BinaryTreeFind(root->right, x);if (ret2)return ret2;return NULL;
}

在这里插入图片描述
这里的关键动作就是判断ret1和ret2是否为空,如果是,则说明找到正确的值,那么就会一路被送出函数,相反会一直返回NULL,因此就不必担心送出其他元素的情况

二叉树是否为完全二叉树

这个思路也较为复杂,首先,需要解决的问题是用什么思路,基本思路是一个一个看,如果在某一个节点已经为空的前提下,它后面的节点还不为空,那么就说明这个不是完全二叉树

因此这个思路是不是和层序遍历很像呢?我们借助这样的思路继续写下去

当队列不为空就继续入队列出队列进行层序遍历,当遇到空就跳出,跳出循环后此时队列并不为空,继续出队列,如果发现某个元素不为空,那么就证明这并不是完全二叉树

代码实现如下:

int BinaryTreeComplete(BTNode* root)
{assert(root);Queue q;QueueInit(&q);while (!QueueEmpty(&q)){BTNode* front = QueueFront(&q);QueuePop(&q);if (front == NULL){break;}QueuePush(&q,front->left);QueuePush(&q,front->right);}while (!QueueEmpty(&q)){BTNode* front = QueueFront(&q);QueuePop(&q);if (front){BinaryTreeDestory(&q);return 0;}}BinaryTreeDestory(&q);return 1;
}

至此,二叉树的基本功能实现就都结束了,很明显,二叉树的实现并不容易,它不仅需要对递归有一定深度的认识,还需要你对队列有足够的理解,才能解决层序遍历和判断完全二叉树

这需要进行更多的练习,也为后面更复杂的树打基础和铺垫

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/26815.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

idea专业版和idea社区版整合Tomcat,并将war包部署

文章目录 idea专业版和idea社区版整合Tomcat,并将war包部署一、idea专业版部署二、idea社区版部署三、错误案例 idea专业版和idea社区版整合Tomcat,并将war包部署 开发过程中,由于需要运用云平台,所以从新配置开发环境&#xff0…

编程导航算法通关村第 1关 | 单链表的操作

编程导航算法通关村第 1关 | 链表的操作 文章目录 编程导航算法通关村第 1关 | 链表的操作单链表链表的定义初始化链表的遍历获取链表的长度链表的插入链表的节点的删除 双向链表节点的定义双向链表的定义节点的打印获取长度头部插入元素尾部插入元素链表的删除 单链表 链表的…

红队打靶:billu_b0x打靶思路详解(vulnhub)

目录 写在开头 第一步:主机发现和端口扫描 第二步:Web渗透 第三步:利用文件包含进行代码审计 第四步:图片马上传与反弹shell 第五步:敏感文件提权 总结与思考 写在开头 我的博客等级终于到三级了,…

部署langchain+chatglm

先参考:window零基础部署langchain-ChatGLM_飞奔的屎壳郎的博客-CSDN博客 安装一部分, 1.GCC安装 gcc64位下载 一定要装64位的gcc,因为我的电脑是w10 64位的,装32位运行langchain报错并配置环境变量 可直接用压缩包中的文件&am…

Web3的2048,Sui 8192能否打开全链游戏的大门?

作者:Peng SUN,Foresight News Sui 8192:一局游戏就是一个NFT Sui 8192智能合约基于Move语言编写,构成非常简单,包括游戏、Game Board与排行榜(Leaderboard)三部分,覆盖方块移动、…

Leetcode每日一题(困难):834. 树中距离之和(2023.7.16 C++)

目录 834. 树中距离之和 题目描述: 实现代码与解析: DFS 原理思路: 834. 树中距离之和 题目描述: 给定一个无向、连通的树。树中有 n 个标记为 0...n-1 的节点以及 n-1 条边 。 给定整数 n 和数组 edges , edge…

《C++程序设计原理与实践》笔记 第20章 容器和迭代器

本章和下一章将介绍STL,即C标准库的容器和算法部分。关键概念序列和迭代器用于将容器(数据)和算法(处理)联系在一起。 20.1 存储和处理数据 首先考虑一个简单的例子:Jack和Jill各自在测量车速&#xff0c…

GO语言slice

slice: data lencap 以及存取的元素是可以安全读写的 Slice 扩容。 1,预估: 2,预估容量后*字节数 所需的内存 3,各种语言从OS上提前申请内存,匹配GO规则的内存

Windows搭建SVN环境

VisualSVN Server下载 https://www.visualsvn.com/products VisualSVN Server安装创建仓库创建项目创建用户创建组项目分配组VisualSVN下载 https://www.visualsvn.com/products VisualSVN安装项目检出项目检出路径:https://changwq:8443/svn/dev/InterfaceManage 项…

如何实时记录工序流转状态——手机扫码让进度一目了然

对于生产企业来说记录工序的进展状态非常重要,有一部分中小型企业会使用纸制的“工序流转卡”,每日收集齐后再交由专员人工录入到电脑上,这种方式不仅费时费力,还容易出错,更重要的是无法让管理者实时掌握各个订单的进…

【FPGA】基于C5的第一个SoC工程

文章目录 前言SoC的Linux系统搭建 前言 本文是在毕业实习期间学习FPGA的SoC开发板运行全连接神经网络实例手写体的总结。 声明:本文仅作记录和操作指南,涉及到的操作会尽量细致,但是由于文件过大不会分享文件,具体软件可以自行搜…

程序环境和预处理超详细讲解

目录 程序的翻译环境和执行环境 详解编译链接 翻译环境 编译本身也分为几个阶段 运行环境 预处理(预编译)详解 预定义符号 #define #define 定义标识符 #define 定义宏 #define 替换规则 #和## ## 的作用 带副作用的宏参数 宏和函数对比 …