力扣高頻|算法面試題彙總(七):樹
力扣鏈接
目錄:
- 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