Leetcode 總結 -- Talking Recursively: 完全二叉樹和二叉搜索樹

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都是可行的,完全沒有使用到完全二叉樹的性質。
因為完全二叉樹是將節點從左向右放置的(最后一行),因此可以滿足如圖的性質:

兩種情況

  1. 當左子樹的深度和右子樹相等時:
    說明左子樹一定是滿二叉樹,右子樹是否是完全二叉樹不知道。
  2. 當左子樹的深度和右子樹不相等時:
    左子樹不一定是滿二叉樹,但是右子樹一定是滿二叉樹。

對于求深度,這里也不需要使用遞歸的策略,因為是滿二叉樹,所以先填充左側,通過一直看左子樹的深度就知道整棵樹的深度了,代碼如下:

 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;
        }
    }

二叉樹的修剪

修剪二叉搜索樹

  • 修剪二叉搜索樹
    對于這道題,可能一上來就回考慮到刪除是否要包含根節點的問題,如果兩者不分開考慮的話很容易掉進分析的誤區,這里不妨將是否包含根節點分解成兩個子問題來看:
    分解成兩個子問題
  1. 刪除不包含根節點

假設刪除不包含根節點,則刪除時:

  • 返回值:返回刪除完成之后的子樹
  • 子問題:節點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 在中序遍歷之后的序列中的前面一個節點稱為它的前驅節點,后面的節點稱為它的后繼節點。
    前驅和后繼

如果需要刪除二叉搜索樹的節點的話,需要保證刪除完成的中序遍歷也是有序的。那也就是說,可以使用這個被刪除的節點的前驅和后繼頂上。

這道題對于不同種類的節點的刪除操作是不一樣的,我們不妨通過枚舉法枚舉所有的情況


  1. 葉子節點,直接刪除
  2. 只有一個孩子:
    2.1 只有左孩子:


    只有左孩子

    可以使用前驅代替。
    為什么不使用后繼節點呢?因為后繼節點大于當前的值,一定在序列的后面,根據中序遍歷來說,這個值在該節點的右子樹或者父節點或者,該節點不存在右子樹,找父節點也不方便。
    2.2 只有右孩子:


    只有右孩子

    同理,需要使用后繼代替。
  3. 有兩個孩子:
    那就隨便使用前驅還是后繼了。

如何找前驅和后繼

對于節點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;
            }
        }
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,837評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,196評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,688評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,654評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,456評論 6 406
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,955評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,044評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,195評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,725評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,608評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,802評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,318評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,048評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,422評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,673評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,424評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,762評論 2 372