二叉树的基本操作及编程题总结(C++)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/x__016Meliorem/article/details/78697685

**二叉树编程题万变不离其宗在于对递归的理解和使用

要弄懂用好递归 重要的在于一下几条:

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

展开阅读全文

没有更多推荐了,返回首页