力扣高频|算法面试题汇总(一):开始之前
力扣高频|算法面试题汇总(二):字符串
力扣高频|算法面试题汇总(三):数组
力扣高频|算法面试题汇总(四):堆、栈与队列
力扣高频|算法面试题汇总(五):链表
力扣高频|算法面试题汇总(六):哈希与映射
力扣高频|算法面试题汇总(七):树
力扣高频|算法面试题汇总(八):排序与检索
力扣高频|算法面试题汇总(九):动态规划
力扣高频|算法面试题汇总(十):图论
力扣高频|算法面试题汇总(十一):数学&位运算

力扣高频|算法面试题汇总(七):树

力扣链接
目录:

  • 1.二叉搜索树中第K小的元素
  • 2.二叉树的最近公共祖先
  • 3.二叉树的序列化与反序列化
  • 4.天际线问题

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

给定一个二叉搜索树,编写一个函数 kthSmallest 来查找其中第 k 个最小的元素。
说明:
你可以假设 k 总是有效的,1 ≤ k ≤ 二叉搜索树元素个数。
示例 1:
输入: root = [3,1,4,null,2], k = 1
3
/
1 4

2
输出: 1
进阶:
如果二叉搜索树经常被修改(插入/删除操作)并且你需要频繁地查找第 k 小的值,你将如何优化 kthSmallest 函数?

思路
这题剑指offer中做过,详见:剑指offer|解析和答案(C++/Python) (五)上:二叉搜索树的第k个节点
就是通过中序遍历查找。
C++

/*** Definition for a binary tree node.* struct TreeNode {*     int val;*     TreeNode *left;*     TreeNode *right;*     TreeNode(int x) : val(x), left(NULL), right(NULL) {}* };*/
class Solution {public:int kthSmallest(TreeNode* root, int k) {     // 中序遍历获得二叉搜索树的第k个节点TreeNode* pNode =  kthSmallestCore(root, k);return pNode->val;}TreeNode* kthSmallestCore(TreeNode* pRoot,  int& k){TreeNode* target = NULL;if(pRoot->left != NULL)//遍历左子树target = kthSmallestCore(pRoot->left, k);if(target == NULL){if(k == 1)//表示已经到第k小的节点了target = pRoot;k--;}//target = NULL 表示没有找到//开始遍历右子树if(target == NULL && pRoot->right != NULL){target = kthSmallestCore(pRoot->right, k);}return target;}
};

官方的极简写法:

class Solution:def kthSmallest(self, root, k):""":type root: TreeNode:type k: int:rtype: int"""def inorder(r):return inorder(r.left) + [r.val] + inorder(r.right) if r else []return inorder(root)[k - 1]

思路2:
参考官方思路,进行迭代加速。
时间复杂度:O(H+k),其中 HH 指的是树的高度,由于我们开始遍历之前,要先向下达到叶,当树是一个平衡树时:复杂度为 O(logN+k)。当树是一个不平衡树时:复杂度为 O(N+k),此时所有的节点都在左子树。
空间复杂度:O(H+k)。当树是一个平衡树时:O(logN+k)。当树是一个非平衡树时:O(N+k)。

C++

/*** Definition for a binary tree node.* struct TreeNode {*     int val;*     TreeNode *left;*     TreeNode *right;*     TreeNode(int x) : val(x), left(NULL), right(NULL) {}* };*/
class Solution {public:int kthSmallest(TreeNode* root, int k) {     stack<TreeNode*> s;while(1){while(root){s.push(root);root = root->left;}root = s.top();s.pop();--k;if(k == 0)return root->val;root = root->right;}}};

Python:

# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, x):
#         self.val = x
#         self.left = None
#         self.right = Noneclass Solution:def kthSmallest(self, root: TreeNode, k: int) -> int:stack = []while True:while root:stack.append(root)root = root.leftroot = stack[-1]stack.pop()k -= 1if not k:return root.valroot = root.right

2.二叉树的最近公共祖先

给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
例如,给定如下二叉树: root = [3,5,1,6,2,0,8,null,null,7,4]
说明:
所有节点的值都是唯一的。
p、q 为不同节点且均存在于给定的二叉树中。

输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1
输出: 3
解释: 节点 5 和节点 1 的最近公共祖先是节点 3。

思路
递归。参考这位思路清晰的大佬:

  • 1.递归的临界条件。一般就是特殊情况:1.是否为空节点。还有另外一种特殊情况:公共节点就是自己。对于公共节点就是自己的判断为:root == p || root == q。当为临界条件时返回当前节点:root。根据临界条件,实际上可以发现这道题已经被简化为查找以root为根结点的树上是否有p结点或者q结点,如果有就返回p结点或q结点,否则返回null。
  • 2.分别对root节点的左子树和右子树进行递归查找p和q。
  • 3.对左右子树进行查找p和q节点无非四种情况:
    A. 左子树和右子树均为空节点,虽然题目说明p和q一定存在,但是局部情况下是有可能p和q均为找到。
    B.左子树非空节点,右子树空节点。说明在左子树找到p或q,那么返回左子树节点。
    C.左子树空节点,右子树非空节点。说明在右子树找到p或q,那么返回右子树节点。
    D.左子树非空节点,右子树非空节点。说明p和q分别在左右子树找到,返回当下root节点。

放一张图方便理解:

比如查找5和1,这种情况就是左子树和右子树均找到,因此返回当前root节点。
比如查找5和4,在查找到5时就不会继续递归查找结点5的子树(因为满足了邻接条件),5的root结点是3。3的右子树查找,未找到,返回的是空节点,在当前3结点满足:左子树返回的非空结点,右子树返回的空结点情况,因此返回左子树非空结点(这点很巧妙)。
C++

/*** Definition for a binary tree node.* struct TreeNode {*     int val;*     TreeNode *left;*     TreeNode *right;*     TreeNode(int x) : val(x), left(NULL), right(NULL) {}* };*/
class Solution {public:TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {// 中断条件: 找到p或q 或不存在rootif(root == p || root == q || !root)return root;TreeNode* left = lowestCommonAncestor(root->left, p, q);TreeNode* right = lowestCommonAncestor(root->right, p, q);// 如果左子树和右子树均不存在if(!left && !right) return NULL;// 如果存在左子树if(left && !right) return left;// 如果存在右子树if(!left && right) return right;return root;}
};

python:

# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, x):
#         self.val = x
#         self.left = None
#         self.right = Noneclass Solution:def lowestCommonAncestor(self, root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode':if not root or root == p or root == q:return rootleft = self.lowestCommonAncestor(root.left, p, q)right = self.lowestCommonAncestor(root.right, p, q)if not left and not right:return Noneif left and not right:return leftif not left and right :return rightreturn root

3.二叉树的序列化与反序列化

序列化是将一个数据结构或者对象转换为连续的比特位的操作,进而可以将转换后的数据存储在一个文件或者内存中,同时也可以通过网络传输到另一个计算机环境,采取相反方式重构得到原数据。
请设计一个算法来实现二叉树的序列化与反序列化。这里不限定你的序列 / 反序列化算法执行逻辑,你只需要保证一个二叉树可以被序列化为一个字符串并且将这个字符串反序列化为原始的树结构。
示例:
你可以将以下二叉树:
1
/
2 3
/
4 5
序列化为 “[1,2,3,null,null,4,5]”
提示: 这与 LeetCode 目前使用的方式一致,详情请参阅 LeetCode 序列化二叉树的格式。你并非必须采取这种方式,你也可以采用其他的方法解决这个问题。
说明: 不要使用类的成员 / 全局 / 静态变量来存储状态,你的序列化和反序列化算法应该是无状态的。

思路
这个和剑指offer上的一样,详见:剑指offer|解析和答案(C++/Python) (三):序列化二叉树
简单而言,就是前序遍历存储结点,再根据前序遍历结果重新生成。
C++

/*** Definition for a binary tree node.* struct TreeNode {*     int val;*     TreeNode *left;*     TreeNode *right;*     TreeNode(int x) : val(x), left(NULL), right(NULL) {}* };*/
class Codec {private:string ordNums;// 保存序列化数组vector<string> nodes;
public:// Encodes a tree to a single string.string serialize(TreeNode* root) {ordNums.clear();// 清空、重复使用serializeCore(root);// 拷贝tring string最好一个是空格不要string str2 = ordNums.substr(0, ordNums.size() - 1);return str2;}void serializeCore(TreeNode* root) {// 前序遍历if (root == NULL)ordNums += "# "; // 标记空结点//nodes.push_back("#");else {ordNums += to_string(root->val) + " ";serializeCore(root->left);serializeCore(root->right);}}TreeNode* deserializeCore(vector<TreeNode*> nums, int len, int &index) {if (index >= len)return NULL;TreeNode* root = NULL;if (index < len) {root = nums[index];if (root != NULL) {// 左边遇到null会到树底,返回后接着右子树,index始终++root->left = deserializeCore(nums, len, ++index);root->right = deserializeCore(nums, len, ++index);}}return root;}// Decodes your encoded data to tree.TreeNode* deserialize(string data) {if (data.size() == 0)return NULL;// 存储前序遍历的结点vector<TreeNode*> nums;string num;// 构造字符串流的时候,空格会成为字符串参数的内部分界stringstream ss(data);while (ss >> num) {if (num == "#") {nums.push_back(NULL);}else {int temp = atoi(num.c_str());nums.push_back(new TreeNode(temp));}}/**/int len = nums.size();int index = 0;TreeNode* root = deserializeCore(nums, len, index); //解析前序遍历节点得到树return root;}
};
// Your Codec object will be instantiated and called as such:
// Codec codec;
// codec.deserialize(codec.serialize(root));

但是这种写法超时了,参考:

/*** Definition for a binary tree node.* struct TreeNode {*     int val;*     TreeNode *left;*     TreeNode *right;*     TreeNode(int x) : val(x), left(NULL), right(NULL) {}* };*/
class Codec {public:// Encodes a tree to a single string.string serialize(TreeNode* root) {queue<TreeNode*> q;q.push(root);string res;while(!q.empty()){auto p=q.front();q.pop();if(p!=NULL){res+=to_string(p->val);res+=',';q.push(p->left);q.push(p->right);}else res+="null,";}return res;}// Decodes your encoded data to tree.TreeNode* deserialize(string data) {auto vals = split(data);queue<TreeNode*> q;if(vals[0]=="null")return NULL;q.push(new TreeNode(stoi(vals[0])));TreeNode *res=q.front();for(int i=1;i<vals.size();){if(vals[i]!="null"){auto p=new TreeNode(stoi(vals[i]));q.push(p);q.front()->left=p;}++i;if(vals[i]!="null"){auto p=new TreeNode(stoi(vals[i]));q.push(p);q.front()->right=p;}++i;q.pop();}return res;}vector<string> split(string &data){int start=0;vector<string> res;while(1){auto end = data.find(',',start);// 查找字符串a是否包含子串b,不是用strA.find(strB) > 0 而是 strA.find(strB) != string:nposif(end==string::npos)break;res.push_back(data.substr(start,end-start));start=end+1;}return move(res);}
};

Python

# Definition for a binary tree node.
# class TreeNode(object):
#     def __init__(self, x):
#         self.val = x
#         self.left = None
#         self.right = Noneclass Codec:def serialize(self, root):"""Encodes a tree to a single string.:type root: TreeNode:rtype: str"""self.str = ""self.serializeCore(root)return self.str[:-1]def serializeCore(self, root):if root == None:self.str += "# "else:self.str += str(root.val) + " "self.serializeCore(root.left)self.serializeCore(root.right)def deserializeCore(self, nums, length, index):if index >= length:return Noneroot = Noneif index < length:root = nums[index]if root != None:root.left = self.deserializeCore(nums, length, index + 1)root.right = self.deserializeCore(nums, length, index + 1)return rootdef deserialize(self, data):"""Decodes your encoded data to tree.:type data: str:rtype: TreeNode"""if len(data) == 0:return Nonelist = data.split(" ")return self.deserializeCore(list)def deserializeCore(self, list):if len(list) == 0:return Noneroot = Noneval = list.pop(0)if val != "#":root = TreeNode(int(val))root.left = self.deserializeCore(list)root.right = self.deserializeCore(list)return root
# Your Codec object will be instantiated and called as such:
# codec = Codec()
# codec.deserialize(codec.serialize(root))

4.天际线问题

城市的天际线是从远处观看该城市中所有建筑物形成的轮廓的外部轮廓。现在,假设您获得了城市风光照片(图A)上显示的所有建筑物的位置和高度,请编写一个程序以输出由这些建筑物形成的天际线(图B)。

每个建筑物的几何信息用三元组 [Li,Ri,Hi] 表示,其中 Li 和 Ri 分别是第 i 座建筑物左右边缘的 x 坐标,Hi 是其高度。可以保证 0 ≤ Li, Ri ≤ INT_MAX, 0 < Hi ≤ INT_MAX 和 Ri - Li > 0。您可以假设所有建筑物都是在绝对平坦且高度为 0 的表面上的完美矩形。
例如,图A中所有建筑物的尺寸记录为:[ [2 9 10], [3 7 15], [5 12 12], [15 20 10], [19 24 8] ] 。
输出是以 [ [x1,y1], [x2, y2], [x3, y3], … ] 格式的“关键点”(图B中的红点)的列表,它们唯一地定义了天际线。关键点是水平线段的左端点。请注意,最右侧建筑物的最后一个关键点仅用于标记天际线的终点,并始终为零高度。此外,任何两个相邻建筑物之间的地面都应被视为天际线轮廓的一部分。
例如,图B中的天际线应该表示为:[ [2 10], [3 15], [7 12], [12 0], [15 10], [20 8], [24, 0] ]。
说明:
任何输入列表中的建筑物数量保证在 [0, 10000] 范围内。
输入列表已经按左 x 坐标 Li 进行升序排列。
输出列表必须按 x 位排序。
输出天际线中不得有连续的相同高度的水平线。例如 […[2 3], [4 5], [7 5], [11 5], [12 7]…] 是不正确的答案;三条高度为 5 的线应该在最终输出中合并为一个:[…[2 3], [4 5], [12 7], …]

思路
参考大佬的解法,清晰易懂。

总结一下:
挨个遍历坐标,记录每个矩形框块的左上角,如果遇到右上角,则删除其坐标。
左上角会进行排序,每次遇到拐点时,(遇到左上角添加对应的高度,遇到右上角删除对应的高度),进行判断当前高度的最大值和上一个天际线结点的高度,如果不一样说明有新增的天际线拐点,添加到结果种,反之没有。
C++

class Solution {public:vector<vector<int>> getSkyline(vector<vector<int>>& buildings) {multiset<pair<int, int>> all;vector<vector<int>> res;for (auto& e : buildings) {all.insert(make_pair(e[0], -e[2])); // critical point, left cornerall.insert(make_pair(e[1], e[2])); // critical point, right corner}multiset<int> heights({0}); // 保存当前位置所有高度。vector<int> last = {0, 0}; // 保存上一个位置的横坐标以及高度for (auto& p : all) {if (p.second < 0) heights.insert(-p.second); // 左端点,高度入堆else heights.erase(heights.find(p.second)); // 右端点,移除高度// 当前关键点,最大高度// c.rbegin() 返回一个逆序迭代器,它指向容器c的最后一个元素auto maxHeight = *heights.rbegin();// 当前最大高度如果不同于上一个高度,说明这是一个转折点if (last[1] != maxHeight) {// 更新 last,并加入结果集last[0] = p.first;last[1] = maxHeight;res.push_back(last);}}return res;}
};

这里使用了C++的特性,引用热评:

很巧妙的做法,利用了 muliset 这一数据结构自动排序的特性。
multiset中的元素是 pair,对pair排序默认的方式是,先比较 first,哪个小则排在前;first 相等则 second小的排在前。 而 first 这里表示横坐标,second 为负时,表示建筑的左侧在这一位置,其绝对值表示建筑在的高度;second 为正时,表示建筑的右侧在这一位置。
所以对muliset遍历时,首先会取出横坐标小的点。如果2个点横坐标相等,会先取出 second 小的点,对于负数来说,其实就是高度更高的建筑。也就是说,两个点上有高度不同的建筑,会先取高的出来放入高度集合,集合中高度最大值和之前高度不同,就直接放入结果。后面更低高度的建筑加入并不会改变最大高度。
如果second为正,表示建筑物在此处结束,需要把相应高度从高度集合中删除。有相同建筑同时在此结束,则会先让较低的建筑离开,因为它们不会改变最大高度。只有当最高的建筑物离开时,才进行改变。
如果一个位置既有建筑物进来,又有建筑物离开,会先选择进来的,同理。 总结起来,我就是想说,这里把建筑物起始点的高度设为负数,真的很巧妙。

Python:

class Solution:def getSkyline(self, buildings: List[List[int]]) -> List[List[int]]:all = [] # 存储所有结点res = [] # 天际线结点for e in buildings:all.append([e[0], -e[2]]) # 左上角all.append([e[1], e[2]]) # 右上角all = sorted(all)# 保存当前位置所有高度。heights = [0]  # last = [0, 0]   #  保存上一个位置的横坐标以及高度for p in all:if p[1] < 0:# 左端点 高度入堆heapq.heappush(heights, p[1]) # python默认最小堆,模拟最大堆else:# 删除heights.remove(-p[1])# 重新生成堆heapq.heapify(heights)# 当前关键点, 最大高度maxHeight = -heights[0]if last[1] != maxHeight:# 更新lastlast[0] = p[0]last[1] = maxHeightres.append(last.copy())return res

力扣高频|算法面试题汇总(七):树相关推荐

  1. 2018 年力扣高频算法面试题汇总-难题记录-鸡蛋掉落

    题目描述: 你将获得 K 个鸡蛋,并可以使用一栋从 1 到 N  共有 N 层楼的建筑. 每个蛋的功能都是一样的,如果一个蛋碎了,你就不能再把它掉下去. 你知道存在楼层 F ,满足 0 <= F ...

  2. 算法面试题汇总 leetcode

    算法面试题汇总 leetcode

  3. 【机器学习算法面试题】七.如何进行线上A/B测试,如何划分实验组和对照组?

    欢迎订阅本专栏:<机器学习算法面试题> 订阅地址:https://blog.csdn.net/m0_38068876/category_11810806.html [机器学习算法面试题]一 ...

  4. leetcode 最常见的前端基础算法面试题汇总

    目录 二叉树 二叉树前中后遍历套路详解 对称二叉树 二叉树的最大深度 将有序数组转化为二叉搜索树 栈 有效的括号 最小栈 动态规划 最大子序和 爬楼梯 数学问题 加一 x的平方根 Excel表序列号 ...

  5. PHP面试高频算法面试题 -- 判断链表是否有环

    题目:给定一个链表,判断链表中是否有环. 关联力扣141题目 https://leetcode-cn.com/problems/linked-list-cycle/ 题解: PHP题解 functio ...

  6. 最近面试的几个大厂算法面试题汇总

    最近接受了一波社会毒打,几个大厂都经历了好几轮技术面试,面试形式有电话.语音.视频.现场面,内容主要集中在以往工作经历及项目经验.算法知识及理论.算法编程及工程能力.场景案例及分析,好在面试结果比较满 ...

  7. 高频算法面试题(字符串) 242. 有效的字母异位词

    leetcode 242. 有效的字母异位词 给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的一个字母异位词. 示例 1: 输入: s = "anagram", ...

  8. 算法面试题汇总(更新中)

    1.根据数字返回相应位置数字 def get_digit(num, i):# i=0 个位 1 十位 2 百位...return num // (10 ** i) % 10# print(get_di ...

  9. 2018年LeetCode高频算法面试题刷题笔记——只出现一次的数字(开始之前)

    1.解答前的碎碎念: 这个系列是写给找工作前的我看的,毕竟作为一个记性很差的人,可能过一段时间又忘了怎么写了...然后作为开胃菜的第一题就没有思路,真的是让人非常有挫败感了... 2.问题描述: 给定 ...

最新文章

  1. mybatis like的用法
  2. 图解首次使用流光5.0
  3. dagger android 学习(二):AndroidInjector的使用
  4. Bound Found POJ - 2566(尺取法)
  5. ecshop api.php,api.php
  6. Windows端高仿超级逼真Mac系统方法
  7. java jni librtmp_编译Android平台libRTMP库
  8. 电商促销必备优惠券醒目设计插画
  9. 小米 11 不送充电器;苹果已修复 iCloud 登录激活问题;Ruby 3.0.0 发布|极客头条...
  10. 中级软件设计师知识点总结
  11. 聚类算法之K-Means,K-Means++,elkan K-Means和MiniBatch K-Means算法流程
  12. 谜底是计算机的谜语英语,英语谜语大全及答案
  13. Armv8架构虚拟化
  14. msi(微星)UEFI模式 机械加固态双硬盘安装Ubuntu16
  15. 使用浏览器转化ASCII码为字符
  16. 电话机器人是如何筛选意向客户的
  17. Spark支持三种分布式部署方式
  18. 官网下载eclipse被墙、无法访问解决
  19. 记一次云服务器配置mysql 远程连接失败的解决方案
  20. 城管希课堂之Servlet

热门文章

  1. GPS北斗定位模块应用于车辆
  2. MATLAB中如何用对数方式显示图形坐标?
  3. gitlab下载地址
  4. 中国大学排名python爬虫_Python爬虫入门实例三之爬取软科中国大学排名
  5. 2021年起重机司机(限桥式起重机)考试内容及起重机司机(限桥式起重机)最新解析
  6. 杭州python培训兄弟连
  7. 码住 学网页制作需要的基本步骤都在这里
  8. crypto lib
  9. Xenu-web开发死链接检測工具应用
  10. java实现移动云mas短信对接