数据结构—二叉树

文章目录

  • 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.hqwc.cn/news/239034.html

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

相关文章

无需繁琐编程 开启高效数据分析之旅!

不学编程做R统计分析&#xff1a;图形界面R Commander官方手册 R Commander是 R 的图形用户界面&#xff0c;不需要键入命令就可通过熟悉的菜单和对话框来访问 R 统计软件。 R 和 R Commander 均可免费安装于所有常见的操作系统——Windows、Mac OS X 和 Linux/UNIX。 本书作…

shareMouse 使用中遇到的问题

一、shareMouse 使用中遇到的问题 1、鼠标不能移动到另一个显示器 明明是两个显示器&#xff0c;但是 只显示一个&#xff0c;鼠标也不能移到另一个显示器上 后来&#xff0c; 设置了 wrap mouse pointer around display就好了&#xff0c;虽然还是显示一个显示器&#xff0c…

YOLOv8创新魔改教程(一)如何进行模块创新

YOLOv8创新魔改教程&#xff08;一&#xff09;如何进行模块创新 YOLOv8创新魔改教程 本人研一&#xff0c;最近好多朋友问我要如何修改模型创新模块&#xff0c;就想着不如直接开个专栏歇一歇文章&#xff0c;也算是对自己学习的总结&#xff0c;本专栏以YOLOv8为例&#xf…

Gee教程5.中间件

鉴权认证、日志记录等这些保障和支持系统业务属于全系统的业务&#xff0c;和具体的系统业务没有关联&#xff0c;对于系统中的很多业务都适用。 因此&#xff0c;在业务开发过程中&#xff0c;为了更好的梳理系统架构&#xff0c;可以将上述描述所涉及的一些通用业务单独抽离…

「C++」类和对象2

&#x1f387;个人主页&#xff1a;Ice_Sugar_7 &#x1f387;所属专栏&#xff1a;C启航 &#x1f387;欢迎点赞收藏加关注哦&#xff01; 文章目录 &#x1f349;前言&#x1f349;构造函数&#x1f34c;参数&#x1f34c;默认构造函数&#x1f95d;两种类型&#x1f95d;编译…

C++基础 -28- 友元

友元用于访问类中的所有数据成员 类中的私有成员&#xff0c;类外不可访问 定义友元的格式&#xff08;友元函数必须要在类内&#xff0c;声明&#xff09; friend void show(person &b); 使用友元访问类的所有成员 #include "iostream"using namespace std…

深入理解Docker容器核心技术

文章目录 1. Linux命名空间&#xff08;Namespaces&#xff09;1.1 示例&#xff1a;PID命名空间 2. 控制组&#xff08;cgroups&#xff09;2.1 示例&#xff1a;内存控制组 3. 联合文件系统&#xff08;UnionFS&#xff09;3.1 示例&#xff1a;查看镜像的分层结构 4. Docker…

二、ZooKeeper集群搭建

目录 1、概述 2、安装 2.1 第一步&#xff1a;下载zookeeeper压缩包 2.2 第二步&#xff1a;解压 ​​​​​​​2.3 第三步&#xff1a;修改配置文件 ​​​​​​​2.4 第四步&#xff1a;添加myid配置 ​​​​​​​2.5 第五步&#xff1a;安装包分发并修改myid的值…

2023_Spark_实验二十四:SparkStreaming读取Kafka数据源:使用Direct方式

SparkStreaming读取Kafka数据源&#xff1a;使用Direct方式 一、前提工作 安装了zookeeper 安装了Kafka 实验环境&#xff1a;kafka zookeeper spark 实验流程 二、实验内容 实验要求&#xff1a;实现的从kafka读取实现wordcount程序 启动zookeeper zk.sh start# zk.sh…

【方案】智慧林业:如何基于EasyCVR视频能力搭建智能林业监控系统

随着人类进程的发展。城市化范围的扩大&#xff0c;森林覆盖率越来越低&#xff0c;为保障地球环境&#xff0c;保护人类生存的净土&#xff0c;森林的保护与监管迫在眉睫。TSINGSEE青犀智慧林业智能视频监控系统方案的设计&#xff0c;旨在利用现代科技手段提高林业管理的效率…

2023年AI时代中小企业智能化发展报告

今天分享的是AI系列深度研究报告&#xff1a;《2023年AI时代中小企业智能化发展报告》。 &#xff08;报告出品方&#xff1a;创业邦&#xff09; 报告共计&#xff1a;47页 AI——中小企业的智能化增长利器 继蒸汽机、电气化、信息化时代之后&#xff0c;由第四次工业革命开…

[HTML]Web前端开发技术6(HTML5、CSS3、JavaScript )DIV与SPAN,盒模型,Overflow——喵喵画网页

希望你开心&#xff0c;希望你健康&#xff0c;希望你幸福&#xff0c;希望你点赞&#xff01; 最后的最后&#xff0c;关注喵&#xff0c;关注喵&#xff0c;关注喵&#xff0c;佬佬会看到更多有趣的博客哦&#xff01;&#xff01;&#xff01; 喵喵喵&#xff0c;你对我真的…