二叉树面试复习题集(二)

根据二叉树创建字符串 – leetcode

描述:
你需要采用前序遍历的方式,将一个二叉树转换成一个由括号和整数组成的字符串。
空节点则用一对空括号 “()” 表示。而且你需要省略所有不影响字符串与原始二叉树之间的一对一映射关系的空括号对。
在这里插入图片描述

分析:
当一个结点存在时, 我们会把这个结点存放到数组中,如果它的左孩子存在我们继续添加一个左括号, 进行左孩子递归, 当左孩子递归完毕后, 我们会添加一个右括号, 然后接着判断右孩子是否存在, 如果右孩子存在,我们也会添加一个括号,右孩子递归, 最后添加一个右括号。

请注意这块int和char之间的转换!

void StringGet(struct TreeNode* root, char* str)
{
    if(root)
    {
        if(root->left == NULL && root->right == NULL)
        {
            char s[12];
            sprintf(s, "%d", root->val);
            strcat(str, s);
            return;
        }
        
        char s[12];
        sprintf(s, "%d", root->val);
        strcat(str, s);
        strcat(str, "(");
        
        StringGet(root->left, str);
        strcat(str, ")");
        
        if(root->right)
        {    
            strcat(str, "(");
        }
        StringGet(root->right, str);
        
        if(root->right)
        {
            strcat(str, ")");
        }
    }
}

char * tree2str(struct TreeNode* t)
{
    char* str = (char*)malloc(sizeof(char) * (1024 * 1024));
    str[0] = '\0';
    StringGet(t, str);
    return str;
}

测试结果:
在这里插入图片描述

二、二叉树的层序遍历 – leetcode

题目链接

描述:
给你一个二叉树,请你返回其按 层序遍历 得到的节点值。 (即逐层地,从左到右访问所有节点)。
在这里插入图片描述

分析:
层序遍历我会使用到队列, 首先创建队列qe, 把根节点插入到队列当中, 然后进入一个循环中, 循环条件为While(!qe.empty()), 进去之后我们先取堆顶元素, 然后出队, 接着判断堆顶元素的结点是否存在左右孩子, 如果存在就把左右孩子存放进去, 以此类推!这是层序遍历的思想。
但是在这块,我们需要把每一层的元素存放在一个二维数组当中。
因此我们需要知道每一层有多少个结点是至关重要的。
很简单, 使用qe.size()可以知道当前队列有多少个结点, 在一次循环中我们把队列中元素插入到数组中,并且把每个结点的左右孩子就插入进去。这样一来下一层的结点我们存放进去了,以此类推!

class Solution {
public:
	//求树的深度接口
	int DepthOfTree(TreeNode* root) {
        if(root) {
            return max(DepthOfTree(root->left), DepthOfTree(root->right)) + 1;
        }
        else {
            return 0;
        }
    }

    vector<vector<int>> levelOrder(TreeNode* root) {
        

        vector<vector<int>> vec;
        
        if(!root) {
            return vec;
        }
        
        int high = DepthOfTree(root);
        vec.resize(high);

        int index = 0;
        
        queue<TreeNode*> qe;
        qe.push(root);

        while(!qe.empty()) {
            
            vec[index].resize(qe.size());
            int size = qe.size();    
            for(int i = 0; i < size; ++i) {
                TreeNode* front = qe.front();
                qe.pop();
                //插入
                //vec[index].push_back(front->val);
                vec[index][i] = front->val;

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

            }
            ++index;
        }

        return vec;
    }
};

测试结果:
在这里插入图片描述

三、二叉树的最近公共祖先 – leetcode

题目链接

描述:
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
在这里插入图片描述在这里插入图片描述

分析:
我们的思想是对于每个结点, 他都会存在左子树和右子树。有可能p,q两个结点在左子树中, 也有可能在右子树当中, 当然也有可能根节点占用一个, 左右子树占用一个。 这些都是满足条件的例子。
因此在设计的时候分为左递归的返回值A, 右递归的返回值B
对于本身的判断我们需要保证A或B有一个成功,并且头结点还要包含一个, 这样头结点这个结点才是最近公共祖先结点。
或者是A和B一起成功,则代表着头结点也是成立的。

当判断出头结点是最近公共祖先时, 我们会把它保存起来。
毕竟递归的返回值时bool值, 并不是结点指针, 这块我才用一个全局变量进行存储。

class Solution {
public:

    bool DFS(TreeNode* root, TreeNode* p, TreeNode* q) {

        if(root == nullptr) { return false; }
        bool A = DFS(root->left, p, q);
        bool B = DFS(root->right, p, q);

        if( (A && B) || (((root == p) || root == q ) && (A || B)) ) {
            node = root;
        }

        return A || B || (root == p || root == q);
    }

    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q){
        DFS(root, p, q);
        return node;
    }

private:
    TreeNode* node;
};

测试结果:
在这里插入图片描述

四、二叉搜索树和双向链表 – 牛客网

题目链接

描述:
输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。要求不能创建任何新的结点,只能调整树中结点指针的指向。

分析:
二叉搜索树当中序遍历时是有序的序列。
这种题如果结点信息包含父结点,那是很简单的,因为回溯起来很简单。
但是这道题没有, 因此不好回溯, 我们需要创建一个用于存储的结点指针。

举个例子: 当root到一个结点时, 我们需要知道root结点的左孩子结点。
当root为一个结点的右孩子时, 我们需要知道root结点的父结点。
也就是上一层递归的root值。

一开始我们定义tmp临时指针指向nullptr。
我们首先左递归到最左孩子结点leftnode, leftnode左孩子指向tmp, 更新tmp为leftnode, 然后判断leftnode是否存在右子树, 如果存在我们调用该结点的右递归,
如果不存在, 则代表着该结点走完毕, 接着回溯。

class Solution {
public:
    
    //采用中序遍历进行拼接
    void BinaryToRList(TreeNode* root, TreeNode** tmp) {
        
        if(!root) { return;} 
        if(root->left) {
            BinaryToRList(root->left, tmp);
        }
        root->left = *tmp;						//连接左孩子
        if(*tmp) {								//如果tmp存在, 也就是左孩子存在, 则tmp的右孩子指向root, 成环。
            (*tmp)->right = root;
        }
        (*tmp) = root;							//更新tmp.
        
        if(root->right) {
            BinaryToRList(root->right, tmp);
        }
    }
    
    TreeNode* Convert(TreeNode* pRootOfTree)
    {
        if(pRootOfTree == nullptr) {
            return nullptr;
        }
        
        TreeNode* tmp = nullptr;            //临时变量
        BinaryToRList(pRootOfTree, &tmp);
        
        TreeNode* start = tmp;					
        while(start && start->left) {		//左移
            start = start->left;
        }
        
     // 加这两个条件就是双向循环链表
     // start->left = tmp;
     // tmp->right = start;
        
        return start;
    }
};

上面这样设计, 最终临时指针tmp指向的是成型后最后一个结点的地址。
我们需要循环左移到链表起始结点。
这道题是剑指offer中的一道题, 本题的思想也是借鉴书上写出来的。

测试结果:
在这里插入图片描述

五、从前序与中序遍历序列构造二叉树 – leetcode

题目链接

描述:这里是引用

分析:
我们都知道给我们两个序列, 如果想要还原二叉树的结构,这两个序列中必须包含中序序列。

preorder: [3, 9, 20, 15, 7]
inorder: [9, 3, 15, 20, 7]

我们以题目为例, 先序序列的第一个值就是根节点3, 然后在中序序列中我们找到3的位置, 3之前的9就是3为头结点的左子树元素, 3之后的15, 20, 7为右子树元素。 再看preorder先序序列,对于3中3就是左子树的根节点; 对于20,15,7中20就是右子树的根节点, 以此类推。 最终我们可以画出整棵树。

在设计的时候我们会利用这些信息, 在牛客网给出官方解析很明确,也很清晰,希望大家可以看一下, 有递归和非递归(迭代)两种方法实现。
但是实现的原理都是借助上面的思想。

我的写法是借鉴牛客网的, 之前也写过一个自己的写法,不过那种写法太乱了。 我还是喜欢牛客网给出的写法, 思路明确, 好理解。

class Solution {
public:

    TreeNode* TreeByPreAndIor(const vector<int>& preorder, const vector<int>& inorder, int preor_left, int preor_right,
                                    int inord_left, int inord_right) {
        if(preor_left > preor_right) {
            return nullptr;
        }

        //首先取头结点 - 先序遍历一开始就是头结点
        int preor_root = preorder[preor_left];
        //构建结点
        TreeNode* root = new TreeNode(preor_root);

        //根据头结点的值,我们可以在中序遍历找到它的位置, 从而区分左子树和右子树
        int inord_root = _map[preor_root];
        //判断左子树长度
        int child_left_size = inord_root - inord_left;

        //调用递归构建左子树
        root->left = TreeByPreAndIor(preorder, inorder, preor_left + 1, preor_left + child_left_size, inord_left, inord_root - 1);
        //调用递归构建右子树
        root->right = TreeByPreAndIor(preorder, inorder, preor_left + child_left_size + 1, preor_right, inord_root + 1, inord_right);

        return root;
    }

    TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
        if(preorder.size() == 0 || inorder.size() == 0) {
            return nullptr;
        }

        int size = preorder.size();
        //以结点值-下标位置为键值对构建map
        for(int i = 0; i < size; ++i) {
            _map[inorder[i]] = i;
        }

        return TreeByPreAndIor(preorder, inorder, 0, size - 1, 0, size - 1);
    }

private:
    unordered_map<int, int> _map;
};

写法中利用了无序map容器(以中序序列每个元素为键, 下标为值构成键值对插入),通过这个map我们就可以巧妙的知道每个根节点在中序序列的位置。从而得出左子树长度和右子树长度。

我们是采用递归实现的, root结点创建好之后,它的左孩子指向会调用递归, 递归传入的参数, 先序、中序序列的下标范围需要发生变化。右孩子一样。以此类推!

对于左孩子调用的递归我们可以看出preor_left + 1, 需要加1, 这个加1是对应头结点构建好。不包含头结点的下标。这个很重要。

测试结果:
在这里插入图片描述

六、从中序与后序遍历序列构造二叉树 – leetcode

题目链接

描述:
这里是引用

分析:
如果你掌握了第五大题, 这道题也游刃而解, 没错,只是设计的时候调用左递归和右递归时传入的参数范围发生变化而已。

对于后序序列而言, 最后一个元素为根节点, 在根节点构建好之后, 在调用右孩子递归是, 右边的范围是需要-1的, 即post_right - 1。 思想和上面讲的一样。
这块不作多描述, 只要会第五题,这道题就出来了。 (建议画图分析)

class Solution {
public:

    TreeNode* TreeByInordAndPost(const vector<int>& inorder, const vector<int>& postorder, int post_left, int post_right,
                                        int inord_left, int inord_right) {

        if(post_left > post_right) {
            return nullptr;
        }
        //取头结点, 在后续序列的最后一个元素
        int post_root = postorder[post_right];
        //创建新结点
        TreeNode* root = new TreeNode(post_root);

        //从中序序列中计算左子树的长度
        int inord_root = _map[post_root];
        int left_child_size = inord_root - inord_left;

        root->left = TreeByInordAndPost(inorder, postorder, post_left, post_left + left_child_size - 1, inord_left, inord_root - 1);
        root->right = TreeByInordAndPost(inorder, postorder, post_left + left_child_size, post_right - 1, inord_root + 1, inord_right);

        return root;
    }

    TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {
        if(inorder.size() == 0 || postorder.size() == 0) {
            return nullptr;
        }

        int size = inorder.size();
        for(int i = 0; i < size; ++i) {
            _map[inorder[i]] = i;
        }

        return TreeByInordAndPost(inorder, postorder, 0, size - 1, 0, size - 1);
    }
private:
    unordered_map<int,int> _map;
};

测试结果:
在这里插入图片描述
希望大家在牛客网找到解析之后, 把非递归(迭代)方法也看看, 自己实现。
毕竟递归大家都知道的, 不可能用来项目开发之中, 时间效率太差。

好啦,到此结束, 说心里话,这篇文章感觉分析内容太少了, 我承认我也没认真写, 原因有下。
1、数据结构递归方法, 确实不好讲。 在于思路理解,多看多写总结。
2、时间问题, 我在这章复习浪费时间过多。 有些赶紧,毕竟不是计算机本专业, 不会一心只考虑写代码问题。

希望理解, 这篇文章适合有基础的人, 复习使用, 勿喷!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值