【LeetCode学习计划】《算法-入门-C++》第8天 广度优先搜索 / 深度优先搜索



617. 合并二叉树

LeetCode

简 单 \color{#00AF9B}{简单}

给定两个二叉树,想象当你将它们中的一个覆盖到另一个上时,两个二叉树的一些节点便会重叠。
你需要将他们合并为一个新的二叉树。合并的规则是如果两个节点重叠,那么将他们的值相加作为节点合并后的新值,否则不为 NULL 的节点将直接作为新二叉树的节点。

示例 1:

输入: 
	Tree 1                     Tree 2                  
          1                         2                             
         / \                       / \                            
        3   2                     1   3                        
       /                           \   \                      
      5                             4   7                  
输出: 
合并后的树:
	     3
	    / \
	   4   5
	  / \   \ 
	 5   4   7

注意: 合并必须从两个树的根节点开始。


前言

测试用例应写作树的前序遍历,如例1中Tree 1Tree 2

  • Tree 1: [1,3,2,5]
  • Tree 2: [2,1,3,null,4,null,7]

合并前2棵二叉树的对应结点一共有3种情况:

  • 合并前对应结点均为空,则合并后的对应结点也为空
  • 合并前只有一个为空,则合并后结点的值是非空结点的值
  • 合并前2个结点都不为空,则合并后结点的值是2个结点的值求和

方法1:深度优先遍历(递归)

对一个节点进行合并之后,还要对该节点的左右子树分别进行合并,这是一个递归的过程。于是我们普通地递归深度优先遍历二叉树即可。

class Solution
{
public:
    TreeNode *mergeTrees(TreeNode *p1, TreeNode *p2)
    {
        if (!p1)
            return p2;
        if (!p2)
            return p1;
        TreeNode *merged = new TreeNode(p1->val + p2->val);
        merged->left = mergeTrees(p1->left, p2->left);
        merged->right = mergeTrees(p1->right, p2->right);
        return merged;
    }
};

复杂度分析

  • 时间复杂度:O(min(m,n))。其中 m 和 n 分别是两个二叉树的节点个数。假设某次合并时,有一个结点为空,则合并时将非空结点的整个分支都链接到了新树上,该分支下的结点都没有被遍历。因此这里是取最小值。

  • 空间复杂度:O(min(m,n))。同上,递归调用的层数最多不会超过较小的二叉树的最大高度。最坏情况下,二叉树的高度等于结点数。

参考结果

Accepted
182/182 cases passed (40 ms)
Your runtime beats 40.09 % of cpp submissions
Your memory usage beats 24.81 % of cpp submissions (32.1 MB)

方法1优化:解决方法1中存在的隐患(选择性理解)

对地址和指针懂一点、或者说敏感一点的小伙伴来说应该能马上看出来方法1中存在的问题。

对于只有一个结点为空时,方法1将整个分支直接连接到了新树上,这导致新树的某些结点是原来2棵树上的结点。一旦原树上的结点发生了变化,新树的对应结点也跟着发生变化;若原树结点被释放,则新树的对应结点也被释放了。

如果说我们选择考虑这个问题,那我们对于整个分支上的结点都要重新构造。

解决方法的代码也不难想到。我们这道题本身就是创建一个新树,树中结点的值是2棵树合并出来的。那么我们对于分支也是一个道理:创建一个新分支,分支结点的值是一颗树和一颗空树合并出来的(nullptr)。所以创建分支的代码和合并树的代码一样。只是把另一边换成了空树。

如果在面试笔试的过程中遇到了这种问题,建议问一下考官。如果说不用考虑这个问题,那就直接连上吧。

class Solution
{
public:
    TreeNode *mergeTrees(TreeNode *p1, TreeNode *p2)
    {
        if (!p1 && !p2)
            return nullptr;
        else if (p1 && !p2)
        {
            TreeNode *branchroot = new TreeNode(p1->val);
            branchroot->left = mergeTrees(p1->left, nullptr);
            branchroot->right = mergeTrees(p1->right, nullptr);
            return branchroot;
        }
        else if (!p1 && p2)
        {
            TreeNode *branchroot = new TreeNode(p2->val);
            branchroot->left = mergeTrees(p2->left, nullptr);
            branchroot->right = mergeTrees(p2->right, nullptr);
            return branchroot;
        }
        else
        {
            TreeNode *merged = new TreeNode(p1->val + p2->val);
            merged->left = mergeTrees(p1->left, p2->left);
            merged->right = mergeTrees(p1->right, p2->right);
            return merged;
        }
    }
};

复杂度分析

  • 时间复杂度:O(C(m,n)+I(m)+I(n))。其中 m 和 n 分别是两个二叉树的节点个数;C(m,n)是两棵树的均不为空的对应结点;I(m)和I(n)分别为对方树上为空,而自身不为空的对应结点。由于我们要遍历所有结点,首先大家都有的结点要合并起来;其次在对方没有结点的位置,自己的结点需要重构一次。

  • 空间复杂度:O(C(m,n)+I(m)+I(n))。虽然答案树不算在空间复杂度内,而且空间复杂度仍不变,但实际消耗当然会多出新树的空间。

参考结果

Accepted
182/182 cases passed (52 ms)
Your runtime beats 9.22 % of cpp submissions
Your memory usage beats 5.04 % of cpp submissions (34 MB)

方法2:深度优先遍历(非递归)

我们要想将递归具象为栈,那我们必须把原来2棵树,和新树的对应结点存入栈中。我们可以定义一个结构体StackElement

struct StackElement
{
    TreeNode *p, *p1, *p2;
    StackElement(TreeNode *p, TreeNode *p1, TreeNode *p2) : p(p), p1(p1), p2(p2) {}
};

如果有一个结点非空,我们可以选择直接将整个分支连接上去(即p->left=p1->leftp->right=p1->rightp->left=p2->left等),也可以重构整个分支(详见上文方法1优化)。

本方法中选择了重构所有的结点。可以从代码中给p->leftp->right赋值的地方看出,每次都是使用了new关键字。

#include <stack>
using namespace std;

struct StackElement
{
    TreeNode *p, *p1, *p2;
    StackElement(TreeNode *p, TreeNode *p1, TreeNode *p2) : p(p), p1(p1), p2(p2) {}
};

class Solution
{
public:
    TreeNode *mergeTrees(TreeNode *root1, TreeNode *root2)
    {
        if (!root1 && !root2)
            return nullptr;

        TreeNode *root = new TreeNode();
        stack<StackElement> stk;
        stk.push({root, root1, root2});

        while (!stk.empty())
        {
            auto [p, p1, p2] = stk.top();
            stk.pop();

            p->val = (p1 ? p1->val : 0) + (p2 ? p2->val : 0);

            if (p1 && p1->left || p2 && p2->left)
            {
                p->left = new TreeNode();
                stk.emplace(p->left, p1 && p1->left ? p1->left : nullptr, p2 && p2->left ? p2->left : nullptr);
            }
            if (p1 && p1->right || p2 && p2->right)
            {
                p->right = new TreeNode();
                stk.emplace(p->right, p1 && p1->right ? p1->right : nullptr, p2 && p2->right ? p2->right : nullptr);
            }
        }

        return root;
    }
};

复杂度分析

  • 时间复杂度:O(C(m,n)+I(m)+I(n))。其中 m 和 n 分别是两个二叉树的节点个数;C(m,n)是两棵树的均不为空的对应结点;I(m)和I(n)分别为对方树上为空,而自身不为空的对应结点。由于我们要遍历所有结点,首先大家都有的结点要合并起来;其次在对方没有结点的位置,自己的结点需要重构一次。

  • 空间复杂度:O(C(m,n)+I(m)+I(n))。同上。

参考结果

Accepted
182/182 cases passed (44 ms)
Your runtime beats 20.82 % of cpp submissions
Your memory usage beats 8.46 % of cpp submissions (32.8 MB)

方法3:广度优先搜索

我们直接将方法2的代码中的栈替换成队列即可。深度和广度的区别就体现在栈和队列上,而那些区别已经帮我们实现好了。当然你应该或多或少懂一点数据结构并了解栈和队列了。

摆烂不写了。就把方法2里的栈换成队列。

深度和广度的时间、空间复杂度同理。



116. 填充每个节点的下一个右侧节点指针

LeetCode

中 等 \color{#FFB800}{中等}

给定一个 完美二叉树 ,其所有叶子节点都在同一层,每个父节点都有两个子节点。二叉树定义如下:

struct Node {
  int val;
  Node *left;
  Node *right;
  Node *next;
}

填充它的每个 next 指针,让这个指针指向其下一个右侧节点。如果找不到下一个右侧节点,则将 next 指针设置为 NULL
初始状态下,所有 next 指针都被设置为 NULL

进阶:

  • 你只能使用常量级额外空间。
  • 使用递归解题也符合要求,本题中递归程序占用的栈空间不算做额外的空间复杂度。

示例:

在这里插入图片描述

输入:root = [1,2,3,4,5,6,7]
输出:[1,#,2,3,#,4,5,6,7,#]
解释:给定二叉树如图 A 所示,你的函数应该填充它的每个 next 指针,以指向其下一个右侧节点,如图 B 所示。序列化的输出按层序遍历排列,同一层节点由 next 指针连接,'#' 标志着每一层的结束。

提示:

  • 树中节点的数量少于 4096
  • -1000 <= node.val <= 1000

方法1:层次遍历

题目的意思是让我们在树的每一层上附着一个链表,表头为这一层最左侧的结点。因此层次遍历是我们的首选。

层次遍历是广度优先搜索的变种。它与广度优先的区别在于:广度优先每次循环选择一个结点,扩展它的孩子结点层次遍历每次循环选择一层的结点,然后扩展它们的孩子结点,这样就能保证每次循环内从队列中拿出来的结点是属于同一层的。

基于层次遍历,我们每从队列中拿出一个结点后,将该结点的next指针指向队首的元素即可。

因为我们从队列中拿出一个结点后,要把它的左右孩子入队,这样一来我们就不能通过队空来终止“从队列中拿结点”这个循环。所以每次循环开始时,我们需要一个变量n记录当前队列中存在多少元素,也就是这一层有多少元素,这样我们就能知道循环n次然后结束。

由于上述循环是对每一层进行操作,因此我们需要外层循环来“指定哪一层”,外层循环的终止条件即为队空。因为内层循环结束后,会向队列添加2倍于上一层的结点;直至遍历到最后一层时,结点均无孩子结点,没有元素入栈,此时即为整棵树遍历完成。

#include <queue>
using namespace std;
class Solution
{
public:
    Node *connect(Node *root)
    {
        if (!root)
            return nullptr;

		root->next = nullptr;

        queue<Node *> que;
        que.push(root);

        while (!que.empty())
        {
            size_t n = que.size();
            for (size_t i = 0; i < n; i++)
            {
                Node *p = que.front();
                que.pop();
                if (i < n - 1)
                {
                    p->next = que.front();
                }
                if (p->left)
                {
                    que.push(p->left);
                }
                if (p->right)
                {
                    que.push(p->right);
                }
            }
        }
        return root;
    }
};

复杂度分析

  • 时间复杂度:O(n)。每个结点最多被遍历一次。
  • 空间复杂度:O(n)。完美二叉树的最后一层有n/2个结点,因此队列长度最大为n/2。因此空间复杂度为O(n/2)=O(n)。

参考结果

Accepted
58/58 cases passed (12 ms)
Your runtime beats 99.02 % of cpp submissions
Your memory usage beats 47.48 % of cpp submissions (16.6 MB)

方法2:使用上一层已建立的next指针

建立next指针有两种情况:

  1. 第一种情况是一个父节点下的两个左右孩子之间,左孩子的next需要指向右孩子。这一步只要我们选定父节点即可,parent->left->next=parent->right
  2. 第二种情况是一个父节点的右孩子的next需要指向右边父节点的左孩子,这种情况不能直接连接,因为我们既要知道父节点的位置,又要知道父节点的next

至此,我们的目标已经明了了。我们建立某一层需要知道上一层的next情况;要建立上一层的next情况需要知道再上一层。那么我们只要把第一层的next建立起来,之后我们就能用第i层的链表去对第i+1层的结点进行操作。因此上述第二种情况的代码即为:parent->right->next=parent->next->left

每一层里面,我们需要从最左侧开始,只要拿到最左边的结点,就可以从该结点开始往右走,同时对下一层结点进行操作。因此内层循环我们要遍历这一层的链表。

同时,最左边的结点又是上一层最左侧结点的左孩子。因此,外层循环也明了了,当最左侧结点,即这一层的链表头结点head不为空时代表循环还未结束,还有下一层要去;每次循环结束后head=head->left,前往下一层。

#include <queue>
using namespace std;
class Solution
{
public:
    Node *connect(Node *root)
    {
        if (!root)
            return nullptr;
        root->next = nullptr;

        Node *head = root;
        while (head->left)
        {
            Node *p = head;
            while (p)
            {
                p->left->next = p->right;

                if (head->next)
                {
                    p->right->next = p->next->left;
                }

                p = p->next;
            }
            head = head->left;
        }
        return root;
    }
};

复杂度分析

  • 时间复杂度:O(n)。每个结点最多被遍历一次。
  • 空间复杂度:O(1)。我们只需要常量空间存储若干变量,不需要存储额外的结点。

参考结果

Accepted
58/58 cases passed (20 ms)
Your runtime beats 70.81 % of cpp submissions
Your memory usage beats 75.46 % of cpp submissions (16.4 MB)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

亡心灵

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

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

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

打赏作者

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

抵扣说明:

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

余额充值