数据结构—二叉树

news2024/11/17 7:48:16

文章目录

  • 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.coloradmin.cn/o/1276747.html

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

相关文章

Elasticsearch 优化查询中获取字段内容的方式,性能提升5倍!

1、背景 集群配置为&#xff1a;8 个 node 节点&#xff0c;16 核 32G&#xff0c;索引 4 分片 1 副本。应用程序的查询逻辑是按经纬度排序后找前 200 条文档。 1、应用对查询要求比较高&#xff0c;search 没有慢查询的状态。 2、集群压测性能不能上去&#xff0c;cpu 使用未打…

VSC++=》 友数对友质数()

void 友数对友质数() {//缘由https://bbs.csdn.net/topics/396498706?page1#post-411382586int aa 2, aaa 20; while (aa * aaa < 119)if (判断质数(aa * aaa - 1))cout << aa << ends << aaa << ends << (aa*aaa - 1) << endl, aaa…

JavaFramework JDK Version Test

测试JDK8 JDK17编译包 当前环境JDK8 CASE 1&#xff1a; /*** * author ZengWenFeng* email 117791303QQ.com* mobile 13805029595* date 2023-08-07*/ package zwf;import a.T; import ce.pub.util.GUID;/*** 测试高版本JDK编译JAR&#xff0c;低版本错误** author ZengWenF…

C++作业2

自己封装一个矩形类(Rect)&#xff0c;拥有私有属性:宽度(width)、高度(height)&#xff0c; 定义公有成员函数: 初始化函数:void init(int w, int h) 更改宽度的函数:set_w(int w) 更改高度的函数:set_h(int h) 输出该矩形的周长和面积函数:void show() 代码&#xff1a…

FastAPI中如何调用同步函数

目录 一、使用app.sync装饰器 二、使用asyncio.run()函数 三、使用background参数 四、注意事项 总结 FastAPI是一个基于Python 3.6的快速Web框架&#xff0c;用于构建高效、可扩展的Web应用程序。在FastAPI中&#xff0c;可以使用同步函数来处理请求并返回响应。本文将介…

Unity 与 虚拟机ROS连接

Unity 与 虚拟机ROS连接 知识储备前期准备ROS部分Unity部分 连接测试 知识储备 unity官方教程&#xff1a; https://github.com/Unity-Technologies/Unity-Robotics-HubWin11家庭版开启HyperV&#xff1a; https://zhuanlan.zhihu.com/p/577980646HyperV安装Ubuntu: https://b…

设计模式---第三篇

系列文章目录 文章目录 系列文章目录前言一、模板方法模式二、知道享元模式吗?三、享元模式和单例模式的区别?前言 前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站,这篇文章男女通用,看懂了就去分享给你的码吧。 一…

2023年第十二届数学建模国际赛小美赛D题望远镜的微光系数求解分析

2023年第十二届数学建模国际赛小美赛 D题 望远镜的微光系数 原题再现&#xff1a; 当我们使用普通光学望远镜在昏暗的光线下观察远处的目标时&#xff0c;入射孔径越大&#xff0c;进入双筒望远镜的光线就越多。望远镜的放大倍数越大&#xff0c;视野越窄&#xff0c;图像显示…

前端大文件上传webuploader(react + umi)

使用WebUploader还可以批量上传文件、支持缩略图等等众多参数选项可设置&#xff0c;以及多个事件方法可调用&#xff0c;你可以随心所欲的定制你要的上传组件。 分片上传 1.什么是分片上传 分片上传&#xff0c;就是将所要上传的文件&#xff0c;按照一定的大小&#xff0c;将…

stm32项目中重定向printf打印不出来东西?三种解决方案

项目场景&#xff1a; 在stm32项目中为了调试将某些参数打出来&#xff0c;重定向printf 问题描述 printf打印不出东西 缓冲区满了才打印出来 原因分析&#xff1a; 使用printf函数必须等到缓冲区满或程序结束时&#xff0c;才进行写入到屏幕 解决方案&#xff1a; 解决方…

react实现加载动画

1.Spinning.tsx import "./Spinning.scss";interface Props {isLoading: boolean;children?: React.ReactNode; }const Spinning: React.FC<Props> ({isLoading true,children }) > {return <div className{spinning-wrapper${isLoading ? " l…

MySQL:找回root密码

一、情景描述 我们在日常学习中&#xff0c;经常会忘记自己的虚拟机中MySQL的root密码。 这个时候&#xff0c;我们要想办法重置root密码&#xff0c;从而&#xff0c;解决root登陆问题。 二、解决办法 1、修改my.cnf配置文件并重启MySQL 通过修改配置文件&#xff0c;来跳…

海外IP罗拉rola正版去哪里找?

免费海外IP代理能用吗&#xff1f;和收费的有哪些差异&#xff1f; 如今在这个大数据时代&#xff0c;无论你从事哪个行业&#xff0c;都离不开数据&#xff0c;尤其是做跨境电商的&#xff0c;更一步都离不开海外IP代理&#xff0c;无论是网站引擎优化还是营销推广、数据抓取…

基于SpringBoot实现SSMP整合

&#x1f648;作者简介&#xff1a;练习时长两年半的Java up主 &#x1f649;个人主页&#xff1a;程序员老茶 &#x1f64a; ps:点赞&#x1f44d;是免费的&#xff0c;却可以让写博客的作者开心好久好久&#x1f60e; &#x1f4da;系列专栏&#xff1a;Java全栈&#xff0c;…

Android File Transfer for Mac:畅享强大的安卓文件传输工具

作为一款功能强大的安卓文件传输工具&#xff0c;Android File Transfer for Mac&#xff08;以下简称AFT&#xff09;为Mac用户提供了便捷快速的安卓设备文件管理体验。无论是传输照片、音乐、视频还是文档&#xff0c;AFT都能满足你的需求&#xff0c;让你的文件传输更加高效…

Windows11系统下内存占用率过高如何下降

. # &#x1f4d1;前言 本文主要是win11系统下CPU占用率过高如何下降的文章&#xff0c;如果有什么需要改进的地方还请大佬指出⛺️ &#x1f3ac;作者简介&#xff1a;大家好&#xff0c;我是青衿&#x1f947; ☁️博客首页&#xff1a;CSDN主页放风讲故事 &#x1f304;每日…

FH Admin Shiro反序列化漏洞复现

0x01 产品简介 FH Admin 是一款 java 快速开发平台。 0x02 漏洞概述 FH Admin CMS 存在 shiro 反序列化漏洞&#xff0c;该漏洞源于软件存在硬编码的 shiro-key&#xff0c;攻击者可利用该 key 生成恶意的序列化数据&#xff0c;在服务器上执行任意代码&#xff0c;执行系统命…

从零开发短视频电商 在AWS上用SageMaker部署开源模型并用Java SDK调用

文章目录 1.创建AWS账户2.登录AWS3.创建域4.部署模型方式一 使用JumpStart可视化界面部署内置的模型方式二 采用python脚本部署私有模型5.调用模型AWS Java SDK调用Http调用6.监控7.自动扩缩容1.创建AWS账户 需要准备好邮箱一个,支持visa功能的信用卡一个。然后到aws上自己去…

vue 修改 this.$confirm 的文字样式、自定义样式

通常使用 confirm 确认框时&#xff0c;一般这样写&#xff1a; <template><el-button type"text" click"open">点击打开 Message Box</el-button> </template><script>export default {methods: {open() {this.$confirm(此…

什么样的SSL证书比较好?

首先需要明确的是最适合自己的就是最好的SSL证书。目前市场上的证书种类很多&#xff0c;那怎么才能挑选出最适合自己的呢&#xff1f;我罗列了几个需要考虑的方面。 1.证书类型&#xff1a;根据您的需求选择合适的证书类型。例如&#xff0c;如果您需要验证公司信息&#xff0…