0. 前言
小编最近的leetcode刷题战绩,前两百道里的所有二叉树相关的题目,基本上全部做完,简单题+中等题,一共28道。在这篇文章中,小编就以这些题目为例子,系统的整理一下树、二叉树和二叉搜索树相关的内容。主要的知识点有三个:
- 树、二叉树、二叉搜索树的概念
- 树的两种遍历方式:深度优先遍历(DFS)、广度优先遍历(BFS)
- 二叉树的:前序遍历、中序遍历和后序遍历
在文章的最后,小编整理几道相关的题目。连续做了将近30道题之后,发现二叉树所有的操作都是基于三种遍历顺序和两种遍历方式的。因此,只要把二叉树的遍历方式和遍历顺序搞明白,可以解决百分之八十的二叉树相关题目。
1. 树、二叉树和二叉搜索树
- 树:树是一种数据结构,对于每一棵树而言都有一个根节点和子节点。除了树的根节点之外,每个节点都有一个父节点和多个子节点;树的根节点没有父节点,但是有子节点。树的结构示例如图所示:
- 二叉树:二叉树首先是个树,然后它在树的基础上添加了一个条件 —— 每个节点最多有两个子节点,分别称为左节点和右节点。二叉树结构示例如图所示:
二叉树节点的构造函数代码如下:
function TreeNode(val, left, right) {
this.val = val === undefined ? 0 : val;
this.left = left === undefined ? null : left;
this.right = right === undefined ? null : right;
}
- 二叉搜索树:二叉搜索树是用于特定功能的二叉树,其构建规则如下:
- 节点的左子树只包含小于当前节点的数
- 节点的右子树只包含大于当前结点的数
- 所有的左子树和右子树自身也必须是二叉搜索树
2. DFS 和 BFS
对于二叉树的遍历,有两种方式深度优先遍历(DFS)和广度优先遍历(BFS).废话不多说,上代码:
- DFS:
function dfs(root) {
if(!root) {
return;
}
console.log(root.val);
dfs(root.left);
dfs(root.right);
}
- BFS:
function bfs(root) {
if(!root) {
return;
}
const q = [];
q.push(root);
while(q.length) {
const node = q.shift();
cosole.log(node.val);
if(node.left) q.push(node.left);
if(node.right) q.push(node.right);
}
}
3. 遍历顺序
对于二叉树而言,根据DFS中root值打印的顺序,分为前序遍历、中序遍历和后序遍历三种类型。
3.1 前序遍历(preorder)
function dfs(root) {
if(!root) {
return;
}
console.log(root.val);
dfs(root.left);
dfs(root.right);
}
3.2 中序遍历(inorder)
function dfs(root) {
if(!root) {
return;
}
dfs(root.left);
console.log(root.val);
dfs(root.right);
}
3.3 后序遍历(postorder)
function dfs(root) {
if(!root) {
return;
}
dfs(root.left);
dfs(root.right);
console.log(root.val);
}
4. 相关题目
二叉树相关的题目可以分成两个类型:
- 二叉树的构建
- 二叉树的遍历
4.1 二叉树的构建
对于二叉树的构建而言,一般的思路都是确定好左子树和右子树的构建范围,采用递归的方式构建二叉树。
- 95:不同的二叉搜索树II
从题目中可以看出,这道题是构建二叉树的题目。这类型题目的关键是确定好左子树和右子树的构建范围。对于本题而言,确定构建范围的关键字是二叉搜索树。根据二叉搜索树的构建规则:节点的左子树都是比该节点的值小的数,右子树都是比该节点的值大的数。因此,我们的构建思路是:选定一个数值,比它小的是左子树范围,比它大的是右子树范围,递归构建即可。具体的实现代码如下:
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {number} n
* @return {TreeNode[]}
*/
var generateTrees = function(n) {
function build(start, end) {
let res = [];
if(start > end) {
res.push(null);
return res;
}
for(let i = start; i <= end; i++) {
let leftTree = build(start, i - 1);
let rightTree = build(i + 1, end);
for(let l of leftTree) {
for(let r of rightTree) {
let curTree = new TreeNode(i);
curTree.left = l;
curTree.right = r;
res.push(curTree);
}
}
}
return res;
}
if(n === 0) return [];
return build(1, n);
};
- 105:从前序和中序遍历序列构造二叉树
这道题中,题目给的信息是前序遍历和中序遍历的结果,想要达到的目的是构建二叉树。我们知道构建二叉树需要知道左子树和右子树的范围。这个范围只能从题目中给的前序遍历和中序遍历中寻找。我们知道,前序遍历的结果是:[根节点,左子树,右子树],中序遍历的结果是[左子树,根节点,右子树]. 因此,前序遍历的第一个数就是树的根节点,然后我们在中序遍历中找到对应的值,以该值为分界点就可以得到左子树和右子树的范围。最后,进行递归构建即可。具体代码如下:
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {number[]} preorder
* @param {number[]} inorder
* @return {TreeNode}
*/
var buildTree = function(preorder, inorder) {
const build = (preorder, inorder) => {
if(!preorder.length) {
return null;
}
const nodeVal = preorder[0];
const node = new TreeNode(nodeVal);
let valIndex = -1;
for(let i = 0; i < inorder.length; i++) {
if(inorder[i] === nodeVal) {
valIndex = i;
break;
}
}
const leftTreeInorder = inorder.slice(0, valIndex);
const rightTreeInorder = inorder.slice(valIndex + 1, inorder.length);
const leftTreePreorder = preorder.slice(1, valIndex + 1);
const rightTreePreorder = preorder.slice(valIndex + 1, preorder.length);
node.left = build(leftTreePreorder, leftTreeInorder);
node.right = build(rightTreePreorder, rightTreeInorder);
return node;
}
return build(preorder, inorder);
};
- 106:从中序和后序遍历序列构造二叉树
这道题和前一道题的思路是一样的,只不过树的根节点放在了后序遍历的最后。代码如下:
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {number[]} inorder
* @param {number[]} postorder
* @return {TreeNode}
*/
var buildTree = function(inorder, postorder) {
const build = (inorder, postorder) => {
if(inorder.length === 0) {
return null;
}
const nodeVal = postorder[postorder.length - 1];
const node = new TreeNode(nodeVal);
let valIndex = -1;
for(let i = 0; i < inorder.length; i++) {
if(inorder[i] === nodeVal) {
valIndex = i;
}
}
const leftTreeInorder = inorder.slice(0, valIndex);
const rightTreeInorder = inorder.slice(valIndex + 1, inorder.length);
const leftTreePostorder = postorder.slice(0, valIndex);
const rightTreePostorder = postorder.slice(valIndex, postorder.length - 1);
node.left = build(leftTreeInorder, leftTreePostorder);
node.right = build(rightTreeInorder, rightTreePostorder);
return node;
}
return build(inorder, postorder);
};
- 108:将有序数组转换为二叉搜索树
这道题的输入本身就是一个有序数组,而且要求生成的二叉树左右子树高度差不超过1.因此,我们在每次选择根节点的时候选择数组最中间的那个元素即可。具体实现代码如下:
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {number[]} nums
* @return {TreeNode}
*/
var sortedArrayToBST = function(nums) {
const build = (nums) => {
const n = nums.length;
if(n === 0) {
return null;
}
if(n === 1) {
return new TreeNode(nums[0]);
}
if(n === 2) {
const node = new TreeNode(nums[1]);
node.left = new TreeNode(nums[0]);
return node;
}
const mid = Math.floor(n / 2);
const node = new TreeNode(nums[mid]);
node.left = build(nums.slice(0, mid));
node.right = build(nums.slice(mid + 1, n));
return node;
}
return build(nums);
};
4.2 二叉树的遍历
二叉树的遍历逃脱不了上面的内容提到的深度优先遍历,广度优先遍历,以及三种遍历顺序。
- 98:验证二叉搜索树
这道题是二叉搜索树相关的,我们采用DFS对树进行遍历,全部满足条件时返回true,否则返回false
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @return {boolean}
*/
var isValidBST = function(root) {
function check(root, lower, higher) {
if(!root) return true;
const left = root.left;
const right = root.right;
if(lower < root.val && root.val < higher) {
const leftRes = check(left, lower, root.val);
const rightRes = check(right, root.val, higher);
return leftRes && rightRes;
}
return false;
}
return check(root, -Infinity, Infinity);
};
- 99:恢复二叉搜索树
这道题的解决方案需要两个步骤:
1. 找到错误的节点
2. 交换两个节点
其中,寻找错位的节点是解决这道题的关键。我们知道二叉搜索树的定义是左子树 < 根节点 < 右子树。而中序遍历的结果是[左子树,根节点,右子树]。因此,如果我们采用中序遍历,遍历的结果一定是从小到大排列。这样,寻找错位结点的思路就有了,采用中序遍历,将不满足从小到大排列的节点记录下来,最后交换两个节点的值即可。具体代码如下:
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @return {void} Do not return anything, modify root in-place instead.
*/
var recoverTree = function(root) {
const nums = [];
const inorder = (root) => {
if(root === null) {
return;
}
inorder(root.left);
nums.push(root.val);
inorder(root.right);
}
const swap = (nums) => {
const n = nums.length;
let index1 = -1, index2 = -1;
for(let i = 0; i < n; i++) {
if(nums[i] > nums[i + 1]) {
index2 = i + 1;
if(index1 === -1) {
index1 = i;
} else {
break;
}
}
}
return [nums[index1], nums[index2]];
}
const recover = (r, x, y, count) => {
if(r !== null) {
if(r.val === x || r.val === y) {
r.val = r.val === x ? y : x;
if(--count === 0) {
return;
}
}
recover(r.left, x, y, count);
recover(r.right, x, y, count);
}
}
inorder(root);
let [x, y] = swap(nums);
recover(root, x, y, 2);
};
- 100:相同的树
这道题是个简单题,思路也很简单。将两棵树的节点挨个比较即可。由于没什么难度,这里小编就不赘述了,直接上代码。
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} p
* @param {TreeNode} q
* @return {boolean}
*/
var isSameTree = function(p, q) {
if(p === null && q === null) {
return true;
} else if(!p && q) {
return false;
} else if(p && !q) {
return false;
}
if(!isSameTree(p.left, q.left)) {
return false;
}
if(p.val !== q.val) {
return false;
}
return isSameTree(p.right,q.right);
};
- 101:对称二叉树
这道题的关键是搞清楚镜像对称这个概念在树这种数据结构中的体现即可。对于二叉树而言,镜像对称意味着左子树和右子树的根节点相同,并且左子树的左子树等于右子树的右子树,左子树的右子树等于右子树的左子树。 在这道题中,我们采用两种遍历方式来判断。
- DFS:
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @return {boolean}
*/
var isSymmetric = function(root) {
if(root === null) {
return true;
}
const check = (p,q) => {
if(!p && !q) return true;
if(!p || !q) return false;
if(p.val !== q.val) return false;
return check(p.left, q.right) && check(p.right, q.left);
}
return check(root.left, root.right);
};
- BFS:
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @return {boolean}
*/
var isSymmetric = function(root) {
const check = (u, v) => {
const q = [];
q.push(u);
q.push(v);
while(q.length) {
u = q.shift();
v = q.shift();
if(!u && !v) {
continue;
}
if((!u || !v) || (u.val !== v.val)) {
return false;
}
q.push(u.left);
q.push(v.right);
q.push(u.right);
q.push(v.left);
}
return true;
}
return check(root, root);
};
- 102:二叉树的层序遍历
这道题最简单的方法就是采用BFS进行遍历。只不过,在遍历的开始需要记录一下队列当前的个数,即这一层有多少个节点。代码如下:
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @return {number[][]}
*/
var levelOrder = function(root) {
const ans = [];
if(!root) {
return ans;
}
const q = [];
q.push(root);
while(q.length) {
ans.push([]);
const curLevelSize = q.length;
for(let i = 0; i < curLevelSize; i++) {
const node = q.shift();
ans[ans.length - 1].push(node.val);
if(node.left) q.push(node.left);
if(node.right) q.push(node.right);
}
}
return ans;
};
它还可以采用DFS来做,只需要在每次递归的时候带上层数信息即可。代码如下:
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @return {number[][]}
*/
var levelOrder = function(root) {
const ans = [];
const dfs = (node, level) => {
if(!node) {
return;
}
if(ans.length === level) {
ans.push([]);
}
ans[level].push(node.val);
dfs(node.left, level + 1);
dfs(node.right, level + 1);
}
dfs(root, 0);
return ans;
};
- 103:二叉树的锯齿形层序遍历
这道题和上一道题的区别就是每一层的遍历顺序不同。我们只需要在上一个题目的基础上对每一层的遍历顺序进行一个标记即可,具体代码实现如下:
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @return {number[][]}
*/
var zigzagLevelOrder = function(root) {
const ans = [];
if(!root) {
return ans;
}
const q = [];
let leftOrder = true;
q.push(root);
while(q.length) {
let level = [];
const curLen = q.length;
for(let i = 0; i < curLen; i++){
const node = q.shift();
if(leftOrder) {
level.push(node.val);
} else {
level.unshift(node.val);
}
if(node.left) {
q.push(node.left);
}
if(node.right) {
q.push(node.right);
}
}
ans.push(level);
leftOrder = !leftOrder;
}
return ans;
};
- 113:路径总和II
这种找路径的题目最好的办法就是回溯,然后它又是一个树的遍历。因此,这道题的解决方案就是回溯模板 + DFS。具体代码如下:
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @param {number} targetSum
* @return {number[][]}
*/
var pathSum = function(root, targetSum) {
let ans = [];
let path = [];
const dfs = (root, targetSum) => {
if(!root) {
return;
}
path.push(root.val);
targetSum -= root.val;
if(!root.left && !root.right && targetSum === 0) {
ans.push(path.slice());
}
dfs(root.left, targetSum);
dfs(root.right, targetSum);
path.pop();
}
dfs(root, targetSum);
return ans;
};
- 114:二叉树展开为链表
从这道题的例题中可以看出来,二叉树展开为链表的规则是前序遍历的结果。因此,我们首先需要保存前序遍历的结果,然后对二叉树的左右节点进行重新赋值。具体的代码实现如下:
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @return {void} Do not return anything, modify root in-place instead.
*/
var flatten = function(root) {
if(!root) return [];
const ans = [];
const preorder = (root) => {
if(root === null) {
return;
}
ans.push(root);
preorder(root.left);
preorder(root.right);
}
preorder(root);
for(let i = 1; i < ans.length; i++) {
let prev = ans[i - 1], cur = ans[i];
prev.left = null;
prev.right = cur;
}
};
- 116:填充每个节点的下一个右侧节点指针
这道题是在二叉树的层序遍历的基础上实现的。其解题思路为对二叉树进行层序遍历,然后依次添加下一个指针即可。具体代码实现如下:
/**
* // Definition for a Node.
* function Node(val, left, right, next) {
* this.val = val === undefined ? null : val;
* this.left = left === undefined ? null : left;
* this.right = right === undefined ? null : right;
* this.next = next === undefined ? null : next;
* };
*/
/**
* @param {Node} root
* @return {Node}
*/
var connect = function(root) {
if(!root) {
return root;
}
const q = [];
q.push(root);
while(q.length) {
const levelLen = q.length;
for(let i = 0; i < levelLen; i++) {
const node = q.shift();
if(i < levelLen - 1) {
node.next = q[0];
}
if(node.left) q.push(node.left);
if(node.right) q.push(node.right);
}
}
return root;
};
- 129:求根节点到叶节点数字之和
这道题目的要求是计算从根节点道叶节点生成的所有数字之和。 这个要求分为两部分:首先要得到从根节点道叶节点组成的所有数字,然后在计算它们的和。从根节点到叶节点,最好的方式就是进行深度优先遍历(DFS),将计算得到的每条路经的数字都记录下来,然后再去计算他们的和。具体代码实现如下:
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @return {number}
*/
var sumNumbers = function(root) {
const nums = [];
const dfs = (root, num) => {
if(!root) {
return;
}
num = num * 10 + root.val;
if(!root.left && !root.right) {
nums.push(num);
}
dfs(root.left, num);
dfs(root.right, num);
}
let ans = 0;
dfs(root, 0);
nums.forEach((num, index) => ans += num);
return ans;
};
- 199:二叉树的右视图
这道题同样可以利用树的遍历来解决。最开始我才用的是深度优先遍历,每一次都寻找节点的右子树。但是这样做会忽略掉右子树的深度比左子树深度小的情况。在这道题中,采用广度优先遍历,先把每一层的节点记录下来,然后选取最右边的那个节点,即当前层队列的最后一个节点。具体代码如下:
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @return {number[]}
*/
var rightSideView = function(root) {
const ans = [];
if(!root) {
return ans;
}
const q = [];
q.push(root);
while(q.length) {
const levelLen = q.length;
ans.push(q[levelLen - 1].val);
for(let i = 0; i < levelLen; i++) {
const node = q.shift();
if(node.left) q.push(node.left);
if(node.right) q.push(node.right);
}
}
return ans;
};