力扣DAY40-45 | 热100 | 二叉树:直径、层次遍历、有序数组->二叉搜索树、验证二叉搜索树、二叉搜索树中第K小的元素、右视图

前言

简单、中等 √ 好久没更了,感觉二叉树来回就那些。有点变懒要警醒,不能止步于笨方法!!

二叉树的直径

我的题解

遍历每个节点,左节点最大深度+右节点最大深度+当前节点=当前节点为中心的直径。如果左节点深度更大,向左遍历,直到直径不再更新。

class Solution {
public://最大深度int deepOfTree(TreeNode* root){if (!root)return 0;return max(deepOfTree(root->left), deepOfTree(root->right))+1;}int diameterOfBinaryTree(TreeNode* root) {TreeNode* node = root;int d = 0;int maxd = 0;while (node){int deepl = deepOfTree(node->left);int deepr = deepOfTree(node->right);d = deepl + deepr;maxd = max(maxd, d);if (deepl > deepr)node = node->left;else if (deepl < deepr)node = node->right;elsebreak;}return maxd;}
};

 上述方法耗时较长,原因是求最大深度和遍历每个节点的直径步骤重复了。优化后把直径设为全局节点。保留遍历最大深度函数,遍历过程中顺便更新直径maxd = max(maxd, deepl+deepr);

class Solution {
public:int maxd = 0;//最大深度int deepOfTree(TreeNode* node){if (!node)return 0;int deepl = deepOfTree(node->left);int deepr = deepOfTree(node->right);maxd = max(maxd, deepl+deepr);return max(deepl, deepr)+1;}int diameterOfBinaryTree(TreeNode* root) {deepOfTree(root);return maxd;}
};

官解

与笔者的方法二一致

心得

最大深度的延申题。

二叉树的层次遍历

我的题解

广度优先搜索,用一个队列存节点和深度,根据深度保存答案。

class Solution {
public:vector<vector<int>> levelOrder(TreeNode* root) {if (!root) return {};vector<vector<int>> ans;queue<pair<TreeNode*, int>> q;q.push({root, 0});while (!q.empty()){TreeNode* node = q.front().first;int level = q.front().second;if (level >= ans.size())ans.push_back({node->val});elseans[level].push_back(node->val);if (node->left) q.push({node->left, level + 1});if (node->right) q.push({node->right, level + 1});q.pop();}return ans;}
};

官解

广度优先搜索
思路和算法

我们可以用广度优先搜索解决这个问题。

我们可以想到最朴素的方法是用一个二元组 (node, level) 来表示状态,它表示某个节点和它所在的层数,每个新进队列的节点的 level 值都是父亲节点的 level 值加一。最后根据每个点的 level 对点进行分类,分类的时候我们可以利用哈希表,维护一个以 level 为键,对应节点值组成的数组为值,广度优先搜索结束以后按键 level 从小到大取出所有值,组成答案返回即可。

考虑如何优化空间开销:如何不用哈希映射,并且只用一个变量 node 表示状态,实现这个功能呢?

我们可以用一种巧妙的方法修改广度优先搜索:

首先根元素入队
当队列不为空的时候
求当前队列的长度 
依次从队列中取 元素进行拓展,然后进入下一次迭代
它和普通广度优先搜索的区别在于,普通广度优先搜索每次只取一个元素拓展,而这里每次取 元素。在上述过程中的第 i 次迭代就得到了二叉树的元素。

class Solution {
public:vector<vector<int>> levelOrder(TreeNode* root) {vector <vector <int>> ret;if (!root) {return ret;}queue <TreeNode*> q;q.push(root);while (!q.empty()) {int currentLevelSize = q.size();ret.push_back(vector <int> ());for (int i = 1; i <= currentLevelSize; ++i) {auto node = q.front(); q.pop();ret.back().push_back(node->val);if (node->left) q.push(node->left);if (node->right) q.push(node->right);}}return ret;}
};

心得

官解用了一个巧妙的方法节省了节点的深度信息。简单来说就是同时把同一层的节点遍历完。用currentLevelSize = q.size();记录当前层有多少个节点,然后内嵌循环把这些节点都遍历并且输出。后续我也尝试了这个方法但是漏了记录当前层节点的关键步骤。之后会留意..

将有序数组转换为二叉搜索树

我的题解

有点像分治法,取中点作为当前节点建树,左子树取左边数组作为新的数组建树,右子树取右边。

class Solution {
public:TreeNode* sort(vector<int>& nums, int l, int r){if (l > r)return nullptr;int mid = (l + r)/2;TreeNode* root = new TreeNode(nums[mid]);root->left = sort(nums, l, mid-1);root->right = sort(nums, mid+1, r);return root; }TreeNode* sortedArrayToBST(vector<int>& nums) {if (nums.empty())return nullptr;TreeNode* root = new TreeNode();root = sort(nums, 0, nums.size()-1);return root;}
};

官解

官解与笔者思路一致,只是策略不同(左、右,随机节点为子节点)

心得

有序数组转换为二叉搜索树还是比较简单的,可以研究下无序数组如何转换并且维护,应该跟最大堆差不多?

验证二叉搜索树

我的题解

左右子树有三个点要验证:1)子树值与当前节点值比对;2)子树是否也是二叉搜索树;3)子树的最大(右)/小(左)节点值与当前节点值比对。凡是一点不符合直接返回false,否则返回true。

class Solution {
public:bool isValidBST(TreeNode* root) {if (!root)return true;if (root->left){if (root->val <= root->left->val)return false;if (!isValidBST(root->left))return false;if (root->left->right){TreeNode* node = root->left->right;while (node->right){node = node->right;}if (root->val <= node->val)return false;}}if (root->right){if (root->val >= root->right->val)return false;if (!isValidBST(root->right))return false;if (root->right->left){TreeNode* node = root->right->left;while (node->left){node = node->left;}if (root->val >= node->val)return false;}}return true;}
};

官解

递归

要解决这道题首先我们要了解二叉搜索树有什么性质可以给我们利用,由题目给出的信息我们可以知道:如果该二叉树的左子树不为空,则左子树上所有节点的值均小于它的根节点的值; 若它的右子树不空,则右子树上所有节点的值均大于它的根节点的值;它的左右子树也为二叉搜索树。

这启示我们设计一个递归函数 helper(root, lower, upper) 来递归判断,函数表示考虑以 root 为根的子树,判断子树中所有节点的值是否都在 (l,r) 的范围内(注意是开区间)。如果 root 节点的值 val 不在 (l,r) 的范围内说明不满足条件直接返回,否则我们要继续递归调用检查它的左右子树是否满足,如果都满足才说明这是一棵二叉搜索树。

那么根据二叉搜索树的性质,在递归调用左子树时,我们需要把上界 upper 改为 root.val,即调用 helper(root.left, lower, root.val),因为左子树里所有节点的值均小于它的根节点的值。同理递归调用右子树时,我们需要把下界 lower 改为 root.val,即调用 helper(root.right, root.val, upper)。

函数递归调用的入口为 helper(root, -inf, +inf), inf 表示一个无穷大的值。

class Solution {
public:bool helper(TreeNode* root, long long lower, long long upper) {if (root == nullptr) {return true;}if (root -> val <= lower || root -> val >= upper) {return false;}return helper(root -> left, lower, root -> val) && helper(root -> right, root -> val, upper);}bool isValidBST(TreeNode* root) {return helper(root, LONG_MIN, LONG_MAX);}
};

中序遍历

基于方法一中提及的性质,我们可以进一步知道二叉搜索树「中序遍历」得到的值构成的序列一定是升序的,这启示我们在中序遍历的时候实时检查当前节点的值是否大于前一个中序遍历到的节点的值即可。如果均大于说明这个序列是升序的,整棵树是二叉搜索树,否则不是,下面的代码我们使用栈来模拟中序遍历的过程。

可能有读者不知道中序遍历是什么,我们这里简单提及。中序遍历是二叉树的一种遍历方式,它先遍历左子树,再遍历根节点,最后遍历右子树。而我们二叉搜索树保证了左子树的节点的值均小于根节点的值,根节点的值均小于右子树的值,因此中序遍历以后得到的序列一定是升序序列。

class Solution {
public:bool isValidBST(TreeNode* root) {stack<TreeNode*> stack;long long inorder = (long long)INT_MIN - 1;while (!stack.empty() || root != nullptr) {while (root != nullptr) {stack.push(root);root = root -> left;}root = stack.top();stack.pop();// 如果中序遍历得到的节点的值小于等于前一个 inorder,说明不是二叉搜索树if (root -> val <= inorder) {return false;}inorder = root -> val;root = root -> right;}return true;}
};

心得

两个官解都是好方法。递归:输入最大值和最小值,遍历每个节点的值,在限定范围内,左子树的最大值更新为当前节点值,右子树的最小值更新为当前节点值,子树都为二叉搜索树则返回true;迭代;迭代:中序遍历,由于二叉搜索树遍历出来应该是升序排列,故如果当前节点小于等于前一节点,直接返回false。这个题考察对二叉搜索树的理解,我的方法虽然绕过了long long但是太笨了!

二叉搜索树中第K小的元素

我的题解

中序遍历到第k个元素,return。

class Solution {
public:int kthSmallest(TreeNode* root, int k) {stack<TreeNode*> forder;TreeNode* node = root;vector<int> ans;int count = 0;forder.push(node);while(!forder.empty()){while(node){forder.push(node);node = node->left;}node = forder.top();ans.push_back(node->val);forder.pop();node = node->right;count++;if (count == k)break;}return ans[k-1];}
};

官解

中序遍历与笔者一致,不赘述。平衡二叉搜索树的方法太长了,粗略看每个函数也不太难,性价比不高,以后再学习吧。

记录子树的结点数

我们之所以需要中序遍历前 k 个元素,是因为我们不知道子树的结点数量,不得不通过遍历子树的方式来获知。

因此,我们可以记录下以每个结点为根结点的子树的结点数,并在查找第 k 小的值时,使用如下方法搜索:

令 node 等于根结点,开始搜索。

对当前结点 node 进行如下操作:

如果 node 的左子树的结点数 left 小于 k−1,则第 k 小的元素一定在 node 的右子树中,令 node 等于其的右子结点,k 等于 k−left−1,并继续搜索;
如果 node 的左子树的结点数 left 等于 k−1,则第 k 小的元素即为 node ,结束搜索并返回 node 即可;
如果 node 的左子树的结点数 left 大于 k−1,则第 k 小的元素一定在 node 的左子树中,令 node 等于其左子结点,并继续搜索。
在实现中,我们既可以将以每个结点为根结点的子树的结点数存储在结点中,也可以将其记录在哈希表中。

class MyBst {
public:MyBst(TreeNode *root) {this->root = root;countNodeNum(root);}// 返回二叉搜索树中第k小的元素int kthSmallest(int k) {TreeNode *node = root;while (node != nullptr) {int left = getNodeNum(node->left);if (left < k - 1) {node = node->right;k -= left + 1;} else if (left == k - 1) {break;} else {node = node->left;}}return node->val;}private:TreeNode *root;unordered_map<TreeNode *, int> nodeNum;// 统计以node为根结点的子树的结点数int countNodeNum(TreeNode * node) {if (node == nullptr) {return 0;}nodeNum[node] = 1 + countNodeNum(node->left) + countNodeNum(node->right);return nodeNum[node];}// 获取以node为根结点的子树的结点数int getNodeNum(TreeNode * node) {if (node != nullptr && nodeNum.count(node)) {return nodeNum[node];}else{return 0;}}
};class Solution {
public:int kthSmallest(TreeNode* root, int k) {MyBst bst(root);return bst.kthSmallest(k);}
};

心得

中序遍历的迭代写法还需要再巩固,以当前节点为判断条件,而不是以下一节点为判断条件。记录子树的结点数的方法:首先要用一个哈希表记录每个节点有多少颗子树,计算过程定义count函数,获取过程定义get函数。然后遍历每个节点,如果子树数量小于k,移到right,如果等于k,返回当前节点,大于k,移到left。感觉这个方法不是很能复用到其他题目上,但是count和get(哈希)这种分开的方式很值得我学习,是一种安全高效的获取方式。

二叉树的右视图

我的题解

对二叉树进行层次遍历,把每一层的最后一个元素取出放入ans容器中。

class Solution {
public:vector<int> rightSideView(TreeNode* root) {if (!root)return {};queue<pair<TreeNode*, int>> q;vector<vector<int>> bfs;vector<int> ans;TreeNode* node = root;int level = 0;q.push({node, level});while (!q.empty()){node = q.front().first;level = q.front().second;q.pop();if (node->left) q.push({node->left, level+1});if (node->right) q.push({node->right, level+1});if (level >= bfs.size()) bfs.push_back({node->val});else bfs[level].push_back(node->val);}for (int i = 0; i < bfs.size(); i++){ans.push_back(bfs[i].back());}return ans;}
};

官解

广度优先搜索/层次遍历与笔者思路一致,不赘述。

深度优先搜索

我们对树进行深度优先搜索,在搜索过程中,我们总是先访问右子树。那么对于每一层来说,我们在这层见到的第一个结点一定是最右边的结点。

算法

这样一来,我们可以存储在每个深度访问的第一个结点,一旦我们知道了树的层数,就可以得到最终的结果数组。

上图表示了问题的一个实例。红色结点自上而下组成答案,边缘以访问顺序标号。

class Solution {
public:vector<int> rightSideView(TreeNode* root) {unordered_map<int, int> rightmostValueAtDepth;int max_depth = -1;stack<TreeNode*> nodeStack;stack<int> depthStack;nodeStack.push(root);depthStack.push(0);while (!nodeStack.empty()) {TreeNode* node = nodeStack.top();nodeStack.pop();int depth = depthStack.top();depthStack.pop();if (node != NULL) {// 维护二叉树的最大深度max_depth = max(max_depth, depth);// 如果不存在对应深度的节点我们才插入if (rightmostValueAtDepth.find(depth) == rightmostValueAtDepth.end()) {rightmostValueAtDepth[depth] =  node -> val;}nodeStack.push(node -> left);nodeStack.push(node -> right);depthStack.push(depth + 1);depthStack.push(depth + 1);}}vector<int> rightView;for (int depth = 0; depth <= max_depth; ++depth) {rightView.push_back(rightmostValueAtDepth[depth]);}return rightView;}
}; 

心得

层次遍历的解法是直观的解法。深度优先搜索的解法则是定义了哈希表记录每个深度最右的节点。维护一个节点栈和深度栈,然后对二叉树进行后序遍历,如果当前深度没有最右节点,则放入ans中。这个解法其实也与层次遍历类似,只是显式地用栈来维护深度信息。

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

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

相关文章

头歌数据库【数据库概论】第10-11章 故障恢复与并发控制

第1关&#xff1a;数据库恢复技术 1、事务的&#xff08; A&#xff09;特性要求事务必须被视为一个不可分割的最小工作单元 A、原子性 B、一致性 C、隔离性 D、持久性 2、事务的&#xff08;C &#xff09;特性要求一个事务在执行时&#xff0c;不会受到其他事务的影响。 A、原…

windows下,cursor连接MCP服务器

1.下载并安装node 安装后&#xff0c;在cmd命令框中&#xff0c;输入命令node -v可以打印版本号&#xff0c;证明安装完成 2.下载MCP服务器项目 在MCP服务器找到对应项目&#xff0c;这里以server-sequential-thinking为例子 在本地cmd命令窗口&#xff0c;使用下面命令下载…

前端配置husky,commit-lint导致的git提交错误:git xx@0.0.0 lint:lint-staged

前端配置husky&#xff0c;commit-lint导致的git提交错误&#xff1a;git xx0.0.0 lint:lint-staged git commit -m "xxx"时出现以下报错&#xff0c;可能是前端配置husky&#xff0c;commit-lint的原因 //报错信息 git xx0.0.0 lint:lint-staged首先要知道出现这个错…

各种场景的ARP攻击描述笔记(超详细)

1、ARP报文限速 上一章我们说过ARP报文也是需要上送CPU进行处理的协议报文,如果设备对收到的大量ARP报文全部进行处理,可能导致CPU负荷过重而无法处理其他业务。因此,在处理之前需要对ARP报文进行限速,以保护CPU资源。 1.根据源MAC地址或源IP地址进行ARP限速 当设备检测到某一…

Django 创建CSV文件

Django使用Python内置的CSV库来创建动态的CSV&#xff08;逗号分隔值&#xff09;文件。我们可以在项目的视图文件中使用这个库。 让我们来看一个例子&#xff0c;这里我们有一个Django项目&#xff0c;我们正在实现这个功能。创建一个视图函数 getfile() 。 Django CSV例子 …

HTTPS为何仍有安全漏洞?解析加密协议下的攻击面

本文深度剖析HTTPS协议在传输层、证书体系、配置管理三个维度的安全盲区&#xff0c;揭示SSL/TLS加密掩盖下的11类攻击路径。基于Equifax、SolarWinds等重大事件的技术复盘&#xff0c;提供包含自动化证书巡检、动态协议升级、加密流量威胁检测的立体防御方案。 HTTPS不等于绝…

MyBatis 动态 SQL 使用详解

&#x1f31f; 一、什么是动态 SQL&#xff1f; 动态 SQL 是指根据传入参数&#xff0c;动态拼接生成 SQL 语句&#xff0c;不需要写多个 SQL 方法。MyBatis 提供了 <if>、<choose>、<foreach>、<where> 等标签来实现这类操作 ✅ 二、动态 SQL 的优点…

乐观锁与悲观锁的使用场景

悲观锁的应用场景 悲观锁的基本思想是假设并发冲突会发生&#xff0c;因此在操作数据时会先锁定数据&#xff0c;直到完成操作并提交事务后才释放锁。这种方式适用于写操作较多、并发冲突可能性较高的场景。 高写入比例的数据库操作&#xff1a;如果系统中有很多写操作&#x…

cpp(c++)win 10编译GDAL、PROJ、SQLite3、curl、libtiff

cpp&#xff08;c&#xff09;编译GDAL、PROJ、SQLite3 Sqlite3libtiffcurlprojGDAL Sqlite3 1、下载 Sqlite3 源码、工具、二进制预编译 exe Sqlite3 官网&#xff1a;https://www.sqlite.org/download.html 下载 sqlite-amalgamation-3430200.zipsqlite-dll-win64-x64-3430…

【愚公系列】《高效使用DeepSeek》062-图书库存管理

🌟【技术大咖愚公搬代码:全栈专家的成长之路,你关注的宝藏博主在这里!】🌟 📣开发者圈持续输出高质量干货的"愚公精神"践行者——全网百万开发者都在追更的顶级技术博主! 👉 江湖人称"愚公搬代码",用七年如一日的精神深耕技术领域,以"…

链表算法中常用操作和技巧

目 1.常用技巧 1.1.画图 1.2.添加虚拟头节点 1.3.大胆引入中间变量 1.4.快慢双指针 1.4.1判断链表是否有环 1.4.2找链表中环的入口 ​2.常用操作 2.1. 创建一个新节点 2.2.尾插 2.3.头插 1.常用技巧 1.1.画图 画图可以让一些抽象的文字语言更加形象生动 画图&#…

【9】数据结构的串篇章

目录标题 串的定义顺序串的实现初始化赋值打印串求串的长度复制串判断两个串长度是否相等连接两个串比较两个串内容是否相等插入操作删除操作调试与代码合集 串的模式匹配算法朴素的模式匹配算法KMP算法实现模式匹配 串的定义 定义&#xff1a;由0个或多个字符组成的有限序列&…

GMSL Strapping Pins CFG0/CFG1 应用

GMSL device 使用起来还是比较简单 ADI 已经充分考虑了用户的需求&#xff0c;尽可能的降低的芯片的使用和配置复杂度 一对加串器和解串器&#xff0c;只要工作模式匹配得当&#xff0c;Link Locked&#xff0c;便能够正常工作 如果遇到 Link 无法建立&#xff08;Locked&…

`uia.WindowControl` 是什么:获取窗口文字是基于系统的 UI 自动化接口,而非 OCR 方式

uia.WindowControl 是什么:获取窗口文字是基于系统的 UI 自动化接口,而非 OCR 方式 uia.WindowControl 通常是基于 Windows 系统的 UI 自动化框架(如 pywinauto 中的 uia 模块)里用于表示窗口控件的类。在 Windows 操作系统中,每个应用程序的窗口都可以看作是一个控件,ui…

Easysearch VS Opensearch 数据写入与存储性能对比

本文记录 Easysearch 和 Opensearch 数据写入和数据存储方面的性能对比。 准备 压测工具&#xff1a;INFINI Loadgen 对比版本&#xff1a; Easysearch 1.11.1&#xff08;lucene 8.11.4&#xff09;Opensearch 2.19.1&#xff08;lucene 9.12.1&#xff09; 节点 JVM 配置…

力扣题解:142. 环形链表 II

在链表学习中&#xff0c;我们已经了解了单链表和双链表&#xff0c;两者的最后一个结点都会指向NULL&#xff1b;今天我们介绍的循环列表则不同&#xff0c;其末尾结点指向的这是链表中的一个结点。 循环链表是一种特殊类型的链表&#xff0c;其尾节点的指针指向头节点&#…

区间 dp 系列 题解

1.洛谷 P4342 IOI1998 Polygon 我的博客 2.洛谷 P4290 HAOI2008 玩具取名 题意 某人有一套玩具&#xff0c;并想法给玩具命名。首先他选择 W, I, N, G 四个字母中的任意一个字母作为玩具的基本名字。然后他会根据自己的喜好&#xff0c;将名字中任意一个字母用 W, I, N, G …

天基光学图像仿真原理简介

一、原理简介 天基光学图像仿真通过数学模型和算法模拟空间目标在光学系统中的成像过程&#xff0c;核心原理可归纳为以下四部分&#xff1a; 1. 目标与背景建模‌ 目标运动建模‌&#xff1a;利用轨道动力学模型&#xff08;如SGP4&#xff09;解析空间目标轨迹&#xff0c;…

Jetpack Compose 状态保存机制全面解析:让UI状态持久化

在Android开发中&#xff0c;Jetpack Compose 的状态管理是一个核心话题&#xff0c;而状态保存则是确保良好用户体验的关键。本文将深入探讨Compose中各种状态保存技术&#xff0c;帮助你在配置变更和进程重建时保持UI状态。 一、基础保存&#xff1a;rememberSaveable reme…

【Json-Rpc #1】项目背景及环境搭建

&#x1f4c3;个人主页&#xff1a;island1314 &#x1f525;个人博客&#xff1a;island ⛺️ 欢迎关注&#xff1a;&#x1f44d;点赞 &#x1f442;&#x1f3fd;留言 &#x1f60d;收藏 &#x1f49e; &#x1f49e; &#x1f49e; 生活总是不会一帆风顺&#xff0c;前进…