数据结构—二叉树

文章目录

  • 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.rhkb.cn/news/210454.html

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

相关文章

用HeidiSQL在MySQL中新建用户

用HeidiSQL登录到MySQL数据库&#xff0c;注意登录的时候要使用有权限的用户&#xff1a; 选择工具-》用户管理&#xff1a; 点击左上角的“添加”&#xff1a; 输入用户名、密码&#xff0c;并且分配权限&#xff1a; 点击右边的“添加对象”&#xff1a; 可以根据自己…

数据库中的笛卡尔积:定义、生成与避免策略

笛卡尔积&#xff08;Cartesian Product&#xff09;是一个在数据库和数据仓库中常见的概念。它来源于数学中的集合论&#xff0c;主要用于描述两个集合中元素之间所有可能的配对情况。在数据库领域&#xff0c;当你在查询中连接两个表时&#xff0c;如果没有指定适当的连接条件…

解决git action发布报错:Input required and not supplied: upload_url

现象&#xff1a; 这个问题死活都找不到原因&#xff0c;后来打了一段调试的代码 - name: Debug Create Release Output run: | echo "Release ID: ${{ env.RELEASE_ID }}" echo "Release Upload URL: ${{ env.RELEASE_UPLOAD_URL }}" env: RELEASE_ID: ${…

inBuilder低代码平台新特性推荐-第十三期

各位知乎的友友们&#xff0c;大家好~ 今天来给大家介绍一下inBuilder低代码平台社区版中特性推荐系列第十三期——登录配置&#xff01; inBuilder低代码平台内置了多种表单登录方式&#xff1a;用户名密码、AD域、数字证书。用户可以通过系统的登录页面进行登录。登录界面样…

Ansys Speos SSS|执行 Camera Sensor模拟结果后处理

附件下载 联系工作人员获取附件 概述 本文是Speos Sensor System&#xff08;SSS&#xff09;的使用指南&#xff0c;这是一个强大的解决方案&#xff0c;用于camera sensor模拟结果的后处理。本文的目的是通过一个例子来理解如何正确使用SSS。当然本文描述的分析步骤适合任…

python 堆与栈

【一】堆与栈 【 1 】简介 栈&#xff08;stack&#xff09;&#xff0c;有些地方称为堆栈&#xff0c;是一种容器&#xff0c;可存入数据元素、访问元素、删除元素&#xff0c;它的特点在于只能允许在容器的一端&#xff08;称为栈顶端指标&#xff0c;英语&#xff1a;top&a…

1.qml-3D入门讲解介绍

本章我们来学习QML 3D教程&#xff0c;QML 3D能够支持windows linux等多平台跨平台并且显示效果大部分一致&#xff0c;非常方便&#xff0c;学习的qt版本最低为qt6.5。 要使用qml 3D类&#xff0c;需要导入QtQuick3D模块。 这是使用空间渲染器和场景图的 QML 前端。目前&…

(六)Tiki-taka算法(TTA)求解无人机三维路径规划研究(MATLAB)

一、无人机模型简介&#xff1a; 单个无人机三维路径规划问题及其建模_IT猿手的博客-CSDN博客 参考文献&#xff1a; [1]胡观凯,钟建华,李永正,黎万洪.基于IPSO-GA算法的无人机三维路径规划[J].现代电子技术,2023,46(07):115-120 二、Tiki-taka算法&#xff08;TTA&#xf…

360公司-2019校招笔试-Windows开发工程师客观题合集解析

360公司-2019校招笔试-Windows开发工程师客观题合集 API无法实现进程间数据的相互传递是PostMessage2.以下代码执行后,it的数据为(异常) std::list<int> temp; std::list<int>::iterator it = temp.begin(); it = --it; 3.API在失败时的返回值跟其他不一样是 …

【EI会议征稿】第五届人工智能、网络与信息技术国际学术会议(AINIT 2024)

第五届人工智能、网络与信息技术国际学术会议&#xff08;AINIT 2024&#xff09; 第五届人工智能、网络与信息技术国际学术会议&#xff08;AINIT 2024&#xff09;将于2024年3月22-24日在中国南京举行。本届会议将主要关注人工智能、网络与信息技术面临的新的挑战问题和研究…

服务器主机安全用什么防护软件好?

一直以来服务器是许多企业、机构和个人进行关键任务操作的基础&#xff0c;而保护服务器主机安全是一项重要的任务&#xff0c;其中使用高防ip进行保护是有效且实用的方法&#xff0c;因为服务器主机安全受到危害的影响是多方面的&#xff0c;这边对于这方面也是进行了一定的了…

MagicPipe3D地下管网三维建模数据规格

经纬管网建模系统MagicPipe3D&#xff08;www.magic3d.net&#xff09;本地离线参数化构建三维地下管网&#xff08;含管道、接头、附属物等&#xff09;模型&#xff0c;输出标准3DTiles、Obj等格式&#xff0c;支持Cesium、Unreal、Unity等引擎可视化查询。MagicPipe3D三维建…

Linux last命令教程:如何查看用户的登录和注销历史(附案例详解和注意事项)

Linux last命令介绍 last命令在Linux中用于显示自文件/var/log/wtmp创建以来所有用户的登录和注销列表。可以给出一个或多个用户名作为参数&#xff0c;以显示他们的登录&#xff08;和注销&#xff09;时间和主机名。 Linux last命令适用的Linux版本 last命令在大多数Linux…

数据结构——希尔排序(详解)

呀哈喽&#xff0c;我是结衣 不知不觉&#xff0c;我们的数据结构之路已经来到了&#xff0c;排序这个新的领域&#xff0c;虽然你会说我们还学过冒泡排序。但是冒泡排序的性能不高&#xff0c;今天我们要学习的希尔排序可就比冒泡快的多了。 希尔排序 希尔排序的前身是插入排…

CETN01 - How to Use Cloud Classroom

文章目录 I. Introduction to Cloud ClassroomII. How to Use Cloud Classroom1. Publish Resources2. Conduct Activities3. Class Teaching Reports4. View Experience Values5. Performance in Cloud Classroom I. Introduction to Cloud Classroom “Cloud Classroom” is …

LeNet对MNIST 数据集中的图像进行分类--keras实现

我们将训练一个卷积神经网络来对 MNIST 数据库中的图像进行分类&#xff0c;可以与前面所提到的CNN实现对比CNN对 MNIST 数据库中的图像进行分类-CSDN博客 加载 MNIST 数据库 MNIST 是机器学习领域最著名的数据集之一。 它有 70,000 张手写数字图像 - 下载非常简单 - 图像尺…

QT 中 QDateTime::currentDateTime() 输出格式备查

基础 QDateTime::currentDateTime() //当前的日期和时间。 QDateTime::toString() //以特定的格式输出时间&#xff0c;格式 yyyy: 年份&#xff08;4位数&#xff09; MM: 月份&#xff08;两位数&#xff0c;07表示七月&#xff09; dd: 日期&#xff08;两位数&#xff0c…

【unity3D】unity中如何查找和获取游戏物体

&#x1f497; 未来的游戏开发程序媛&#xff0c;现在的努力学习菜鸡 &#x1f4a6;本专栏是我关于游戏开发的学习笔记 &#x1f236;本篇是unity中游戏物体的查找与获取 这里写自定义目录标题 获取当前物体的基本属性查找其它物体- 通过名称查找其它物体- 通过标签查找- 通过类…

计算机网络入侵检测技术研究

摘 要 随着网络技术的发展&#xff0c;全球信息化的步伐越来越快&#xff0c;网络信息系统己成为一个单位、一个部门、一个行业&#xff0c;甚至成为一个关乎国家国计民生的基础设施&#xff0c;团此&#xff0c;网络安全就成为国防安全的重要组成部分&#xff0c;入侵检测技术…

C++系统思维导图

自己在复盘C的时候做了 一些笔记&#xff0c;用思维导图形式记录下来的一些概念&#xff0c;多线程的内容较少&#xff0c;主要是派生和继承&#xff0c;以及虚函数和多态内容多一些&#xff0c;其他也有一些零碎的小知识点&#xff0c;和大家分享一下。有任何问题请留言。原图…