C++ - 二叉树OJ题

 二叉树的两种层序遍历

在写之前,我们先来看两种二叉树的层序遍历:


1.给你二叉树的根节点 root ,返回其节点值的 层序遍历 。 (即逐层地,从左到右访问所有节点)。

输入:root = [3,9,20,null,null,15,7]
输出:[[3],[9,20],[15,7]]
示例 2:

这是最基本的层序遍历,可以借用队列,这里我们就借用 vector 和 队列来实现:
 

class Solution {
public:
    vector<vector<int>> levelOrder(TreeNode* root) {
        vector<vector<int>> vv;
        queue<TreeNode*> q;
        int levelsize = 0;

        if(root)
        {
            q.push(root);
            levelsize = 1;
        }

        while(!q.empty())
        {
            vector<int> v;
            for(int i = 0; i < levelsize;i++)
            {
                TreeNode* front = q.front();
                q.pop();
                v.push_back(front->val);

                if(front->left)
                {
                    q.push(front->left);
                }

                if(front->right)
                {
                    q.push(front->right);
                }
            }
            
            vv.push_back(v);
            levelsize = q.size();
        }

        return vv;
    }
};

2.给你二叉树的根节点 root ,返回其节点值 自底向上的层序遍历 。 (即按从叶子节点所在层到根节点所在的层,逐层从左向右遍历)

输入:root = [3,9,20,null,null,15,7]
输出:[[15,7],[9,20],[3]]

这里就要从底向上遍历,其实和上述也是差不多的,简单实现就是先用上述的 题目1 当中计算出正向的层序遍历结果,然后使用 reserve ()函数反转 vector 当中存储的结果即可:
 

class Solution {
public:
    vector<vector<int>> levelOrder(TreeNode* root) {
        vector<vector<int>> vv;
        queue<TreeNode*> q;
        int levelsize = 0;

        if(root)
        {
            q.push(root);
            levelsize = 1;
        }

        while(!q.empty())
        {
            vector<int> v;
            for(int i = 0; i < levelsize;i++)
            {
                TreeNode* front = q.front();
                q.pop();
                v.push_back(front->val);

                if(front->left)
                {
                    q.push(front->left);
                }

                if(front->right)
                {
                    q.push(front->right);
                }
            }
            
            vv.push_back(v);
            levelsize = q.size();
        }

        return vv;
    }
};

 二叉树的最近公共祖先

法一: 

给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。

百度百科中最近公共祖先的定义为:“对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”

输入:root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1
输出:3
解释:节点 5 和节点 1 的最近公共祖先是节点 3 。

 如上述例子,7 和 4  的最近公共祖先是 2 ,0 和 8 的最近公共祖先是 1 ;4 和 8 的最近公共祖先是3,······

通过多种例子,我们发现 最近公共祖先 一定在两个孩子的中间,也就是说,最近公共祖先 的两个孩子一个在其左子树,一个在其右子树。这样的话就好判断了。

然后题目当中也有说明,比如 5 和 4 的最近公共祖先就是5 ,也就是孩子本身也可以是最近公共祖先。

而且我们发现,像上述 5 和 4 这种情况,出现在根结点的话,我们是直接可以找出最近公共祖先的,就是根结点;像上述例子,3 和 8;  3 和  5······这些孩子的最近公共祖先都是3。所以我们可以对这种特殊情况进行优先处理。

按照上述的思路说明,我们只需要依次判断每一个结点是否在两个孩子的中间,依次判断这个结点左子树和右子树,用4个布尔值,pleft  pright  qleft  qright 用这四个布尔值的变量来记录,p q 两个孩子到底在当前结点的那一边,知道遍历完整颗树。

class Solution {
public:
    bool Find(TreeNode* root, TreeNode* ptr)
    {
        if(root == nullptr)
            return false;

        return root == ptr
        || Find(root->left, ptr) 
        || Find(root->right, ptr);
    }

    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        if(root == nullptr)
            return nullptr;

        // 当其中一个孩子是根结点的情况,根结点一定是 最近公共祖先
        if(root == q || root == p)
        {
            return root;
        }

        bool pleft , pright , qleft , qright;

        pleft = Find(root->left, p);
        pright = !pleft;

        qleft = Find(root->left, q);
        qright = !qleft;

        if(pleft && qleft)
        {
            return lowestCommonAncestor(root->left,p,q);
        }
        else if(pright && qright)
        { 
            return lowestCommonAncestor(root->right,p,q);
        }
            return root;
    }
};

上述方法的时间复杂度是 O(N^2),非常高,因为从根结点开始判断每一个结点是不是最近共有祖先都需要遍历这个结点的左右子树,看是不是左右两边一边一个结点。这实际上是一种暴力求解,可以看下面运行结果:
 

 其实,判断两个孩子是否分别在在某一个结点的左右子树当中,对于二叉搜索树来说就是针对性求解,只需判断结点的值的大小就可以,如果是二叉搜索树的暴力求解,复杂度可以达到O(N)。

 法二:

 其实还有更好的方式,如果写过链表的相交这道OJ题的话,比较好理解。

其实,寻找最近共同祖先这道题,在最近共同祖先结点位置其实就相当于是链表的交点,从根结点开始,到最近共同祖先结点,再到两个孩子,是有路径的。

如果看做是链表的相交的话,有两种情况:


三叉链:
 

 这种情况就是三叉链的情况。

二叉链:
 

这种方法,使用前序遍历,从根节点开始遍历路径,如果不是要找的孩子结点就入栈(先入栈在判断是不是要找的孩子结点,考虑出不出栈),比如下图例子:

 前序从根结点开始走,遍历3  和 5 都不是要找的孩子结点,那么都按前序遍历的顺序入栈。

之所以要把遍历过程当中不是 孩子结点的 入栈,是因为,就算不是要找的结点,但是这个结点可能是 路径 结点祖先 当中的其中一个。

当遇到 6 的之后,先入栈,然后判断。

然后再找 4

 同眼按照前序来进行寻找,找到3 和 5 都不是,都入栈;6 的左右子树都是空,左右子树都返回 false,然后从 6 回溯到 5 的时候,就要把 6 出栈;递归回溯返回到 5 ,从 5 的右子树开始寻找, 2  不是,入栈;遍历 7 ,7不是 入栈,同样  从 7 回溯到 2 的时候,就要把 7 出栈 ,递归回溯到2 之后,访问其右子树,找到4 ,是就入栈,判断。

由上述过程,当把两个孩子都找到之后,我们得到了两个栈的结果,一个存储的是找到 6 孩子的路径;一个存储的是 找到 4 孩子的路径,如下所示:

 得到上述两个栈之后,我们把这两个栈,同时依次出栈;比如上述,栈1 出栈6 之后,栈2 跟着出栈 4 ;

按照上述的顺序,知道两个栈的栈顶元素相等,比如上述出栈到 5 的时候,就相等了,而第一个相等的结点,就是最近共同祖先。

 其实上述出栈思想,就和在相交链表当中,使用快慢指针来寻找交点的思想是一样的,只不过出栈是从尾部开始,而快慢指针是从头开始。

完整代码:

class Solution {
public:
    // 查找孩子结点,算出根结点到 孩子结点路径的函数
    bool Find(TreeNode* root, TreeNode* ptr , stack<TreeNode*>& st)
    {
        // 递归终止条件,当遍历到最后的叶子结点的左右子树nullptr时候,说明下面已经没有结点了
        // 更不可能有要找的孩子,所以返回 false
        if(root == nullptr)
        {
            return false;
        }
        
        // 不管当前 root 指向的结点是不是 要找的孩子结点 都先入栈
        st.push(root);
        
        // 如果 root 是要找的孩子,就直接返回 true
        if(root == ptr)
        {
            return true;
        }
        
        // 递归调用 左子树,找到就返回 true ;没找到就继续往下走
        if(Find(root->left , ptr , st))
            return true;
        // 递归调用 右子树, 找到就返回 true ;此时没有找到就说明这个结点不是要找到结点
        if(Find(root->right , ptr , st))
            return true;
        
        // 走到这说明此时 root 指向的结点不是我们要找到孩子结点
        // 所以就出栈
        st.pop();
        // 出栈之后就返回 false,直接否定掉当前结点,回溯或者 向下迭代
        return false;
    }

    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {

        // 用两个栈来存储 从根结点到两个孩子的路径
        stack<TreeNode*> qst, pst;
        Find(root , p , pst);  // 寻找 p 孩子的路径
        Find(root , q , qst);  // 寻找 q 孩子的路径
        
        // 前两个 while 循环用于判断和删除掉两个栈当中,
        // 其中一个栈多出来的元素
        while(qst.size() > pst.size())
        {
            qst.pop();
        }

        while(qst.size() < pst.size())
        {
            pst.pop();
        }
        
        // 此时就同时删除,因为两个栈此时的元素相等
        // 直到 两栈栈顶元素相等
        while(qst.top() != pst.top())
        {
            qst.pop();
            pst.pop();
        }
        
        // 随便返回一个栈的栈顶元素
        return pst.top();
    }
};

二叉搜索树与双向链表

二叉搜索树与双向链表_牛客题霸_牛客网 (nowcoder.com)

输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。如下图所示:

注意:

  • 1.要求不能创建任何新的结点,只能调整树中结点指针的指向。当转化完成以后,树中节点的左指针需要指向前驱,树中节点的右指针需要指向后继
  • 2.返回链表中的第一个节点的指针
  • 3.函数返回的TreeNode,有左右指针,其实可以看成一个双向链表的数据结构
  • 4.你不用输出双向链表,程序会根据你的返回值自动打印输出

 要把二叉搜索树修改为有序链表,肯定是用中序。

当然,最简单的方法就是,用一个数组,比如容 vector,然后用二叉搜索树走中序,把结果存到 vector 当中,然后利用vector,创建链表;

但是题目当中要求了 空间复杂度为 O(1),不能创建新的空间。

我们先来看中序的结构:

 中序的操作过程在访问左右子树的中间,那么这个题目当中的中序就要修改 二叉搜索数的当中的指针链接。

但是,修改链接的话,我们就需要找到上一个,也就是说在链表当中的前驱结点,所以我们考虑,在inOrder ()函数当中多传入一个参数,prev指针,这个指针指向中序访问的上一个结点。

可以传入之后,我们每一次函数递归,就去把递归之前,当前函数栈帧的结点指针传入到下一个函数栈帧当中。

传参之后,在以后的某一个结点时候,我可以直到上一个结点是什么,那么我就可以把当前结点的 left(前驱指针)指向中序的上一个,把中序上一个结点的right(后继指针)指向当前结点。

需要注意的是,prev在传参的时候,不能直接用指针来传,我们要保证在这么多次递归传参的时候,传的prev指针都是同一个,所以,这里要借助引用来传参,如果是C语言的话,就要借助二级指针。如下所示:
 

 此时你可能会有疑问,如果在 8 的左边还有一个结点,修改 8 的left指针不会让 8 左边这个结点丢失吗?

 根本不会,因为中序遍历是要先遍历左子树,然后在去访问根结点的,也就是说,当访问到4,回溯到 6 的时候,那么就会 修改 6 和 4 的链接关系,紧接着就要访问 6 的右子树,而 要修改 8 的话,要先访问到 7 ,把 7 链接修改之后,回溯到8 才能访问到8,而且修改 8 指向的 7 的左指针,要再次回溯到 6 的时候才会修改。

 代码实现:
 

class Solution {
public:
	void inOrder(TreeNode* ptr, TreeNode*& prev)
	{
		if(ptr == nullptr)
			return;

		inOrder(ptr->left , prev);

		// 中序修改结点指针链接

        // 让当前结点的left 指针指向 中序遍历的上一个结点
		ptr->left = prev;

        // 如果上一个中序遍历结点不为空,就让上述一个结点的 right 指针指向当前结点
		if(prev)
			prev->right = ptr;

        // prev 向后迭代
		prev = ptr;

		inOrder(ptr->right, prev);
	}

    TreeNode* Convert(TreeNode* pRootOfTree) {
		// 中序修改链接关系
        TreeNode* prev = nullptr;
		inOrder(pRootOfTree , prev);

		TreeNode* head = pRootOfTree;
		while(head && head->left)
		{
			head = head->left;
		}

		return head;
    }
};

从前序与中序遍历序列构造二叉树

 题目链接:105. 从前序与中序遍历序列构造二叉树 - 力扣(LeetCode)

给定两个整数数组 preorder 和 inorder ,其中 preorder 是二叉树的先序遍历, inorder 是同一棵树的中序遍历,请构造二叉树并返回其根节点。

过程解析可以查看下面博客:由前序和中序创建二叉树_前序和中序构建二叉树_chihiro1122的博客-CSDN博客 

代码:

class Solution {
public:
    TreeNode* _build(vector<int>& preorder, vector<int>& inorder,
                    int& prei, int inbegin, int inend)
        {
            if(inbegin > inend)
                return NULL;

            TreeNode* root = new TreeNode(preorder[prei]);
            int rooti = inbegin;
            while(rooti <= inend)
            {
                if(preorder[prei] == inorder[rooti])
                    break;

                ++rooti;
            }

            // [inbegin, rooti - 1] root [ rooti + 1, inend]
            ++prei;
            root->left = _build(preorder, inorder, prei, inbegin, rooti - 1);
            root->right = _build(preorder, inorder, prei ,rooti + 1 , inend);
            return root;
        }


    TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
        int i = 0;
        return _build(preorder,inorder , i , 0, inorder.size() - 1);
    }
};

 从中序与后序遍历序列构造二叉树

题目链接:106. 从中序与后序遍历序列构造二叉树 - 力扣(LeetCode) 

 给定两个整数数组 inorder 和 postorder ,其中 inorder 是二叉树的中序遍历, postorder 是同一棵树的后序遍历,请你构造并返回这颗 二叉树 。

 有中序和后序创建二叉树也是和上一个题类似的。只不过现在我们需要用 后序的最后一个来确定根是哪个,在中序当中确定该根结点的左右子树。

而且,因为在后序当中寻找根,根是后序当中的最后一个,所以,我们要先创建右子树,再创建左子树。

因为,假设当前结点有右子树,那么在后序当中的下一个结点就是该结点的右子树的根结点。

以上就是 中序和后序构建 与 前序和中序构建二叉树两题当中的不同点,除此之外,基本一致,这就不再用代码实现。

 二叉树的前序遍历,非递归实现

 非递归模拟递归来实现的话,主要是要模拟实现递归当中的栈帧。如下例子:
 

 上述树如果是前序来遍历的话,先是从 3  开始,遍历 左路上述的结点,而左路就是  3-5-6 这个表示左路,然后再从左路的叶子结点开始反着遍历 左路结点 的右子树:

 这就是前序遍历的基本过程,如果我们要用非递归实现的话,左路结点好遍历,那么反过来调用左路结点的右子树,我们如果和寻找呢?其实,这就要用到栈先进后出的体系结构了。

我们用栈把 遍历左路结点的 路径存储到栈当中,当左路访问完之后,就去访问栈顶元素的右子树,从右子树开始,在访问从该右子树根结点的左路结点,再把该路径入栈,当这条左路结点路径访问到 NULL 最后的时候,就再次找栈顶元素,遍历栈顶元素的右子树。

以上就是使用栈模拟的过程,如上述的反复就可以 用 前序的顺序来遍历完整个二叉树。

 第一步从根结点开始入栈就如下图所示:
 

 遍历到 6 之后,到最后了,就访问栈顶元素的右子树的左路结点,把 栈顶元素 6 出栈。

发现 6 右子树是空,访问到空,接着访问栈顶元素 5 的左路结点,然后入栈:
 

 如上进行反复就是前序遍历了

总结一下:

  • 1.我们访问结点是要用指针来迭代访问的,假设我们使用cur 来迭代访问,当cur 指向一个结点的时候,就代表这要访问从这个结点开始的 左路结点路径
  • 2.而在栈当中存储的结点是访问过了 的,在栈当中存储的结点是访问这些结点的右子树。 

 代码实现:
 

class Solution {
public:
    vector<int> preorderTraversal(TreeNode* root) {
        TreeNode* cur = root;  // 用于迭代的指针
        vector<int> v;         // 用于存储前序输出结果

        // 如果是空树直接返回 空 vector
        // 因为上述 cur 是直接被 root 赋值,所以这里不用判断,下述也会走出空的输出
        if(root == nullptr)
            return v;

        // 模拟栈帧的栈
        stack<TreeNode*> st;

        // 外层循环控制整个树的遍历,相当于是递归当中的函数层级
        while(cur || !st.empty())
        {
            // 内层循环控制 访问 左路结点
            while(cur)
            {
                v.push_back(cur->val);
                st.push(cur);
                cur = cur->left;
            }

            // 取出栈顶元素
            TreeNode* top = st.top();
            // 出栈栈顶元素
            st.pop();

            //cur 指向空之后,cur指向栈顶元素的右子树的根结点
            cur = top->right;
        }

        return v;
    }
};

二叉树的中序,后序遍历,非递归实现

 中序和后序在模拟栈上面和 前序是一样,只是在访问结点的时机不同。

在走左路结点路径的时候:

前序是先访问结点,后入栈。

中序则是先入栈,当某一条左路结点路径走到空的时候,就要出栈,中序就是要访问出栈结点,代码实现:
 

class Solution {
public:
    vector<int> inorderTraversal(TreeNode* root) {
        TreeNode* cur = root;  // 用于迭代的指针
        vector<int> v;         // 用于存储前序输出结果

        // 如果是空树直接返回 空 vector
        // 因为上述 cur 是直接被 root 赋值,所以这里不用判断,下述也会走出空的输出
        if(root == nullptr)
            return v;

        // 模拟栈帧的栈
        stack<TreeNode*> st;

        // 外层循环控制整个树的遍历,相当于是递归当中的函数层级
        while(cur || !st.empty())
        {
            // 内层循环控制 访问 左路结点
            // 中序只在访问左路结点时候,只入栈不访问
            while(cur)
            {
                st.push(cur);
                cur = cur->left;
            }

            // 取出栈顶元素
            TreeNode* top = st.top();
            // 出栈栈顶元素
            st.pop();
            v.push_back(top->val);// 出栈的时候才访问

            //cur 指向空之后,cur指向栈顶元素的右子树的根结点
            cur = top->right;
        }

        return v;
    }
};

后序的访问时机在右子树访问结束之后,在原本的前序和中序当中无法直接套用,直接再次控制。

首先我们想到,当一个结点的左右子树都访问过了,那么这个结点就是可以访问的了,如下例子:
 

 当访问到 6 的时候,在访问其左右子树,发现都是 空,说明 6 这个结点就可以访问了。换个说法,当从某个结点的右子树回溯带该结点的时候,说明这个结点就可以访问了。比如 5 这个结点,当我们从 2 回溯到 5 的时候,说明此时5 就是可以访问的结点了,此时就应该把 5 push_back到 vector 当中。

但是,此时又多出一个问题,我们如果知道 此时的 5 的右子树已经访问过了呢?

 按照我们之前在前序当中说的过程,5 总共要 路过 两次,在这两次当中我们该如何判断,哪一次是 右子树的回溯呢?

相信你已经发现了:

  • 如果 5 的右子树没有访问过,那么此时上一次访问的结点就是 5 的左子树的根结点,也就是 6;
  • 如果 5 的右子树访问过了,那么此时上一次访问的结点U就是 5 的右子树的根结点,也就是 2;

 按照这个逻辑,我们只需要从记录 结果的 vector 当中取出上一次访问的结果,和右子树进行对比,就知道此时是到底是那一次 路过了

而且在访问结点的情况有两种,一种是访问叶子结点,此时条件是 右孩子是空;另一种是访问左右都有孩子(子树)的结点,此时就是要按照我们上述的描述来进行判断。 

 代码实现:

class Solution {
public:
    vector<int> postorderTraversal(TreeNode* root) {
        TreeNode* cur = root;  // 用于迭代的指针
        vector<int> v;         // 用于存储前序输出结果
        TreeNode* prev = nullptr;

        // 如果是空树直接返回 空 vector
        // 因为上述 cur 是直接被 root 赋值,所以这里不用判断,下述也会走出空的输出
        if(root == nullptr)
            return v;

        // 模拟栈帧的栈
        stack<TreeNode*> st;

        // 外层循环控制整个树的遍历,相当于是递归当中的函数层级
        while(cur || !st.empty())
        {
            // 内层循环控制 访问 左路结点
            // 中序只在访问左路结点时候,只入栈不访问
            while(cur)
            {
                st.push(cur);
                cur = cur->left;
            }

            // 取出栈顶元素
            TreeNode* top = st.top();

            // 当 当前结点的右子树为空,或者 当前结点的右子树的根结点是上一次访问过的
            // 都说明 右子树是访问过的,该结点现在就可以访问
            if(top->right == nullptr || top->right == prev)
            {
                prev = top;
                 // 出栈栈顶元素
                st.pop();
                v.push_back(top->val);// 出栈的时候才访问
            }
            else
            {
                //cur 指向空之后,cur指向栈顶元素的右子树的根结点
                cur = top->right;
            }
        }

        return v;
    }
};

  • 5
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

chihiro1122

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值