l2-004 这是二叉搜索树吗?_LeetCode 例题精讲 | 11 二叉树转化为链表:二叉树遍历中的相邻结点...
本期例題:
- LeetCode 98. Validate Binary Search Tree 驗證二叉搜索樹(Medium)
- LeetCode 426. Convert Binary Tree to Sorted Doubly Linked List 二叉樹轉化為鏈表(Medium)
本文將介紹二叉樹問題中一個特殊的技巧:「在二叉樹的前/中/后序遍歷時對相鄰結點進行操作」。這種方法不適用于大多數題目,但在一些特定的題目中使用這個技巧,能起到「秒殺」的效果。
還記得當年數據結構課上,老師對于二叉樹的前序、中序、后序遍歷的諄諄教誨嗎?可能你一看到二叉樹,前/中/后序三種遍歷就在腦海中浮現出來。然而,當你開始在 LeetCode 上刷題,做了許多二叉樹題目之后,就會發現,做這些題目似乎根本用不上什么前/中/后序遍歷!
是的,求解二叉樹問題最重要的思路是子問題思路,前面的幾篇關于二叉樹的文章一直在討論的就是這種子問題思路。因為很多二叉樹問題都是通過劃分子問題,利用遞歸求解。對于這種子問題的思路來說,我們只是讓左子樹和右子樹遞歸地完成計算任務,至于是左子樹先計算還是右子樹先計算,根本不重要,更不用說什么前/中/后序遍歷了。
不過,二叉樹的子問題思路并不是萬能的。有些時候,用子問題來解題會比較麻煩。有時候題目具有特殊的性質,把前/中/后序遍歷的思想掏出來會更加有效。本文就來討論一下在什么時候適合用前/中/后序遍歷的解題思路。
驗證二叉搜索樹:比較相鄰結點
LeetCode 98. Validate Binary Search Tree(Medium)
給定一個二叉樹,判斷其是否是一個有效的二叉搜索樹(BST)。二叉搜索樹需要滿足以下特征:
- 結點的左子樹只包含小于當前結點的值;
- 結點的右子樹只包含大于當前結點的值;
- 所有左子樹和右子樹自身也是二叉搜索樹。
驗證二叉搜索樹這道題,其實完全可以用子問題的思路來做。我們可以定義三個子問題:「二叉樹是否為 BST」、「二叉樹的最小值」、「二叉樹的最大值」。(至于為什么是這三個子問題,其實是有固定的套路的,詳見上一篇文章:二叉樹問題太復雜?「三步走」方法解決它!)對于每個子樹而言,如果根結點的值小于左子樹的最大值、或者大于右子樹的最小值,那么就不滿足二叉搜索樹的性質。題解代碼如下所示:
//?注意:此代碼不完整,僅用于展示思路boolean?res;
public?boolean?isValidBST(TreeNode?root)?{
????res?=?true;
????traverse(root);
????return?res;
}
//?返回兩個值
//?返回值0:二叉樹的最小值
//?返回值1:二叉樹的最大值
int[]?traverse(TreeNode?root)?{
????if?(root?==?null)?{
????????return???;?//?邊界情況不好定義
????}
????int[]?left?=?traverse(root.left);
????int[]?right?=?traverse(root.right);
????if?(root.val?<=?left[1]?||?root.val?>=?right[0])?{
????????res?=?false;
????}
????return?new?int[]{left[0],?right[1]};
}
這段代碼中有一個空缺的地方:root == null 的時候,子問題如何返回?對于空子樹而言,其最大值和最小值不存在,我們還需要使用特殊的 null 值來表示不存在最大值和最小值的情況,在代碼中要多出很多條件判斷,非常麻煩。
我們不妨換一種思路考慮這道題。二叉搜索樹其實還有另一個性質:它的中序遍歷序列一定是遞增的!我們只需要兩兩比較中序遍歷序列中相鄰的兩個數字即可。這不需要考慮各種子問題的特殊情況。
二叉搜索樹的中序遍歷序列一定是遞增的不過,這里雖然提到了中序遍歷的「序列」,但并不需要用額外的空間保存這個序列。我們可以在遍歷的過程中,用一個 prev 變量維護「上一個結點」,每次把當前結點的值和上一個結點的值進行比較。這樣一個 prev 變量需要定義為全局變量,才能不受遞歸調用的影響。
最終,我們可以寫出這樣的題解代碼:
TreeNode?prev;?//?全局變量:指向中序遍歷的上一個結點boolean?valid;
public?boolean?isValidBST(TreeNode?root)?{
????valid?=?true;
????prev?=?null;
????traverse(root);
????return?valid;
}
void?traverse(TreeNode?curr)?{
????if?(curr?==?null)?{
????????return;
????}
????traverse(curr.left);
????//?中序遍歷的寫法,把操作寫在兩個遞歸調用中間
????if?(prev?!=?null?&&?prev.val?>=?curr.val)?{
????????//?如果中序遍歷的相鄰兩個結點大小關系不對,則二叉搜索樹不合法
????????valid?=?false;
????}
????//?維護?prev?指針
????prev?=?curr;
????traverse(curr.right);
}
這段代碼的思路要簡單很多。我們在遍歷到每個結點時都比較 prev 和 curr 結點的值。如果出現上一個結點的值大于當前結點的情況,就說明二叉搜索樹不合法。
「相鄰結點」遍歷框架
我們可以在上面的題解代碼中,抽出一個二叉樹中序遍歷的基本框架:
TreeNode?prev;?//?prev?指向中序遍歷的上一個結點//?curr?指向中序遍歷的當前結點
void?traverse(TreeNode?curr)?{
????if?(curr?==?null)?{
????????return;
????}
????traverse(curr.left);
????if?(prev?!=?null)?{
????????//?在這里對?prev?和?curr?進行操作
????????//?...
????}
????prev?=?curr;?//?維護?prev?指針
????traverse(curr.right);
}
這段代碼其實就是在二叉樹中序遍歷的基礎上,增加了一個 prev 變量指向中序遍歷的上一個結點,并在每次遍歷的時候對 prev 和 curr 這一對結點進行操作。
還記得我們在本系列第一講中的鏈表遍歷框架嗎?鏈表遍歷框架是在鏈表遍歷中維護一個 prev 指針指向前一個結點,而這里是在二叉樹遍歷中維護一個 prev 指針指向前一個結點。不同的是,二叉樹的遍歷需要做大量遞歸調用,所以需要把 prev 指針寫成一個全局變量。
語言小貼士: 很多二叉樹題目的代碼都需要用到全局變量。不過在軟件開發中,使用全局變量有不少弊端。除了用「真正的」全局變量,我們還可以用一些特殊的函數參數,達到和全局變量一樣的效果。
在 C++ 中,可以使用引用類型的參數。引用類型的參數可以直接改變上層函數中變量的值,從而能夠穿過一層層的遞歸函數。
int?foo(TreeNode*?root)?{????int?res?=?0;
????traverse(root,?res);
????return?res;
}
void?traverse(TreeNode*?root,?int&?res)?{
????res?=?1;
}
在 Java 中,可以使用長度為 1 的數組,因為數組元素的空間分配在堆上,與遞歸調用無關。
int?foo(TreeNode?root)?{????int[]?res?=?new?int[1];
????traverse(root,?res);
????return?res[0];
}
void?traverse(TreeNode?root,?int[]?res)?{
????res[0]?=?1;
}
每次對 prev 和 curr 這一對相鄰結點進行操作,意味著什么呢?以中序遍歷為例,我們可以想象有一根線按照中序遍歷的順序依次穿過了每一個結點:
想象一根線按照遍歷順序穿過每一個結點這樣,每次遍歷的時候,prev 和 curr 都指向這根線上的相鄰兩個結點。「驗證二叉搜索樹」這道題,正是比較了這根線上所有的相鄰結點。
這種對「相鄰結點」進行操作的思路,就和子問題沒有任何關系了。因為中序遍歷相鄰的兩個結點,在二叉樹上可能風馬牛不相及,如上圖中的 4 號和 5 號結點。也正是因為這個特點,這種方法可以不受遞歸的限制,把中序遍歷中任何相鄰的兩個結點拉過來。在某些情況下,這種方法比子問題的方法更方便。
二叉樹轉鏈表:串聯相鄰結點
LeetCode 426. Convert Binary Tree to Sorted Doubly Linked List(Medium)
這道題是一道會員專享的題目。不過沒關系,我們可以看中文站題庫中的一道相同的題目:面試題36. 二叉搜索樹與雙向鏈表(Medium)
給定一棵二叉搜索樹,將其轉換成一個排序的循環雙向鏈表。鏈表不使用新的結點,而是用二叉樹的結點,調整其中指針的指向(left 指針表示鏈表的 prev 指針,right 指針表示鏈表的 next 指針)。
題目示例需要注意的是,這道題雖然題干上說的是二叉搜索樹,但并沒有涉及到太多二叉搜索樹的性質。這道題實際上就是讓我們把一棵二叉樹按中序遍歷的順序轉化為鏈表。
同樣的,對于這道題目,我們可以先思考思考子問題的思路。我們可以讓左右子樹遞歸地構造出兩段鏈表,再把根結點和這兩段鏈表拼接起來。不過,拼接三段鏈表涉及到的指針操作很多,還需要考慮左右子樹為空的情況。這種方法寫成代碼的話,會很麻煩。
用相鄰結點的思路,能不能讓解法更簡單呢?我們再回顧一下這個用一根線穿過每一個結點的圖,同樣是中序遍歷的順序:
想象一根線按照遍歷順序穿過每一個結點這些結點同時也是鏈表的結點,而這根線穿過的順序正是我們所需要的鏈表的順序。那么,我們只要把其中的相鄰結點用指針鏈接起來,就得到了符合題意的鏈表。沒錯,思路就是這么簡單。我們可以寫出大概的偽代碼:
void?traverse(Node?curr)?{????if?(curr?==?null)?{
????????return;
????}
????//?中序遍歷
????traverse(curr.left);?//?遍歷左子樹
????list.append(curr);?//?(偽代碼)將當前結點加入鏈表
????last?=?curr;?//?更新?last?指針
????traverse(curr.right);?//?遍歷右子樹
}
和任何鏈表問題一樣,我們要檢查有沒有出現指針丟失的情況。我們發現,當根結點加入鏈表后,它的 left 和 right 指針都會被修改,這樣我們就無法通過 root.right 找到其右子樹了。解決指針丟失的方法也很簡單,使用一個臨時變量保存 root.left 和 root.right 即可。
最終,我們得到的題解代碼如下:
Node?head;?//?鏈表的頭結點Node?last;?//?二叉樹遍歷中的前一個結點,也是鏈表的尾結點
public?Node?treeToDoublyList(Node?root)?{
????head?=?null;
????last?=?null;
????traverse(root);
????//?將雙向鏈表轉為雙向循環鏈表
????if?(head?!=?null)?{
????????head.left?=?last;
????????last.right?=?head;
????}
????return?head;
}
void?traverse(Node?curr)?{
????if?(curr?==?null)?{
????????return;
????}
????//?提前保存右子樹指針
????Node?right?=?curr.right;
????//?中序遍歷
????traverse(curr.left);
????//?將當前結點加入鏈表
????curr.left?=?null;
????curr.right?=?null;
????if?(head?==?null)?{
????????head?=?curr;
????}?else?{
????????curr.left?=?last;
????????last.right?=?curr;
????}
????last?=?curr;?//?更新?last?指針
????traverse(right);
}
這段代碼有兩個需要注意的地方:
- 在遍歷二叉樹的時候,我們是把結點串連成一個雙向鏈表,但是題目要求返回的是雙向循環鏈表,所以我們在最后把鏈表的頭尾結點連接起來。
- 實際上我們只需要提前保存右子樹的指針,因為中序遍歷會先遍歷左子樹,再處理當前結點。如果是前序遍歷的話,左右子樹的指針都需要保存。
總結
我將這種在二叉樹遍歷中關注相鄰結點的方法稱為二叉樹的「迭代式遍歷」。使用這種方法的時候,我們不關注二叉樹的左右子樹所對應的子問題,只關注在某個特定的遍歷順序(前/中/后序)時,處理遍歷的兩個相鄰結點。這種方法并不適用于大多數題目,但對于特定的題目,這種方法卻是快速解題的利器。
雖然本文的兩道題使用的都是中序遍歷序列,但這種方法對于前/中/后序遍歷都適用,只需要調整「處理當前結點」和「遞歸調用兩個子樹」的順序即可。
這里列出幾個也用到該技巧的題目,各位小伙伴可以嘗試練習一下:
- LeetCode 114. Flatten Binary Tree to Linked List
- LeetCode 116. Populating Next Right Pointers in Each Node
- LeetCode 117. Populating Next Right Pointers in Each Node II
往期文章
- 二叉樹問題太復雜?「三步走」方法解決它!
- LeetCode 例題精講 | 10 二叉樹直徑:二叉樹遍歷中的全局變量
- OF40. 數組中最小的 k 個數:Top K 問題的兩種經典解法
總結
以上是生活随笔為你收集整理的l2-004 这是二叉搜索树吗?_LeetCode 例题精讲 | 11 二叉树转化为链表:二叉树遍历中的相邻结点...的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python开发效率高吗_提升pytho
- 下一篇: 无监督学习与有监督学习的本质区别是什么_