二叉樹的基本操作及編程題總結(C++)

**二叉樹編程題萬變不離其宗在於對遞歸的理解和使用

要弄懂用好遞歸 重要的在於一下幾條:

1.搞清楚函數遞歸調用棧幀的變化 特別是二叉遞歸時的棧幀變化

2.搞清楚各個函數參數 傳值和傳引用 的函數參數在遞歸調用時值的變化。

本文中不加註明 所說的二叉樹都是普通二叉樹

1.定義並構建一顆二叉樹

對於一顆普通二叉樹的節點 至少要定義出他的值域 和指向其左右子樹的左右指針域。
並定義出節點的構造函數 該函數中參數應爲要賦予該節點的值 左右指針初始化爲空 有樹的構造函數處理。

template<class T>
struct BinaryTreeNode
{
    T _value;
    BinaryTreeNode<T>* _pleft;
    BinaryTreeNode<T>* _pright;
    BinaryTreeNode(const T& value)
        : _value(value)
        , _pleft(NULL)
        , _pright(NULL)
    {}
};

對於一顆二叉有 類成員變量中至少應有該樹根節點的指針。
在實現構造函數時 運用遞歸思想 把一棵樹分成左子樹 右子樹 根 三部分 。 每個子樹也分成這三部分。

//按前序遍歷構造二叉樹 ‘#’代表該位置的節點爲空  
char arr[] = { '1', '2', '3', '#', '#', '4', '#', '#', '5', '6' };

//變量invalid代表該節點爲空                           //數組的索引必須傳引用 
BinaryTree<char> BiTree(arr, sizeof(arr) / sizeof(arr[0]), '#');

    Node*  _CreateBinaryTree2(const T arr[], size_t size, size_t& index, const T invalid)
    {
        Node* pRoot = NULL;
        if (index < size && invalid != arr[index])
        {
            pRoot = new Node(arr[index]);
            pRoot->_pleft = _CreateBinaryTree2(arr, size, ++index, invalid);
            pRoot->_pright = _CreateBinaryTree2(arr, size, ++index, invalid);
        //在這裏接住下一層的返回值
        }
        return pRoot;
    }

    BinaryTree()
        :_pRoot(NULL)
    {}

    BinaryTree(const T arr[], size_t size, const T& invalid)
    {
        assert(arr);
        size_t index = 0;
        //_CreateBinaryTree1(_pRoot, arr, size, index, invalid);
        _pRoot = _CreateBinaryTree2(arr, size, index, invalid);

 }

二叉樹的拷貝構造函數 類似於二叉樹的構造函數

    Node*  _CopyBinaryTree(Node* pRoot)
    {
        Node* pNewRoot = NULL;
        if (pRoot){
            pNewRoot = new Node(pRoot->_value);
            pNewRoot->_pleft = _CopyBinaryTree(pRoot->_pleft);
            pNewRoot->_pright = _CopyBinaryTree(pRoot->_pright);
        }
        return pNewRoot;
    }

    BinaryTree(const BinaryTree<T>& bt)
    {
        _pRoot = _CopyBinaryTree(bt._pRoot);
    }

二叉樹的析構函數

有所不同 它的邏輯是 先析構左右子樹
再析構根節點 因爲如果先析構·根節點的話 左右子樹將找不到 形成內存泄漏。

~BinaryTree()
    {
        _DestoryBinaryTree(_pRoot);
    }

    void _DestoryBinaryTree(Node*& pRoot)
    {
        if (pRoot){
            _DestoryBinaryTree(pRoot->_pleft);
            _DestoryBinaryTree(pRoot->_pright);
            delete pRoot;
            pRoot = NULL;
        }
    }

二叉樹類的賦值運算符重載 注意三個問題

1.不可以自己給自己複製。
2.賦值之前必須進行析構,釋放原來自己的內存空間
3.調用二叉樹的構造邏輯在根結點上構造新二叉樹

    BinaryTree<T>& operator = (const BinaryTree<T>& bt)

    {

        if (this != &bt){

            _DestoryBinaryTree(_pRoot);

            _pRoot = _CopyBinaryTree(bt._pRoot);

        }

        return *this;
    }

2.二叉樹的前序、中序、後序、層序遍歷

1.前序遍厲根 -> 左 ->右

先遍歷樹的根和每一棵子樹的根 再遞歸遍歷左子樹 左子樹遍歷好以後退回到根節點這一層函數棧幀再遞歸遍歷右子樹 知道每一個節點
都經過遍歷:

    void _PreOrder(Node* pRoot)
    {
        if (pRoot){
            cout << pRoot->_value << " ";
            _PreOrder(pRoot->_pleft);
            _PreOrder(pRoot->_pright);
        }

    }

    void PreOrder()
    {
        cout << "PreOrder" << endl;
        _PreOrder(_pRoot);
        cout << endl;
    }

前序遍歷的非遞歸實現

    //把問題分成 樹的左路 與 樹的其他部分 兩塊
    //先將節點壓棧 遍歷後再出棧 以此模仿遞歸
    void Pre_Order()
    {
        stack<Node*> s;
        Node* cur = _pRoot;
        if (_pRoot == NULL)
            return;

        while (cur || !s.empty()){//棧不空說明還有節點未遍歷
            while (cur){
                cout << cur->_value << " ";
                s.push(cur);
                cur = cur->_pleft;
            }
            Node* top = s.top();//top 的左邊不可能有還沒遍歷的節點
            s.pop();
            if (top->_pright != NULL)
                cur = top->_pright;
        }
        cout << endl;
    }

2. 中序遍歷 左->根->右

    void MidOrder()
    {
        cout << "MidOrder" << endl;
        _MidOrder(_pRoot);
        cout << endl;
    }

    void _MidOrder(Node* pRoot)
    {
        if (pRoot){
            _MidOrder(pRoot->_pleft);
            cout << pRoot->_value << " ";
            _MidOrder(pRoot->_pright);
        }

    }

非遞歸實現:

把問題分爲 樹的左路 和樹的根。

    //相較於先序遍歷 只需要改變訪問節點的時機
    void Mid_Order()
    {
        Node* cur = _pRoot;
        stack<Node*> s;
        if (_pRoot == NULL)
            return;
        while (cur || !s.empty()){//如果 cur爲真 !s.empty() 爲假 是爲了程序第一次進入循環
                                    //如果 cur爲假 !s.empty()爲真程序退回到遍歷它的父節點
            while (cur){//找最左邊的節點
                s.push(cur);
                cur = cur->_pleft;
            }
            Node* top = s.top();
            cout << top->_value << " ";
            s.pop(); 
            if (top->_pright != NULL)
                cur = top->_pright;
        }
        cout << endl;
    }

3.後序遍歷 左->右->根

    void PostOrder()
    {
        cout << "PostOrder" << endl;
        _PostOrder(_pRoot);
        cout << endl;
    }

    void _PostOrder(Node* pRoot)
    {
        if (pRoot){
            _PostOrder(pRoot->_pleft);
            _PostOrder(pRoot->_pright);
            cout << pRoot->_value << " ";
        }
    }

後序遍歷非遞歸:

和前中序遍歷的非遞歸不同的是 有考慮一個問題 即:
先遍歷了左子樹 (1) ——> 然後棧頂退到根節點(2)———> 再找到根節點的右子樹遍歷(3)——–>棧頂再退回到根節點(4)。此時棧頂又到了根節點但是並不知道是過程(2)還是過程(4) 也就是說不知道此時根節點的右子樹是否已經便利了。
要解決此問題就必須 保存上一次剛剛遍歷的節點。

    void Post_Order()
    {
        Node* cur = _pRoot;
        Node* prev = NULL;//用prev記錄下來已經遍歷過的節點
        stack<Node*> s;
        while (cur || !s.empty()){
            while (cur){
                s.push(cur);
                cur = cur->_pleft;
            }
            Node* top = s.top();
            if (top->_pright == NULL || top->_pright == prev){
                //爲防止死循環 已遍歷的右子樹不會再被遍歷
                cout << top->_value << " ";
                s.pop();
                prev = top;
            }
            else{//遍歷未被遍歷的右子樹
                cur = top->_pright;
            }
        }
        cout << endl;
    }

小結:

遞歸沒什麼可說的 。
對於非遞歸 前序、中序、後序 思路都是把整棵樹分成樹的左路和樹的其他部分 兩部分。 把樹的每一顆子樹也這樣看
前序的時候 遍歷樹和樹的每一課子樹時先沿着樹的左路一邊遍歷一遍入棧。找到最左節點以後從棧頂拿到它並出棧 只需要看它有沒有右字數
有—則把右子樹作爲根進入循環子邏輯。
沒有 —-就再從 棧頂拿一個節點 看看它的右子樹是什麼情況。
中序遍歷的時候 先沿着樹的左路找到最左節點把沿途節點都入棧,之後遍歷棧頂節點(最左節點)並讓它出棧 看它有沒有右子樹
有——-循環子問題
沒有——通過棧找到它的父親 遍歷之 讓其出棧 再看它的又子樹。
後序遍歷的時候先沿着樹的左路找到最左節點把沿途節點都入棧,找到棧頂節點後看他的有子樹是否存在且未被遍歷。
是—–遍歷該節點並讓該節點出棧 記錄下已被遍歷的這個節點
不是——進入右子樹 遞歸子問題
接着通過棧找被遍歷過的節點的父親。

層序遍歷:

利用隊列先進先出的特點
根進隊列——>拿到對頭———->頭的左入隊列———->頭的右入隊列——–>遍歷頭——>頭出——–>拿頭—-> ……….直到隊列空。

    void level_Order()
    {
        queue<Node*> q;
        if (_pRoot == NULL)
            return;
        q.push(_pRoot);
        while (!q.empty()){
            Node* cur = q.front();
            q.pop();
            if (cur->_pleft != NULL)
                q.push(cur->_pleft);
            if (cur->_pright != NULL)
                q.push(cur->_pright);
            cout << cur->_value << " ";
        }
        cout```
<< endl;
    }

3.二叉樹的鏡像

把每一個根結點的左右結點的值交換

    void _Mirror(Node* pRoot)
    {
        if (pRoot == NULL)
            return;
        std::swap(pRoot->_pleft, pRoot->_pright);
        _Mirror(pRoot->_pleft);
        _Mirror(pRoot->_pright);
    }

    void Mirror()
    {
        _Mirror(_pRoot);
    }

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

和層序遍歷思想很像
一層一層檢查 當發現同一層有邊的節點有孩子而左邊的節點沒有孩子時說明不是完全二叉樹 直到遍歷完所有節點未發現這樣的情況 是完全二叉樹。

    bool IsTotalTree()
    {
        queue<Node*> q;
        if (_pRoot == NULL)
            return false;
        bool flag = true;
        q.push(_pRoot);
        while (!q.empty()){
            Node* cur = q.front();
            q.pop();
            if (cur->_pleft != NULL){
                q.push(cur->_pleft);
                if (flag == false)
                    return false;
            }
            else{
                flag = false;
            }
            if (cur->_pright != NULL){
                q.push(cur->_pleft);
                if (flag == false)
                    return false;
            }
            else{
                flag = false;
            }
        }
        return true;
    }

求二叉樹的高度

遞歸找的樹的葉子結點 葉子結點的高度爲一 。
每個樹的高度是它的左右子樹中較高的子樹的高度加一。

    size_t TreeHeight()
    {
        return _TreeHeight(_pRoot);
    }
    size_t _TreeHeight(Node*& pRoot)
    {
        if (NULL == pRoot)
            return 0;
        /*if (NULL == pRoot->_pleft && NULL == pRoot->_pright)
        return 1;*/
        size_t LeftHeight = _TreeHeight(pRoot->_pleft);
        size_t RightHeight = _TreeHeight(pRoot->_pright);

        return (LeftHeight > RightHeight) ? LeftHeight + 1 : RightHeigh +  1;

    }

求第k層節點

    size_t _k_nodes(Node*& pRoot, size_t k)
    {
        if (pRoot == NULL)
            return 0;
        if (k == 1)
//樹的一條支路遞歸的走到了制定的那一層 ,且那一層的節點不爲空 加上這個節點
            return 1;
        return _k_nodes(pRoot->_pleft, k - 1) + _k_nodes(pRoot->_pright, k - 1);
//返回所有支路遞歸到到第k層有節點的個數。
    }

    size_t k_nodes(size_t k)
    {
        if (k < 0)
            return 0;

        return _k_nodes(_pRoot, k);
    }

求二叉樹中兩個節點的最低公共父節點

這道題是一道經典題,常常出現在各大企業的招聘面試中,綜合考察學生對數據結構的理解能力。
第一種情況 普通二叉樹
思路:判斷這兩個節點是不是分別在根節點的左右子樹中 如果是—-根節點就是最近公共父節點 如果不是 在根節點的左右子樹中判斷是不是分別在它們的左右子樹中。
時間複雜度O^2(n)

    bool Find2(Node* pRoot, Node* pNode)
    {
        Node* cur = pRoot;
        if (cur == NULL)
            return NULL;
        if (cur == pNode)
            return true;
        if (Find2(cur->_pleft, pNode))
            return true;
        return Find2(cur->_pright, pNode);
    }

    Node* _GetLastCommonAncestor(Node* pRoot, Node* pNode1, Node* pNode2)
    {
        if(pRoot == NULL)
            return NULL;
        if (pNode1 == pRoot && pNode2 == pRoot)
            return pRoot;
        bool Node1InLeft = false, Node2InLeft = false, Node1InRight = false, Node2InRight = false;
        Node1InLeft = Find2(pRoot->_pleft, pNode1);
        if (!Node1InLeft)
            Node1InRight = Find2(pRoot->_pright, pNode1);
        Node2InLeft = Find2(pRoot->_pleft, pNode2);
        if (!Node2InLeft)
            Node2InRight = Find2(pRoot->_pright, pNode2);
        if ((Node1InLeft && Node2InRight) || (Node2InLeft && Node1InRight))
            return pRoot;
        else if (Node1InLeft && Node2InLeft)
            _GetLastCommonAncestor(_pRoot->_pleft, pNode1, pNode2);
        else if (Node2InLeft && Node2InRight)
            _GetLastCommonAncestor(_pRoot->_pright, pNode1, pNode2);
        else{
            assert(false);
        }

    }

    Node* GetLastCommonAncestor(Node* pNode1, Node* pNode2)
    {
        assert(pNode1 && pNode2);
        if (_pRoot == NULL)
            return NULL;
        return _GetLastCommo`
Ancestor(_pRoot, pNode1, pNode2);
    }

第二種情況
時間複雜度爲O(1)
思路:
寫一個查找函數把兩個節點所處的路徑保存下來 用兩個棧做容器保存 棧頂爲各自節點的指針 棧底爲根節點。
保存下來後 找到自頂到底第一個相同的節點就是最低公共子節點。

    bool Find3(Node* pNode, stack<Node*>& s)
    {
        if (_pRoot == NULL)
            return false;
        return _Find3(_pRoot, pNode, s);
    }

    bool _Find3(Node* pRoot, Node* pNode, stack<Node*>& s)
    {
        if (pRoot == NULL)
            return false;
        s.push(pRoot);
        if (pRoot == pNode)
            return true;
        if (true == _Find3(pRoot->_pleft, pNode, s))
            return true;
        if (true == _Find3(pRoot->_pright, pNode, s))
            return true;
        s.pop();//走到這裏說明在一條從根到葉子的路徑中沒有找到要找的節點 出棧去上一層找
        return false;
    }

    Node* GetLastCommonAncestor2(Node* pNode1, Node* pNode2)
    {
        assert(pNode1 && pNode2);
        if (_pRoot == NULL)
            return NULL;
        stack<Node*> s1;
        stack<Node*> s2;
        Find3(pNode1, s1);
        Find3(pNode2, s2);
        while (s1.size() != s2.size()){
            if (s1.size() < s2.size()){
                s2.pop();
            }
            else{
                s1.pop();
            }
        }
        while (!s1.empty() && !s2.empty() && s1.top() != s2.top()){
            s1.pop();
            s2.pop();
        }
        if (s1.top() == s2.top())
            return s1.top();
        else if (!s1.empty()){
            return pNode1;
        }
        else{
            return pNode2;
        }
    }

第三種情況 該二叉樹有指向父節點的指針
如此一來爲題簡化很多 參照尋找兩個單鏈表的交點就可以找到最低公共子節點
請參考:http://blog.csdn.net/x__016meliorem/article/details/61944604

第四種情況 該二叉樹是二叉搜索樹
根據搜索樹的性質 左子樹節點比根小 右子樹節點比根大
找到一個節點 比一個節點的值小 比另一個節點的值大就行了。

根據前序中序序列重建二叉樹

構造該二叉樹的過程如下:
1. 根據前序序列的第一個元素建立根結點;
2. 在中序序列中找到該元素,確定根結點的左右子樹的中序序列;
3. 在前序序列中確定左右子樹的前序序列;
4. 由左子樹的前序序列和中序序列建立左子樹;
5. 由右子樹的前序序列和中序序列建立右子樹。

struct TreeNode
{
    char _value;
    TreeNode* _pleft;
    TreeNode* _pright;

    TreeNode(char value)
        :_value(value)
        , _pleft(NULL)
        , _pright(NULL)
    {}
};

//這裏一定要傳引用 否則退回這一層後preindex還是之前的值
TreeNode* _ReBulidBinaryTree(int& preindex, int inBegin, int inEnd, const vector<char>& preOrder \
    , const vector<char>& inOrder)
{
    if (inBegin > inEnd)
        return NULL;
    TreeNode* root = new TreeNode(preOrder[preindex]);//創建一個樹的節點
    int rootindex = inBegin;
    while (rootindex <= inEnd){
        if (root->_value == inOrder[rootindex])
            break;
        ++rootindex;
    }//程序走到這裏 找到中序遍歷序列中的根節點的值的位置 該位置左邊的數是左子樹節點 右邊的值是右字數節點 
    assert(rootindex <= inEnd);

    if (inBegin <= rootindex - 1)//構建左子樹 連到根的左指針上去
        root->_pleft = _ReBulidBinaryTree(++preindex, inBegin, rootindex - 1, preOrder, inOrder);
    else                                  //每一個preindex都可以看成一棵子樹的根
        root->_pleft = NULL;
    if (rootindex + 1 <= inEnd)//構建右子樹  連到根的右指針上去
        root->_pright = _ReBulidBinaryTree(++preindex, rootindex + 1, inEnd, preOrder, inOrder);
    else
        root->_pright = NULL;
    return root;
}

TreeNode* ReBulidBinaryTree(const vector<char>& preOrder, const vector<char>& inOrder)
{
    if (preOrder.size() != inOrder.size())
        return NULL;
    int preindex = 0;
    int inBegin = 0;
    int inEnd = inOrder.size() - 1;
    return _ReBulidBinaryTree(prei`
dex, inBegin, inEnd, preOrder, inOrder);
}

根據中序加後序遍歷重建二叉樹

構造該二叉樹的過程如下:
1. 根據後序序列的最後一個元素建立根結點;
2. 在中序序列中找到該元素,確定根結點的左右子樹的中序序列;
3. 在後序序列中確定左右子樹的後序序列;
4. 由左子樹的後序序列和中序序列建立左子樹;
5. 由右子樹的後序序列和中序序列建立右子樹。

TreeNode* _ReBuildBinaryTree2(int& postindex, int inBegin, int inEnd, \
    const vector<char>& postOrder, const vector<char>& inOrder)
{
    if (inBegin > inEnd)
        return NULL;
    TreeNode* root = new TreeNode(postOrder[postindex]);
    int rootindex = inBegin;
    while (rootindex <= inEnd){
        if (root->_value == inOrder[rootindex])
            break;
        rootindex++;
    }
    assert(rootindex <= inEnd);
    if (rootindex + 1 <= inEnd)
         root->_pright = _ReBuildBinaryTree2(--postindex, rootindex + 1, inEnd, postOrder, inOrder);
    else
         root->_pright = NULL;
    if (inBegin <= rootindex - 1)
        root->_pleft = _ReBuildBinaryTree2(--postindex, inBegin , rootindex - 1, postOrder, inOrder);
    else
        root->_pleft = NULL;
    return root;
}

TreeNode* ReBulidBinaryTree2(const vector<char>& postOrder, const vector<char>& inOrder)
{
    if (postOrder.size() != inOrder.size())
        return NULL;
    int postindex = postOrder.size() - 1;
    int inBegin = 0;
    int inEnd = inOrder.size() - 1;
    return _ReBuildBinaryTree2(postindex, inBegin, inEnd, postOrder, inOrder);
}

小結:必須有中序遍歷序列才能重建二叉樹 因爲 只有中序序列能分清左子樹 右子樹 根。

二叉樹兩個節點的最遠距離

  1. O^2(n)算法
    常規思路 算每個節點的左右高度和 找出最大的左右高度和
    size_t GetMaxLength()
    {
        size_t maxlength = 0;
        _GetMaxLength(_pRoot, maxlength);
        //_GetMaxLength2(_pRoot, maxlength);
        return maxlength;
    }
    void _GetMaxLength(Node* pRoot, size_t& maxlength)
    {
        if (pRoot == NULL)
            return;
        size_t leftDepth = _TreeHeight(pRoot->_pleft);
        size_t rightDepth = _TreeHeight(pRoot->_pright);
        if (leftDepth + rightDepth > maxlength)
            maxlength = leftDepth + rightDepth;

        _GetMaxLength(pRoot->_pleft, maxlength);
        _GetMaxLength(pRoot->_pright, maxlength);
    }

2.O(n)的算法
先遞歸函數壓棧 回退的時候 記錄左右子樹高度 並算出最大距離

    size_t GetMaxLength()
    {
        size_t maxlength = 0;
        //_GetMaxLength(_pRoot, maxlength);
        _GetMaxLength2(_pRoot, maxlength);
        return maxlength;
    }

    size_t _GetMaxLength2(Node* pRoot, size_t& maxlength)
    {
        if (pRoot == NULL)
            return 0;
        size_t leftLength = _GetMaxLength2(pRoot->_pleft, maxlength);
        size_t rightLength = _GetMaxLength2(pRoot->_pright, maxlength);

        if (leftLength + rightLength > maxlength)
            maxlength = leftLength + rightLength;
        return leftLength > rightLength ? leftLength + 1 : rightLength + 1;
    }

將二叉樹變成雙向鏈表

思路 最左邊的節點一定沒有前驅節點 最右邊的節點沒有後繼節點
保存上一次遍歷的節點
將當前節點的左指針指向上遍歷的節點。
將上次遍歷的節點的右指針指向當前節點。
也就是說 指向前一個節點的指針在當前節點的函數棧幀中處理
由於當前棧幀中不知到後一個節點 所以指向後一個節點的指針在後一個節點的棧幀中處理。

void _TreeToList(TreeNode* cur, TreeNode*& prev)
{
    if (cur == NULL)
        return;
    _TreeToList(cur->_pleft, prev);
    cur->_pleft = prev;
    if (prev)
        prev->_pright = cur;
    prev = cur;
    _TreeToList(cur->_pright, prev);
}

TreeNode* TreeToList(TreeNode* root)
{
    if (root == NULL)
        return NULL;
    TreeNode* prev = NULL;
    _TreeToList(root, prev);
    TreeNode* head = root;
    while (head && head->_pleft){
        head = head->_pleft;
``
}
    return head;
}

全部完整代碼:
https://github.com/xym97/DataStructure/blob/master/BinaryTree/BinaryTree.h/BinaryTree.cpp

發佈了47 篇原創文章 · 獲贊 21 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章