力扣初级算法(四)【树】

力扣初级算法(四)【树】

本文中的题目均来自力扣,代码默认以C#实现,伪代码仅用来帮助描述,不严格遵循某种语言的语法。

树比链表稍微复杂,因为链表是线性数据结构,而树不是。  树的问题可以由 广度优先搜索 或 深度优先搜索 解决。 在本章节中,我们提供了一个对于练习 广度优先遍历 很好的题目。

我们推荐以下题目:二叉树的最大深度,验证二叉搜索树,二叉树的层次遍历 和 将有序数组转换为二叉搜索树。

  • 在开始题目之前,我们先给出树中节点的定义。
/* Definition for a binary tree node. */
public class TreeNode
{
    public int val;
    public TreeNode left;
    public TreeNode right;
    public TreeNode(int x) { val = x; }
}

104. 二叉树的最大深度

难度:简单

给定一个二叉树,找出其最大深度。

二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。

说明: 叶子节点是指没有子节点的节点。

示例: 给定二叉树 [3,9,20,null,null,15,7]

   3
  / \
 9  20
   /  \
  15   7 

返回它的最大深度 3 。

解题思路

  • 朴素的想法是,进行深度优先遍历,找到一个最大的深度并返回。

  • 通常,我们通过递归的方式来实现深度优先遍历,

    本题需要的结果是深度,那么我们就应该返回一个深度

  • 然后从返回的深度中找到一个最大值。

  • 对于任意一个节点来说,寻找深度的问题可以拆分成两个子问题。

    • 即左子树的深度和右子树的深度,我们将问题交给他们来解决。
    • 选择得到的结果中更大的那一个,在加上此节点本身的深度1
    • 最后将得到的结果返回即可。
  • 递归的过程不可能无止境的进行下去,通常有一个终点,

  • 对于本题来说,当一个节点为空节点的时候,肯定没有左右节点,

    所以不用抛出问题,直接返回 0 即可。

方法一:深度优先

public int MaxDepth(TreeNode root)
{
    if (root is null) return 0; // 递归的终点

    return Math.Max(MaxDepth(root.left), MaxDepth(root.right)) + 1;
}
  • 那么如何通过广度优先来得到最大深度呢?

  • 对于广度优先来说,我们通常借助队列来实现,

    这里我们先简单介绍一下C#语言当中的队列结构。

    // 摘要: 表示对象的先进先出集合。
    // 类型参数: T: 指定队列中元素的类型。
    public class Queue<T> : IEnumerable<T>, ICollection, IEnumerable
    {
        /* ... */
        public int Count { get; } // 包含的元素数。
        public void Clear(); // 移除所有对象。
        public T Dequeue(); // 移除并返回位于Queue开始处的对象
        public void Enqueue(T item); // 将对象添加到Queue的结尾处
        public T Peek(); // 返回位于Queue开始处的对象但不将其移除。
        /* ... */
    }
    
  • 通常的广度优先是,一层一层的遍历二叉树,

    在遍历的同时将每个节点的左右节点加入到队列当中。

  • 由于队列先进先出的性质我们可以在保证在遍历到下一层之前,当前层全部访问到。

  • 但是广度优先要想求得深度,也就是当前遍历到了第几层,

    就需要做一些微妙的调整了。

  • 我们可以一次性拿出队列中全部的节点,

    然后将新添加的节点再次加入到队列当中,

    这样,每次从队列中拿出全部的节点,

    都相当于遍历到了新的一层,也就是深度加一。

方法二:广度优先

public int MaxDepth(TreeNode root)
{
    var res = 0;
    var queue = new Queue<TreeNode>();
    if (root!=null) queue.Enqueue(root);

    while (queue.Any())
    {
        res++;
        // 一次性拿出这一层的节点
        List<TreeNode> list = queue.ToList();
        queue.Clear(); 

        foreach (var note in list)
        {
            if (note.left != null) queue.Enqueue(note.left);
            if (note.right != null) queue.Enqueue(note.right);
        }
    }

    return res;
}

98. 验证二叉搜索树

难度:中等

给定一个二叉树,判断其是否是一个有效的二叉搜索树。

假设一个二叉搜索树具有如下特征:

  • 节点的左子树只包含小于当前节点的数。
  • 节点的右子树只包含大于当前节点的数。
  • 所有左子树和右子树自身必须也是二叉搜索树。

示例 1:

输入:
    2
   / \
  1   3
输出: true

示例 2:

输入:
    5
   / \
  1   4
     / \
    3   6
输出: false
解释: 输入为: [5,1,4,null,null,3,6]。
     根节点的值为 5 ,但是其右子节点值为 4 。

解题思路

  • 朴素的想法是,对于每一个节点,我们都验证一下它满不满足二叉搜索树的规则,一旦找到一个不满足的节点,就停止搜索。

  • 一种朴素的做法是,对于每一个节点,我们遍历它的左子树和右子树,如果左子树中的每个节点都小于根节点,则符合要求,右子树同理。

    bool IsValidBST(TreeNode root)
    {
    	if (FindLeft(root) and FindRight(root)) :
    		return IsValidBST(root.left) and IsValidBST(root.right); 
    	else :
    		return False;
    }
    
  • 但是我们很快就发现,这个解法中有大量重复的过程,对于每一个节点,我们都遍历了它的左右子树,这其实是没有必要的。

  • 我们完全可以判断一下每个节点的范围区间,在一次遍历中得到结果。

    • 我们可以动态的维护一个大小区间,只要当前节点在这个区间内即可。

方法一:深度优先

public bool IsValidBST(TreeNode root)
{
    return IsValidBST(root, null, null);

    bool IsValidBST(TreeNode node, int? min, int? max)
    {
        if (node is null) return true;

        if ((min is null || node.val > min) && (max is null || node.val < max))
        {
            return IsValidBST(node.left, min, node.val) && IsValidBST(node.right, node.val, max);
        }

        return false;
    }
}
  • 当然,对于二叉搜索树来说,中序遍历的结果一定是升序的,我们可以利用这个特性来验证是否是二叉搜索树。

方法二:中序遍历

public bool IsValidBST(TreeNode root)
{
    int? last = null;
    return IsValidBST(root);

    bool IsValidBST(TreeNode note)
    {
        if (note is null) return true;
        // 中序遍历
        if (IsValidBST(note.left)) // 左
        {
            if (last is null || note.val > last) // 根
            {
                last = note.val;
                return (IsValidBST(note.right)); // 右
            }
        }

        return false;
    }
}

101. 对称二叉树

难度:简单

给定一个二叉树,检查它是否是镜像对称的。

例如,二叉树 [1,2,2,3,4,4,3] 是对称的。

    1
   / \
  2   2
 / \ / \
3  4 4  3

但是下面这个 [1,2,2,null,3,null,3] 则不是镜像对称的:

    1
   / \
  2   2
   \   \
   3    3

进阶:

你可以运用递归和迭代两种方法解决这个问题吗?

解题思路

  • 如果我们直接观察这颗二叉树,是不是很容易就能看出它是不是镜像?
  • 我们的大脑是如何思考的呢?
    • 我们大概率会用我们的双眼去扫描这颗二叉树,看看左右两边哪里不同。
  • 那么对于计算机来说,我们也可以让它分别扫描根节点的左右子树,然后判断是否相同。
  • 这里我们借助两个额外的容器来存放扫描的记录,
    • 我们分别对左右子树进行进行根左右根右左顺序的遍历,如果左右子树是镜像的,那么我们遍历得到的记录应该是相同的。

方法一:左右遍历

public bool IsSymmetric(TreeNode root)
{
    var leftList = new List<int?>();
    var rightList = new List<int?>();

    FindLeft(root?.left);
    FindRight(root?.right);

    return leftList.SequenceEqual(rightList);
    // 根左右 遍历左子树
    void FindLeft(TreeNode node)
    {
        leftList.Add(node?.val);
        if (node is null) return; // 递归的终点

        FindLeft(node.left);
        FindLeft(node.right);
    }
    // 根右左 遍历右子树
    void FindRight(TreeNode node)
    {
        rightList.Add(node?.val);
        if (node is null) return; // 递归的终点

        FindRight(node.right);
        FindRight(node.left);
    }
}
  • 在上面的做法当中,我们对左右子树进行了完全遍历,并使用两个容器来保存,这其实是没有必要的。

  • 我们人脑在识别的时候,也不过是两个节点两个节点的对比,那么计算机能否也这样呢?

  • 我们找到了一个最小的操作,对比两个节点的值,

    如果相等,就抛出问题,继续对比后面的节点,如果不相等,就立刻终止。

方法二:递归

public bool IsSymmetric(TreeNode root)
{
    return IsSymmetric(root?.left, root?.right);

    bool IsSymmetric(TreeNode left, TreeNode right)
    {
        if (left is null && right is null) return true; // 递归的终点

        return (left?.val == right?.val) && IsSymmetric(left.left, right.right) && IsSymmetric(left.right, right.left);
    }
}
  • 除此之外,广度优先也是一种不错的解法,我们在前面的题目当中已经做过逐层扫描的操作了,这一次我们也可以这么干。
  • 对于每一层的节点,如果二叉树是镜像的,那么这一层的节点也应该是镜像的,我们可以利用老朋友双指针来判断。

方法三:广度优先

public bool IsSymmetric(TreeNode root)
{
    if (root is null) return true;
	
    var queue = new Queue<TreeNode>();
    queue.Enqueue(root);

    while (queue.Any())
    {
        List<TreeNode> list = queue.ToList();
        // 双指针判断是否镜像
        for (int i = 0, j = list.Count - 1; i < j; i++, j--)
            if (list[i]?.val != list[j]?.val) return false;

        queue.Clear();
        foreach (var node in list)
        {
            if (node != null)
            {
                queue.Enqueue(node.left);
                queue.Enqueue(node.right);
            }
        }
    }

    return true;
}

102. 二叉树的层序遍历

难度:中等

给你一个二叉树,请你返回其按 层序遍历 得到的节点值。 (即逐层地,从左到右访问所有节点)。

示例: 二叉树:[3,9,20,null,null,15,7],

    3
   / \
  9  20
    /  \
   15   7

返回其层次遍历结果:

[
  [3],
  [9,20],
  [15,7]
] 

解题思路

  • 朴素的想法是,利用广度优先逐层遍历,像我们之前做的那样,每次直接取出一层的节点。
  • 不过在此之前,我们先考虑一下深度优先的解法,我们是不是可以通过一个额外的字段深度来记录当前遍历节点的深度,然后再把它加入到对应的深度集合当中呢?

方法一:深度优先

public IList<IList<int>> LevelOrder(TreeNode root)
{
    var res = new List<IList<int>>();
    FindP(root, 0);
    return res;

    void FindP(TreeNode node, int depth)
    {
        if (node is null) return; // 递归的终点

        if (res.Count <= depth) res.Add(new List<int>()); // 扩容
        res[depth].Add(node.val);

        FindP(node.left, depth + 1);
        FindP(node.right, depth + 1);
    }
}
  • 广度优先的写法很简单,除了直接取出一层节点外,

    我们还可以取出队列的前N个节点,N为当前层的节点数量,这样可以保证每次取的都是同一层节点。

方法二:广度优先

public IList<IList<int>> LevelOrder(TreeNode root)
{
    var res = new List<IList<int>>();
    var queue = new Queue<TreeNode>();
    if (root != null) queue.Enqueue(root);

    for (var depth = 0; queue.Any(); depth++)
    {
        var temp = new List<int>();
        var n = queue.Count; // 取n个节点填充
        for (int i = 0; i < n; i++)
        {
            var note = queue.Dequeue();
            temp.Add(note.val);
            if (note.left != null) queue.Enqueue(note.left);
            if (note.right != null) queue.Enqueue(note.right);
        }
        res.Add(temp);
    }

    return res;
}

108. 将有序数组转换为二叉搜索树

难度:简单

将一个按照升序排列的有序数组,转换为一棵高度平衡二叉搜索树。

本题中,一个高度平衡二叉树是指一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1。

示例:

给定有序数组: [-10,-3,0,5,9],

一个可能的答案是:[0,-3,9,-10,null,5],它可以表示下面这个高度平衡二叉搜索树:

      0
     / \
   -3   9
   /   /
 -10  5

解题思路

  • 朴素的想法是,先找到一个最小的操作,对于平衡的二叉搜索树来说,由于给定的数组是有序的,那么我们很容易想到,把这个数组最中间的元素当做根节点的值。
  • 这样,我们就把数组划分成了三部分,左边、中间、右边。
  • 左边和右边区间的长度差值不会超过1,而且都是升序的,用来构造平衡的二叉搜索树再好不过了。
  • 我们可以将问题抛出,只利用最中间的元素来构造一个根节点,左右子树的构造交给一个函数来解决。

方法一:递归

public TreeNode SortedArrayToBST(int[] nums)
{
    if (nums.Length < 1) return null;
    var mid = nums.Length / 2;
    return new TreeNode(nums[mid])
    {
        left = SortedArrayToBST(nums.Take(mid).ToArray()),
        right = SortedArrayToBST(nums.Skip(mid + 1).ToArray())
    };
}
  • 对二叉平衡树感兴趣的同学,还可以自行了解一下二叉平衡树插入节点和删除节点的实现。

最后

  • 递归是非常有魅力的一种写法,你可以只关心局部的工作,然后将剩余的问题抛出。
  • 二叉树的遍历和链表的遍历是不是非常相似?只不过每个节点可以延伸到两个节点,而链表只会延伸一个节点。
posted @ 2020-10-17 16:29  惟手熟尔  阅读(185)  评论(0编辑  收藏  举报