二叉树的“查”

前言:今天来重点讲一下二叉树的“查”,二叉树也是一种数据存储方式,类比于数组来说,最基本的“查”应该有以下几种:

  1. 二叉树的大小即节点个数
  2. 二叉树的最大深度和最小深度
  3. 二叉树的最近公共祖先

方法论:

本文中主要用到了两种思维模式,如下:

  1. 是否遍历一遍二叉树就可以得到结果,如果可以,只需要对二叉树的递归遍历方式或者迭代遍历方式便可以可到结果,这种思维模式称为遍历思维模式
  2. 是否需要借助子树的返回值?如果需要可以定义一个递归函数求解(注重逻辑的自洽),这种思维模式下:递归函数需要返回值代码通常写在后序位置,这种思维模式称为分治思维模式。

无论那种思维模式,你都需要思考:

如果单独抽出一个节点,他需要做什么事情,需要在前序、中序、后序哪个位置做事情?不同位置的代码仅能接收到该代码之前的结果,前序位置的代码自然接收不到子树的消息,因为前序代码执行的时候,二叉树还没有开始遍历。

一、二叉树的节点个数

1. 普通二叉树的节点个数

1.1 遍历的思维模式

  • 递归方式
class Solution {
    int count = 0;
    public int countNodes(TreeNode root) {
        if(root == null) return 0;
        count(root);
        return count;
    }
    void count(TreeNode root) {
        if(root == null) return;
        count(root.left);
        count(root.right);
        count++;//count++的位置在前、中、后序都可以放置,本质就是数组中的循环体中的count++
    }
}
  • 迭代方式
    迭代方式,深度迭代和广度迭代都可以,只需要将迭代代码中的result.add(curNode.val)改为count++即可,具体代码可参考我的上一篇文章二叉树遍历

1.2 分治的思维模式

首先思考:如果你是一个树节点,你如何知道以你为父节点的二叉树节点个数?你的子树需要给你返回什么?
我的思考:二叉树的节点个数 = 父节点 + 左子树节点个数 + 右子树节点大小

class Solution {
    public int countNodes(TreeNode root) {
        if(root == null) return 0;
        return 1 + countNodes(root.left) + countNodes(root.right);
    }
}

2. 完全二叉树的节点个数

说完普通二叉树的节点个数,其实完全二叉树、满二叉树的节点个数也可以求,但是没有利用完全二叉树和满二叉树的性质,去提高减少代码的时间复杂度和空间复杂度。
既然要利用完全二叉树的性质,那就需要知道他的性质是什么:

  1. 完全二叉树的子树必定可以分解为多个满二叉树,这里的子树也可以是子树的子树,单个节点也可以是满二叉树。
  2. 满二叉树的节点个数: 2^h-1,这里的h是层数。root节点为第一层。
    知道了以上的性质,我们就不需要遍历左右子树,只需要知道满二叉子树的高度即可。
class Solution {
    public int countNodes(TreeNode root) {
        if(root == null) return 0;
        int hl = 1; int hr = 1;//考虑极端情况,root.left == null或者root.right == null;
        TreeNode left = root.left;
        TreeNode right = root.right;
        while(left != null) {
            left = left.left;
            hl++;
        }
        while(right != null) {
            right = right.right;
            hr++;
        }
        if(hl == hr) return (2<<(hl-1)) - 1;//如果是满二叉树,按照2^h-1计算,2<<1等于2^2;
        //如果不是满二叉树,按照普通二叉树计算
        return 1 + countNodes(root.left) + countNodes(root.right);
    }
}

3. 满二叉树的节点个数

class Solution {
    public int countNodes(TreeNode root) {
        if(root == null) return 0;
        int h = 0;//考虑极端情况,root.left == null&&root.right == null;
        while(root != null) {
            root = root.left;
            h++;
        }
        return (2<<(h-1)) - 1;
    }
}

关于完全二叉树和满二叉树求深度h的代码总结:
明确一下几点:
1、root节点是谁?是root还是root.left还是root.right,即你所求二叉树的root节点是谁?
2、二叉树的h从0还是从1开始,我觉得这里的h代表的是所求二叉树的父节点层数-1,为什么减去1,是因为紧接着下面h++,这也就意味着只要你的root != null,就会+1

二、二叉树的最大深度和最小深度

1. 二叉树的最大深度

1.1 遍历的思维模式

  • 递归方式
  1. 一遍遍历是否可以得到结果?可以,可以维护一个全局变量用来记录深度。
  2. 记录什么的深度?自然是每个叶子节点的深度,然后取深度的最大值就是二叉树的最大深度。
  3. 站在一个树节点的角度,深度该如何计算?进入节点的时候深度加1,离开节点的时候深度减1。
class Solution {
    int depth = 0;//当前叶子结点深度
    int max = 0;//全局最大深度
    public int maxDepth(TreeNode root) {
        traverse(root);
        return max;
    }
    void traverse(TreeNode root) {//
        if(root == null) {
            max = Math.max(depth,max);
            return;
        }
        depth++;//进入节点,深度+1
        traverse(root.left);
        traverse(root.right);
        depth--;//离开节点,深度-1
    }
}
  • 迭代方式
    采用层序迭代的方式用来计算二叉树的最大深度最方便,因为层序迭代代码的内部会维护当前队列的大小,以一层为一个循环,仅需要维护一个全局变量depth,每次循环后加1便可以计算出二叉树的最大深度。
class Solution {
    public int maxDepth(TreeNode root) {
        if(root == null) return 0;
        LinkedList<TreeNode> queue = new LinkedList<>();
        queue.offer(root);
        int depth = 0;//维护全局深度变量,初始为层数-1,即1-1=0;
        while(!queue.isEmpty()) {
            int n = queue.size();
            for(int i=0; i<n; i++) {
                TreeNode curNode = queue.poll();
                if(curNode.left != null) queue.offer(curNode.left);
                if(curNode.right != null) queue.offer(curNode.right);
            }
            depth++;
        }
        return depth;
    }
}

只要涉及到深度相关的问题,要着重考虑初始深度h的取值,考虑极端情况去对h取值。

1.2 分治的思维模式

首先思考:如果你是一个树节点,你如何知道以你为父节点的二叉树深度?你的子树需要给你返回什么?
我的思考:二叉树的最大深度 = 父节点深度 + 左右子树深度的最大值

class Solution {
    public int maxDepth(TreeNode root) {
        if(root == null) return 0;
        return 1 + Math.max(maxDepth(root.left),maxDepth(root.right));
    }
}

2. 二叉树的最小深度[重点复习😵]

最小深度:最小深度是从根节点到最近叶子节点的最短路径上的节点数量。
这道题初看和求最大深度没有什么差别,只需要将max函数改为min就可以,但是实则不一样,最大深度的判断条件是root == null,如果按照这个判断条件:
max集合中包括了非叶子节点的深度和叶子结点的深度,但是最大深度肯定在叶子结点中间,所以无伤大雅。
同样,min集合包括了非叶子节点的深度和叶子结点的深度,但是最小深度也有可能是非叶子节点,所以需要多加一层限制,让min集合中只有叶子结点,即root.left == null && root.right == null

1.1 遍历的思维模式

  • 迭代方式
class Solution {
    public int minDepth(TreeNode root) {
        if(root == null) return 0;
        LinkedList<TreeNode> queue = new LinkedList<>();
        queue.offer(root);
        //维护全局深度变量,下面代码存在一个判断条件,导致depth有可能不返回,所以depth为层数
        int depth = 1;
        while(!queue.isEmpty()) {
            int n = queue.size();
            for(int i=0; i<n; i++) {
                TreeNode curNode = queue.poll();
                //判断是否为叶子结点
                if(curNode.left == null && curNode.right == null) return depth;
                if(curNode.left != null) queue.offer(curNode.left);
                if(curNode.right != null) queue.offer(curNode.right);
            }
            depth++;
        }
        return depth;
    }
}

1.2 分治的思维模式

首先思考:如果你是一个树节点,你如何知道以你为父节点的二叉树深度?你的子树需要给你返回什么?
我的思考:二叉树的最小深度 = 父节点深度 + 左右子树深度的最小值,但是有个例,如果左右子树某一个为null,最小值应该去另一个子树寻找,而不是直接取为0。

class Solution {
    public int minDepth(TreeNode root) {
        if(root == null) return 0;
        if(root.left == null && root.right == null) return 1;
        if(root.left == null && root.right != null) {
            return 1 + minDepth(root.right);
        }
        if(root.left != null && root.right == null) {
            return 1 + minDepth(root.left);
        }
        return 1 + Math.min(minDepth(root.left),minDepth(root.right));
    }
}

三、二叉树的公共祖先

最近的公共祖先:Lowest Common Ancestor,简称LCA
如何判断是否是p、q两个节点的最近公共祖先:如果在一个节点的左右子树中能够分别找到p、q,那么该节点就是p、q节点的最近公共祖先。

1. 二叉树节点值不重复,如何判断二叉树中是否有个节点

注:以下代码引自labuladong的算法小抄

// 定义:在以 root 为根的二叉树中寻找值为 val 的节点
TreeNode find(TreeNode root, int val) {
    // base case
    if (root == null) {
        return null;
    }
    // 看看 root.val 是不是要找的
    if (root.val == val) {
        return root;
    }
    // root 不是目标节点,那就去左子树找
    TreeNode left = find(root.left, val);
    if (left != null) {
        return left;
    }
    // 左子树找不着,那就去右子树找
    TreeNode right = find(root.right, val);
    if (right != null) {
        return right;
    }
    // 实在找不到了
    return null;
}

进一步做修改,但是修改之后效率降低了,因为遍历了左右子树,但是当root恰好等于p时除外。

TreeNode find(TreeNode root, int val) {
    if (root == null) {
        return null;
    }
    // 前序位置
    if (root.val == val) {
        return root;
    }
    // root 不是目标节点,去左右子树寻找
    TreeNode left = find(root.left, val);
    TreeNode right = find(root.right, val);
    // 看看哪边找到了
    return left != null ? left : right;
}

进一步修改,代码效率在此降低,这次百分百要遍历左右子树。

TreeNode find(TreeNode root, int val) {
    if (root == null) {
        return null;
    }
    // 先去左右子树寻找
    TreeNode left = find(root.left, val);
    TreeNode right = find(root.right, val);
    // 后序位置,看看 root 是不是目标节点
    if (root.val == val) {
        return root;
    }
    // root 不是目标节点,再去看看哪边的子树找到了
    return left != null ? left : right;
}

但是为什么要提出上面三种方法呢?继续往下看。

先抛出一个找两个节点的框架:

// 定义:在以 root 为根的二叉树中寻找值为 val1 或 val2 的节点
TreeNode find(TreeNode root, int val1, int val2) {
    // base case
    if (root == null) {
        return null;
    }
    // 前序位置,看看 root 是不是目标值
    if (root.val == val1 || root.val == val2) {
        return root;
    }
    // 去左右子树寻找
    TreeNode left = find(root.left, val1, val2);
    TreeNode right = find(root.right, val1, val2);
    // 后序位置,已经知道左右子树是否存在目标值

    return left != null ? left : right;
}

2. 二叉树节点值不重复,且p、q存在于二叉树中

情况1:p、q其中一个节点是另一个节点的祖先
情况2:p、q两个节点能找到最近公共祖先,但不是他们其中一个
分析:你站在一个节点处,你需要知道p、q在不在你的左右,或者是否有一个节点就是你自己,这个是需要你前面节点给你反馈的,分治模式的思想就是分而治之,需要借助子树的力量,很明显该问题分治模式更适用,一次遍历模式我暂时未想到解法。

class Solution {
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
       if(root == null) return null;
       if(root == p || root == q) return root;//情况1
       //情况2:左右子树来回找,直到找到,因为p、q都存在于二叉树中,最坏的情况就是最近公共祖先是root。
       TreeNode left = lowestCommonAncestor(root.left,p,q);
       TreeNode right = lowestCommonAncestor(root.right,p,q);
       if(left == null) {
           return right;
       } else if (right == null) {
           return left;
       } else {
           return root;
       }
    }
}

3. 二叉树节点值不重复,求多个节点的最近公共祖先,且节点组存在于二叉树中

class Solution {
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode[] nodes) {
        if(root == null) return null;
        //情况1
        for(TreeNode node : nodes) {
            if(root == node) {
                return root;
            }
        }
        //情况2
        TreeNode left = lowestCommonAncestor(root.left,nodes);
        TreeNode right = lowestCommonAncestor(root.right,nodes);
        if(left == null) {
            return right;
        } else if(right == null) {
            return left;
        } else {
            return root;
        }
    }
}

4. 二叉树节点值不重复,且p、q不一定存在于二叉树中[😵]

写了一种方法,不过超时了:

class Solution {
    HashSet<TreeNode> set = new HashSet<>();
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        if(root == null) return null;
        set = traverser(root,set);
        if(root == p || root == q) {
            if(set.contains(p) && set.contains(q)) {
                return root;
            } else {
                return null;
            }
        }
        TreeNode left = lowestCommonAncestor(root.left,p,q);
        TreeNode right = lowestCommonAncestor(root.right,p,q);
        if(left == null) {
            return right;
        } else if(right == null) {
            return left;
        } else {
            return root;
        }
    }
    HashSet<TreeNode> traverser(TreeNode root, HashSet<TreeNode> set) {
        if(root == null) return new HashSet<TreeNode>();
        set.add(root);
        traverser(root.left,set);
        traverser(root.right,set);
        return set;
    }
}

那么该如何不超时呢?在递归过程中去判断p、q是否同时存在于二叉树中

class Solution {
    boolean foundP = false;
    boolean foundQ = false;
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        TreeNode res =  find(root,p,q);
        if(!foundP || !foundQ ) {
            return null;
        } else {
            return res;
        }
    }
    TreeNode find(TreeNode root, TreeNode p, TreeNode q) {
        if(root == null) return null;
        TreeNode left = find(root.left,p,q);
        TreeNode right = find(root.right,p,q);
        //放在后序位置上,是为了遍历到全部的节点,以便foundP、foundQ不会判断失误。
        if(root == p || root == q) {
            if(root == p) foundP = true;
            if(root == q) foundQ = true;
            return root;
        }
        if(left == null) {
            return right;
        } else if(right == null) {
            return left;
        } else {
            return root;
        }
    }
}

5. 二叉搜索树节点值不重复,且p、q一定存在于二叉树中

二叉树的方法可用,但是为了提高效率,需要用到二叉搜索树的性质

class Solution {
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        if(root == null) return null;
        if(p.val > q.val) {
            return lowestCommonAncestor(root,q,p);
        }
        //如果root.val比p.val都小,说明p、q都在右子树
        if(root.val < p.val) {
            return lowestCommonAncestor(root.right,p,q);
        }
        //如果root.val比q.val都大,说明p、q都在左子树
        if(root.val > q.val) {
            return lowestCommonAncestor(root.left,p,q);
        }
        //如果root.val处在p.val和q.val之间,说明分布在左右子树
        //if(root.val >= p.val && root.val <= q.val) 
        return root;
    }
}

6. 二叉树节点值不重复,且p、q存在于二叉树中,但是二叉树节点包含指向父节点的指针

暂时还不会,会了补

题目链接

leetcode-222:完全二叉树的节点个数
leetcode-104:二叉树的最大深度
leetcode-111:二叉树的最小深度
leetcode-236:二叉树的最近公共祖先
leetcode-1676:二叉树的最近公共祖先IV
leetcode-1644:二叉树的最近公共祖先II
leetcode-235:二叉搜索树的最近公共祖先

声明:以上内容均来源于对labuladong的算法小抄以及代码随想录相关内容的理解。

环环无敌大可爱😄

posted @ 2022-04-17 20:18  盐小果  阅读(26)  评论(0编辑  收藏  举报