前端面试算法之 --- 二叉树

201 阅读20分钟

二叉树

背景:二叉树的重要性

之前看了一篇文章说的很好,数据结构不是为了去展示自身的难度,而是为了更好的服务于数据的基本操作:增删改查。
其中“查”,说到底就是遍历去找数据,找可以分为:线性结构的查找 && 非线形结构的查找。 1.“线形的查找”:以for/while循环迭代为代表
2.“非线形查找”:以递归为代表。
非线形查找的典型就是二叉树的遍历,而大部分的算法技巧本质上都可以演变为树的遍历问题。因此建议先刷二叉树来学习递归的思想 && DFS(深度遍历) && BFS(广度便利)&& 在遍历的基础上构建二叉树。 面试高频题目:
二叉树中序遍历、将有序数组转为二叉搜索树、相同的树、二叉树的最近公共祖先、二叉搜索树的最近公共祖先、二叉树的序列化和反序列化、根据前序和后序构建二叉树、翻转二叉树、二叉树的最大宽度、最大深度

image.png 盗用一张大神的图

1.DFS-深度遍历

dfs遍历说白了就是前中后序三种遍历方式,说白了就是前根、中根、后根遍历。这三种方式应该管瓜烂熟,这样才能对递归熟练使用

深度优先搜索相关面试:
1.前序和中序构建二叉树 、中序和后序构建
2.二叉树的所有路径、二叉树中和为某一值的路径
3.有序链表转化为二叉搜索树,二叉树转为链表
4.删除无效括号
5.矩阵中的路径

1.1 前序遍历

递归方式实现
(1)二叉树中这里递归的意义是指二叉树中每一层执行相同的函数命令
(2)递归的返回值判断:每一层执行结束后是否需要对上一层负责,如果需要负责就返回这一层对应的结果,结果中可能包含递归函数
(3)递归的终止:在哪里终止?怎么终止?
如果这一层中判断left、right为空,则不调用递归(在这一层终止); 如果不判断left,right,那么就在递归中判断node.value是否为空,为空就返回,而recursion(node.left),recursion(node.right)的调用不做限制。

var preorderTraversal = function(root) {
    var result = [];
    function recursion(node) {
        if (!node) { // 递归的结束是在这一层判断的,如果node为undefined那么就不继续调用递归函数
            return;
        }
        result.push(node.val);
        recursion(node.left)
        recursion(node.right)
    }
    recursion(root);
    return result
};

// 不使用return结束递归,通过if判断是否调用
var preorderTraversal = function(root) {
    var result = [];
    function recursion(node) {
        if (node && node.val) {
            result.push(node.val);
        }
        if (node && node.left) {
            recursion(node.left)
        }
        if (node && node.right) {
            recursion(node.right)
        }
        
        
    }
    recursion(root);
    return result
};

1.2 中序遍历

// 最简单的递归,即使换了一下位置
var inorderTraversal = function(root) {
    var result = [];
    function recursion(node) {
        if (!node) {
            return;
        }
        recursion(node.left);
        result.push(node.val);
        recursion(node.right);
    }
    recursion(root)
    return result
};

可以想象一下压栈的过程,从根节点出发:
1.执行recursion(root.left),即第一层节点。执行 if (!node-level-1) { return; },继续执行recursion(node-level-1.left),遇到递归recursion(node-level-1.left)函数,压栈。
2.执行recursion(node-level-1.left),即第二层节点。执行 if (!node) { return; },(此时node为level-2中节点)继续执行recursion(node.left),遇到递归recursion(node-level-2.left)函数,压栈。
3.遇到node。val === null,返回上一层,第二层。
4.执行第二层的result.push(node-level-2.val);

1.3 后序遍历

// 最简单的递归,即使换了一下位置
var inorderTraversal = function(root) {
    var result = [];
    function recursion(node) {
        if (!node) {
            return;
        }
        recursion(node.left);
        recursion(node.right);
        result.push(node.val);
    }
    recursion(root)
    return result
};

1.先遍历到最后一层左叶子节点,并执行左叶子节点的recursion(叶子节点.left),进入后发现不满足node存在,返回到左叶子的recursion。执行左叶子的recursion(node.right),也不存在。执行最后左叶子的result.push(node.val)。此时相当于左叶子的recursion执行完毕。
2.左叶子所有recursion执行完毕 〈=〉等价于左叶子的父节点的recursion(node.left)这一行执行完。下面需要走这个父节点的recursion(node.right):

function recursion(右叶子节点) {
        if (!node) {
            return;
        }
        recursion(node.left); 
 // 进入右叶子节点的recursion(node.left),本质上右叶子节点已经没有子节点了,但是也要多走一边这个没有节点的recursion(叶子节点的子节点,没有节点),这么做是为了返回,继续走右叶子的下一个recursion
        recursion(node.right);
        result.push(node.val);
        // 最后回到右叶子节点自身,来压入叶子的value
}

2.BFS-层序遍历

广度遍历:是指输出结果是一层一层的输出,有层序性。但是不要被结果所蒙蔽,因为父节点不知道,是不可能通过统一层的某个节点找出这一层多有的节点的。所以要遍历这一层的所有节点,那么对应的上一层的所有节点(这一层所有节点对应的父节点都要知道)。但是上一层节点又怎么知道的呢?上一层的上一层,追本溯源到根节点。根节点知道了,那么下一层的两个节点就知道了。那么我就可以通过这两个节点的左右子节点就是下一层的所有节点,这样一层一层找就行。关键在于,如果我知道了某一层的节点分别有【a,b, c, d】那么我就可以推断出下一层的节点按照从左到右的顺序分别为【a.left, a.right,b.left, b.right, c.left, c.right, d.left, d.right】,那么往下每一层我都知道了。所以关键在于我怎么讲一层节点从左到右排列呢?根节点拿子节点的时候就一次排列【root。left, root.right】,这样可以了。 注意:一层中如果某个节点断开了,那么就不用放在数组中就行。那么什么时候结束呢?当上一层中的子节点都没有的时候就代表结束。

层序遍历的关键在于:通过栈保存当前层的节点,然后通过弹当前层栈来将对应下一层的节点依次放入下一层的栈中,一次类推,直到下一层栈没有压入任何节点,表示当前层为最后一层。

2.1 层序遍历

中心思想:在通过curlevel = 【】保存当前层的节点,然后遍历当前层所有节点的同时将每个节点的儿子节点推入到nextlevel=【】中,curlevel遍历完后将nextlevel赋值给curlevel继续遍历,知道nextlevel或者curlevel为空。 关键: 利用【第一层左节点,第一层右节点】

var levelOrderBottom = function(root) {
    if (!root) { return []}
    var result = [];
    var curlevel = [root];
    var nextlevel = [];
    while (curlevel.length > 0) {
        var res = [];
        for (let index = 0; index < curlevel.length; index++) {
            res.push(curlevel[index].val);
            if (curlevel[index].left) {nextlevel.push(curlevel[index].left); }
            if (curlevel[index].right) {nextlevel.push(curlevel[index].right); }
        }
        result.unshift(res);
        curlevel = nextlevel;
        nextlevel = []
    }
    return result;
};

2.2 锯齿形遍历

image.png 通过迭代实现

var zigzagLevelOrder = function(root) {
    if (!root) { return []; }
    var curlevel = [root];
    var nextlevel = [];
    var result = [];
    var level = 0;
    while (curlevel.length !== 0) {
        var res = [];
        for (let index = 0; index < curlevel.length; index++) {
            res.push(curlevel[index].val);
            if (curlevel[index].left) { nextlevel.push(curlevel[index].left) }
            if (curlevel[index].right) { nextlevel.push(curlevel[index].right) }
        }
        curlevel = nextlevel;
        nextlevel = [];
        if (level % 2 !== 0) {
            result.push(res.reverse())
        } else {
            result.push(res)
        }
        level = level + 1;
    }
    return result;
};

锯齿形遍历可以认为是层序遍历的变种,变化的地方就是在for循环中,讲curlevel中的节点value放在res数组中后,需要push进入result中。锯齿形遍历中需要判断当前level是奇偶性,如果是偶数需要讲res进行reverse()再放入result中。

3.构建二叉树

一个树的构建既可以通过DFS遍历的结果构建,也可以通过BFS遍历的结果构建,下面分别介绍要如何通过BFS或者DFS来构建二叉树。

3.1 DFS构建二叉树

image.png dfs一共有三种,按照排列组合一共有三种组合方式,但是要想构建二叉树必须要要有中序,因为只有中序才可以区分左子树和右子树。故,可以通过前序遍历和中序遍历构建,或者通过后序遍历和中序遍历来构建。
重要思想:

var buildTree = function(preorder, inorder) {
    function recursion(preArr, midArr) {
        if (preArr[0] === midArr[0] && preArr.length === 1) {
            return new TreeNode(preArr[0]);
        }
        else {
            var root = new TreeNode(preArr[0]);
            var index = midArr.indexOf(preArr[0]);
            var midArr_left = []; var midArr_right = [];
            midArr.forEach((item, index_2) => { // 通过index去筛选中序数组的话,不管是空数组还是有数据的子树数组,都可以区分。
                if (index_2 < index) { midArr_left.push(item); }
                if (index_2 > index) { midArr_right.push(item); }
            })
            var pre_left = []; var pre_right = [];
// 对于先序数组的筛选需要注意,这个时候我们已经拿到了左子树和右子树的中序数组
// 前序中第一个节点为根,第一个节点之后向后树(左子树中序数组).length就是左子树的前序数组,如果左子树中序数组长度为3,那么前序数组中index在1~3之间,包括1和3的元素都是前序排列。
// 即,0 < index <= (左子树中序).length,push进入左子树先序数组中。剩下的就是右子树先序数组。
// 关键两点: 1中序数组长度; 2.先序左右子树的index范围。
            preArr.forEach((item, index_2) => {
                if (index_2 > 0) {
                    if (index_2 <= midArr_left.length) {
                        pre_left.push(item);
                    } else {
                        pre_right.push(item);
                    }
                }
            })
            if (pre_left.length > 0) {
                root.left = recursion(pre_left, midArr_left);
            } else {
            }
            if (pre_right.length > 0) {
                root.right = recursion(pre_right, midArr_right);
            }
            return root;
        }
    }
    return recursion(preorder, inorder)
};
var buildTree = function(preorder, inorder) {
    if (preorder.length === 0) {
        console.log(123, new TreeNode(null))
        return null
    }
    function recursion(preorder, inorder) {
        if (preorder.length === 0) { return null }
        if (preorder.length === 1) {
            console.log('111', preorder, inorder)
            return new TreeNode(preorder[0]);
        } else {
            var node_root = new TreeNode(preorder[0]);
            var root_index = inorder.indexOf(preorder[0]); // 找出根节点在中序中的序号
            var left_preorder = [], left_inorder = []; // 左子树的先序,左子树的中序,这样才能构建出左子树
            var right_preorder = [], right_inorder = []; // 右子树的先序,右子树的中序,这样才能构建出右子树
            inorder.forEach((item, index) => {
                if (index < root_index) {
                    left_inorder.push(item);
                }
                if (index > root_index) {
                    right_inorder.push(item);
                }
            })
            left_preorder = preorder.slice(1, left_inorder.length + 1)
            if (right_inorder.length > 0) {
                right_preorder = preorder.slice(-right_inorder.length) // 这里不能倒过来取,因为如果
            } else {
                right_preorder = []
            }
            
            console.log(left_preorder, right_preorder)
            console.log(left_inorder, right_inorder)
            node_root.left = recursion(left_preorder, left_inorder)
            node_root.right = recursion(right_preorder, right_inorder)
            return node_root;
        }

    }
    return recursion(preorder, inorder)
};

中心思想:递归使用 前序的根在中序中的位置,然后这个位置的左边就是左子树,右边就是右子树。 然后根据中序中的左子树和右子树长度,可以得出左子树的前序数组,右子树的前序数组。然后进入下一次递归。 知道传入的前序参数和中序参数的长度都为一的时候,就可以建立节点。
例如: 前序 = [3,9,20,15,7], 中序 = [9,3,15,20,7] 根据前序可得第一个元素为根元素,即为3,在中序的位置idnex = 1, 可得到节点3 的左子树的前序和中序数组,右子树的前序和中序数组。
其中中序可以得到,通过

1.根节点在中序中的index 前序 = [3,9,20,15,7], 中序 = [9,3,15,20,7]

var root_index = inorder.indexOf(preorder[0]); // 找出根节点在中序中的序号1

2.根据这个index找出左子树和右子树的数组left-arr/right-arr,提出来左子树和右子树中序数组为【9】/【15,20,7】 再通过上一步的左右子树中序数组的长度,可以得到左右子树的前序数组,然后进行下一次递归。

注意1:如果传入的是[],是返回null

left_preorder = preorder.slice(1, left_inorder.length + 1)
if (right_inorder.length > 0) { 
    right_preorder = preorder.slice(-right_inorder.length) // 这里不能倒过来取,因为如果 
 } else { 
    right_preorder = [] 
 }

注意2: 当进行左右子树划分的时候,如果没有左子树或者没有右子树,那么对于index需要注意,如果采用substring的时候,如果是按照index去filter的话是不会截取错误的。

4.DFS相关面试题

4.1 二叉树所有路径

image.png 关键: 二叉树的递归模版就不用说了(如下),关键是怎么在迭代模版中完成我们的需要。我们需要全路径,即只有遍历到叶子节点的时候的路径才是我们需要的路径 关键两点:

关键1:什么时候放入路径 我们直接想法就是在node节点不存在的时候加入result,但是我们要确认这个节点是叶子节点才能push进入result,下面这个时候push会导致一个问题,因为这种递归结束方式是要到当前节点的下一个虚拟节点(叶子节点下一层不存在的节点)结束递归,那么上面的2节点进入到left递归之后就可以push路径,这显然是不对的(结果如下) image.png

if (!node) {
   result.push(path); // 直接在node不存在时候push,导致结果出现下图中的错误,将某一中间节点的路径也放入结果中
   return;
}

所以这里需要使用的是在当层节点结束递归,而不是到下一层中判断结束递归
关键2: 哪一种方式结束递归,当前层还是下一层

recursion(node) {
    if (!node) { return; } // 
    node.val // 
    recursion(node.left)
    recursion(node.right)
}
// 还有一种模版是加了(node.left)判断,
recursion(node) {
    if (!node) { return; } // 
    node.val //
    if (node.left) {
        recursion(node.left)
    }
    if (node.right) {
        recursion(node.right)
    } 
}
// 仔细想想,当我们在这一层node判断后,如果left不存在就不调用recursion函数,那么程序就会走下一指令:是否存在node.right,而如果是叶子节点的话,那么叶子节点这一层的recusion函数会执行完毕后弹出来。
// 而如果不加node.left判断,那么即使是叶子节点也会走recursion(叶子节点的left,即null),通过下一层的
if(!node) { return; }弹出叶子节点下一层的null的函数栈,然后叶子节点函数内部的recursion(node.left)函数执行完毕,执行下一指令。

而我们希望通过每一层递归的时候代入val参数,然后到叶子节点的时候push到result中。叶子节点是指(!node.left && !node.right),这个时候讲path放入到result。 注意:这里不能在 if (!node.val) { result.push(path); return; },如果这样会导致一种情况:如果这个节点没有左子树,但有柚子树,那么就会1-2-3,和1-2重复路径出现,2的右子树为3,左子树为null。

var binaryTreePaths = function(root) {
    var result = [];
    function recursion(node, path) { 
        // 这里因为已经在下面加了node。left/right的判断,
        // 相当于在叶子节点层就已经做出了是否递归终止的判断,不会到叶子节点的下一层(虚拟null节点判断)
        // if (!node.val) { 
        // return;
        //}
        var curpath;
        if (path === '') {
            curpath = `${node.val}`
        } else {
            curpath = path + '->' + node.val;
        }
        if (node.left) {
            recursion(node.left, curpath);
        }
        if (node.right) {
            recursion(node.right, curpath);
        }
// 这里是最容易错的地方,已经错了两次,全路径中一定要到叶子节点才能对result结果push,否则如果会将只存在左子节点或者右子节点的节点也放入result中。
        if (!node.left && !node.right) { 
            result.push(curpath); 
        }
        
    }
    recursion(root, '');
    return result;
};


// 错误提交代码
var binaryTreePaths = function(root) {
    let result = [];
    function recursion(node, path) {
        if (!node) {
            result.push(path); // 直接在node不存在时候push,导致结果出现下图中的错误,将某一中间节点的路径也放入结果中
            return;
        }
        let curpath = [...path, node.val];
        recursion(node.left, curpath);
        recursion(node.right, curpath);
    }
    recursion(root, []);
    result = result.map((item) => {
        return item.join('->')
    });
    return result;
};

4.2 二叉树和为某一值的路径

var pathSum = function(root, target) {
    var result = [];
    function recursion(node, sum, path) {
        if (!node) { return; }
        var curpath = [...path, node.val]
        var cursum = sum + node.val;
        recursion(node.left, cursum, curpath);
        recursion(node.right, cursum, curpath);
        if (!node.left && !node.right) {
            if (cursum === target) {
                result.push(curpath);
            }  
        }
    }
    recursion(root, 0, [])
    return result
};

4.3 翻转二叉树

思想:通过递归,到一个节点层之后就调换左右节点,然后继续递归。需要注意的是:这里既是是null节点也要调换,所以不能使用if(node.left)加判断

var invertTree = function(root) {
    function recursion(node) {
        if (!node) { return; }
        let cur;
        cur = node.left;
        node.left = node.right;
        node.right = cur;
        recursion(node.left);
        recursion(node.right);
    }
    recursion(root)
    return root;
};

4.4 二叉树最大深度

思想:最大深度一定是到叶子节点,所以!node.left && !node.right才可以进行深度的比较,才外有一个参数传入到递归中。

var maxDepth = function(root) {
    var deep = 0;
    if (!root) { return 0}
    function recursion(node, level) {
        let cur = level + 1;
        if (node.left !== null) {
            recursion(node.left, cur)
        }
        if (node.right !== null) {
            recursion(node.right, cur)
        }
        if (!node.left && !node.right) {
            deep = Math.max(deep, cur)
        }  
    }
    recursion(root, 0)
    return deep;
};

4.5 二叉树的右视图

题目: image.png 注意:如果右边节点不存在,那么看见的就是左节点,比如3下面的4假如不存在,那么砍价的就是2下面的5.所以递归的话需要想办法回溯,比较麻烦。
思想:因此可以使用层序遍历,找到每一层最后一个元素就一定是最右边看见的元素。

var rightSideView = function(root) {
    if (!root) { return []; }
    var result = [];
    var curlevel = [root];
    var nextlevel = [];
    while (curlevel.length > 0) {
        var res = [];
        for (let index = 0; index < curlevel.length; index++) {
            res.push(curlevel[index].val);
            if (curlevel[index].left) { nextlevel.push(curlevel[index].left) }
            if (curlevel[index].right) { nextlevel.push(curlevel[index].right) }
        }
        result.push(res[res.length - 1])
        curlevel = nextlevel;
        nextlevel = [];
    }
    return result
};

4.6 删除无效括号

4.7 二叉树的序列化和反序列化

什么是序列化?

为什么需要序列化? 其实序列化最终的目的是为了对象可以跨平台存储,和进行网络传输。那么不是我们光转为字符串就可以的,我们的“字符串序列”需要满足一种规则,按照这种规则我们可以将对象转为字符串(即为序列化),并且逆规则可以将字符串反过来转为对象(反序列化)。

image.png 分析:序列化要求的是所以的叶子节点都要遍历到下一层才能结束当前的递归。比如说,加入1是叶子节点,那么之前有一种递归是在递归函数调用之前判断是否需要递归,进而扼杀递归,这种递归方式当叶子节点当前层递归函数就返回,如下:

if (node.left) {
    recursion(node.left)
}

但是另一种在递归函数的开头判断条件,return返回来结束递归,这种方式会到叶子节点的下一层递归函数执行,然后才返回:

function(node) {
    if (!node) { return; } // 结束递归
}

(1)二叉树序列化
序列化的本质上:就是前序遍历采用第二种方式,因为我们需要到下一层拿到null,才能区分出叶子节点。区分出了叶子节点才能进一步区分出左右子树。

// 二叉树序列化
var serialize = function(root) {
    var result = [];
    function recursion(node) {
        if (!node) { result.push('null'); return; }
        result.push(node.val);
        recursion(node.left);
        recursion(node.right);
    }
    recursion(root)
    result = result.join();
    return result;
};

// 注意:序列化的输出是
// [1,2,3,null,null,4,null,null,5,null, null]
// 虽然系统中是[1,2,3,null,null,4,5]
// 但是我们会在4,5作为叶子节点后面都添加‘null’字符串,所以我们push的是‘null’字符串
// 因为如果是push(null)的话,最后join()的字符串结果中是不会出现null的,而是出现【1,2,3,,4】,所以这一点需要注意:最后的结果都是字符串元素,null要手动转化。

(2)反序列化构建二叉树
反序列化本质

// 反序列化构建二叉树
var deserialize = function(data) {
    var sequence = data.split(',');
    function recursion() {
        var curdata = sequence.shift();
        if (curdata === 'null') { return null}
        var node = new TreeNode(curdata); // 创建节点的构造函数
        node.left = recursion();
        node.right = recursion();
        return node;
    }
    return recursion();
};
//  * @return {TreeNode}

构建二叉树的关键在于:我怎么区分左右子树?
这里的反序列化的本质转为为如何通过序列化的输出:[1,2,3,null,null,4,null,null,5,null, null],构建二叉树。
1.先序遍历是根左右,那么第一个值一定是根,后面的一定是左右子树。而之前的前序遍历是没有null的,这就导致一个判别左右子树的问题,我不知道后面的节点是左子树还是在右子树中。因为有一种干扰情况在其中,如果左子树为null,那么【1,2,3】中的2就是右子树,而如果左子树不为null,那么节点2一定是左子树,因为如果左子树为null,先序排列应该为【1,null,2】。
2.所以通过null的加入,可以排除了先序中左右子树不分的问题,所以【1,2,3】中根节点后面如果不为null,那么一定是左子树的根节点。
3.【1,2,3,4,5, null, null】这个序列中一定是1的左子树是2,然后以2为根后面一第一个不为null的话就又是下一个左子树的根3,为3位根的树,其左子树的根2为4,以此类推。知道发现5的左子树为null,那么null的后一个一定是5的右子树,也为null。那么5就是叶子节点。那么【5,null,null】表示4的左子树,【5,null,null】后面的元素第一位就是4的右子树的根。这样下去,就可以构建一颗树了。

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

image.png

image.png

在层序遍历的基础上,进行操作,相当于结合了数组的前后指针问题

var connect = function(root) {
    if (!root || root.length == 0) { return root}
    let current = [root];
    let next = [];
    let result = [];
    while (current.length > 0) {
        for (let index = 0; index < current.length; index++) {
            let node = current[index];
            if (index === current.length - 1) {
                node.next = null
            } else {
                node.next = current[index + 1];
            }
            if (node.left) {
                next.push(node.left);
            }
            if (node.right) {
                next.push(node.right);
            }
        }
        result.push(current)
        current = [...next];
        next = [];
    }
    console.log(result)
    return root
};

4.10 二叉树的最近公共祖先

image.png image.png 思考 - 二叉树最近公共祖先问题
思考1:这个问题有点类似vue中每次dom更新是怎么更新的,是从根节点向下全部更新,还是也是于最近公共祖先相同,对于更新节点找到最近公共祖先更新,又或者是各自更新各自的dom,不用管最近公共祖先节点
思考2:找到最近公共祖先问题,咋一看有点反二叉树结构,因为二叉树只能通过父节点找到子节点,也就是说向下找容易,而找到公共父节点显然是向上找然后找到相同的节点更符合思维习惯。
想法1: 我们能不能通过现有的二叉树的方法解决目前的问题?
现有的方法是:我们通过递归可以找到二叉树中任意目标节点的路径(通过递归),那么比如过我们给出三个目标节点,那么我就可以通过递归拿到从根节点到这三个目标节点的路径。这条路径有什么特点,就是这个路径数组的前后元素一定是父子关系(这一点特征很关键,因为递归深度优先遍历),且目标节点的路径最后一个元素一定是目标节点。
那么这个时候我们就可以二叉树寻找公共父元素问题 => 转化为 路径中向前找到公共元素值。【1,2,3,4,5】、【1,3,6】显然目标节点为最后一个元素5和6,那么分别向前找,发现相同元素是1,3,且3离目标节点最近(或者在相同元素中的最后面),那么显然这是最近的公共元素

var lowestCommonAncestor = function(root, p, q) {

    const recursion = (node, val1, val2) => {
        if (node === null) {
            return null
        }
        // 1.刚刚到这一层需要处理的事情,此时还没有往下递归
        if (node === val1 || node === val2) {
            return node
        }
        // 2.先左递归,再右递归(这个是回溯的关键,在这个之上就是捕获阶段,在这个之下就是冒泡阶段-即回溯阶段)
        const leftHaveNode = recursion(node.left, val1, val2)
        const rightHaveNode = recursion(node.right, val1, val2)
        // 3.(左右递归函数执行结束之后就是回溯到当前节点了)这个时候当前层下面的左右两个节点已经通过递归都遍历过了,又回溯到了当前层
        if (leftHaveNode && rightHaveNode) {
            return node
        }
        if (leftHaveNode && !rightHaveNode) {
            return leftHaveNode
        }
        if (!leftHaveNode && rightHaveNode) {
            return rightHaveNode
        }
    
    }
   return recursion(root, p, q)
};

4.11 二叉树展开为链表

image.png

可以参考一位大神的视频讲解;www.bilibili.com/video/BV1eY…

本文参考以下文献:(感谢)
mp.weixin.qq.com/s/q0GGTYkNd…