Talking Recursively Part II :
Complete Binary Tree & Binary Search Tree
這篇文章相較于Part I 而言會通過題目展示完全二叉樹和二叉搜索樹的性質。
題目列表
完全二叉樹
二叉查找樹
完全二叉樹和滿二叉樹
滿二叉樹
首先先介紹一下滿二叉樹的概念
在圖中可以看到,滿二叉樹除了葉子節點之外,其余的所有節點都有兩個子節點。
完全二叉樹
一棵深度為h的有n個結點的二叉樹,對樹中的結點按從上至下、從左到右的順序進行編號,如果編號為i(1≤i≤n)的結點與滿二叉樹中編號為i的結點在二叉樹中的位置相同,則這棵二叉樹稱為完全二叉樹。
完全二叉樹則其中有一部分是滿二叉樹,并且最后一層的節點需要連續集中在左側。
很顯然滿二叉樹是完全二叉樹的一個特例。
小根堆
如果將一個完全二叉樹按照層次遍歷的順序標記每個節點,我們不難發現,每一個父節點的值小于兩個子節點的值。
如果將這個完全二叉樹按照層次遍歷的方式進行序列化,可以得到一個數組,對于這個數組中的下標為i的個體,他的值(用value(i))表示,是小于value(2i)和value(2i+1)的。
所以對于一個這樣標記過的完全二叉樹,對于節點值為i的元素進行(i/2)向下取整就可以找到父節點的值。
二叉樹尋路
利用這個性質可以完成二叉樹尋路這道題。
本文的解法來源來自于這篇題解,二叉樹尋路,下面我結合這篇原文的思想說的更清楚一點。
對于一個編號完全的滿二叉樹(就像上面的圖一樣),將偶數行進行翻轉,就成了下面的二叉樹(下面的圖)。
對于數組反轉的策略,我們通常使用數組兩側對調位置(軸對稱翻轉)的策略來實現:
所以很明顯,原本在滿二叉樹
i
的父節點x
會被進行軸對稱翻轉到另外一側。但是盡管做了翻轉,上圖中對應顏色的兩個數字加起來的值是相等的(因為是按照順序編號),所以這個公式應該成立:但是Java中沒有提供直接的log2函數,這個地方可以使用換底公式。將公式寫成代碼如圖:
class Solution {
public List<Integer> pathInZigZagTree(int label) {
LinkedList<Integer> result= new LinkedList<>();
int N = (int) (Math.log(label) / Math.log(2)) + 1;
while(label > 1){
result.add(label);
N--;
label = (int) (3 * Math.pow(2, N - 1) - label/ 2- 1);
}
result.add(1);
Collections.reverse(result);
return result;
}
}
完全二叉樹的結點個數
-
完全二叉樹的節點個數
使用上一節講過的『要素分析法』: - 子問題: 對于子樹root的節點個數等于左子樹的節點個數+右子樹的節點個數
- 遞歸出口:對于root == null時 return 0
- 返回值: 表示節點root的節點數量
class Solution {
public int countNodes(TreeNode root) {
if(root == null) return 0;
int left = countNodes(root.left);
int right = countNodes(root.right);
return left + right + 1;
}
}
我也就寫到了上面這里,真正好的思路的題解來自這里:
這樣的代碼對于任何一棵樹root都是可行的,完全沒有使用到完全二叉樹的性質。
因為完全二叉樹是將節點從左向右放置的(最后一行),因此可以滿足如圖的性質:
- 當左子樹的深度和右子樹相等時:
說明左子樹一定是滿二叉樹,右子樹是否是完全二叉樹不知道。 - 當左子樹的深度和右子樹不相等時:
左子樹不一定是滿二叉樹,但是右子樹一定是滿二叉樹。
對于求深度,這里也不需要使用遞歸的策略,因為是滿二叉樹,所以先填充左側,通過一直看左子樹的深度就知道整棵樹的深度了,代碼如下:
class Solution {
private int depth(TreeNode root){
int depth = 0;
TreeNode node = root;
while(node != null) {
node = node.left;
depth++;
}
return depth;
}
public int countNodes(TreeNode root) {
if(root == null) return 0;
int left = depth(root.left);
int right= depth(root.right);
if(left == right) {
//左子樹數量 + 節點 + 右子樹
return (1 << left ) + countNodes(root.right);
}else{
//右子樹數量 + 節點 + 左子樹
return (1 << right) + countNodes(root.left);
}
}
}
二叉查找樹
二叉查找樹可以說是數據結構中非常優美和工整的一類了,它的定義是這樣的:
對于樹root,他的左子樹的所有節點全部小于root的value,右子樹的所有節點值大于root的value。
二叉查找樹的英文名字是:Binary Search Tree,這個名字一看就有一種『二分查找』的意思,確實,二叉查找樹的主要功能就是提供一個非常方便的二分搜索。
二叉搜索樹的基本性質
二叉搜索樹中的搜索
先通過一個題快速了解二叉搜索樹:
返回值: 如果存在這個節點,返回這個節點對應的Treenode
子問題:對于節點root,如果root的value等于給定值,返回root,如果小于給定值,在樹的右邊繼續搜索,如果大于給定值,在樹的左邊繼續搜索。
遞歸出口:當root為空時返回null,表示這個樹root不存在給定值對應的節點。
很顯然,這個題是一個遍歷結構的問題,可以直接使用遍歷模板:
class Solution {
//帶有返回值的遞歸搜索
public TreeNode searchBST(TreeNode root, int val) {
if(root == null || root.val == val) return root;
return root.val < val ? searchBST(root.right,val) :searchBST(root.left, val);
}
}
合法二叉搜索樹
這個題本身實際上是對二叉樹的定義的一個鞏固:
-
驗證二叉搜索樹
二叉搜索樹的定義在更深的層面上揭示了一棵樹root的左右子樹要滿足一個數量區間的約束關系,這道題思路比較簡單,直接上代碼:
public class 驗證二叉搜索樹 {
class Solution {
public boolean isValidBST(TreeNode root) {
return validate(root,Long.MIN_VALUE, Long.MAX_VALUE);
}
public boolean validate(TreeNode node, long min, long max){
if(node == null) return true;
if(node.val <= min || node.val >= max) return false;
return validate(node.left, min, node.val) && validate(node.right,node.val,max);
}
}
}
二叉搜索樹的構建
二叉搜索樹還有一個非常好的性質,二叉搜索樹的中序遍歷是有序的。下面看看兩個和有序數列相關的二叉搜索樹問題:
將有序鏈表轉換為二叉搜索樹
這個問題在最開始講鏈表的總結的時候講過,當時只是當做一個快慢指針的應用講的,這次我們從遞歸以及構建二叉平衡搜索樹的角度來看看這道題。
之前說過,二叉平衡搜索樹的定義是這樣的:對于樹root,其左子樹和右子樹的節點絕對值差不超過2(可以等于0、1)并且需要是一個二叉搜索樹。 因此我們需要從鏈表的中間節點下手來構建這棵二叉平衡搜索樹:
- 返回值: 需要返回最終構成的節點
- 子問題:將mid的左邊作為左子樹,將mid的右邊作為右子樹
-
遞歸出口:當鏈表head為空,或者mid等于head時(此時說明鏈表已經被分解成了惟一的節點,可以作為一一顆子樹了)
代碼如下:
class Solution {
public ListNode findMiddleElement(ListNode node){
ListNode prev = null;
ListNode slow = node;
ListNode fast = node;
while(fast != null && fast.next != null){
prev = slow;
slow = slow.next;
fast = fast.next.next;
}
if(prev != null) prev.next = null;
return slow;
}
public TreeNode sortedListToBST(ListNode head) {
if(head == null) return null;
ListNode mid = findMiddleElement(head);
if(mid == head) return new TreeNode(mid.val);
TreeNode midTreeNode = new TreeNode(mid.val);
midTreeNode.left = sortedListToBST(head);
midTreeNode.right = sortedListToBST(mid.next);
return midTreeNode;
}
}
二叉樹的修剪
修剪二叉搜索樹
-
修剪二叉搜索樹
對于這道題,可能一上來就回考慮到刪除是否要包含根節點的問題,如果兩者不分開考慮的話很容易掉進分析的誤區,這里不妨將是否包含根節點分解成兩個子問題來看:
分解成兩個子問題
- 刪除不包含根節點
假設刪除不包含根節點,則刪除時:
- 返回值:返回刪除完成之后的子樹
- 子問題:節點root的value 小于刪除下界,說明待刪除都聚集在root的右邊,繼續刪除右子樹;root的value大于上界說明待刪除都聚集在root的左邊。
- 遞歸出口:這個明顯有一個遍歷樹的特征,當root為空時,返回null
這樣的三個步驟是否也適用于包含根節點的情況呢?很顯然,確實!
對于圖二的情況,只要左移到左子樹,之后返回以3為根節點的子樹就可以了:
class Solution {
public TreeNode trimBST(TreeNode root, int L, int R) {
if(root == null) return null;
if(root.val < L) return trimBST(root.right, L, R);
if(root.val > R) return trimBST(root.left, L, R);
root.left = trimBST(root.left, L, R);
root.right= trimBST(root.right,L, R);
return root;
}
}
這道題和我們之前寫過的帶有返回值的遞歸函數有點點不同,以往寫的遞歸函數,如斐波拉契數列,求樹的深度,尾遞歸調用了兩個遞歸函數,求他們之間的一個數量關系,所以整體來看,這樣的遞歸函數的返回值并不是自身計算的結果,下面用一個形象的圖展示:
所以在最后驗證的時候,不需要有過多的顧慮。
刪除二叉搜索樹中的節點
-
刪除二叉搜索樹中的節點
二叉搜索樹還有一個非常重要的性質:二叉搜索樹的中序遍歷是一個有序的序列。節點node 在中序遍歷之后的序列中的前面一個節點稱為它的前驅節點,后面的節點稱為它的后繼節點。
前驅和后繼
如果需要刪除二叉搜索樹的節點的話,需要保證刪除完成的中序遍歷也是有序的。那也就是說,可以使用這個被刪除的節點的前驅和后繼頂上。
這道題對于不同種類的節點的刪除操作是不一樣的,我們不妨通過枚舉法枚舉所有的情況
- 葉子節點,直接刪除
-
只有一個孩子:
2.1 只有左孩子:
只有左孩子
可以使用前驅代替。
為什么不使用后繼節點呢?因為后繼節點大于當前的值,一定在序列的后面,根據中序遍歷來說,這個值在該節點的右子樹或者父節點或者,該節點不存在右子樹,找父節點也不方便。
2.2 只有右孩子:
只有右孩子
同理,需要使用后繼代替。 - 有兩個孩子:
那就隨便使用前驅還是后繼了。
如何找前驅和后繼
對于節點root,他的前驅應該小于root的value,但是需要是小于中的最大的,所以這個節點應該在root的左子樹的最右邊;同理可以找到后繼節點。
代碼如下:
class Solution {
private int forward(TreeNode node){
node = node.left;
while(node.right!= null) node = node.right;
return node.val;
}
private int backward(TreeNode node){
node = node.right;
while(node.left != null) node = node.left;
return node.val;
}
public TreeNode deleteNode(TreeNode node, int key) {
//需要
//為什么需要: 是中序遍歷的出口
if(node == null) return null;
if(key > node.val) node.right = deleteNode(node.right, key);
else if(key < node.val) node.left = deleteNode(node.left, key);
else{
if(node.left == null && node.right == null) node = null;
else if(node.right == null){
node.val = forward(node);
node.left = deleteNode(node.left, node.val);
}else{
node.val = backward(node);
node.right = deleteNode(node.right, node.val);
}
}
return node;
}
}