数据结构—二叉树

文章目录

  • 10.二叉树
    • (1).二叉树的基本概念
    • (2).遍历
      • #1.前序遍历
      • #2.中序遍历
      • #3.后序遍历
      • #4.非递归中序遍历
    • (3).中序+前/后序建树
      • #1.中序+前序遍历建树
      • #2.中序+后序遍历建树
    • (4).递归和二叉树基本操作
      • #1.求树高
      • #2.求结点数
      • #3.求叶子结点数
      • #4.复制树
      • #5.判断两棵树是否相等
    • (5).特殊二叉树
      • #1.满二叉树
      • #2.完全二叉树
    • (6).堆
      • #1.基本思想
      • #2.堆的基本操作
        • i.上浮
        • ii.下沉
        • iii.建堆
        • iv.插入元素
        • v.删除元素
      • #3.堆排序
    • 小结

10.二叉树

  说完了树,接下来就该说说一些我们比较常用的树了,二叉树就是其中之一,它的基本结构如下:

struct TreeNode
{int val;TreeNode* left;TreeNode* right;
};

  很高兴,这次我们不用再存一个vector或者其他什么了,这样操作起来可要方便不少呢!

(1).二叉树的基本概念

  二叉树是由 n ( n ≥ 0 ) n(n\ge0) n(n0)个结点所构成的集合,这个集合或者为空;或者由一个根结点及表示根结点的左、右子树的两个互不相交的结点集合所组成,而根结点的左、右子树也都是二叉树

二叉树是有序树,把第一个和第二个子结点(或子树)分别称为左子结点和右子结点(或子树),严格区分左右子树

(2).遍历

  因为二叉树的每个结点只有左、中、右三个方向,因此二叉树的遍历方式在树的基础上还包含了中序遍历,所以我们接下来给出三种遍历方式的代码

#1.前序遍历

  前序遍历的打印顺序是中、左、右

void PreOrderTraversal(const TreeNode* root)
{if (root) {cout << root->val << " ";PreOrderTraversal(root->left);PreOrderTraversal(root->right);}
}

#2.中序遍历

  前序遍历的打印顺序是左、中、右

void InOrderTraversal(const TreeNode* root)
{if (root) {InOrderTraversal(root->left);cout << root->val << " "; InOrderTraversal(root->right);}
}

#3.后序遍历

  前序遍历的打印顺序是左、右、中

void PostOrderTraversal(const TreeNode* root)
{if (root) {PostOrderTraversal(root->left);PostOrderTraversal(root->right);cout << root->val << " "; }
}

#4.非递归中序遍历

  非递归的中序遍历的流程是这样的:每个结点都往左子结点走,把自己压入栈中,一旦某个左子结点为空,就回溯到上一个根(即栈顶),然后再遍历根的右子树,这么一直重复就好了:

void NoRecursionInOrderTraversal(const TreeNode* root)
{if (!root) return;stack<const TreeNode*> s;const TreeNode* ptr{ root };while (ptr || !s.empty()) {if (ptr) {s.push(ptr);ptr = ptr->left;}else {ptr = s.top();s.pop();cout << ptr->val << " ";ptr = ptr->right;}}
}

(3).中序+前/后序建树

  前序遍历+后序遍历的结果是不能建出一棵二叉树的,比如下面这个例子:
p34

  T1和T2两棵树的前序遍历都是AB,后序遍历都是BA,因此我们靠前序+后序两种序列是没有办法还原一棵树的,但是如果是中序遍历呢? 比如T1的中序遍历是BA,T2的中序遍历是AB,这样一来两棵树就可以被区别出来了,在这里我不给出证明了,我们需要的一点是,只要我们有中序遍历结果+前序或后序二选一的结果就可以还原出唯一的一棵二叉树了

  还有需要注意的点,根据根结点可以把中序遍历序列拆解成左子树和右子树两个部分,例如左子树有 k k k个结点,而右子树有 n n n个结点,则在前序遍历序列中, 1 ∼ k 1\sim k 1k 的结点即为左子树的前序遍历结果, k + 1 ∼ k + n k+1\sim k+n k+1k+n 的结点即为右子树的前序遍历结果,这个是比较自然的,比如我们在做前序遍历的时候,我们也是首先打印根结点,在把所有左子树结点打印出来之后,再打印出右子树结点,因此,这一点在之后对我们会非常有用

#1.中序+前序遍历建树

  我们先来思考一下,对于中序:FDHGIBEAC,前序:ABDFGHIEC,这样的结果,我们要怎么还原呢?首先先序遍历的第一个结点一定是根结点,所以取出A,前序变为BDFGHIEC,中序找到A后拆分成左子树FDHGIBE和右子树C
  然后递归地,我们再找到前序序列中的B,作为A左子树的根结点,再在左子树的中序遍历中找到B,拆分成B的左子树FDHGI和右子树E,然后就这么一直做下去,最后我们就可以得到一棵唯一的树:
p35

  所以,我们只要每次根据索引和位置把序列切分成左子树和右子树对应的序列,再利用递归的方法分别完成左右子树的构建过程即可:

void buildTree(TreeNode*& root, const string& preorder, const string& inorder)
{if (inorder.size() == 0) {root = nullptr; // 切记当切分到0的时候,要将对应的结点设置为空指针return;}root = new TreeNode;int k{ 0 };while (preorder[0] != inorder[k]) k++;root = new TreeNode{ inorder[k], nullptr, nullptr };buildTree(root->left, preorder.substr(1, k), inorder.substr(0, k));buildTree(root->right, preorder.substr(k+1, preorder.size() - 1 - k), inorder.substr(k+1, inorder.size() - 1 - k));
}

  这里采取了字符串来传入前序和中序遍历序列,因为这里可以采取substr方法,可以比较简单地完成整个流程,当然,我们用迭代器的方法就可以用vector来完成的,在中序+后序遍历中我会给出vector的方法

#2.中序+后序遍历建树

  中序+后序遍历的方法其实和前序一样,只是这一次我们每次都从后序遍历的最后一个结点中取出来,作为根结点,所以代码也很容易写出来:

TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder)
{if (postorder.size() == 0) return nullptr;int val{ postorder[postorder.size() - 1] };TreeNode* root = new TreeNode{ val, nullptr, nullptr };if (postorder.size() == 1) return root;int k{ 0 };while (inorder[k] != val) k++;vector<int> leftInorder{ inorder.begin(), inorder.begin() + k };vector<int> rightInorder{ inorder.begin() + k + 1, inorder.end() };postorder.resize(postorder.size() - 1); // 去掉最后一个元素,比较方便后续直接使用.end()获取最后一个元素vector<int> leftPostorder{ postorder.begin(), postorder.begin() + leftInorder.size() };vector<int> rightPostorder{ postorder.begin() + leftInorder.size(), postorder.end() };root->left = buildTree(leftInorder, leftPostorder);root->right = buildTree(rightInorder, rightPostorder);return root;
}

  其实本质是一样的,我们还是对整个序列进行了一系列的分割,然后再完成后续的建树过程

(4).递归和二叉树基本操作

  和前文中提过的多叉树一样,二叉树的定义同样是递归的,因此我们仍然可以利用递归的方法完成二叉树的各种操作

#1.求树高

  树的高度定义为所有结点的深度的最大值,我们前面认为:树根的高度为0,空树高度为-1,所以求最大值我们可以写出下面的递归式: h e i g h t = m a x { l e f t _ h e i g h t , r i g h t _ h e i g h t } + 1 height = max\{left\_height, right\_height\} + 1 height=max{left_height,right_height}+1  所以,代码也是很好写的咯,让我们来试试吧:

int max(int a, int b)
{return (a > b) ? a : b;
}int tree_height(const TreeNode* root)
{if (!root) return -1;if ((!root->left) && (!root->right)) return 0;int hL{ tree_height(root->left) };int rL{ tree_height(root->right) };return max(hL, rL) + 1;
}

#2.求结点数

  求结点数也很简单,我们只需要: c n t = l e f t _ c n t + r i g h t _ c n t + 1 cnt = left\_cnt + right\_cnt + 1 cnt=left_cnt+right_cnt+1

int count(const TreeNode* root)
{if (!root) return 0;return count(root->left) + count(root->right) + 1;
}

#3.求叶子结点数

  叶子结点数也是一样,我们需要左子树的叶子结点数+右子树的叶子结点树的值即可: l e a v e s = l e f t _ l e a v e s + r i g h t _ l e a v e s leaves = left\_leaves + right\_leaves leaves=left_leaves+right_leaves

int leaves(const TreeNode* root)
{if ((!root->left) && (!root->right)) return 1;return leaves(root->left) + leaves(root->right);
}

#4.复制树

  复制树就是把一棵树完整地复制另外一棵出来,那么其实也就是说,遇到某个结点,把左子树复制一次,再把右子树复制一次,就好了

TreeNode* BiTreeCopy(const TreeNode* root)
{if (!root) return nullptr;TreeNode* p{ new TreeNode{root->val, nullptr, nullptr} };p->left = BiTreeCopy(root->left);p->right = BiTreeCopy(root->right);return p;
}

#5.判断两棵树是否相等

  一样的套路,左子树相等 + 本身相等 + 右子树相等

bool equal(const TreeNode* t1, const TreeNode* t2)
{if (!t1 && !t2) return true;if (t1 && t2) {return (t1->val == t2->val) && equal(t1->left, t2->left) && equal(t1->right, t2->right);}return false;
}

(5).特殊二叉树

#1.满二叉树

  满二叉树中,每一层结点数目都达到了最大,比如下面这种:
p36

  比如这样,高度为k的满二叉树有 2 k + 1 − 1 2^{k+1}-1 2k+11个结点,在这里我采取根结点编号为1的策略,有些地方可能采取根结点编号为0的策略,但是这种情况下子结点不是很好找,我们把所有结点依据从左到右,从上到下按照层序依次编号的方式给每个结点分配一个编号:
p37

  为啥要考虑这个呢?因为我们会发现,根结点1的两个左右结点分别是2和3,而2的两个结点是4和5,所以说,对于任意一个非叶结点编号为 k k k的结点,它的两个子结点必存在,并且编号为 2 k 和 2 k + 1 2k和2k+1 2k2k+1,所以满二叉树可以在完全不浪费空间的情况下,使用数组完成存储,并且其编号的关系可以在不开出额外空间的情况下很轻松地存储结点的双亲结点和子结点的位置

#2.完全二叉树

  完全二叉树则是在满二叉树的基础上去除最后一层从最后一个结点开始的连续若干个结点,比如
p38

  所以满二叉树也算完全二叉树,对于具有 n ( n > 0 ) n(n>0) n(n>0)个结点的完全二叉树,其深度为 ⌊ log ⁡ 2 n ⌋ \lfloor{\log_2n}\rfloor log2n

(6).堆

#1.基本思想

  堆基于完全二叉树来实现,堆定义为:如果树T中任一结点的值不小于(不大于)其左右结点的值,则树T为一个堆,如果根结点是所有结点中的最大值,则这个堆称为大根堆;否则如果是最小值,则这个堆称为小根堆

  这么做的思想其实很简单,堆排序维护了一个基本有序的序列,再每次插入/删除之后,都能以 O ( log ⁡ n ) O(\log n) O(logn)的时间复杂度代价上完成下一次维护,因此,堆排序的时间复杂度是 O ( n log ⁡ n ) O(n\log n) O(nlogn)

#2.堆的基本操作

  堆的基本操作主要是上浮和下沉两个流程,上浮就是把结点放在最后的叶结点的位置,然后把它逐渐上浮到合适的位置即可,而下沉则是把新加入的结点放在根结点上,其左右子树都是堆,但是根结点本身不满足堆的性质,所以我们逐渐操作,把根结点下沉,沉到符合条件的位置

i.上浮
void swap(int& a, int& b)
{int tmp{a};a = b;b = tmp;
}void siftup(vector<int>& a, int k)
{bool check{ false };while (k > 1 && !check) {if (a[k] > a[k/2]) {swap(a[k/2], a[k]);k /= 2;}else check = true;}
}
ii.下沉

  下沉的流程要稍微复杂一点,结点会首先和它的左右两个子结点进行比较(先左后右),直到有序/找到最后一个结点才停下来:

void siftdown(vector<int>& a, int i)
{int n = a.size() - 1;bool check{ false };int t{ 0 };while (i*2 <= n && !check) {if (a[i] < a[i*2]) t = i*2;else t = i;if (i*2+1 <= n) {if (a[t] < a[i*2+1]) t = i*2+1;}if (t != i) {swap(a[i], a[t]);i = t;}else check = true;} 
}
iii.建堆
void buildHeap(vector<int>& a)
{int n = a.size() - 1;for (int i = n / 2; i >= 1; i--) {siftdown(a, i);}
}
iv.插入元素
void addElement(vector<int>& a, int num)
{a.push_back(num);siftup(a, a.size()-1);
}
v.删除元素
int deleteRoot(vector<int>& a)
{int tmp{ a[1] };a[1] = a[a.size()-1];a.pop_back();siftdown(a, 1);return tmp;
}

#3.堆排序

  所以,我们只需要每一次都从堆的根上拿出一个元素即可,堆排序的流程非常简单:

void heap_sort(vector<int>& a)
{vector<int> ans;while (a.size() > 1) {int val{ deleteRoot(a) };ans.push_back(val);}a = ans;
}

  流程比较简单,但是上面的代码并不能保证没有bug,堆的这一部分思路比较简单,因此我只提供一个思路

小结

  这一篇我们比较粗略地介绍了一下二叉树相关的内容,其实应该还有一个二叉搜索树的,但是鉴于它的复杂度,我决定把它放到下一篇:树的应用中再来介绍

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

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

相关文章

零信任组件和实施

零信任是一种安全标准&#xff0c;其功能遵循“从不信任&#xff0c;始终验证”的原则&#xff0c;并确保没有用户或设备受信任&#xff0c;无论他们是在组织网络内部还是外部。简而言之&#xff0c;零信任模型消除了信任组织安全边界内任何内容的概念&#xff0c;而是倡导严格…

操作系统复习总结——文件管理

&#x1f525;博客主页&#xff1a;真的睡不醒 &#x1f680;系列专栏&#xff1a;深度学习环境搭建、环境配置问题解决、自然语言处理、语音信号处理、项目开发 &#x1f498;每日语录&#xff1a;但愿每次回忆&#xff0c;对生活都不感到负疚。 &#x1f389;感谢大家点赞…

搜维尔科技:AI时代,迈向2030元宇宙数字人戏曲教育数字化思维、战略与未来!

一场关于中国传统戏曲与数字媒体交汇的探讨之旅将于今日在清华大学开讲&#xff0c;本次活动旨在推动AI时代大背景下&#xff0c;利用元宇宙、数字人等创新技术焕发中国传统戏曲全新活力。 讲座以“AI时代&#xff0c;迈向2030元宇宙数字人戏曲教育数字化思维、战略与未来”为主…

【Linux系统编程】开发工具yum和vim

目录 一&#xff0c;yum工具的使用 1&#xff0c;yum的介绍 2&#xff0c;yum的使用 二&#xff0c;vim工具的开发 1&#xff0c;vim的介绍 2&#xff0c;模式的使用 3&#xff0c;vim配置文件 4&#xff0c;sudo配置文件 一&#xff0c;yum工具的使用 1&#xff0c;y…

弱口令防护和网站防盗链有什么用

弱口令防护主要针对用户账户的安全。弱口令是指容易被猜测或破解的密码&#xff0c;如常见的密码、简单的数字序列或字典中的单词等。弱口令防护的目的是防止恶意用户或攻击者通过猜测或暴力破解密码的方式获取合法用户的账户权限。通过实施强密码策略、密码复杂度要求和账户锁…

【EI会议征稿中|航空航天领域】第二届航空航天与控制工程国际学术会议(ICoACE 2023)

第二届航空航天与控制工程国际学术会议&#xff08;ICoACE 2023&#xff09; 2023 2nd International Conference on Aerospace and Control Engineering 2023年第二届航空航天与控制工程国际学术会议&#xff08;ICoACE 2023&#xff09;将于2023年12月15-17日在江苏南京召开…

Selenium+Unittest+HTMLTestRunner框架更改为Selenium+Pytest+Allure(一)

背景&#xff1a;之前的框架&#xff0c;Selenium是3.x版本&#xff0c;现在更新到4.15版本后&#xff0c;一些写法如find_element_by_xxx 不再支持&#xff0c;改为find_element(By.xxx)的方式&#xff0c;同时由于Unittest不如Pytest在执行方面灵活&#xff08;比如只执行冒烟…

数据库安全运维系统厂家在深圳的有哪些?咨询电话多少?

IT小伙伴都知道&#xff0c;数据库安全运维至关重要&#xff0c;因为随着信息技术的不断发展&#xff0c;数据库已经成为企业存储、管理和处理数据的关键平台&#xff0c;数据库承载着企业不少数据资产。因此使用数据库安全运维系统是必要的。那你知道数据库安全运维系统厂家在…

计算机网络:可靠数据传输(rdt)、流水协议、窗口滑动协议

文章目录 前言一、Rdt1.Rdt1.02.Rdt2.03.Rdt2.14.Rdt2.25.Rdt3.0 二、流水线协议1.滑动窗口&#xff08;slide window&#xff09;协议发送窗口接收窗口正常情况下的2个窗口互动异常情况下GBN的2个窗口互动异常情况下SR的2窗口互动GBN协议和SR协议的异同 2.小结 总结 前言 Rdt…

宝塔上安装mysql遇到的问题

宝塔上安装mysql遇到的问题 文章目录 宝塔上安装mysql遇到的问题一、下载mysql二、启动报错三、设置密码四、解决报错bash未找到命令mysql五、继续修改root密码五、宝塔中设置端口六、使用连接工具连接数据库 一、下载mysql 宝塔软件商店里下载mysql&#xff0c;然后点击启动。…

ELasticsearch:什么是语义搜索?

语义搜索定义 语义搜索是一种解释单词和短语含义的搜索引擎技术。 语义搜索的结果将返回与查询含义匹配的内容&#xff0c;而不是与查询中的单词字面匹配的内容。 语义搜索是一组搜索引擎功能&#xff0c;其中包括根据搜索者的意图及其搜索上下文理解单词。 此类搜索旨在通过…

python自动化操作:批量处理照片尺寸并输出到word文档中【第14篇—python-照片尺寸批量处理输出】

文章目录 一.背景二.需求三.实现源码3.1 项目结构3.2 源码3.3 核心调节照片大小 四.代码解释五.实现效果六.心得总结 一.背景 在当今数字化时代&#xff0c;照片处理和文档编辑是许多领域中不可或缺的任务。从个人创作到企业文档&#xff0c;人们经常需要快速而有效地处理大量…

Linux IO多路转接之epoll

文章目录 一、epoll初识 二、epoll的相关系统调用 1.epoll_create 2.epoll_ctl 3.epoll_wait 三、epoll工作原理 四、epoll的工作方式 本文主要介绍了epoll内部工作机制&#xff0c;如何达到高性能的多路转接。技术有限&#xff0c;如有错误请指正。参考文献&#xff1a;…

【Redis】Redis 的学习教程(十三)Redis 各场景

由于Redis 支持比较丰富的数据结构&#xff0c;因此他能实现的功能并不仅限于缓存&#xff0c;而是可以运用到各种业务场景中&#xff0c;开发出既简洁、又高效的系统 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-bo…

第三节:提供者、消费者、Eureka

一、 提供者 消费者&#xff08;就是个说法、定义&#xff0c;以防别人叭叭时听不懂&#xff09; 服务提供者&#xff1a;业务中被其他微服务调用的服务。&#xff08;提供接口给其他服务调用&#xff09;服务消费者&#xff1a;业务中调用其他微服务的服务。&#xff08;调用…

QThread之moveToThread用法

简介 使用moveToThread函数的流程如下&#xff1a; 1、创建一个类继承自QObject类或其子类&#xff0c;并在其中定义所要执行的多个任务&#xff0c;执行多个任务就要定义相应的信号。 2、任务通过moveToThread指定所要执行的线程。 3、线程通过start启动 4、通过信号与槽机制…

【AI绘画】万字长文——(超详细)ControlNet的详细介绍使用Stable Diffusion的艺术二维码完全生成攻略

目录 前言一、名词解释1-1、Stable Diffusion介绍1-2、ControlNet介绍1-2-1、ControlNet介绍&工作原理1-2-2、ControlNet控制方法介绍 1-3、案例分析1-3-1、室内装修设计1-3-2、品牌创意海报 1-4、stable-diffusion-webui 的参数解释 二、生成方法2-1、图像到图像2-1-1、二…

Python使用netmiko配置华为交换机

一、netmiko介绍 1.更适合网络设备的自动化运维模块。 二、场景 1、批量查询 2、批量配置变更、备份 三、项目地址 GitHub - ktbyers/netmiko: Multi-vendor library to simplify Paramiko SSH connections to network devices 三、使用步骤 1.安装netmiko pip install ne…

D6208单片双向马达驱动电路国产芯片,工作电源电压范围宽(4.5V~15.0V),内设保护二极管采用SOP8封装

D6208 是一块单片双向马达驱动电路&#xff0c;它使用TTL电平的逻辑信号就能控制卡式录音机和其它电子设备中的双向马达。该电路由一个逻辑部分和一个功率输出部分组成。逻辑部分控制马达正、反转向及制动&#xff0c;功率输出部分根据逻辑控制能提供100mA&#xff08;典型值&a…

腾讯地图系列(二):微信小程序添加插件(三种方法)以及插件AppId获取

目录 第一章 前言 第二章 添加插件 2.1 微信小程序添加插件方法一&#xff08;微信公众平台添加插件&#xff09; 2.2 微信小程序添加插件方法二&#xff08;通过项目配置添加插件&#xff09; 2.3 微信小程序添加插件方法三&#xff08;微信公众平台服务市场添加插件&…