代码随想录算法训练营 | day21 二叉树 530.二叉搜索树的最小绝对差,501.二叉搜索树中的众数,236.二叉树的最近祖宗

Java 补充知识

用stream流方法转化为int数组

文章参考:用stream流方法转化为int数组_stream转int-CSDN博客

::双冒号:把方法当做参数传到stream内部,使stream的每个元素都传入到该方法里面执行一下

如果转化的是存Integer类型数据的集合,以下的所有intValue都可以用valueOf代替

但如果是存Long或Double等数据类型的数据须用对应类型的intValue()方法

intValue()是java.lang.Number类的方法。Java中所有的数值类都继承它。也就是说 Integer,Double,Long等都有intValue()方法

Integer 用stream流转化为 int数组 //Integer[] 转换为 int[] Integer[] arr = new Integer[10]; int[] intArr = Arrays.stream(arr).mapToInt(Integer::intValue).toArray();

hashset转化为Integer数组 Set<Integer> set = new HashSet<>(); Integer[] arr = new Integer[set.size()]; set.toArray(arr);

hashset用stream流转化为int数组 Set<Integer> set = new HashSet<>(); int[] arr = set.stream().mapToInt(Integer::intValue).toArray();

ArrayList转化为Integer数组 List<Integer> list = new ArrayList<>(); Integer[] arr = new Integer[list.size()]; list.toArray(arr);

ArrayList用stream流转化为int数组 List<Integer> list = new ArrayList<>(); int[] arr = list.stream().mapToInt(Integer::intValue).toArray();

不难发现: set 或 list 转化为 Integer 数组或者 int 数组的方式是一样的

Map转化成List

文章参考:【Java 8 新特性】使用Collectors.toList()方法将Map转化成List的示例_collect.tolist-CSDN博客

提供 Java8转换 Map到 List的方法 Collectors.toList()示例。 一个映射有键和值,我们可以将所有键和值作为列表来获取。

如果我们想在一个类属性中设置key和value,然后把这个对象添加到List中,我们可以用java8的一行代码来实现Collectors.toList().

现在让我们看看怎么做。

1.使用Lambda表达式将Map转化成List 在Collectors.toList()方法中使用lambda表达式将Map转换为List,如下示例

List<String> valueList = map.values().stream().collect(Collectors.toList()); 

如果我们想在放入List之前对值进行排序,我们将按如下方式进行。

List<String> sortedValueList = map.values().stream()
                                .sorted().collect(Collectors.toList());

我们还可以使用给定的比较器(Comparator.comparing())将Map转换为用户对象List

List<Person> list = map.entrySet().stream().sorted(Comparator.comparing(e -> e.getKey()))
        .map(e -> new Person(e.getKey(), e.getValue())).collect(Collectors.toList()); 

这里的Person是一个用户类。我们也可以使用Map.Entry获取Map的键和值如下。

这里的Person是一个用户类。我们也可以使用Map.Entry获取Map的键和值如下。

List<Person> list = map.entrySet().stream().sorted(Comparator.comparing(Map.Entry::getValue))
        .map(e -> new Person(e.getKey(), e.getValue())).collect(Collectors.toList()); 

作为比较,我们也可以使用Map.Entry.comparingByValue()和Map.Entry.comparingByKey()分别根据值和键对数据进行排序。

List<Person> list = map.entrySet().stream().sorted(Map.Entry.comparingByKey())
    .map(e -> new Person(e.getKey(), e.getValue())).collect(Collectors.toList()); 

2.简单Map转化成List示例 SimpleMapToList.java

package com.concretepage;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class SimpleMapToList {
    public static void main(String[] args) {
        Map<Integer, String> map = new HashMap<>();
        map.put(23, "Mahesh");
        map.put(10, "Suresh");
        map.put(26, "Dinesh");
        map.put(11, "Kamlesh");
        System.out.println("--Convert Map Values to List--");
        List<String> valueList = map.values().stream().collect(Collectors.toList());
        valueList.forEach(n -> System.out.println(n));
        
        System.out.println("--Convert Map Values to List using sort--");
        List<String> sortedValueList = map.values().stream()
            .sorted().collect(Collectors.toList());
        sortedValueList.forEach(n -> System.out.println(n));        
    
        System.out.println("--Convert Map keys to List--");
        List<Integer> keyList = map.keySet().stream().collect(Collectors.toList());
        keyList.forEach(n -> System.out.println(n));
    
        System.out.println("--Convert Map keys to List using sort--");
        List<Integer> sortedKeyList = map.keySet().stream()
            .sorted().collect(Collectors.toList());
        sortedKeyList.forEach(n -> System.out.println(n));
    }
 } 

输出

--Convert Map Values to List--
Mahesh
Suresh
Dinesh
Kamlesh
--Convert Map Values to List using sort--
Dinesh
Kamlesh
Mahesh
Suresh
--Convert Map keys to List--
23
10
26
11
--Convert Map keys to List using sort--
10
11
23
26 

3.用户对象Map转化成List示例 MapToListOfUserObject.java

package com.concretepage;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class MapToListOfUserObject {
    public static void main(String[] args) {
        Map<Integer, String> map = new HashMap<>();
        map.put(23, "Mahesh");
        map.put(10, "Suresh");
        map.put(26, "Dinesh");
        map.put(11, "Kamlesh");
        List<Person> list = map.entrySet().stream().sorted(Comparator.comparing(e ->    e.getKey()))
            .map(e -> new Person(e.getKey(), e.getValue())).collect(Collectors.toList());
    
        list.forEach(l -> System.out.println("Id: "+ l.getId()+", Name: "+ l.getName()));       
}} 

Person.java

package com.concretepage;
public class Person {
    private Integer id;
    private String name;
    public Person(Integer id, String name) {
        this.id = id;
        this.name = name;
    }
    public Integer getId() {
        return id;
    }
    public String getName() {
        return name;
    }
} 

输出

Id: 10, Name: Suresh
Id: 11, Name: Kamlesh
Id: 23, Name: Mahesh
Id: 26, Name: Dinesh 

如果我们需要按键来排序的话,我们可以使用键来排序

Comparator.comparing(e -> e.getValue())

然后输出如下。

然后输出如下。

Id: 26, Name: Dinesh
Id: 11, Name: Kamlesh
Id: 23, Name: Mahesh
Id: 10, Name: Suresh

Stream—sort排序

使用sorted排序

public static void main(String[] args) {
        List<Student> list = new ArrayList<>();
 
        Student s1 = new Student("zhangsan","beijing",30);
        list.add(s1);
 
        Student s2 = new Student("lisi","shanghai",29);
        list.add(s2);
 
        Student s3 = new Student("lining","shandong",31);
        list.add(s3);
 
        // forEach循环
        list.stream().forEach(student -> System.out.println(student.getAge()));
 
        System.out.println("----------使用stream和sort--默认升序----------");
        // sort排序:原集合不变,新集合按顺序排序
        List<Student> sortList1 = list.stream().sorted((a, b) -> a.getAge().compareTo(b.getAge())).collect(Collectors.toList());
        sortList1.stream().forEach(s-> System.out.println(s.getAge()));
        System.out.println();
 
        System.out.println("---------使用stream和sort--降序排列-----------");
        List<Student> sortDesList = list.stream().sorted(Comparator.comparingInt(Student::getAge).reversed()).collect(Collectors.toList());
        sortDesList.stream().forEach(s-> System.out.println(s.getAge()));
        System.out.println();
 
        System.out.println("----------不使用stream和sort------------");
        // 使用集合的sort排序,集合自身排序发生变化
        list.sort((a,b)->a.getAge().compareTo(b.getAge()));
        list.stream().forEach(student -> System.out.println(student.getAge()));
        System.out.println();
}
运行结果:
Connected to the target VM, address: '127.0.0.1:51549', transport: 'socket'
30
29
31
----------使用stream和sort--默认升序----------
29
30
31
---------使用stream和sort--降序排列-----------
Disconnected from the target VM, address: '127.0.0.1:51549', transport: 'socket'
31
30
29
----------不使用stream和sort------------
29
30
31

刷题

530.二叉搜索树的最小绝对差

题目链接 | 文章讲解 | 视频讲解

题目:给你一棵所有节点为非负值的二叉搜索树,请你计算树中任意两节点的差的绝对值的最小值。

示例:

提示:树中至少有 2 个节点。

思路及实现

题目中要求在二叉搜索树上任意两节点的差的绝对值的最小值。

注意是二叉搜索树,二叉搜索树可是有序的。

遇到在二叉搜索树上求什么最值啊,差值之类的,就把它想成在一个有序数组上求最值,求差值,这样就简单多了。

递归

那么二叉搜索树采用中序遍历,其实就是一个有序数组

在一个有序数组上求两个数最小差值,这是不是就是一道送分题了。

最直观的想法,就是把二叉搜索树转换成有序数组,然后遍历一遍数组,就统计出来最小差值了。

把二叉搜索树转化为有序数组了,其实在二叉搜素树中序遍历的过程中,我们就可以直接计算了。

需要用一个pre节点记录一下cur节点的前一个节点。

如图:

代码如下:

class Solution {
    TreeNode pre;// 记录上一个遍历的结点
    int result = Integer.MAX_VALUE;
    public int getMinimumDifference(TreeNode root) {
       if(root==null)return 0;
       traversal(root);
       return result;
    }
    public void traversal(TreeNode root){
        if(root==null)return;
        //左
        traversal(root.left);
        //中
        if(pre!=null){
            result = Math.min(result,root.val-pre.val);
        }
        pre = root;
        //右
        traversal(root.right);
    }
}
迭代

下面给出其中的一种中序遍历的迭代法,代码如下:

統一迭代法-中序遍历

class Solution {
    public int getMinimumDifference(TreeNode root) {
        Stack<TreeNode> stack = new Stack<>();
        TreeNode pre = null;
        int result = Integer.MAX_VALUE;
​
        if(root != null)
            stack.add(root);
        while(!stack.isEmpty()){
            TreeNode curr = stack.peek();
            if(curr != null){
                stack.pop();
                if(curr.right != null)
                    stack.add(curr.right);
                stack.add(curr);
                stack.add(null);
                if(curr.left != null)
                    stack.add(curr.left);
            }else{
                stack.pop();
                TreeNode temp = stack.pop();
                if(pre != null)
                    result = Math.min(result, temp.val - pre.val);
                pre = temp;
            }
        }
        return result;
    }
}
​

迭代法-中序遍历

class Solution {
    TreeNode pre;
    Stack<TreeNode> stack;
    public int getMinimumDifference(TreeNode root) {
        if (root == null) return 0;
        stack = new Stack<>();
        TreeNode cur = root;
        int result = Integer.MAX_VALUE;
        while (cur != null || !stack.isEmpty()) {
            if (cur != null) {
                stack.push(cur); // 将访问的节点放进栈
                cur = cur.left; // 左
            }else {
                cur = stack.pop(); 
                if (pre != null) { // 中
                    result = Math.min(result, cur.val - pre.val);
                }
                pre = cur;
                cur = cur.right; // 右
            }
        }
        return result;
    }
}

501.二叉搜索树中的众数

题目链接 | 文章讲解 | 视频讲解

题目:给定一个有相同值的二叉搜索树(BST),找出 BST 中的所有众数(出现频率最高的元素)。

假定 BST 有如下定义:

  • 结点左子树中所含结点的值小于等于当前结点的值

  • 结点右子树中所含结点的值大于等于当前结点的值

  • 左子树和右子树都是二叉搜索树

例如:

给定 BST [1,null,2,2],

返回[2].

提示:如果众数超过1个,不需考虑输出顺序

进阶:你可以不使用额外的空间吗?(假设由递归产生的隐式调用栈的开销不被计算在内)

思路及实现

递归法
如果不是二叉搜索树

如果不是二叉搜索树,最直观的方法一定是把这个树都遍历了,用map统计频率,把频率排个序,最后取前面高频的元素的集合。

具体步骤如下:

1.这个树都遍历了,用map统计频率

至于用前中后序哪种遍历也不重要,因为就是要全遍历一遍,怎么个遍历法都行,层序遍历都没毛病!

2.把统计的出来的出现频率(即map中的value)排个序

3.取前面高频的元素

代码如下:

class Solution {
    public int[] findMode(TreeNode root) {
        Map<Integer, Integer> map = new HashMap<>();
        List<Integer> list = new ArrayList<>();
        if (root == null) return list.stream().mapToInt(Integer::intValue).toArray();
        // 获得频率 Map
        searchBST(root, map);
        List<Map.Entry<Integer, Integer>> mapList = map.entrySet().stream()
                .sorted((c1, c2) -> c2.getValue().compareTo(c1.getValue()))
                .collect(Collectors.toList());
        list.add(mapList.get(0).getKey());
        // 把频率最高的加入 list
        for (int i = 1; i < mapList.size(); i++) {
            if (mapList.get(i).getValue() == mapList.get(i - 1).getValue()) {
                list.add(mapList.get(i).getKey());
            } else {
                break;
            }
        }
        return list.stream().mapToInt(Integer::intValue).toArray();
    }
​
    void searchBST(TreeNode curr, Map<Integer, Integer> map) {
        if (curr == null) return;
        map.put(curr.val, map.getOrDefault(curr.val, 0) + 1);
        searchBST(curr.left, map);
        searchBST(curr.right, map);
    }
​
}
是二叉搜索树

既然是搜索树,它中序遍历就是有序的

如图:

遍历有序数组的元素出现频率,从头遍历,那么一定是相邻两个元素作比较,然后就把出现频率最高的元素输出就可以了。

弄一个指针指向前一个节点,这样每次cur(当前节点)才能和pre(前一个节点)作比较。

而且初始化的时候pre = NULL,这样当pre为NULL时候,我们就知道这是比较的第一个元素。

此时又有问题了,因为要求最大频率的元素集合(注意是集合,不是一个元素,可以有多个众数),如果是数组上大家一般怎么办?

应该是先遍历一遍数组,找出最大频率(maxCount),然后再重新遍历一遍数组把出现频率为maxCount的元素放进集合。(因为众数有多个)

这种方式遍历了两遍数组。

那么我们遍历两遍二叉搜索树,把众数集合算出来也是可以的。

但这里其实只需要遍历一次就可以找到所有的众数。

那么如何只遍历一遍呢?

如果频率count 等于 maxCount(最大频率),当然要把这个元素加入到结果集中(以下代码为result数组)

result怎么能轻易就把元素放进去了呢,万一,这个maxCount此时还不是真正最大频率呢。

所以下面要做如下操作:

频率count 大于 maxCount的时候,不仅要更新maxCount,而且要清空结果集(以下代码为result数组),因为结果集之前的元素都失效了。

代码如下:

class Solution {
    ArrayList<Integer> resList;
    int maxCount;
    int count;
    TreeNode pre;
​
    public int[] findMode(TreeNode root) {
        resList = new ArrayList<>();
        maxCount = 0;
        count = 0;
        pre = null;
        findMode1(root);
        int[] res = new int[resList.size()];
        for (int i = 0; i < resList.size(); i++) {
            res[i] = resList.get(i);
        }
        return res;
    }
​
    public void findMode1(TreeNode root) {
        if (root == null) {
            return;
        }
        findMode1(root.left);
​
        int rootValue = root.val;
        // 计数
        if (pre == null || rootValue != pre.val) {
            count = 1;
        } else {
            count++;
        }
        // 更新结果以及maxCount
        if (count > maxCount) {
            resList.clear();
            resList.add(rootValue);
            maxCount = count;
        } else if (count == maxCount) {
            resList.add(rootValue);
        }
        pre = root;
​
        findMode1(root.right);
    }
}
迭代法

只要把中序遍历转成迭代,中间节点的处理逻辑完全一样。

代码如下:

class Solution {
    public int[] findMode(TreeNode root) {
        TreeNode pre = null;
        Stack<TreeNode> stack = new Stack<>();
        List<Integer> result = new ArrayList<>();
        int maxCount = 0;
        int count = 0;
        TreeNode cur = root;
        while (cur != null || !stack.isEmpty()) {
            if (cur != null) {
                stack.push(cur);
                cur =cur.left;
            }else {
                cur = stack.pop();
                // 计数
                if (pre == null || cur.val != pre.val) {
                    count = 1;
                }else {
                    count++;
                }
                // 更新结果
                if (count > maxCount) {
                    maxCount = count;
                    result.clear();
                    result.add(cur.val);
                }else if (count == maxCount) {
                    result.add(cur.val);
                }
                pre = cur;
                cur = cur.right;
            }
        }
        return result.stream().mapToInt(Integer::intValue).toArray();
    }
}

236.二叉树的最近祖宗

题目链接 | 文章讲解 | 视频讲解

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

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

例如,给定如下二叉树: root = [3,5,1,6,2,0,8,null,null,7,4]

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

示例 2: 输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4 输出: 5 解释: 节点 5 和节点 4 的最近公共祖先是节点 5。因为根据定义最近公共祖先节点可以为节点本身。

说明:

  • 所有节点的值都是唯一的。

  • p、q 为不同节点且均存在于给定的二叉树中。

思路及实现

遇到这个题目首先想的是要是能自底向上查找就好了,这样就可以找到公共祖先了。

那么二叉树如何可以自底向上查找呢?

回溯啊,二叉树回溯的过程就是从低到上。

后序遍历(左右中)就是天然的回溯过程,可以根据左右子树的返回值,来处理中节点的逻辑。

接下来就看如何判断一个节点是节点q和节点p的公共祖先呢。

首先最容易想到的一个情况:如果找到一个节点,发现左子树出现结点p,右子树出现节点q,或者 左子树出现结点q,右子树出现节点p,那么该节点就是节点p和q的最近公共祖先。 即情况一:

判断逻辑是 如果递归遍历遇到q,就将q返回,遇到p 就将p返回,那么如果 左右子树的返回值都不为空,说明此时的中节点,一定是q 和p 的最近祖先。

那么有录友可能疑惑,会不会左子树 遇到q 返回,右子树也遇到q返回,这样并没有找到 q 和p的最近祖先。

这么想的录友,要审题了,题目强调:二叉树节点数值是不重复的,而且一定存在 q 和 p

但是很多人容易忽略一个情况,就是节点本身p(q),它拥有一个子孙节点q(p)。 情况二:

其实情况一 和 情况二 代码实现过程都是一样的,也可以说,实现情况一的逻辑,顺便包含了情况二。

因为遇到 q 或者 p 就返回,这样也包含了 q 或者 p 本身就是 公共祖先的情况。

这一点是很多录友容易忽略的,在下面的代码讲解中,可以再去体会。

递归三部曲:

  • 确定递归函数返回值以及参数

需要递归函数返回值,来告诉我们是否找到节点q或者p,那么返回值为bool类型就可以了。

但我们还要返回最近公共节点,可以利用上题目中返回值是TreeNode ,那么如果遇到p或者q,就把q或者p返回,返回值不为空,就说明找到了q或者p。

  • 确定终止条件

遇到空的话,因为树都是空了,所以返回空。

那么我们来说一说,如果 root == q,或者 root == p,说明找到 q p ,则将其返回,这个返回值,后面在中节点的处理过程中会用到,那么中节点的处理逻辑,下面讲解。

  • 确定单层递归逻辑

值得注意的是 本题函数有返回值,是因为回溯的过程需要递归函数的返回值做判断,但本题我们依然要遍历树的所有节点。

如果递归函数有返回值,如何区分要搜索一条边,还是搜索整个树呢?

在递归函数有返回值的情况下:如果要搜索一条边,递归函数返回值不为空的时候,立刻返回,如果搜索整个树,直接用一个变量left、right接住返回值,这个left、right后序还有逻辑处理的需要,也就是后序遍历中处理中间节点的逻辑(也是回溯)

那么为什么要遍历整棵树呢?直观上来看,找到最近公共祖先,直接一路返回就可以了。

如图:

就像图中一样直接返回7。

但事实上还要遍历根节点右子树(即使此时已经找到了目标节点了),也就是图中的节点4、15、20。

因为在如下代码的后序遍历中,如果想利用left和right做逻辑处理, 不能立刻返回,而是要等left与right逻辑处理完之后才能返回。

所以此时大家要知道我们要遍历整棵树。知道这一点,对本题就有一定深度的理解了。

那么先用left和right接住左子树和右子树的返回值:

如果left 和 right都不为空,说明此时root就是最近公共节点。这个比较好理解

如果left为空,right不为空,就返回right,说明目标节点是通过right返回的,反之依然

这里有的同学就理解不了了,为什么left为空,right不为空,目标节点通过right返回呢?

如图:

图中节点10的左子树返回null,右子树返回目标值7,那么此时节点10的处理逻辑就是把右子树的返回值(最近公共祖先7)返回上去!

这里也很重要,可能刷过这道题目的同学,都不清楚结果究竟是如何从底层一层一层传到头结点的。

那么如果left和right都为空,则返回left或者right都是可以的,也就是返回空。

那么寻找最小公共祖先,完整流程图如下:

从图中,大家可以看到,我们是如何回溯遍历整棵二叉树,将结果返回给头结点的!

整体代码如下:

class Solution {
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        if (root == null || root == p || root == q) { // 递归结束条件
            return root;
        }
​
        // 后序遍历
        TreeNode left = lowestCommonAncestor(root.left, p, q);
        TreeNode right = lowestCommonAncestor(root.right, p, q);
​
        if(left == null && right == null) { // 若未找到节点 p 或 q
            return null;
        }else if(left == null && right != null) { // 若找到一个节点
            return right;
        }else if(left != null && right == null) { // 若找到一个节点
            return left;
        }else { // 若找到两个节点
            return root;
        }
    }
}

总结

这道题目刷过的同学未必真正了解这里面回溯的过程,以及结果是如何一层一层传上去的。

那么我给大家归纳如下三点

  1. 求最小公共祖先,需要从底向上遍历,那么二叉树,只能通过后序遍历(即:回溯)实现从底向上的遍历方式。

  2. 在回溯的过程中,必然要遍历整棵二叉树,即使已经找到结果了,依然要把其他节点遍历完,因为要使用递归函数的返回值(也就是代码中的left和right)做逻辑判断。

  3. 要理解如果返回值left为空,right不为空为什么要返回right,为什么可以用返回right传给上一层结果。

可以说这里每一步,都是有难度的,都需要对二叉树,递归和回溯有一定的理解。

本题没有给出迭代法,因为迭代法不适合模拟回溯的过程。理解递归的解法就够了。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值