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

開篇

如下內容可能偏應試但很好理解,因此你們必定要堅持看下去,由於咱們變強的過程註定孤獨的,堅持下來就會看到明天的太陽。php

回顧

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

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

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

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

常見平衡二叉樹的類型:github

  • AA樹
  • AVL樹
  • 紅黑樹

這些基礎的內容,你們不明白的話能夠前往開頭提到的文章查看詳細內容。算法

熱身

那咱們廢話很少說,來看《劍指Offer》中的第一個關於Tree的題目。segmentfault

輸入某二叉樹的前序遍歷和中序遍歷的結果,請重建出該二叉樹。假設輸入的前序遍歷和中序遍歷的結果中都不含重複的數字。例如輸入前序遍歷序列{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語言特色的深刻研究。

相關文章
相關標籤/搜索