数据结构—二叉树

文章目录

  • 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,堆的这一部分思路比较简单,因此我只提供一个思路

小结

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

xss漏洞后端进行html消毒

import org.jsoup.Jsoup;public static String sanitizeHtml(String input) {// 使用 Jsoup 消毒 HTMLreturn Jsoup.clean(input, Safelist.relaxed());}public static void main(String[] args) {String userInput "<p><script>alert(1)</script>Safe…

Linux:vim的简单使用

个人主页 &#xff1a; 个人主页 个人专栏 &#xff1a; 《数据结构》 《C语言》《C》《Linux》 文章目录 前言一、vim的基本概念二、vim的基本操作三、vim正常模式命令集四、vim底行模式命令集五、.xxx.swp的解决总结 前言 本文是对Linux中vim使用的总结 一、vim的基本概念 …

02-使用Git命令操作远程仓库,如克隆或添加远程仓库,拉取或推送内容

操作远程仓库 创建远程仓库 第一步: 访问GitHub官网,登录自己的账号创建一个远程仓库 第二步: 设置远程仓库的信息(一般远程库的名字和本地库的名字一样),仓库创建完成后可以看到仓库地址(每个仓库都会对应一个网络地址) 第三步: 查看本地仓库对应的Https/SSH连接 远程仓库命…

apk和小程序渗透测试

apk和小程序渗透测试 文章目录 apk和小程序渗透测试小程序渗透测试apk和小程序的抓包安装证书apk渗透 小程序渗透测试 小程序的默认路径在 C:\Program Files (x86)\Tencent\WeChat\WeChatApp 使用UnpackMiniApp、wxappUnpacker工具完成逆向 先打开UnpackMiniApp.exe工具 选…

Kotlin学习——kt里的作用域函数scope function,let,run,with,apply,also

Kotlin 是一门现代但已成熟的编程语言&#xff0c;旨在让开发人员更幸福快乐。 它简洁、安全、可与 Java 及其他语言互操作&#xff0c;并提供了多种方式在多个平台间复用代码&#xff0c;以实现高效编程。 https://play.kotlinlang.org/byExample/01_introduction/02_Functio…

超全能!PDF转换+套用模板+在线制作电子画册的网站

上班族的朋友们&#xff0c;制作电子画册很苦恼吧&#xff01;没有专业设计水准的&#xff0c;或是想偷偷小懒的看看这里&#xff01;小编说的这些对你的工作非常有效&#xff01; 想要轻轻松松就能制作能与专业设计师们媲美的电子画册&#xff0c;当然是需要一款超全能的工具&…

ESP32CAM视频查看实践

利用ESP32cam的摄像头进行视频查看。视频画质确实模糊&#xff0c;不过不妨碍其高性价比。 1、选择开发板及串口 2、打开例程 3、修改热点和摄像模式 4、复制链接查看 手机浏览器打开

LeetCode Hot100 169.多数元素

题目&#xff1a; 给定一个大小为 n 的数组 nums &#xff0c;返回其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋ 的元素。 你可以假设数组是非空的&#xff0c;并且给定的数组总是存在多数元素。 方法一&#xff1a;哈希表 ​ class Solution {public int…

Go 语言注释教程

注释是在执行时被忽略的文本。 注释可用于解释代码&#xff0c;使其更易读。 注释还可用于在测试替代代码时防止代码执行。 Go支持单行或多行注释。 Go单行注释 单行注释以两个正斜杠&#xff08;//&#xff09;开头。 在//和行尾之间的任何文本都将被编译器忽略&#xf…

常用OSPF命令有哪些?详细解释~赶快收藏起来!

OSPF&#xff08;Open Shortest Path First开放式最短路径优先&#xff09;是一种动态路由协议&#xff0c;属于内部网关协议(Interior Gateway Protocol,简称IGP)&#xff0c;是基于链路状态算法的路由协议。 配置OSPF会使用到哪些命令&#xff1f; 1、OSPF &#xff08;1&…

C/C++11 语法/概念易错总结(1)

文章目录 缺省参数函数重载引用引用和指针内联宏的优缺点auto范围forNULL和nullptr 缺省参数 半缺省参数必须从右往左依次来给出&#xff0c;不能间隔着给 void Func(int a, int b 10, int c 20){cout<<"a "<<a<<endl;cout<<"b &…

uniapp运行到安卓基座app/img标签不显示

img是html中的标签&#xff0c;他也是一个单标签 image属于服务器控件&#xff0c;是个双标签 问题&#xff1a;uniapp运行到app安卓基座后图片无法显示 原因&#xff1a;自己使用了img标签&#xff0c;而且输入路径无提示&#xff0c;img标签导致图片不显示 解决&#xff…

阅读文献总结2023

阅读文献基于卷积神经网络多源融合的网络安全态势感知模型 阅读文献 基于卷积神经网络多源融合的网络安全态势感知模型 题目基于卷积神经网络多源融合的网络安全态势感知模型文章信息&#xff1a;年份2023发文单位山西财经大学收录刊会计算机科学 &#xff08;北大核心&#…

微服务架构:解析分布式系统的演进

目录 微服务是什么&#xff1f; 微服务的优势 微服务的挑战 应对微服务挑战的方法 结论 在当今快速发展的软件开发领域&#xff0c;微服务架构成为一种备受瞩目的设计理念&#xff0c;被广泛应用于构建灵活、可扩展的分布式系统。本文将深入探讨什么是微服务&#xff0c;为…

掌握HarmonyOS框架的ArkTs如何管理和共享状态数据

ARKTS&#xff08;Ark TypeScript&#xff09;是HarmonyOS应用框架的一部分&#xff0c;提供了一种灵活而强大的状态管理机制。在ARKTS中&#xff0c;AppStorage和LocalStorage是两个关键的概念&#xff0c;它们分别用于应用级和页面级的状态共享。通过深入了解这两个特性&…

管理员配置Jupterhub

在Ubuntu上演示如何部署R语言环境&#xff0c;包括 posit的已经编译了一些R的安装 系统已经装了R&#xff0c;我额外编译的R如何让大家都能用到 如何配置RStudio jupyterhub的配置和使用 管理员如何配置jupyterhub 用户如何配置自己的jupyter环境 Ubuntu上的R部署 这一次…

【C语言】与文件有关的操作

目录 1. 前言2. 什么是文件&#xff1f;2.1 程序文件2.2 数据文件2.3 文件名 3. 二进制文件和文本文件&#xff1f;4. 文件的打开和关闭4.1 流和标准流4.1.1 流4.1.2 标准流 4.2 文件指针4.3 文件的打开和关闭 5. 文件的顺序读写5.1 顺序读写函数介绍5.2 对比一组函数 6. 文件的…

不会代码(零基础)学语音开发(语音开发板)

语音开发板板载资源介绍 通过上图可以看到VDB-150S语音开发板应该是集目前上市的所有语音开发板中板载资源最为丰富的一款开发板。板载的主要资源有&#xff1a; 双色LED状态指示灯&#xff1b;两个测试时使用的按键开关&#xff1b;红外寻迹/接近传感器&#xff1b;固件烧录器…

Redis分布式锁学习总结

⭐️ 前言 想必大家都有过并发编程的经验&#xff0c;在一个单体应用中&#xff0c;可以通过java提供的各种锁机制来控制多线程对于单体应用中同一资源的并发访问&#xff1b;那么在分布式场景下&#xff0c;想要控制多个应用对于同一外部资源的并发访问&#xff0c;就要用到分…

使用YOLOv8训练自己的数据集

&#x1f368; 本文为&#x1f517;365天深度学习训练营 中的学习记录博客&#x1f356; 原作者&#xff1a;K同学啊 | 接辅导、项目定制 拉取项目 git clone https://github.com/ultralytics/ultralytics安装依赖 cd ultralytics pip install -r requirement.txt pip instal…