1. 程式人生 > >二叉樹的面試題

二叉樹的面試題

1. 求兩個節點的最近公共祖先節點

這裡寫圖片描述

假設現在找的是節點X1和X2的最近的公共祖先節點。
這裡由於這棵樹的結構或者說是特點從而導致這顆樹的解法會有多種思路:

第一種就是這棵二叉樹是一個搜尋二叉樹,那麼可以根據搜尋二叉樹的特點從而找出兩個節點的最近的公共祖先節點。
可以想到,如果X1在一個節點的左子樹,X2在一個節點的右子樹,或者X1在一個節點的右子樹,而X2在一個節點的左子樹,這樣就可以說明該節點一定就是X1和X2的最近的公共祖先節點。
這裡應該注意一個小問題:就是圖中2和3的最近的公共祖先節點應該是3.

簡單描述下這個過程:
如果X1這個節點比遍歷到的當前節點大,則說明X1這個節點肯定在當前節點的左子樹中,如果X1這個節點比遍歷到的當前節點小,則說明X1這個節點一定在當前節點的右子樹中,然後就是隻要可以判斷出節點X1和節點X2分別位於一個節點的左子樹和右子樹中,那麼就可以返回這個節點(這個節點即就是我們所求的X1和X2的最近的公共祖先節點)。
使用遞迴就很容易寫出程式碼。這種方法的時間複雜度是o(N)。

第二種是如果這棵二叉樹的結構是一種三叉鏈的結構,那麼可以從X1節點開始,直至根節點,將這條路徑儲存在一個數組裡,然後再從X2節點開始遍歷至根節點,將這條路徑儲存在另一個數組裡。然後用其中一個數組中的第一個節點開始去另一個數組中查詢,如果找到相同的,則就說明該節點就是這兩個節點的公共祖先節點,如果沒有找到相同的,則繼續用第二個節點用同樣的方法遍歷另一個數組,直至找到相同的節點。
但是這種方法的時間複雜度是O(N^2)。

第三種就是上面圖中最為普通的二叉樹,這種普通二叉樹去查詢任意兩個節點的最近的公共祖先節點思路可以參考一下上面二叉搜尋樹的方法,利用一個Find的介面函式,在一個節點的左子樹去查詢X1,如果找不到,再去該節點的右子樹去查詢,將查詢的結果記錄下來,再利用Find的介面函式去右子樹查詢X2,如果找不到,則去該節點個左子樹去查詢。最後利用他們查詢的結果來判斷這兩個節點的最近的公共祖先節點。
這種思路的時間複雜度依然是O(N^2)。

template<class T>
class BinaryTree
{
public:

    template<class T>
    struct BinaryTreeNode
    {
        BinaryTreeNode* _left;
        BinaryTreeNode* _right;
        T _val;
        BinaryTreeNode(const T& val)
            :_left(NULL)
            , _right(NULL)
            , _val(val)
        {}
        BinaryTreeNode()
            :_left(NULL
) , _right(NULL) {} }; typedef BinaryTreeNode<T> Node; BinaryTree() :_root(NULL) {} bool Find(Node* x) { assert(x); return _Find(_root,x); } //時間複雜度是O(N^2) Node* GetLastCommonAncestor(Node* x1, Node* x2) { assert(x1 && x2); if (_root == NULL) { return NULL; } return _GetLastCommonAncestor(_root, x1, x2); } private: Node* _Find(Node* root, const T& x) { if (root == NULL) { return NULL; } if (root->_val == x) { return root; } Node* l = _Find(root->_left, x); if (l == NULL) { return _Find(root->_right, x); } } Node* _GetLastCommonAncestor(Node* root, Node* x1, Node* x2) { //在子樹中 if (root == x1 || _root == x2) { return root; } bool x1InLeft = false, x1InRight = false, x2InLeft = false, x2InRight = false; x1InLeft = _Find(root->_left, x1); if (x1InLeft == false) { x1InRight = _Find(root->_right, x1); } x2InLeft = _Find(root->_left, x2); if (x2InLeft == false) x2InRight = _Find(root->_right, x2); if (x1InLeft && x2InRight || x1InRight && x2InLeft)//一個節點在左子樹,一個節點在右子樹 { return root; } else if (x1InLeft && x2InLeft)//兩個節點都在左子樹 { return _GetLastCommonAncestor(root->_left, x1, x2); } else if (x1InRight && x2InRight)//兩個節點都在右子樹 { return _GetLastCommonAncestor(root->_right, x1, x2); } } bool _Find(Node* root, Node* x) { if (root == NULL) { return false; } if (root == x) { return true; } bool l = _Find(root->_left, x); if (l) return true; return _Find(root->_right, x); } Node* _root; };

不過這種思路還是可以優化的,邏輯思路類似於上面三叉鏈的方法。
就是在找一個節點時,就將查詢的路徑直接儲存到一個棧裡。
另一個節點採用同樣的方法,將查詢的路徑儲存到另一個棧裡。
然後需要保證這兩個棧中節點的個數是相等的,很簡單,讓節點個數大的出幾次棧就好,直至兩個棧中節點的個數是相等的。然後取出兩個棧的棧頂元素,如果相等,則該棧頂元素就是我們所求的最近的公共祖先節點,如果不等,那麼就讓兩個棧同時出棧,接著比較。
這種思路的時間複雜度是O(N)。

template<class T>
class BinaryTree
{
public:

    template<class T>
    struct BinaryTreeNode
    {
        BinaryTreeNode* _left;
        BinaryTreeNode* _right;
        T _val;
        BinaryTreeNode(const T& val)
            :_left(NULL)
            , _right(NULL)
            , _val(val)
        {}
        BinaryTreeNode()
            :_left(NULL)
            , _right(NULL)
        {}
    };

    typedef BinaryTreeNode<T> Node;

    BinaryTree()
        :_root(NULL)
    {}


    bool Find(Node* x)
    {
        assert(x);
        return _Find(_root,x);
    }

        //時間複雜度為O(N)
    Node* GetLastCommonAncestor(Node* x1, Node* x2)
    {
        assert(x1 && x2);
        stack<Node*> x1paths;
        stack<Node*> x2paths;
        Find(x1, x1paths);
        Find(x2, x2paths);
        while (x1paths.size() != x2paths.size())
        {
            if (x1paths.size() > x2paths.size())
            {
                x1paths.pop();
            }
            else
            {
                x2paths.pop();
            }
        }
        while (!x1paths.empty() && !x2paths.empty() && x1paths.top() != x2paths.top())
        {
            x1paths.pop();
            x2paths.pop();
        }
        if (x1paths.top() == x2paths.top())
        {
            return x1paths.top();
        }
        return NULL;
    }

    bool Find(Node* x, stack<Node*>& paths)
    {
        return _Find(_root, x, paths);
    }

private:
    bool _Find(Node* root, Node* x, stack<Node*>& paths)
    {
        if (root == NULL)
        {
            return false;
        }
        paths.push(root);

        if (root == x)
        {
            return true;
        }
        //說明:只要找到就可以直接返回了
        bool l = _Find(root->_left, x, paths);
        if (l == true)
        {
            return true;
        }
        bool r = _Find(root->_right, x, paths);
        if (r == true)
        {
            return true;
        }
            paths.pop();//到這裡就說明在一個節點的左右子樹中都沒有找到,那麼就可以pop出該節點,給遞迴程式的上層返回false
            return false;
    }


    bool _Find(Node* root, Node* x)
    {
        if (root == NULL)
        {
            return false;
        }
        if (root == x)
        {
            return true;
        }
        bool l = _Find(root->_left, x);
        if (l)
            return true;
        return _Find(root->_right, x);
    }

    Node* _root;
};

2. 判斷一棵二叉樹是否是平衡二叉樹【y】

平衡二叉樹,即就是要求一棵二叉樹的左右高度差是否小於等於1。

思路參考:
第三題
時間複雜度為O(N^2)
先判斷當前節點是否為平衡二叉樹就需要遍歷整棵二叉樹,求得左右子樹的高度,然後遞迴判斷左子樹是否為平衡二叉樹,又需要遍歷左子樹求得左子樹的高度,判斷右子樹是否為平衡二叉樹,需要遍歷右子樹求得右子樹的高度,這裡面會有很多的冗餘值。將越接近葉子節點的節點的高度求了很多次,這都是沒有必要的,下面會將其優化。

//時間複雜度為O(N^2)
    bool _IsBalance(Node* root)
    {
        if (root == NULL)
        {
            return true;
        }
        int l = _Height(root->_left);
        int r = _Height(root->_right);

        //到這裡說明已經是二叉樹了
        return abs(l - r) < 2
            && _IsBalance(root->_left)
        && _IsBalance(root->_right);
    }

size_t _Height(Node* root)
    {
        if (root == NULL)
        {
            return 0;
        }
        size_t leftlen = 0, rightlen = 0;
        if (root->_left)
         leftlen = _Height(root->_left);
        if (root->_right)
         rightlen = _Height(root->_right);
        return leftlen > rightlen ? leftlen + 1 : rightlen + 1;
    }

時間複雜度為O(N)

利用函式的引數列表將每一個節點的高度帶回上層遞迴程式中。
也就是從葉子節點開始判斷該棵子樹是否為平衡二叉樹。

//時間複雜度為O(N)
    bool _IsBalance(Node* root, int& depth)
    {
        if (root == NULL)
        {
            depth = 0;
            return true;
        }
        int ldepth = 0, rdepth = 0;
        if (_IsBalance(root->_left, ldepth) == false)
        {
            return false;
        }
        if (_IsBalance(root->_right, rdepth) == false)
        {
            return false;
        }
        if (abs(ldepth - rdepth) > 1)
        {
            return false;
        }
        depth = ldepth > rdepth ? ldepth + 1 : rdepth + 1;//這棵子樹已經是平衡二叉樹了
        return true;
    }

3. 求二叉樹中最遠的兩個節點的距離

在遇到這個問題的時候,一開始我想的就是使用遞迴,我們先來考慮這樣一個問題,如何判斷一個子樹中最遠兩個節點的距離?是不是隻要把一個子樹的左子樹和右子樹的高度分別求出來,然後將這兩個值相加就是該子樹最遠兩個節點的距離。但是需要不斷更新最遠距離,因為當前樹中的最遠的兩個 節點的距離不一定就是該二叉樹中最遠兩個節點的距離,就用上圖來說明這個問題吧

這裡寫圖片描述

根節點為6的這棵二叉樹的最遠距離就是6.
而根節點為5的這棵二叉樹的最遠距離就是8,所以並不是根節點為6的這棵子樹所求出的最遠距離就是這棵二叉樹真正的最遠距離。

求最遠距離時需要求每個子樹的左右高度,第一方法是按照正常思路從根節點開始求出根節點的左右高度,然後將左右高度相加得到第一個maxlen(最遠距離)的值,隨後就是不斷遞迴左子樹向下查詢每個節點的左右高度,然後更新maxlen的值,左子樹遍歷到葉子節點時就可以回溯了,回溯到達根節點然後不斷遞迴右子樹求出每個節點的左右高度然後更新maxlen的值,最終回溯回根節點後就求出了maxlen的值。

但是這個過程有很多的冗餘值,就是在求根節點6的左右高度時,其實已經把左子樹和右子樹中每一個節點的高度求過一次了,一次求節點的高度,假設左右子樹每次平分,時間複雜度就是O(N),而需要遞迴求每個節點的左右子樹高度,遍歷每個節點,這個時間複雜度又是O(N),遞迴的時間複雜度就是遞迴的總次數*每次遞迴的次數。所以這種思路的時間複雜度就是O(N^2),這種思路的效率是不是太低了呢。

template<class T>
class BinaryTree
{
public:

    template<class T>
    struct BinaryTreeNode
    {
        BinaryTreeNode* _left;
        BinaryTreeNode* _right;
        T _val;
        BinaryTreeNode(const T& val)
            :_left(NULL)
            , _right(NULL)
            , _val(val)
        {}
        BinaryTreeNode()
            :_left(NULL)
            , _right(NULL)
        {}
    };

    typedef BinaryTreeNode<T> Node;

    BinaryTree()
        :_root(NULL)
    {}
//時間複雜度:O(N^2)
    void FindMaxLen(size_t& Maxlen)//不斷更新最遠距離
    {
        _FindMaxLen(_root, Maxlen);
    }

private:

void _FindMaxLen(Node* root, size_t& Maxlen)
    {

        size_t leftlen = 0, rightlen = 0;
        if (root->_left)
            leftlen = _Height(root->_left);
        if (root->_right)
            rightlen = _Height(root->_right);
        if (leftlen + rightlen > Maxlen)
        {
            Maxlen = leftlen + rightlen;
        }
        if (root->_left)
            _FindMaxLen(root->_left, Maxlen);
        if (root->_right)
            _FindMaxLen(root->_right, Maxlen);
    }

size_t _Height(Node* root)
    {
        if (root == NULL)
        {
            return 0;
        }
        size_t leftlen = 0, rightlen = 0;
        if (root->_left)
         leftlen = _Height(root->_left);
        if (root->_right)
         rightlen = _Height(root->_right);
        return leftlen > rightlen ? leftlen + 1 : rightlen + 1;
    }

    Node* _root;
};

我們來思考一下上面的思路可不可以優化呢?
如果我們想從葉子節點4開始求高度,然後將4的高度返回給4的父親8,接著再求8的左右子樹高度(利用4返回的高度)不需要再重新求4的高度,而且只要求出每個節點的左右子樹高度就可以更新該節點的最遠距離,那麼要完成這樣的邏輯其實使用遞迴就可以,當求根節點6的最遠距離時先不要求6節點的最遠距離,而是先去求它的左子樹和右子樹的最遠距離,這樣子不斷遞迴,最終先求出最遠距離的肯定會是葉子節點,然後將每個節點的高度都返回給該節點的父親。這樣等於遍歷一遍就可以求出整棵二叉樹的最遠距離。所以時間複雜度是O(N)。

這道面試題也考慮到了多執行緒的問題,因為這個maxlen我們需要不斷的更新,所以可能會有很多人考慮使用全域性變數,但是使用全域性變數,我們就需要考慮到在多執行緒環境下,要使得執行緒安全的話,可能需要加鎖,為了簡化問題,既然全域性變數會引起執行緒安全問題,那麼就不要使用全域性變數,而是使用區域性變數,我們都知道,每個執行緒都會有自己的私有棧空間,那麼每個執行緒看見的maxlen就不是同一個,這樣就使得該函式是一個可重入函式。

template<class T>
class BinaryTree
{
public:

    template<class T>
    struct BinaryTreeNode
    {
        BinaryTreeNode* _left;
        BinaryTreeNode* _right;
        T _val;
        BinaryTreeNode(const T& val)
            :_left(NULL)
            , _right(NULL)
            , _val(val)
        {}
        BinaryTreeNode()
            :_left(NULL)
            , _right(NULL)
        {}
    };

    typedef BinaryTreeNode<T> Node;

    BinaryTree()
        :_root(NULL)
    {}
//時間複雜度是O(N)
    size_t FindMaxLen()//不斷更新最遠距離
    {
        //這裡的Maxlen考慮到執行緒安全的問題,也就是使得下面呼叫的函式是可重入的(所有將Maxlen給成臨時變數而不是全域性變數)
        size_t MaxLen = 0;
        _FindMaxLen(_root, MaxLen);
        return MaxLen;
    }
private:
    size_t _FindMaxLen(Node* root, size_t& Maxlen)
    {
        if (root->_left)
            _FindMaxLen(root->_left, Maxlen);
        if (root->_right)
            _FindMaxLen(root->_right, Maxlen);


        size_t leftlen = 0, rightlen = 0;
            leftlen = _Height(root->_left);
            rightlen = _Height(root->_right);
        if (leftlen + rightlen > Maxlen)
        {
            Maxlen = leftlen + rightlen;
        }
        return leftlen > rightlen ? leftlen + 1 : rightlen + 1;
    }

    size_t _Height(Node* root)
    {
        if (root == NULL)
        {
            return 0;
        }
        size_t leftlen = 0, rightlen = 0;
        if (root->_left)
         leftlen = _Height(root->_left);
        if (root->_right)
         rightlen = _Height(root->_right);
        return leftlen > rightlen ? leftlen + 1 : rightlen + 1;
    }

    Node* _root;
};

4. 由前序遍歷和中序遍歷重建二叉樹(前序序列:1 2 3 4 5 6 - 中序序列:3 2 4 1 6 5)

這裡寫圖片描述


template<class T>
class BinaryTree
{
public:

    template<class T>
    struct BinaryTreeNode
    {
        BinaryTreeNode* _left;
        BinaryTreeNode* _right;
        T _val;
        BinaryTreeNode(const T& val)
            :_left(NULL)
            , _right(NULL)
            , _val(val)
        {}
        BinaryTreeNode()
            :_left(NULL)
            , _right(NULL)
        {}
    };

    typedef BinaryTreeNode<T> Node;

    BinaryTree()
        :_root(NULL)
    {}
Node* ReBulidTree(char* pre, char* In, size_t len)
    {
        if (pre == NULL || In == NULL || len <= 0)
        {
            return NULL;
        }
        return _ReBulidTree(pre, In, In + len);//兩段區間都是閉區間;
    }

private:
    Node* _ReBulidTree(char*& prev,char* In,char* InEnd)
    {
        if (*prev == '\0')//說明子二叉樹已經建立成功了
        {
            return NULL;
        }
        Node* newRoot = new Node(*prev);//有兩種情況:1.是葉子節點 2.不是葉子節點
        //1.是葉子節點
        if (In == InEnd)
        {
            return newRoot;
        }
        //2.不是葉子節點,這時就可以確定左右子樹的區間了
        char* pos = In;
        while (pos != InEnd)
        {
            if (*pos == *prev)
            {
                break;//說明已經找到根節點了
            }
            pos++;
        }
        //這裡的pos一定不能等於InEnd
        assert(pos <= InEnd);
        //左右子樹可以利用遞迴去解決
        newRoot->_left = _ReBulidTree(++prev,In, pos - 1);
        newRoot->_right = _ReBulidTree(++prev,pos + 1, InEnd);
        return newRoot;
    }

    Node* _root;
};


最近做劍指offer上的這道題時,發現了一種邏輯更清晰的做法:

#include <algorithm>
class Solution {
public:
    TreeNode* reConstructBinaryTree(vector<int> pre,vector<int> vin)
    {
        if(pre.empty())
            {
            return NULL;
        }
        TreeNode* newRoot = new TreeNode(pre[0]);
        vector<int>::iterator it;
        it =find(vin.begin(),vin.end(),pre[0]);
        int leftNum = 0;//統 計左子樹中節點的個數
        if(it != vin.end())
            {
            leftNum = it - vin.begin();
        }else
            {
            return NULL;
        }
        //pre(begin+ 1,begin + 1 + leftNum)--》去掉頭結點(第一個節點)    vin(begin,begin + leftNum) 
        //pre(begin + 1 + leftNum,end);   in(begin + 1 + leftNum,end)//去掉頭結點,頭結點在中間
        newRoot->left = reConstructBinaryTree(vector<int> (pre.begin() + 1,pre.begin() + leftNum + 1),vector<int>(vin.begin(),vin.begin() + leftNum));
        newRoot->right = reConstructBinaryTree(vector<int>(pre.begin() + leftNum + 1,pre.end()),vector<int>(vin.begin() + leftNum + 1,vin.end()));
        return newRoot;
    }
};

5.判斷一棵樹是否是完全二叉樹

思路:

這裡寫圖片描述

需要設定一個標記位來標記一個節點有沒有孩子節點。

注意以下幾點:
1.將每一個節點沒有左孩子或右孩子歸為一種情況,不要分開進行分析,分開的話容易搞混
2.只要有一個節點有孩子節點那麼就判斷標誌位是否為false,如果為false,那麼就說明該節點前面有節點沒有左孩子或者右孩子,不符合完全二叉樹的特定,這時就可以得出結論:不是完全二叉樹,就不必向下繼續遍歷,如果是true的話,說明該節點前面的所有節點的左右孩子節點都存在,那就按照前面的方法繼續向後判斷。

bool IsCompleteBinaryTree(Node* _root)
    {
        queue<Node*>  q;
        q.push(_root);
        bool tag = true;
        while (!q.empty())
        {
            Node* front = q.front();
            q.pop();
            if (front->_left)
            {
                if (tag == false)
                {
                    return false;
                }
                q.push(front->_left);
            }
            else
            {
                tag = false;
            }
            if (front->_right)
            {
                if (tag == false)
                {
                    return false;
                }
                q.push(front->_right);
            }
            else
            {
                tag = false;
            }
        }
        return true;
    }

6. 求二叉樹的映象【y】

這裡寫圖片描述

這個問題用遞迴解決其實很簡單,不斷遞迴交換一個根節點的左右子樹即可。

void  _Mirror(Node* root)
    {
        if (root == NULL)
        {
            return;
        }
        swap(root->_left, root->_right);
        _Mirror(root->_left);
        _Mirror(root->_right);
    }

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

利用的是二叉搜尋樹的線索化。
left相當於是prev,right相當於是next。


template<class T>
class BinaryTree
{
public:

    template<class T>
    struct BinaryTreeNode
    {
        BinaryTreeNode* _left;
        BinaryTreeNode* _right;
        T _val;
        BinaryTreeNode(const T& val)
            :_left(NULL)
            , _right(NULL)
            , _val(val)
        {}
        BinaryTreeNode()
            :_left(NULL)
            , _right(NULL)
        {}
    };

typedef BinaryTreeNode<T> Node;
BinaryTree()
        :_root(NULL)
    {}
//轉換為雙向連結串列
    Node* ToDuplexingList()
    {
        Node* prev = NULL;
        _ToDuplexingList(_root,prev);
        Node* cur = _root;
        while (cur->_left)
        {
            cur = cur->_left;
        }
        return cur;//返回單鏈表的頭
    }

private:
    void _ToDuplexingList(Node* cur, Node*& prev)
    {
        if (cur == NULL)
        {
            return ;
        }
        _ToDuplexingList(cur->_left, prev);
        cur->_left = prev;
        if (prev)
        {
            prev->_right = cur;
        }
        prev = cur;
        _ToDuplexingList(cur->_right, prev);
    }

    Node* _root;
};