C++小记 - 二叉树

文章目录

  • 二叉树
  • 一、二叉树理论基础篇
    • 二叉树的种类
      • 满二叉树
      • 完全二叉树
      • 二叉搜索树
      • 平衡二叉搜索树
    • 二叉树的存储方式
      • 链式存储:
      • 顺序存储:
        • 遍历规则:
        • 构造实现:
    • 二叉树的遍历方式
    • 二叉树的定义
  • 二、二叉树的递归遍历
    • 递归算法的三个要素:
    • 递归版遍历
      • 前序遍历:
      • 中序遍历:
      • 后序遍历:
    • 迭代版遍历
      • 栈:
      • 前序遍历:
      • 中序遍历:
      • 后序遍历:
    • 三、完整代码

二叉树

一、二叉树理论基础篇

二叉树的种类

满二叉树

满二叉树:如果一棵二叉树只有度为0的结点和度为2的结点,并且度为0的结点在同一层上,则这棵二叉树为满二叉树。

图片

这棵二叉树为满二叉树,也可以说深度为k,有2^k-1个节点的二叉树。

完全二叉树

完全二叉树:除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h 层,则该层包含 1~ 2^h -1 个节点。

图片

之前我们刚刚讲过优先级队列其实是一个堆,堆就是一棵完全二叉树,同时保证父子节点的顺序关系。

二叉搜索树

前面介绍的树,都没有数值的,而二叉搜索树是有数值的了,二叉搜索树是一个有序树

  • 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
  • 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
  • 它的左、右子树也分别为二叉排序树

下面这两棵树都是搜索树:

图片

平衡二叉搜索树

平衡二叉搜索树:又被称为AVL(Adelson-Velsky and Landis)树,且具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。

图片

C++中map、set、multimap,multiset的底层实现都是平衡二叉搜索树,所以map、set的增删操作时间时间复杂度是log(n),注意我这里没有说unordered_map、unordered_set,unordered_map、unordered_map底层实现是哈希表。

二叉树的存储方式

二叉树可以链式存储,也可以顺序存储。

链式存储:

通过指针把分布在散落在各个地址的节点串联一起。存储方式是用指针

图片

顺序存储:

顺序存储的元素在内存是连续分布的,储的方式就是用数组

图片

遍历规则:

用数组来存储二叉树如何遍历的呢?

如果父节点的数组下表是i,那么它的左孩子就是i \* 2 + 1,右孩子就是 i \* 2 + 2

构造实现:

二叉树的遍历方式

二叉树主要有两种遍历方式:

  1. 深度优先遍历:先往深走,遇到叶子节点再往回走。
  2. 广度优先遍历:一层一层的去遍历。
  • 深度优先遍历

    • 前序遍历(递归法,迭代法)
    • 中序遍历(递归法,迭代法)
    • 后序遍历(递归法,迭代法)
  • 广度优先遍历

    • 层次遍历(迭代法)

这里前中后,其实指的就是中间节点的遍历顺序,只要大家记住 前中后序指的就是中间节点的位置就可以了。

  • 前序遍历:中左右
  • 中序遍历:左中右
  • 后序遍历:左右中

图片

最后再说一说二叉树中深度优先和广度优先遍历实现方式,我们做二叉树相关题目,经常会使用递归的方式来实现深度优先遍历,也就是实现前中后序遍历,使用递归是比较方便的。

之前我们讲栈与队列的时候,就说过栈其实就是递归的一种是实现结构,也就说前中后序遍历的逻辑其实都是可以借助栈使用非递归的方式来实现的。

而广度优先遍历的实现一般使用队列来实现,这也是队列先进先出的特点所决定的,因为需要先进先出的结构,才能一层一层的来遍历二叉树。

二叉树的定义

struct TreeNode {int val;TreeNode *left;TreeNode *right;TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};

二、二叉树的递归遍历

本篇将介绍前后中序的递归写法,一些同学可能会感觉很简单,其实不然,我们要通过简单题目把方法论确定下来,有了方法论,后面才能应付复杂的递归。

递归算法的三个要素:

每次写递归,都按照这三要素来写,可以保证大家写出正确的递归算法!

  1. **确定递归函数的参数和返回值:**确定哪些参数是递归的过程中需要处理的,那么就在递归函数里加上这个参数, 并且还要明确每次递归的返回值是什么进而确定递归函数的返回类型。
  2. **确定终止条件:**写完了递归算法, 运行的时候,经常会遇到栈溢出的错误,就是没写终止条件或者终止条件写的不对,操作系统也是用一个栈的结构来保存每一层递归的信息,如果递归没有终止,操作系统的内存栈必然就会溢出。
  3. **确定单层递归的逻辑:**确定每一层递归需要处理的信息。在这里也就会重复调用自己来实现递归的过程。

递归版遍历

前序遍历:

class Solution {
public:void traversal(TreeNode* cur, vector<int>& vec) {if (cur == NULL) return;vec.push_back(cur->val);    // 中traversal(cur->left, vec);  // 左traversal(cur->right, vec); // 右}vector<int> preorderTraversal(TreeNode* root) {vector<int> result;traversal(root, result);return result;}
};

中序遍历:

void traversal(TreeNode* cur, vector<int>& vec) {if (cur == NULL) return;traversal(cur->left, vec);  // 左vec.push_back(cur->val);    // 中traversal(cur->right, vec); // 右
}

后序遍历:

void traversal(TreeNode* cur, vector<int>& vec) {if (cur == NULL) return;traversal(cur->left, vec);  // 左traversal(cur->right, vec); // 右vec.push_back(cur->val);    // 中
}

迭代版遍历

栈:

(Stack):是只允许在一端进行插入或删除的线性表。首先栈是一种线性表,但限定这种线性表只能在某一端进行插入和删除操作。后进先出

头文件:#include<stack>

定义:

stack<int> st;
stack<string> st;

基本操作:

s.push(x);  //入栈
s.pop();  //出栈,只是删除栈顶元素,并不返回该元素
s.top()//访问栈顶元素
s.empty()//判断栈空,当栈空时,返回true
s.size()//访问栈中元素个数

迭代都是依据栈Stack来实现的

前序遍历:

前序遍历是中左右,每次先处理的是中间节点,那么先将跟节点放入栈中,然后将右孩子加入栈,再加入左孩子。

为什么要先加入 右孩子,再加入左孩子呢?
因为这样出栈的时候才是中左右的顺序。

在这里插入图片描述

① 进栈: 5 出栈 : 5 当前: 5
② 进栈: 64 出栈 :4 当前:6
③ 进栈: 21 出栈 : 1 当前:62
④ 进栈: 出栈 : 2 当前: 6
⑤ 进栈: 出栈 : 6 当前:

/*-----------迭代前序(左右中)-------------*/
void preTraversalByStack(TreeNode* root)
{stack<TreeNode*> st;if (root != NULL){st.push(root);		//进栈while (!st.empty()){TreeNode* cur = st.top();st.pop();		//出栈printVal(cur);	//中if (cur->right) st.push(cur->right);	//右if (cur->left) st.push(cur->left);//左}}
}

中序遍历:

为了解释清楚,我说明一下 刚刚在迭代的过程中,其实我们有两个操作:

  1. 处理:将元素放进result数组中
  2. 访问:遍历节点

分析一下为什么刚刚写的前序遍历的代码,不能和中序遍历通用呢,因为前序遍历的顺序是中左右,先访问的元素是中间节点,要处理的元素也是中间节点,所以刚刚才能写出相对简洁的代码,因为要访问的元素和要处理的元素顺序是一致的,都是中间节点。

那么再看看中序遍历,中序遍历是左中右,先访问的是二叉树顶部的节点,然后一层一层向下访问,直到到达树左面的最底部,再开始处理节点(也就是在把节点的数值放进result数组中),这就造成了处理顺序和访问顺序是不一致的。

那么在使用迭代法写中序遍历,就需要借用指针的遍历来帮助访问节点,栈则用来处理节点上的元素。

在这里插入图片描述

① 进栈: 541 出栈 : 1 4 当前: 5
② 进栈: 2 出栈 :2 当前:5
③ 进栈: 出栈 : 5 当前:
④ 进栈: 6 出栈 : 6 当前:

/*-----------迭代中序(左中右)-------------*/
void midTraversalByStack(TreeNode* root)
{stack<TreeNode*> st;TreeNode* cur = root;while (cur != NULL || !st.empty()){if (cur != NULL)			 指针来访问节点,访问到最底层{st.push(cur);			// 将访问的节点放进栈cur = cur->left;		//左}else{cur = st.top();			// 从栈里弹出的数据,就是优先遍历的数据st.pop();printVal(cur);			 //中cur = cur->right;		 // 右}}
}

后序遍历:

再来看后序遍历,先序遍历是中左右,后续遍历是左右中,那么我们只需要调整一下先序遍历的代码顺序,就变成中右左的遍历顺序,然后在反转result数组,输出的结果顺序就是左右中了,如下图:

图片`

/*-----------迭代后序(左右中)-------------*/
void preorderTraversalByStack(TreeNode* root)
{stack<TreeNode*> st;vector<int> result;if (root != NULL) {st.push(root);while (!st.empty()) {TreeNode* node = st.top();st.pop();result.push_back(node->val);if (node->left) st.push(node->left); // 相对于前序遍历,这更改一下入栈顺序 (空节点不入栈)if (node->right) st.push(node->right); // 空节点不入栈}reverse(result.begin(), result.end()); // 将结果反转之后就是左右中的顺序了}for (int i = 0; i< result.size(); ++i){cout << result[i] << " ";}
}

三、完整代码

#include <iostream>
#include <vector>
#include <stack>
using namespace std;struct TreeNode {int val;TreeNode *left;TreeNode *right;TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};// 数组构造二叉树
TreeNode* construct_binary_tree(const vector<int>& vec) {vector<TreeNode*> vecTree(vec.size(), NULL);TreeNode* root = NULL;// 把输入数值数组,先转化为二叉树节点数组for (int i = 0; i < vec.size(); i++) {TreeNode* node = NULL;if (vec[i] != -1) node = new TreeNode(vec[i]); // 用 -1 表示nullvecTree[i] = node;if (i == 0) root = node;}// 遍历一遍,中据规则左右孩子赋值就可以了// 注意这里 结束规则是 i * 2 + 1 < vec.size(),避免空指针// 为什么结束规则不能是i * 2 + 2 < arr.length呢?// 如果i * 2 + 2 < arr.length 是结束条件// 那么i * 2 + 1这个符合条件的节点就被忽略掉了// 例如[2,7,9,-1,1,9,6,-1,-1,10] 这样的一个二叉树,最后的10就会被忽略掉// 遍历一遍,中据规则左右孩子赋值就可以了for (int i = 0; i * 2 + 1 < vec.size(); i++) {if (vecTree[i] != NULL) {// 线性存储转连式存储关键逻辑vecTree[i]->left = vecTree[i * 2 + 1];if (i * 2 + 2 < vec.size())vecTree[i]->right = vecTree[i * 2 + 2];}}return root;
}// 数组构造二叉树(简约版)
TreeNode* simple_construct_binary_tree(vector<int> array, int index)
{TreeNode* root = nullptr;if (index < array.size() && array[index] != -1){root = new TreeNode(array[index]);root->left = simple_construct_binary_tree(array, 2 * index + 1);root->right = simple_construct_binary_tree(array, 2 * index + 2);}return root;
}/*-----------打印节点的数据-------------*/
void printVal(TreeNode* cur)
{if (cur != NULL)cout << cur->val << ' ';
}/*-----------递归前序(中左右)-------------*/
void preTraversal(TreeNode* root)
{//访问跟节点if (root != NULL){printVal(root);preTraversal(root->left);preTraversal(root->right);}
}/*-----------迭代前序(左右中)-------------*/
void preTraversalByStack(TreeNode* root)
{stack<TreeNode*> st;if (root != NULL){st.push(root);		//进栈while (!st.empty()){TreeNode* cur = st.top();st.pop();		//出栈printVal(cur);	//中if (cur->right) st.push(cur->right);	//右if (cur->left) st.push(cur->left);//左}}
}/*-----------递归中序(左中右)-------------*/
void midTraversal(TreeNode* root)
{if (root != NULL){midTraversal(root->left);printVal(root);midTraversal(root->right);}
}/*-----------迭代中序(左中右)-------------*/
void midTraversalByStack(TreeNode* root)
{stack<TreeNode*> st;TreeNode* cur = root;while (cur != NULL || !st.empty()){if (cur != NULL)			 指针来访问节点,访问到最底层{st.push(cur);			// 将访问的节点放进栈cur = cur->left;		//左}else{cur = st.top();			// 从栈里弹出的数据,就是优先遍历的数据st.pop();printVal(cur);			 //中cur = cur->right;		 // 右}}
}/*-----------递归后序(左右中)-------------*/
void preorderTraversal(TreeNode* root)
{if (root != NULL){preorderTraversal(root->left);preorderTraversal(root->right);printVal(root);}
}/*-----------迭代后序(左右中)-------------*/
void preorderTraversalByStack(TreeNode* root)
{stack<TreeNode*> st;vector<int> result;if (root != NULL) {st.push(root);while (!st.empty()) {TreeNode* node = st.top();st.pop();result.push_back(node->val);if (node->left) st.push(node->left); // 相对于前序遍历,这更改一下入栈顺序 (空节点不入栈)if (node->right) st.push(node->right); // 空节点不入栈}reverse(result.begin(), result.end()); // 将结果反转之后就是左右中的顺序了}for (int i = 0; i< result.size(); ++i){cout << result[i] << " ";}
}int main() {vector<int> arry = { 5, 4, 6, 1, 2, -1, -1 };TreeNode* root = simple_construct_binary_tree(arry, 0);cout << "递归后序(中左右):" << endl;;preorderTraversal(root);cout << "\n迭代后序(左中右):" << endl;preorderTraversalByStack(root);system("PAUSE");return 0;
}

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

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

相关文章

vue+element UI中给指定日期添加标记

1.日期控件中添加:picker-options属性&#xff0c;即:picker-options“myPickerOptions” <el-date-picker:class"item.scds !null ?xtsjBlue:xtsjRed"v-model"item.date"value-format"yyyy-MM-dd"type"date":picker-options"…

如何进行弱网测试?

&#x1f345; 视频学习&#xff1a;文末有免费的配套视频可观看 &#x1f345; 点击文末小卡片&#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 如今这个高度互联的时代里&#xff0c;网络环境对于应用程序的影响越来越重要。 而弱网测试就是…

leetcode--接雨水(双指针法,动态规划,单调栈)

目录 方法一&#xff1a;双指针法 方法二&#xff1a;动态规划 方法三&#xff1a;单调栈 42. 接雨水 - 力扣&#xff08;LeetCode&#xff09; 黑色的是柱子&#xff0c;蓝色的是雨水&#xff0c;我们先来观察一下雨水的分布情况: 雨水落在凹槽之间&#xff0c;在一个凹槽的…

使用js写一个登录验证码效果

面试题 登录页面获取验证码的功能&#xff0c;用户点击获取验证码按钮(id”btn1”)&#xff0c;按文字变为“(N)后获取验证码”&#xff0c;N为倒计对秒数&#xff0c;从 60 开始&#xff0c;每秒减一&#xff0c;减到 0的时候&#xff0c;按钮文字变为“获取验证码”&#xff…

Beans模块之工厂模块Aware

博主介绍:✌全网粉丝5W+,全栈开发工程师,从事多年软件开发,在大厂呆过。持有软件中级、六级等证书。可提供微服务项目搭建与毕业项目实战,博主也曾写过优秀论文,查重率极低,在这方面有丰富的经验✌ 博主作品:《Java项目案例》主要基于SpringBoot+MyBatis/MyBatis-plus+…

【JavaWeb】

Javaweb 数据库相关概念MySQL数据库MySQL数据模型SQLDDL--操作数据库图形化客户端工具DML--操作数据DQL数据库约束 数据库设计多表查询事务 数据库相关概念 数据库 存储数据的仓库&#xff0c;数据是有组织的进行存储 英文&#xff1a;DataBase&#xff0c;简称DB 数据库管理系…

Android T 远程动画显示流程其三——桌面侧动画启动到系统侧结束流程

前言 接着前文分析Android T 远程动画显示流程其二 我们通过IRemoteAnimationRunner跨进程通信从系统进程来到了桌面进程&#xff0c;这里是真正动画播放的逻辑。 之后又通过IRemoteAnimationFinishedCallback跨进程通信回到系统进程&#xff0c;处理动画结束时的逻辑。 进入…

使用maven项目引入jQuery

最近在自学 springBoot &#xff0c;期间准备搞一个前后端不分离的东西&#xff0c;于是需要在 maven 中引入jQuery 依赖&#xff0c;网上百度了很多&#xff0c;这里来做一个总结。 1、pom.xml 导入依赖 打开我们项目的 pom.xml 文件&#xff0c;输入以下坐标。这里我使用的是…

FPGA-学会使用vivado中的存储器资源ROM(IP核)

问题&#xff1a; 某芯片,有500个寄存器,需要在上电的时候由FPGA向这些寄存器中写入初始值,初始值已经通过相应的文档给出了具体值,这些值都是已知的。 分析关键点&#xff1a; 数据量比较多&#xff08;Verilog代码&#xff0c;通过case语句、always语句这种查找表的方式,数…

Linux——匿名管道

Linux——匿名管道 什么是管道匿名管道的底层原理观察匿名管道现象读写端的几种情况写端慢&#xff0c;读端快写端快&#xff0c;读端慢 管道的大小写端关闭&#xff0c;读端一直读写端一直写&#xff0c;读端关闭 我们之前一直用的是vim来编写代码&#xff0c;现在有了vscode这…

bert 相似度任务训练,简单版本

目录 任务 代码 train.py predit.py 数据 任务 使用 bert-base-chinese 训练相似度任务&#xff0c;参考&#xff1a;微调BERT模型实现相似性判断 - 知乎 参考他上面代码&#xff0c;他使用的是 BertForNextSentencePrediction 模型&#xff0c;BertForNextSentencePred…

thinkphp学习10-数据库的修改删除

数据修改 使用 update()方法来修改数据&#xff0c;修改成功返回影响行数&#xff0c;没有修改返回 0 public function index(){$data [username > 孙悟空1,];return Db::name(user)->where(id,11)->update($data);}如果修改数据包含了主键信息&#xff0c;比如 i…

STM32标准库开发——BKP备份RTC时钟

备份寄存器BKP(Backup Registers) 由于RTC与BKP关联性较高&#xff0c;所以RTC的时钟校准寄存器以及一些功能都放在了BKP中。TAMPER引脚主要用于防止芯片数据泄露&#xff0c;可以设计一个机关当TAMPER引脚发生电平跳变时自动清除寄存器内数据不同芯片BKP区别&#xff0c;主要体…

c++入门(2)

上期我们说到了部分c修补C语言的不足&#xff0c;今天我们将剩下的一一说清楚。 函数重载 (1).函数重载的形式 C语言不允许函数名相同的同时存在&#xff0c;但是C允许同名函数存在&#xff0c;但是有要求&#xff1a;函数名相同&#xff0c;参数不同&#xff0c;构成函数重…

【数据结构-图论】并查集

并查集&#xff08;Union-Find&#xff09;是一种数据结构&#xff0c;它提供了处理一些不交集的合并及查询问题的高效方法。并查集主要支持两种操作&#xff1a; 查找&#xff08;Find&#xff09;&#xff1a;确定某个元素属于哪个子集&#xff0c;这通常意味着找到该子集的…

人大金仓与mysql的差异与替换

人大金仓中不能使用~下面的符号&#xff0c;字段中使用”&#xff0c;无法识别建表语句 创建表时语句中只定义字段名.字段类型.是否是否为空 Varchar类型改为varchar&#xff08;长度 char&#xff09; Int(0) 类型为int4 定义主键&#xff1a;CONSTRAINT 键名 主键类型&#x…

Found option without preceding group in config file 问题解决

方法就是用记事本打开 然后 左上角点击 文件 有另存为 就可以选择编码格式

Linux设置程序任意位置执行(设置环境变量)

问题 直接编译出来的可执行程序在执行时需要写出完整路径比较麻烦&#xff0c;设置环境变量可以实现在任意位置直接运行。 解决 1.打开.bashrc文件 vim ~/.bashrc 2.修改该文件&#xff08;实现将/home/zhangziheng/file/seqrequester/build/bin&#xff0c;路径下的可执…

第六节:Vben Admin权限-后端控制方式

系列文章目录 第一节:Vben Admin介绍和初次运行 第二节:Vben Admin 登录逻辑梳理和对接后端准备 第三节:Vben Admin登录对接后端login接口 第四节:Vben Admin登录对接后端getUserInfo接口 第五节:Vben Admin权限-前端控制方式 文章目录 系列文章目录前言一、角色权限(后端…

【办公类-18-03】(Python)中班米罗可儿证书批量生成打印(班级、姓名)

作品展示——米罗可儿证书打印幼儿姓名 背景需求 2024年3月1日&#xff0c;中4班孩子一起整理美术操作材料《米罗可儿》的操作本——将每一页纸撕下来&#xff0c;分类摆放、确保纸张上下位置正确。每位孩子们都非常厉害&#xff0c;不仅完成了自己的一本&#xff0c;还将没有…