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

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

力扣链接
目录:

  • 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 = None

class Solution:
    def kthSmallest(self, root: TreeNode, k: int) -> int:
        stack = []
        while True:
            while root:
                stack.append(root)
                root = root.left
            
            root = stack[-1]
            stack.pop()
            k -= 1
            if not k:
                return root.val
            root = 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 或不存在root
        if(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 = None

class Solution:
    def lowestCommonAncestor(self, root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode':
        if not root or root == p or root == q:
            return root
        left = self.lowestCommonAncestor(root.left, p, q)
        right = self.lowestCommonAncestor(root.right, p, q)
        if not left and not right:
            return None
        if left and not right:
            return left
        if not left and right :
            return right
        return 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:npos
            if(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 = None

class 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 None
        root = None
        if 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 root
    def deserialize(self, data):
        """Decodes your encoded data to tree.
        
        :type data: str
        :rtype: TreeNode
        """
        if len(data) == 0:
            return None
        list = data.split(" ")
        return self.deserializeCore(list)
    def deserializeCore(self, list):
        if len(list) == 0:
            return None
        root = None
        val = 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 corner
            all.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:
                # 更新last
                last[0] = p[0]
                last[1] = maxHeight
                res.append(last.copy())
        
        return res
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章