PHPer面試必看:分門別類帶你擼《劍指Offer》之二叉樹

蕭瀟發表於2018-08-05

開篇

以下內容可能偏應試但很好理解,所以大家一定要堅持看下去,因為我們變強的過程註定孤獨的,堅持下來就會看到明天的太陽。

回顧

PHPer面試必看:分門別類帶你擼《劍指Offer》之二叉樹

我們接著說說你理解的二叉樹吧這篇文章來的。下面我們來快速複習下二叉樹相關的概念:

  • 度:特定父節點的子節點的總數被稱為它的度數。
  • 路徑:從源節點到目標節點的節點和邊的序列稱為兩個節點之間的路徑。
  • 節點的高度:節點的高度由節點與最深節點之間的邊數決定。
  • 樹的高度:樹的高度是由它的根節點的高度定義的。
  • 深度:節點的深度由節點和根節點之間的邊數決定。

還有二叉樹的分類相關的概念:

  • 二叉搜尋樹:二叉搜尋樹(BST)是一種特殊型別的二叉樹,其中節點以排序的方式儲存,即在任何給定的點上,節點值必須大於或等於左子節點值,小於右子節點值。
  • 自平衡二叉樹:自平衡二叉搜尋樹或高度平衡二叉搜尋樹是一種特殊型別的二叉搜尋樹,它試圖通過自動調整來儘量保持樹的高度或層次儘可能小。

常見平衡二叉樹的型別:

  • AA樹
  • AVL樹
  • 紅黑樹

這些基礎的內容,大家不明白的話可以前往開頭提到的文章檢視詳細內容。

熱身

那我們廢話不多說,來看《劍指Offer》中的第一個關於Tree的題目。

輸入某二叉樹的前序遍歷和中序遍歷的結果,請重建出該二叉樹。假設輸入的前序遍歷和中序遍歷的結果中都不含重複的數字。例如輸入前序遍歷序列{1,2,4,7,3,5,6,8}和中序遍歷序列{4,7,2,1,5,3,8,6},則重建二叉樹並返回。

思路分析:根據二叉樹前序遍歷的特點(根-左-右),每次讀取的第一個值一定是根節點,這樣我們可以在中序遍歷的序列中找到當前的根節點的位置。

根據中序遍歷的特點(左-根-右),當確定了一個根節點後,其左邊序列就是這個根節點的左子樹,右邊序列就是其右子樹。

我們每次都需要在前序遍歷中找根節點並建立一個根節點,然後在中序遍歷中確定根節點位置,並確定當前根節點的左右子樹,然後以同樣的方法去構建左右子樹。

這就是一個遞迴的過程。什麼是遞迴?不慌,不清楚的同學可以看我之前寫的什麼是遞迴,一定要弄清楚遞迴,因為下面的題目中會大量運用到遞迴的思想。

來看下具體程式碼實現:

/*class TreeNode{
    var $val;
    var $left = NULL;
    var $right = NULL;
    function __construct($val){
        $this->val = $val;
    }
}*/

function reConstructBinaryTree($pre, $vin)
{
    if (empty($pre) || empty($vin)) {
        return null;
    }
    //在前序中尋找根節點
    $root = new TreeNode($pre[0]);
    //確定根節點在中序遍歷中的位置
    $indexInVin = array_search($pre[0], $vin, true);
    //左子樹先序遍歷結果
    $leftPrev = array_slice($pre, 1, $indexInVin); 
    //左子樹中序遍歷結果
    $leftVin = array_slice($vin, 0, $indexInVin); 
    //右子樹先序遍歷結果
    $rightPrev = array_slice($pre, $indexInVin + 1);
    //右子樹中序遍歷結果
    $rightVin = array_slice($vin, $indexInVin + 1);
    //遞迴構建樹
    $root->left = reConstructBinaryTree($leftPrev, $leftVin);
    $root->right = reConstructBinaryTree($rightPrev, $rightVin);
    //返回根節點
    return $root;
}
複製程式碼

完整的程式碼在這裡,需要的同學可以點選檢視。

好了,我們繼續。來看第二道。

輸入兩棵二叉樹A,B,判斷B是不是A的子結構。(ps:我們約定空樹不是任意一個樹的子結構)

思路分析:第一種情況如果根節點相同,那麼就分別去子節點裡面匹配。不符合的話看 第二種情況,如果根節點不同,就去用pRoot1的左孩子和pRoot2去比較。還不符合的話就嘗試用pRoot1的右孩子和pRoot2去比較。

還是遞迴的運用,看下面的解答。

/*class TreeNode{
    var $val;
    var $left = NULL;
    var $right = NULL;
    function __construct($val){
        $this->val = $val;
    }
}*/
function HasSubtree($pRoot1, $pRoot2)
{
    if (empty($pRoot1) || empty($pRoot2)) {
        return false;
    }
    return isSubtree($pRoot1, $pRoot2) || HasSubtree($pRoot1->left, $pRoot2)
    || HasSubtree($pRoot1->right, $pRoot2);
}
function isSubtree($pRoot1, $pRoot2)
{
    if (empty($pRoot2)) {
        return true;
    }
    if (empty($pRoot1)) {
        return false;
    }
    return $pRoot1->val === $pRoot2->val && isSubtree($pRoot1->left, $pRoot2->left) && isSubtree($pRoot1->right, $pRoot2->right);
}
複製程式碼

來看下一道。

操作給定的二叉樹,將其變換為源二叉樹的映象。

這個題目很有名哈,可能你也經常看到。但是不難,10行程式碼就搞定了。依然要使用遞迴的思想,看程式碼的話秒懂哦。

<?php
/*class TreeNode{
    var $val;
    var $left = NULL;
    var $right = NULL;
    function __construct($val){
        $this->val = $val;
    }
}*/
//https://www.zhihu.com/question/31202353?rf=31230953
//操作給定的二叉樹,將其變換為源二叉樹的映象。
function Mirror(&$root)
{
    if (empty($root)) {
        return;
    }
    $left = $root->left;
    $right = $root->right;
    $root->right = $left;
    $root->left = $right;
    Mirror($root->left);
    Mirror($root->right);
}
複製程式碼

接著來看一道關於層次遍歷二叉樹的題目,除了層次遍歷之外,我們還應當掌握先序,中序,後續遍歷二叉樹的遞迴演算法以及非遞迴演算法,除此之外還有節點的搜尋、新增以及刪除等常用操作都在這裡了。

從上往下列印出二叉樹的每個節點,同層節點從左至右列印。

思路分析:我們需要建立一個佇列,先將根節點入隊,然後將隊首出隊,然後判斷它的左右子樹是否為空,不為空,則先將左子樹入隊,然後右子樹入隊。

function PrintFromTopToBottom($root)
{
    $traverse = [];
    array_push($traverse, $root->val);
    inQueue($root, $traverse);
    return $traverse;
}
function inQueue($node, &$return)
{
    if (empty($node)) {
        return;
    }
    if ($left = $node->left) {
        array_push($return, $left->val);
    }
    if ($right = $node->right) {
        array_push($return, $right->val);
    }
    inQueue($left, $return);
    inQueue($right, $return);
}
複製程式碼

此題還有非遞迴的解法,點選這裡可以看到原始碼。

恭喜,你堅持看到這裡了,真棒!我們繼續。

《劍指Offer》中還有一道類似的變種題目,就是下面的這道,之字形遍歷二叉樹。

請實現一個函式按照之字形列印二叉樹,即第一行按照從左到右的順序列印,第二層按照從右至左的順序列印,第三行按照從左到右的順序列印,其他行以此類推。

思路分析:當我們在列印某一行的結點時,把下一層的結點儲存到相應的棧中。如果當前列印的是奇數層,則先儲存左子結點再儲存右子結點到一個棧中;如果當前列印的是偶數層,則先儲存右子結點再儲存左子結點到另一個棧中。

/*class TreeNode{
    var $val;
    var $left = NULL;
    var $right = NULL;
    function __construct($val){
        $this->val = $val;
    }
}*/

function MyPrint($pRoot)
{
    if (empty($pRoot)) return [];
    $cur = 0;
    $next = 1;
    $stack[0] = [];
    $stack[1] = [];
    array_push($stack[0], $pRoot);
    $i = 0;
    $return = [];
    $return[0] = [];
    while (!empty($stack[$cur]) || !empty($stack[$next])) {
        $top = array_pop($stack[$cur]);
        array_push($return[$i], $top->val);
        if ($cur == 0) {
            if ($left = $top->left) {
                array_push($stack[$next], $left);
            }
            if ($right = $top->right) {
                array_push($stack[$next], $right);
            }
        } else {
            if ($right = $top->right) {
                array_push($stack[$next], $right);
            }
            if ($left = $top->left) {
                array_push($stack[$next], $left);
            }
        }
        if (empty($stack[$cur])) {
            $cur = 1 - $cur;
            $next = 1 - $next;
            if (!empty($stack[0]) || !empty($stack[1])) {
                $i++;
                $return[$i] = [];
            }
        }
    }
    return $return;
}
複製程式碼

好了,現在老鐵你可以去喝個水休息一下,因為還有不少題目等待著我們,如果累了你可以先按個贊標記一下,明天接著看。另外原始碼點選這裡檢視,老鐵也可以先star一下,以後再看,您的star是我更新的動力。

繼續

輸入一個整數陣列,判斷該陣列是不是某二叉搜尋樹的後序遍歷的結果。如果是則輸出Yes,否則輸出No。假設輸入的陣列的任意兩個數字都互不相同。

思路分析:BST的後序序列的合法序列是,對於一個序列S,最後一個元素是x (也就是根),如果去掉最後一個元素的序列為T,那麼T滿足:T可以分成兩段,前一段(左子樹)小於x,後一段(右子樹)大於x,且這兩段(子樹)都是合法的後序序列。完美的遞迴定義。

function VerifySquenceOfBST($sequence)
{
    if (count($sequence) == 0) return false;
    if (count($sequence) == 1) return true;
    
    if ($sequence) {
        $length = count($sequence);
        
         if ($length == 2) {
            if ($sequence[0] < $sequence[1]) return true;
        }
        
        $root = $sequence[$length - 1];
        $left = [];
        $right = [];
        $leftFlag = false;
        $rightFlag = false;
        $i = 0;
        while($sequence[$i] < $root) {
            array_push($left, $sequence[$i]);
            $i++;
        }
        $i === count($left) && $leftFlag = true;
        $j = $i;
        while($sequence[$j] > $root) {
             array_push($right, $sequence[$j]);
             $j++;
        }
        ($j === ($length - 1)) && $rightFlag = true;
        if ($leftFlag && $rightFlag) {
            if ($left && $right) {
                return VerifySquenceOfBST($left) && VerifySquenceOfBST($right);
            } elseif ($left) {
                return VerifySquenceOfBST($left);
            } else {
                return VerifySquenceOfBST($right);
            }
        } else {
            return false;
        }
    }
    return true;
}

複製程式碼

輸入一顆二叉樹的跟節點和一個整數,列印出二叉樹中結點值的和為輸入整數的所有路徑。路徑定義為從樹的根結點開始往下一直到葉結點所經過的結點形成一條路徑。(注意: 在返回值的list中,陣列長度大的陣列靠前)

思路分析:利用遞迴遍歷所有路徑。

/*class TreeNode{
    var $val;
    var $left = NULL;
    var $right = NULL;
    function __construct($val){
        $this->val = $val;
    }
}*/
function FindPath($root, $expectNumber)
{
    if (empty($root)) return [];
    $a = $q = [];
    buildPath($root, $expectNumber, $q, $a);
    return $a;
}
function buildPath($node, $sum, $q, &$a)
{
    if ($node) {
        $q[] = $node->val;
        $sum -= $node->val;
        if ($sum > 0) {
            buildPath($node->left, $sum, $q, $a);
            buildPath($node->right, $sum, $q, $a);
        } elseif (empty($node->left) && empty($node->right) && $sum == 0) {
            $a[] = $q;
        }
    }
}
複製程式碼

輸入一棵二叉搜尋樹,將該二叉搜尋樹轉換成一個排序的雙向連結串列。要求不能建立任何新的結點,只能調整樹中結點指標的指向。

思路分析:方法一:遞迴版

  • 1.將左子樹構造成雙連結串列,並返回連結串列頭節點。
  • 2.定位至左子樹雙連結串列最後一個節點。
  • 3.如果左子樹連結串列不為空的話,將當前root追加到左子樹連結串列。
  • 4.將右子樹構造成雙連結串列,並返回連結串列頭節點。
  • 5.如果右子樹連結串列不為空的話,將該連結串列追加到root節點之後。
  • 6.根據左子樹連結串列是否為空確定返回的節點。
function Convert($pRootOfTree)
{
    // write code here
    if (empty($pRootOfTree)) {
        return null;
    }
    if (empty($pRootOfTree->left) && empty($pRootOfTree->right)) {
        return $pRootOfTree;
    }
    //將左子樹構造成雙連結串列,並返回連結串列頭節點。
    $left = Convert($pRootOfTree->left);
    $temp = $left;
    // 2.定位至左子樹雙連結串列最後一個節點。
    while($temp !== null && $temp->right != null) {
        $temp = $temp->right;
    }
    // 3.如果左子樹連結串列不為空的話,將當前root追加到左子樹連結串列。
    if ($left != null) {
        $temp->right = $pRootOfTree;
        $pRootOfTree->left = $temp;
    }
    // 4.將右子樹構造成雙連結串列,並返回連結串列頭節點。
    $right = Convert($pRootOfTree->right);
    // 5.如果右子樹連結串列不為空的話,將該連結串列追加到root節點之後。
    if ($right != null) {
        $right->left = $pRootOfTree;
        $pRootOfTree->right = $right;
    }
    return $left != null ? $left : $pRootOfTree;
}
複製程式碼

非遞迴演算法 解題思路:

  • 1.核心是中序遍歷的非遞迴演算法(對的,就是上文提到的那個中序遍歷演算法)。
  • 2.修改當前遍歷節點與前一遍歷節點的指標指向。
function ConvertNotRecursive($pRootOfTree)
{
    if (empty($pRootOfTree)) {
        return null;
    }
    $stack = new \SplStack();
    $p = $pRootOfTree;
    // 用於儲存中序遍歷序列的上一節點
    $pre = null;
    $isFirst = true;
    
    while ($p || !$stack->isEmpty()) {
        while($p) {
            $stack->push($p);
            $p = $p->left;
        }
        $p = $stack->pop();
        if ($isFirst) {
            // 將中序遍歷序列中的第一個節點記為root
            $pRootOfTree = $p;
            $pre = $pRootOfTree;
            $isFirst = false;
        } else {
            $pre->right = $p;
            $p->left = $pre;
            $pre = $p;
        }
        $p = $p->right;
    }
    return $pRootOfTree;
}
複製程式碼

輸入一棵二叉樹,判斷該二叉樹是否是平衡二叉樹。

思路分析:最直接的做法,遍歷每個結點,藉助一個獲取樹深度的遞迴函式,根據該結點的左右子樹高度差判斷是否平衡,然後遞迴地對左右子樹進行判斷。

function IsBalanced_Solution($pRoot)
{
    if (empty($pRoot)) return true;
    return abs(maxDepth($pRoot->left) - maxDepth($pRoot->right)) <= 1 &&
        IsBalanced_Solution($pRoot->left) && IsBalanced_Solution($pRoot->right);
}
function maxDepth($node)
{
    if (empty($node)) return 0;
    return 1 + max(maxDepth($node->left), maxDepth($node->right));
}
複製程式碼

給定一個二叉樹和其中的一個結點,請找出中序遍歷順序的下一個結點並且返回。注意,樹中的結點不僅包含左右子結點,同時包含指向父結點的指標。

思路分析:二叉樹的下一個節點,一共有以下情況:

  • 1.二叉樹為空,則返回空;
  • 2.節點右孩子存在,則設定一個指標從該節點的右孩子出發,一直沿著指向左子結點的指標找到的葉子節點即為下一個節點;
  • 3.節點不是根節點。如果該節點是其父節點的左孩子,則返回父節點;否則繼續向上遍歷其父節點的父節點,重複之前的判斷,返回結果。
function GetNext($pNode)
{
    if (empty($pNode)) return null;
    if ($right = $pNode->right) {
        $currentNode = $right;
        while ($currentNode->left) {
            $currentNode = $currentNode->left;
        }
        return $currentNode;
    }
    
    while ($pNode->next) {
        $parent = $pNode->next;
        if ($parent->left === $pNode) {
            return $parent;
        }
        $pNode = $pNode->next;
    }
    return null;
}
複製程式碼

請實現一個函式,用來判斷一顆二叉樹是不是對稱的。注意,如果一個二叉樹同此二叉樹的映象是同樣的,定義其為對稱的。

思路分析:首先根節點以及其左右子樹,左子樹的左子樹和右子樹的右子樹相同,左子樹的右子樹和右子樹的左子樹相同即可,採用遞迴。非遞迴也可,採用棧或佇列存取各級子樹根節點。

function isSymmetrical($pRoot)
{
    // write code here
    if (empty($pRoot)) return true;
    
    return compare($pRoot->left, $pRoot->right);
}
function compare($left, $right)
{
    if ($left === null) return $right === null;
    if ($right === null) return false;
    if ($left->val != $right->val) return false;
    return compare($left->right, $right->left) && compare($left->left, $right->right);
}
複製程式碼

可以看到遞迴的強大,合適運用的時候真的是事半功倍。最後下面的兩道題目分別運用了二叉樹先序、中序遍歷演算法。

請實現兩個函式,分別用來序列化和反序列化二叉樹

/*class TreeNode{
    var $val;
    var $left = NULL;
    var $right = NULL;
    function __construct($val){
        $this->val = $val;
    }
}*/
function MySerialize($pRoot)
{
    $arr = [];
    doSerialize($pRoot, $arr);
    return implode(',', $arr);
}
function doSerialize($pRoot, &$arr)
{
    if (empty($pRoot)) {
        $arr[] = '#';
        return;
    }
    $arr[] = $pRoot->val;
    doSerialize($pRoot->left, $arr);
    doSerialize($pRoot->right, $arr);
}
function MyDeserialize($s)
{
    $arr = explode(',', $s);
    $i = -1;
    return doDeserialize($arr, $i); 
}
function doDeserialize($arr, &$i)
{
    $i++;
    if ($i >= count($arr)) {
        return null;
    }
    if ($arr[$i] == '#') return null;
    $node = new TreeNode($arr[$i]);
    $node->left = doDeserialize($arr, $i);
    $node->right = doDeserialize($arr, $i);
    return $node;
}
複製程式碼

給定一棵二叉搜尋樹,請找出其中的第k小的結點。例如,(5,3,7,2,4,6,8)中,按結點數值大小順序第三小結點的值為4。

/*class TreeNode{
    var $val;
    var $left = NULL;
    var $right = NULL;
    function __construct($val){
        $this->val = $val;
    }
}*/
function KthNode($pRoot, $k)
{
    $traverse = inOrderTraverse($pRoot);
    if ($k <= count($traverse)) {
        return $traverse[$k - 1];
    }
    return null;
}
function inOrderTraverse($pRoot)
{
    $traverse = [];
    if ($left = $pRoot->left) {
        $traverse = array_merge($traverse, inOrderTraverse($left));
    }
    array_push($traverse, $pRoot);
    if ($right = $pRoot->right) {
        $traverse = array_merge($traverse, inOrderTraverse($right));
    }
    return $traverse;
}
複製程式碼

完整內容

PHP基礎資料結構專題系列目錄地址:地址 主要使用PHP語法總結基礎的資料結構和演算法。還有我們日常PHP開發中容易忽略的基礎知識和現代PHP開發中關於規範、部署、優化的一些實戰性建議,同時還有對Javascript語言特點的深入研究。

相關文章