剑指Offer-树部分
重建二叉树
输入某二叉树的前序遍历和中序遍历的结果,请重建该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。
例如,给出
前序遍历 preorder = [3,9,20,15,7]
中序遍历 inorder = [9,3,15,20,7]
返回如下的二叉树:
3
/ \
9 20
/ \
15 7
解题思路
首先前序/后序遍历 + 中序遍历可以重建二叉树。题目考察的就是前序+中序来重建二叉树,后序+中序的思路是类似的。
例子与思路
假设有二叉树如下:
1
/ \
2 3
/ \
4 5
它的前序遍历的顺序是:1 2 4 5 3。中序遍历的顺序是:4 2 5 1 3
因为前序遍历的第一个元素就是当前二叉树的根节点。那么,这个值就可以将中序遍历分成 2 个部分。在以上面的例子,中序遍历就被分成了 4 2 5 和 3 两个部分。4 2 5就是左子树,3就是右子树。
最后,根据左右子树,继续递归即可
var buildTree = function(preorder, inorder) {
if(!preorder.length || !inorder.length){
return null
};
let rootVal = preorder[0];
let node = new TreeNode(rootVal);
let i = 0;
for(;i<inorder.length;i++){
if(rootVal == inorder[i]){
break
}
};
node.left = buildTree(preorder.slice(1,i+1),inorder.slice(0,i));
node.right = buildTree(preorder.slice(i+1),inorder.slice(i+1));
return node
};
树的子结构
输入两棵二叉树A和B,判断B是不是A的子结构。(约定空树不是任意一个树的子结构)
B是A的子结构, 即 A中有出现和B相同的结构和节点值
例如:
给定的树 A:
3
/ \
4 5
/ \
1 2
给定的树 B:
4
/
1
返回 true,因为 B 与 A 的一个子树拥有相同的结构和节点值。
示例 1:
输入:A = [1,2,3], B = [3,1]
输出:false
示例 2:
输入:A = [3,4,5,1,2], B = [4,1]
输出:true
解题思路
递归
设计两个函数:
isSubStructure 的职能:判断 B 是否是 A 的子结构。是,返回 true;否则,尝试 A 的左右子树
isSubTree 的职能:封装“判断 B 是否是 A 的子结构”的具体逻辑。
var isSubStructure = function(A, B) {
if(!A || !B) {
return false;
}
return (isSubTree(A,B) || isSubStructure(A.left,B) || isSubStructure(A.right,B))
};
var isSubTree = function(A,B){
if(!B){
return true
}
if(!A){
return false
}
if(A.val != B.val){
return false
}
return isSubTree(A.left,B.left) && isSubTree(A.right,B.right)
}
二叉树的镜像
请完成一个函数,输入一个二叉树,该函数输出它的镜像。
例如输入:
4
/ \
2 7
/ \ / \
1 3 6 9
镜像输出:
4
/ \
7 2
/ \ / \
9 6 3 1
示例 1:
输入:root = [4,2,7,1,3,6,9]
输出:[4,7,2,9,6,3,1]
解题思路1
左右子树镜像递归
var mirrorTree = function(root) {
if (!root) {
return null;
}
// 交换当前节点的左右节点
const leftCopy = root.left;
root.left = root.right;
root.right = leftCopy;
// 对左右子树做相同操作
mirrorTree(root.left);
mirrorTree(root.right);
return root;
};
解题思路2
一句话完成,也是递归
var mirrorTree = function(root) {
return root == null? null : new TreeNode(root.val,mirrorTree(root.right),mirrorTree(root.left))
};
对称的二叉树
请实现一个函数,用来判断一棵二叉树是不是对称的。如果一棵二叉树和它的镜像一样,那么它是对称的。
例如,二叉树 [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
示例 1:
输入:root = [1,2,2,3,4,4,3]
输出:true
示例 2:
输入:root = [1,2,2,null,3,null,3]
输出:false
解题思路
递归
var isSymmetric = function(root) {
if(!root) return true;
var check = function(left,right){
if(!left && !right) return true;
if(!left || !right) return false;
if(left.val != right.val) return false;
return check(left.left,right.right) && check(left.right,right.left)
}
return check(root.left,root.right)
};
从上到下打印二叉树
从上到下打印出二叉树的每个节点,同一层的节点按照从左到右的顺序打印。
例如:
给定二叉树: [3,9,20,null,null,15,7],
3
/ \
9 20
/ \
15 7
返回:
[3,9,20,15,7]
解题思路
层序遍历需要使用一个队列来存储有用的节点。整体的思路如下:
将 root 放入队列
取出队首元素,将 val 放入返回的数组中
检查队首元素的子节点,若不为空,则将子节点放入队列
检查队列是否为空,为空,结束并返回数组;不为空,回到第二步
时间复杂度和空间复杂度是 O(N)。代码实现如下
var levelOrder = function(root) {
if (!root) {
return [];
}
const data = [];
const queue = [root];
while (queue.length) {
const first = queue.shift();
data.push(first.val);
first.left && queue.push(first.left);
first.right && queue.push(first.right);
}
return data;
};
从上到下打印二叉树 II
从上到下按层打印二叉树,同一层的节点按从左到右的顺序打印,每一层打印到一行。
例如:
给定二叉树: [3,9,20,null,null,15,7],
3
/ \
9 20
/ \
15 7
返回其层次遍历结果:
[
[3],
[9,20],
[15,7]
]
解题思路
按层打印,因此增加层这个概念来限制
var levelOrder = function(root) {
if(!root) return [];
let data = [];
let queue = [root];
let level = 0;
while(queue.length){
data[level] = []
levelNum = queue.length;
while(levelNum--){
let first = queue.shift();
data[level].push(first.val);
first.left && queue.push(first.left);
first.right && queue.push(first.right);
}
level++
}
return data
};
从上到下打印二叉树 III
请实现一个函数按照之字形顺序打印二叉树,即第一行按照从左到右的顺序打印,第二层按照从右到左的顺序打印,第三行再按照从左到右的顺序打印,其他行以此类推。
例如:
给定二叉树: [3,9,20,null,null,15,7],
3
/ \
9 20
/ \
15 7
返回其层次遍历结果:
[
[3],
[20,9],
[15,7]
]
解题思路
跟上体类似,只不过加了个左右添加的判断
在这里插入代码片var levelOrder = function(root) {
if(!root) return [];
let data = [];
let queue = [root];
let level = 0;
while(queue.length){
data[level] = [];
levelNum = queue.length;
while(levelNum--){
let first = queue.shift();
level%2==0?data[level].push(first.val):data[level].unshift(first.val);
first.left && queue.push(first.left);
first.right && queue.push(first.right);
}
level++
}
return data
};
二叉树中和为某一值的路径
输入一棵二叉树和一个整数,打印出二叉树中节点值的和为输入整数的所有路径。从树的根节点开始往下一直到叶节点所经过的节点形成一条路径。
示例:
给定如下二叉树,以及目标和 target = 22,
5
/ \
4 8
/ / \
11 13 4
/ \ / \
7 2 5 1
返回:
[
[5,4,11,2],
[5,8,4,5]
]
解题思路1
前序遍历(递归)
算法实现思路是:
每次来到新节点,将节点放入当前保存的路径.检查节点是否是叶节点:是:将路径放入结果中;不是:继续遍历左子树和右子树
上面整个过程就是一个前序遍历,但在遍历的过程中,动态地维护了当前路径与总和的信息。
var pathSum = function(root, sum) {
if (!root) {
return [];
}
const pathes = [];
__pathSum(root, sum, pathes, []);
return pathes;
};
function __pathSum(root, sum, pathes, path) {
if (!root) {
return;
}
path = [...path, root.val]; // 深拷贝
if (!root.left && !root.right && root.val === sum) {
pathes.push(path);
return;
}
__pathSum(root.left, sum - root.val, pathes, path);
__pathSum(root.right, sum - root.val, pathes, path);
}
解题思路2
这里可以借助栈,改造成非递归前序遍历。从而减少运行时间。
为了方便表示节点信息,栈中的元素是形如(node, 剩余路径和, 节点路径组成的数组)这样的元祖。
整体的处理流程是:
取出栈顶的元祖:节点、剩余路径和、路径
如果当前节点是叶节点,且剩余路径和等于节点的 val,那么将路径放入结果中
如果右节点不为空,将(右节点,剩余路径和 - 右节点值,路径+右节点)放入栈中
如果左节点不为空,处理过程和右节点一样
注意,为什么先处理右节点而不是左节点?先遍历左和右都可以。但是因为用的***的 oj 平台,这里要求“数组长度大的数组靠前”,先遍历左节点就是 oc 不了。
var pathSum = function(root, target) {
if(!root) return [];
let stack = [[root,target,[root.val]]];
let pathes = [];
while(stack.length){
let [node,target,path] = stack.pop();
if(!node.left && !node.right && node.val == target){
pathes.push(path)
}
if(node.right){
stack.push([node.right,target-node.val,[...path,node.right.val]])
};
if(node.left){
stack.push([node.left,target-node.val,[...path,node.left.val]])
};
}
return pathes
};
序列化二叉树
请实现两个函数,分别用来序列化和反序列化二叉树。
示例:
你可以将以下二叉树:
1
/ \
2 3
/ \
4 5
序列化为 "[1,2,3,null,null,4,5]"
解题思路1(DFS)
(序列化)
首先对其进行序列化生成字符串,以X代替null
(反序列)
传入由序列化字符串转成的 list 数组。
逐个 pop 出 list 的首项,构建当前子树的根节点,顺着 list,构建顺序是根节点 > 左子树 > 右子树。
如果弹出的字符为 “X”,则返回 null 节点。
如果弹出的字符是数值,则创建root节点,并递归构建root的左右子树,最后返回root。
//序列化
const serialize = (root) => {
if (root == null) { // 遍历到 null 节点
return 'X';
}
const left = serialize(root.left); // 左子树的序列化结果
const right = serialize(root.right); // 右子树的序列化结果
return root.val + ',' + left + ','+ right; // 按 根,左,右 拼接字符串
};
//反序列化
const deserialize = (data) => {
const list = data.split(','); // split成数组
const buildTree = (list) => { // 基于list构建当前子树
const rootVal = list.shift(); // 弹出首项,获取它的“数据”
if (rootVal == "X") { // 是X,返回null节点
return null;
}
const root = new TreeNode(rootVal); // 不是X,则创建节点
root.left = buildTree(list); // 递归构建左子树
root.right = buildTree(list); // 递归构建右子树
return root; // 返回当前构建好的root
};
return buildTree(list); // 构建的入口
};
解题思路2(BFS)
(序列化)
维护一个队列,初始让根节点入列,考察出列节点:
如果出列的节点是 null,将符号 ‘X’ 推入 res 数组。
如果出列的节点是数值,将节点值推入数组 res,并将它的左右子节点入列。
子节点 null 也要入列,它对应 “X”,要被记录,只是它没有子节点可入列。
入列、出列…直到队列为空,就遍历完所有节点,res构建完毕,转成字符串就好。
(反序列化)
依然先转成list数组,用一个指针 cursor 从第二项开始扫描。
起初,用list[0]构建根节点,并让根节点入列。
节点出列,此时 cursor 指向它的左子节点值,cursor+1 指向它的右子节点值。
如果子节点值是数值,则创建节点,并认出列的父亲,同时自己也是父亲,入列。
如果子节点值为 ‘X’,什么都不用做,因为出列的父亲的 left 和 right 本来就是 null
可见,所有的真实节点都会在队列里走一遍,出列就带出儿子入列
* Definition for a binary tree node.
* function TreeNode(val) {
* this.val = val;
* this.left = this.right = null;
* }
*/
/**
* Encodes a tree to a single string.
*
* @param {TreeNode} root
* @return {string}
*/
const serialize = (root) => {
const queue = [root];
let res = [];
while (queue.length) {
const node = queue.shift();
if (node) {
res.push(node.val);
queue.push(node.left);
queue.push(node.right);
} else {
res.push('X');
}
}
return res.join(',');
}
/**
* Decodes your encoded data to tree.
*
* @param {string} data
* @return {TreeNode}
*/
const deserialize = (data) => {
if (data == 'X') return null;
const list = data.split(',');
const root = new TreeNode(list[0]);
const queue = [root];
let cursor = 1;
while (cursor < list.length) {
const node = queue.shift();
const leftVal = list[cursor];
const rightVal = list[cursor + 1];
if (leftVal != 'X') {
const leftNode = new TreeNode(leftVal);
node.left = leftNode;
queue.push(leftNode);
}
if (rightVal != 'X') {
const rightNode = new TreeNode(rightVal);
node.right = rightNode;
queue.push(rightNode);
}
cursor += 2;
}
return root;
};
二叉搜索树的第k大节点
给定一棵二叉搜索树,请找出其中第k大的节点
示例 1:
输入: root = [3,1,4,null,2], k = 1
3
/ \
1 4
\
2
输出: 4
示例 2:
输入: root = [5,3,6,2,4,null,null,1], k = 3
5
/ \
3 6
/ \
2 4
/
1
输出: 4
解题思路1
利用从上到下遍历二叉树的思想
var kthLargest = function(root, k) {
let queue = [root];
let res = [];
while(queue.length){
let first = queue.shift();
res.push(first.val);
first.left && queue.push(first.left);
first.right && queue.push(first.right)
};
function sortNumber(a,b){
return b-a
}
return res.sort(sortNumber)[k-1];
};
解题思路2
递归
var kthLargest = function(root, k) {
let setArray = new Set()
const dfs = function(node) {
if (node === null) {
return
}
setArray.add(node.val)
dfs(node.left)
dfs(node.right)
}
dfs(root)
let array = [...setArray]
array.sort((a,b) => {
return b - a
})
return array[k - 1]
};
解题思路3
二叉搜索树,中序遍历的数组结果刚好是排好序的,故利用反中序遍历。直接遍历到第 k 大的值就停止遍历
var kthLargest = function(root, k) {
// 反中序遍历,记录数值第k个值返回
let num = 0
let result = null
const dfs = function(node) {
if (node === null) {
return
}
dfs(node.right)
num++
if (num === k) {
result = node.val
return
}
dfs(node.left)
}
dfs(root)
return result
};
二叉树的深度
输入一棵二叉树的根节点,求该树的深度。从根节点到叶节点依次经过的节点(含根、叶节点)形成树的一条路径,最长路径的长度为树的深度。
例如:
给定二叉树 [3,9,20,null,null,15,7],
3
/ \
9 20
/ \
15 7
返回它的最大深度 3 。
解题思路1
利用从上到下遍历二叉树的思想
var maxDepth = function(root) {
if(!root) return [];
let queue = [root];
let level = 0;
while(queue.length){
let levelNum = queue.length
while(levelNum--){
let node = queue.shift();
node.left && queue.push(node.left);
node.right && queue.push(node.right)
}
level += 1;
}
return level
};
解题思路2
递归
var maxDepth = function(root) {
return root?Math.max(maxDepth(root.left),maxDepth(root.right))+1:0;
};
平衡二叉树
输入一棵二叉树的根节点,判断该树是不是平衡二叉树。如果某二叉树中任意节点的左右子树的深度相差不超过1,那么它就是一棵平衡二叉树。
示例 1:
给定二叉树 [3,9,20,null,null,15,7]
3
/ \
9 20
/ \
15 7
返回 true 。
示例 2:
给定二叉树 [1,2,2,3,3,null,null,4,4]
1
/ \
2 2
/ \
3 3
/ \
4 4
返回 false 。
解题思路1
递归(深度优先搜索)
自底而上
var isBalanced = function(root) {
if(!root) return true;
let left = dfs(root.left);
let right = dfs(root.right);
if(Math.abs(left-right)>1) return false;
return isBalanced(root.left) && isBalanced(root.right)
};
var dfs = function(root){
if(!root) return 0;
return Math.max(dfs(root.left),dfs(root.right)) + 1
}
解题思路2
广度优先搜索
var isBalanced = function(root) {
if(!root) return true
let queue = [ root ]
let nodes = []
while ( queue.length ) {
let node = queue.shift()
nodes.unshift(node)
node.left && queue.push( node.left )
node.right && queue.push( node.right )
}
for(let node of nodes){
let left = node.left ? node.left.val : 0
let right = node.right ? node.right.val : 0
if(Math.abs(left - right) > 1) return false
node.val = Math.max(left, right) + 1 // 当前节点值变为最大深度
}
return true
};
二叉搜索树的最近公共祖先
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
例如,给定如下二叉树: root = [3,5,1,6,2,0,8,null,null,7,4]
示例 1:
输入: root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 8
输出: 6
解释: 节点 2 和节点 8 的最近公共祖先是 6。
示例 2:
输入: root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 4
输出: 2
解释: 节点 2 和节点 4 的最近公共祖先是 2, 因为根据定义最近公共祖先节点可以为节点本身。
解题思路1
可以看出该题核心在于节点本身比其左子树节点大,比其右子树节点小
递归版本
var lowestCommonAncestor = function(root, p, q) {
if ((root.val - p.val) * (root.val - q.val) <= 0) return root
if (p.val < root.val) return lowestCommonAncestor(root.left, p, q)
return lowestCommonAncestor(root.right, p, q)
};
解题思路2
广度优先搜索版本
var lowestCommonAncestor = function (root, p, q) {
if (!root) {
return null
}
if (p.val === q.val) {
return q
}
while (root) {
if (root.val < q.val && root.val < p.val) {
root = root.right
}
if (root.val > q.val && root.val > p.val) {
root = root.left
}
else {
return root
}
}
};
二叉树的公共祖先
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 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。因为根据定义最近公共祖先节点可以为节点本身。
解题思路
这个跟上一题一个不同点是各个节点之间没有规律性
定义左子树left与右子树right
如果满足 left&&right || ((root.val == p || root.val == q) && (left || right))
即该节点左子树包含q或者p且右子树包含p或q 或者该节点为p或q且其下左右任意一个子树包含p或q 说明该节点为最深公共祖先。
判断是否该节点的左右树包含p或q的条件应该是 left || right|| (root.val === p.val || root.val === q.val) 即该节点左子树包含或右子树包含或本身值为p或q
这里体现递归的思想
var lowestCommonAncestor = function(root, p, q) {
let result;
let dfs = (root,p,q) => {
if(!root) return false;
let left = dfs(root.left,p,q);
let right = dfs(root.right,p,q);
if(left&&right ||((root.val == p || root.val == q) && (left || right))){
result = root
}
return left || right || (root.val === p.val || root.val === q.val)
}
dfs(root, p, q);
return result
};