1.概念理解
1.1 树
树 是一种经常用到的数据结构,用来模拟具有树状结构性质的数据集合。
树里的每一个节点有一个值和一个包含所有子节点的列表。 从图的观点来看,树也可视为一个拥有N 个节点和N-1 条边的一个有向无环图。
二叉树是一种更为典型的树状结构。如它名字所描述的那样,二叉树是每个节点最多有两个子树的树结构,通常子树被称作“左子树”和“右子树”。
1.2二叉树
二叉树(Binary tree)是树形结构的一个重要类型。许多实际问题抽象出来的数据结构往往是二叉树形式,即使是一般的树也能简单地转换为二叉树,而且二叉树的存储结构及其算法都较为简单,因此二叉树显得特别重要。二叉树特点是每个结点最多只能有两棵子树,且有左右之分。
二叉树(binary tree)是指树中节点的度不大于2的有序树,它是一种最简单且最重要的树。二叉树的递归定义为:二叉树是一棵空树,或者是一棵由一个根节点和两棵互不相交的,分别称作根的左子树和右子树组成的非空树;左子树和右子树又同样都是二叉树。
1.3 二叉搜索树
二叉查找树(Binary Search Tree),(又:二叉搜索树,二叉排序树)它或者是一棵空树,或者是具有下列性质的二叉树:
若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值; 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值; 它的左、右子树也分别为二叉排序树。
二叉搜索树作为一种经典的数据结构,它既有链表的快速插入与删除操作的特点,又有数组快速查找的优势;所以应用十分广泛,例如在文件系统和数据库系统一般会采用这种数据结构进行高效率的排序与检索操作。
1.4 完美二叉树(Perfect Binary Tree)
一个深度为k(>=-1)且有2^(k+1) - 1个结点的二叉树称为完美二叉树。 (注: 国内的数据结构教材大多翻译为"满二叉树")
2^0 + 2^1 + … 2^K = 2^(k + 1) – 1
(a1-an*q)/(1-q)
a1(1-q^n)/(1-q)
1.5 完全二叉树(Compete Bianry Tree)
完全二叉树从根结点到倒数第二层满足完美二叉树,最后一层可以不完全填充,其叶子结点都靠左对齐。
1.6 树的遍历
遍历是对树的一种最基本的运算,所谓遍历二叉树,就是按一定的规则和顺序走遍二叉树的所有结点,使每一个结点有且只有被访问一次。由于二叉树是非线性结构,因此,树的遍历实质上是将二叉树的各个结点转换成为一个线性序列来表示。
理解和区分树的遍历方法
能够运用递归方法解决树的为前序遍历、中序遍历和后序遍历问题
能用运用迭代方法解决树的为前序遍历、中序遍历和后序遍历问题
能用运用广度优先搜索解决树的层序遍历问题
遍历方法
前序遍历:根,左,右
中序遍历:左,根,右
后序遍历:左,右,根
当你删除树中的节点时,删除过程将按照后序遍历的顺序进行。 也就是说。删除时:按照【左】【右】【根】(节点本身)
层序遍历:层序遍历就是逐层遍历树结构。
1.6.1 深度优先搜索
1.6.2 广度优先搜素
广度优先搜索是一种广泛运用在树或图这类数据结构中,遍历或搜索的算法。
该算法从一个根节点开始,首先访问节点本身。 然后遍历它的相邻节点,其次遍历它的二级邻节点、三级邻节点,以此类推。当我们在树中进行广度优先搜索时,我们访问的节点的顺序是按照层序遍历顺序的。
1.7 相关术语
①结点:包含一个数据元素及若干指向子树分支的信息 [5] 。
②结点的度:一个结点拥有子树的数目称为结点的度 [5] 。
③叶子结点:也称为终端结点,没有子树的结点或者度为零的结点 [5] 。
④分支结点:也称为非终端结点,度不为零的结点称为非终端结点 [5] 。
⑤树的度:树中所有结点的度的最大值 [5] 。
⑥结点的层次:从根结点开始,假设根结点为第1层,根结点的子节点为第2层,依此类推,如果某一个结点位于第L层,则其子节点位于第L+1层 [5] 。
⑦树的深度:也称为树的高度,树中所有结点的层次最大值称为树的深度 [5] 。
⑧有序树:如果树中各棵子树的次序是有先后次序,则称该树为有序树 [5] 。
⑨无序树:如果树中各棵子树的次序没有先后次序,则称该树为无序树 。
⑩森林:由m(m≥0)棵互不相交的树构成一片森林。如果把一棵非空的树的根结点删除,则该树就变成了一片森林,森林中的树由原来根结点的各棵子树构成 。
2.算法
2.1二叉树的遍历方式
2.1.1.二叉树的遍历
function preorderTraversal( root ) {
// write code here
if(!root || !root.val) return []
const res = [] // 定义用来存储存储根节点
preorder(root, res)
return res
}
function preorder(root, res) {
if(!root) return null
// 前序遍历
res.push(root.val)
preorder(root.left, res)
preorder(root.right, res)
// 中序遍历
preorder(root.left, res)
res.push(root.val)
preorder(root.right, res)
// 后序遍历
preorder(root.left, res)
preorder(root.right, res)
res.push(root.val)
}
function threeOrders( root ) {
// write code here
let preArr = [], inArr = [], lastArr = []
preOrder(root, preArr)
inOrder(root, inArr)
lastOrder(root, lastArr)
return [preArr, inArr, lastArr]
}
function preOrder(root, res) {
if(!root) return []
res.push(root.val)
preOrder(root.left, res)
preOrder(root.right, res)
}
function inOrder(root, res){
if(!root) return []
inOrder(root.left, res)
res.push(root.val)
inOrder(root.right, res)
}
function lastOrder(root, res) {
if(!root) return []
lastOrder(root.left,res)
lastOrder(root.right, res)
res.push(root.val)
}
求二叉树的层序遍历(JZ78 把二叉树打印成多行)
function levelOrder( root ) {
// write code here
if(!root) return []
let res = [] // 存放结果的数组
let queue = [root] // 采用队列进行辅助,将二叉树放入队列
while(queue.length) {
let subRes = [] // 每一层的子数组
let len = queue.length
for (let i=0; i<len; i++) {
let temp = queue.shift() // 提取队列的第一个
subRes.push(temp.val) // 依次放入每一层的数据
if(temp.left) queue.push(temp.left) // 当前数有子节点,则push进入队列
if(temp.right) queue.push(temp.right)
}
res.push(subRes) // 将每一层的push给res数组
}
return res
}
### JZ32 从上往下打印二叉树
从上往下打印出二叉树的每个节点,同层节点从左至右打印\
\

- 1.思路

- 2.方法
1.定义队列和返回数组,并将根节点加入到队列中\
2.如果队列不为空,则打印首元素,队列头部(.shift)弹出值加入到result中\
3.判断队列是否有左右子树。
- 3.代码
function PrintFromTopToBottom(root) { // write code here if(!root) return [] let res = [] // 存放结果的 let queue = [root] // 将根节点加入到队列中
while(queue.length) {
var temp = queue.shift()
res.push(temp.val) // 将弹出的值加入到res中
if(temp.left) queue.push(temp.left)
if(temp.right) queue.push(temp.right)
}
return res
}
# 2.二叉树的属性
### JZ27.二叉树的镜像
操作给定的二叉树,将其变换为源二叉树的镜像。
- 1.思路
 交换左右子树,讲左右子树分别求镜像
- 2.方法
1.判断根节点是否为空\
2.用临时变量存储左子树\
3.对左子树进行镜像,对右子树进行镜像\
4.返回原由根节点
- 3.代码
function Mirror( pRoot ) { // write code here if(!pRoot) return null let temp = pRoot.left // 暂存左子树 pRoot.left = Mirror(pRoot.right) // 对左子树进行镜像 pRoot.right = Mirror(temp) // 对右子树进行镜像 return pRoot }
### JZ28 对称的二叉树
- 1.思路
- 2.方法
采用递归方法,先判断根节点的左子树和右子树是否为空,同时为空,则返回true,一个为空,则为false。同时不为空,则判断其值是否相等,然后递归判断左子树和右子树是否对称
- 3.代码
```js
function isSymmetrical(pRoot)
{
// write code here
function isMirror(r1, r2) {
if(!r1 && !r2) return true
if(!r1) return false
if(!r2) return false
return r1.val==r2.val && isMirror(r1.left, r2.right) && isMirror(r1.right, r2.left)
}
return isMirror(pRoot, pRoot)
}
JZ55 二叉树的深度
输入一棵二叉树,求该树的深度。从根结点到叶结点依次经过的结点(含根、叶结点)形成树的一条路径,最长路径的长度为树的深度,根节点的深度视为 1 。
-
1.思路 递归。树的深度=左子树的深度和右子树深度中最大者+1
-
2.代码
function TreeDepth(pRoot)
{
// write code here
if (pRoot === null) return 0
var left = TreeDepth(pRoot.left)
var right = TreeDepth(pRoot.right)
return Math.max(left, right)+1 // 最大深度
return (Math.min(left, right) || Math.max(left, right))+1 // 最小深度
}
NC84 完全二叉树结点数
NC62平衡二叉树
输入一棵节点数为 n 二叉树,判断该二叉树是否是平衡二叉树。
在这里,我们只需要考虑其平衡性,不需要考虑其是不是排序二叉树
平衡二叉树(Balanced Binary Tree),具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
-
1.思路 遍历左子树和右子树的深度,然后比较二者差值(深度优先遍历)
-
2.方法
-
3.代码
function IsBalanced_Solution(pRoot)
{
// write code here
if(!pRoot) return true
function dfs(root) {
if (!root) return 0
return Math.max(dfs(root.left), dfs(root.right)) + 1
}
let left = dfs(pRoot.left)
let right = dfs(pRoot.right)
return Math.abs(left-right)<=1 && IsBalanced_Solution(pRoot.left) && IsBalanced_Solution(pRoot.right)
}
NC5 二叉树根节点到叶子节点的所有路径和
给定一个二叉树的根节点root,该树的节点值都在数字\ 0-9 0−9 之间,每一条从根节点到叶子节点的路径都可以用一个数字表示。
1.该题路径定义为从树的根结点开始往下一直到叶子结点所经过的结点
2.叶子节点是指没有子节点的节点
3.路径只能从父节点到子节点,不能从子节点到父节点
4.总节点数目为n
- 1.思路
function sumNumbers( root ) {
// write code here
if(!root) return null
let sum = 0
return preOrder(root, sum)
}
function preOrder (root, sum) {
if(!root) {
return 0
}
sum = sum * 10 + root.val
if(!root.left && !root.right) {
return sum
}
return preOrder(root.left, sum) + preOrder(root.right, sum)
}
JZ8 二叉树的下一个结点
给定一个二叉树其中的一个结点,请找出中序遍历顺序的下一个结点并且返回。注意,树中的结点不仅包含左右子结点,同时包含指向父结点的next指针。下图为一棵有9个节点的二叉树。树中从父节点指向子节点的指针用实线表示,从子节点指向父节点的用虚线表示
输入8,输出9, 10, 11
- 1.思路
二叉树为空,则返回空;
这个问题我们可以分为三种情况来讨论。
第一种情况,当前节点含有右子树,这种情况下,中序遍历的下一个节点为该节点右子树的最左子节点。因此我们只要从右子节点出发,一直沿着左子节点的指针,就能找到下一个节点。
第二种情况是,1.当前节点不含有右子树,并且当前节点为父节点的左子节点,这种情况下中序遍历的下一个节点为当前节点的父节点。
2.当前节点不含有右子树,并且当前节点为父节点的右子节点,这种情况下我们沿着父节点一直向上查找,直到找到一个节点,该节点为父节点的左子节点。这个左子节点的父节点就是中序遍历的下一个节点。
- 2.方法
- 3.代码
function GetNext(pNode)
{
// write code here
if(!pNode) return null
// 1.节点含有右子树。中序的下一个节点为该节点右子树的最左子节点。
if(pNode.right) {
var p = pNode.right
while(p.left) {
p = p.left
}
return p
}
while(pNode.next) {
if(pNode == pNode.next.left){ //2.无右边,且当前节点为左子节点。则下一节点为父节点。
return pNode.next
}
pNode = pNode.next // 3.无右子树,且为右节点,向上查找。
}
}
3.二叉树的修改与构造
1.NC117 合并二叉树
已知两颗二叉树,将它们合并成一颗二叉树。合并规则是:都存在的结点,就将结点值加起来,否则空的位置就由另一个树的结点来代替。
- 1.思路 采用递归来解决此问题。
t1 t2 都不空,则创建新节点,值为两者之和,他的左右子结点分别递归调用,若一者为空,则直接将此节点合并到新数上面。
- 3.代码
function mergeTrees( t1 , t2 ) {
// write code here
var t
// 1。两者都存在节点,则将结点值加起来
if(t1 && t2) {
t = new TreeNode(t1.val + t2.val)
t.left = mergeTrees(t1.left, t2.left)
t.right = mergeTrees(t1.right, t2.right)
} else if (t1 && !t2) {
// 2.t2为空, t的节点直接由不空的t1来替代
t = t1
} else if(!t1 && t2) {
t = t2
} else {
return null
}
return t
}
2.序列化二叉树
序列化,将节点值存入数组中,空节点则使用特殊标记存入数组中。
反序列化,从数组中获取元素,为number类型则生成节点,为特殊标记,则为空节点
- 1.思路
- 3.代码
function TreeNode(x) {
this.val = x;
this.left = null;
this.right = null;
}
var arr = []
function Serialize(pRoot)
{
// write code here
if(!pRoot) {
arr.push('#')
return
}
arr.push(pRoot.val)
Serialize(pRoot.left)
Serialize(pRoot.right)
}
// 从数组中获取元素,为number类型则生成节点,为特殊标记,则为空节点
function Deserialize(s)
{
// write code here
if(!arr) return null
if(arr.length <1) return null
var root = null
var temp = arr.shift()
if(typeof temp == 'number') {
root = new TreeNode(temp) // 生成根节点
root.left = Deserialize(arr) // 递归处理左子树
root.right = Deserialize(arr) // 递归处理右子树
}
return root
}
4.求二叉搜索树的属性
NC6.判断一二叉树是否为搜索二叉树和完全二叉树
给定一棵二叉树,已知其中的节点没有重复值,请判断该二叉树是否为搜索二叉树和完全二叉树。
输出描述:分别输出是否为搜索二叉树、完全二叉树。
- 1.思路
搜索二叉树:
使用递归方式遍历
如果该节点有左子树,则左子树上的所以值应当小于当前节点的值
如果该节点有右子树,则由子树上的所以值应当大于当前节点的值
也就是说中序遍历的结果应当是递增的
完全二叉树:\
层次遍历情况下
如果遇到了最后一个节点将 flag 为 true
- 左右孩子不全
- 只有左孩子没有右孩子
如果在之前已经遇到最后一个节点的情况下
当前节点不是叶子结点,那么不是完全二叉树
如果当前节点只有右孩子没有左孩子则不是完全二叉树
- 3.代码
function judgeIt( root ) {
// write code here
var ansSearch = true
var ansAll = true
var flag = false // 标记是否不全的节点
if(!root || (!root.left && !root.right)) return [ansSearch, ansAll]
var pre = null
function dfs(node) {
if(!node) return
dfs(node.left)
if(!pre) {
pre = node.val
} else {
if(node.val < pre) {
ansSearch = false
}
pre = node.val
}
dfs(node.right)
}
dfs(root)
// 完全二叉树
var queue = [root]
while(queue.length) {
var temp = queue.pop() // 提取队列的第一个
//判断该节点是否符合完全二叉树
if(flag && !(!temp.left && !temp.right)) {
ansAll = false
}
// 只有右节点,没有左节点,不是
if(!temp.left && temp.right) ansAll = false
// 都不为空,或只有左,没有右,则true
if((!temp.left && !temp.right) || (temp.left && !temp.right)) {
flag = true
}
if(temp.left) queue.unshift(temp.left)
if(temp.right) queue.unshift(temp.right)
}
return [ansSearch, ansAll]
}
JZ77 按之字形顺序打印二叉树
给定一个二叉树,返回该二叉树的之字形层序遍历,(第一层从左向右,下一层从右向左,一直这样交替)
- 3.代码
function Print(pRoot)
{
// write code here
if(pRoot == null) return [];
let res = [] // 保存结果的空数组
function traversal(root, depth) {// 2.递归函数,参数:节点,深度
if(root !== null) { // 如果当层还没开始打印,则创建当前层的空数组
if(!res[depth]) {
res[depth] = []
}
if(depth & 1) {// 偶数层从右unshift
res[depth].unshift(root.val)// 从0开始放入
}else {
res[depth].push(root.val)
}
// 递归左右子树
traversal(root.left, depth + 1);
traversal(root.right, depth + 1);
}
}
traversal(pRoot, 0)
return res;
}
JZ54 二叉搜索树的第k个节点
给定一棵结点数为n 二叉搜索树,请找出其中的第 k 小的TreeNode结点值。
每个父节点的键值要大于左边子节点小于右边子节点
- 1.思路 二叉搜索树:右>根>左,用中序遍历能够获得从小到大排序
JZ7重建二叉树
二叉树:
前序遍历:根左右
中序遍历:左根右
后序遍历:左右根
给定某二叉树的前序遍历和中序遍历,请重建出该二叉树并返回它的头结点。例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建出如下图所示。
- 1.思路:
1.找到根节点,preNode[0] = 1;
前序遍历:1 【2 4 7】【3 5 6 8】 //(根、【左子树】、【右子树】)
中序遍历:【4 7 2】 1 【5 3 8 6】// (【左子树】、根、【右子树】)
2.继续进行递归构建。
前序遍历:2 【4】 【7】//(根、【左子树】、【右子树】)
中序遍历 【4】 2 【7】// (【左子树】、根、【右子树】)
- 2.方法
1.创建一个根节点 rootVal = pre[0] //前序的第一个节点即为当前节点
2.找到根节点在中序遍历的位置
3.对树进行划分
- 3.代码
function reConstructBinaryTree(pre, vin) // 前序和中序
{
// write code here
if (!pre.length || !vin.length) return null
let root = new TreeNode(pre[0]) // 前序的第一个为根节点
let i = vin.indexOf(pre[0]) // 根节点在中序的下标
root.left = reConstructBinaryTree(pre.slice(1, i + 1), vin.slice(0, i)) // 前序的左,中序的左
root.right = reConstructBinaryTree(pre.slice(i + 1), vin.slice(i + 1)) // 前序的右边,中序的右
return root
}
module.exports = {
reConstructBinaryTree : reConstructBinaryTree
};
5.二叉树的公共祖先问题
1.二叉树的最近公共祖先
- 1.思路 从根节点往下递归:
-
若该节点是第一个值为o1或o2的节点,则该节点是最近公共祖先;
-
否则,看左子树是否包含o1或o2:
2.1 若左子树包含o1或o2,则看右子树有没有:
a. 若右子树没有,则公共祖先在左子树
b. 若右子树有,则o1和o2肯定是左右子树一边一个,则公共祖先是根节点;
2.2 若左子树不包含o1和o2,则公共祖先在右子树或者该根子树不包含o1和o2。(两种情况都取决于右子树)
- 代码
function lowestCommonAncestor( root , o1 , o2 ) {
// write code here
if(!root) return null
// 1.若该节点是第一个值为o1或o2的节点,则该节点是最近公共祖先
if(root.val == o1 || root.val == o2) return root.val
// 2.看左子树是否包含o1,o2
const left = lowestCommonAncestor(root.left, o1, o2)
const right = lowestCommonAncestor(root.right, o1, o2)
if(!left) return right
if(!right )return left
return root.val
}
2.JZ68 二叉搜索树的最近公共祖先
- 1.思路 1.根据二叉搜索树的性质,递归遍历二叉搜索树
2.比较根节点的值和p、q的关系
3.根节点值都大于两个数,则遍历左子树
4.根节点值都小于两个数,则遍历右子树
5.否则,根节点一定在中间
JZ26 树的子结构
输入两棵二叉树A,B,判断B是不是A的子结构。(ps:我们约定空树不是任意一个树的子结构)\
- 1.思路
1.【是否含有子结构】
(1).根节点重合:
(2).【子树含有子结构】。不考虑根节点,判断A的左子树/右子树是否含有B的子结构。 2.【根结点对应,树是否一样】
(0).判断值前先判断是否有空 (1).根节点的值是否相同
(2).左右子树是否可以对应
- 2.方法
1.判断【结构】,判断A、B根节点的【值】,A子树是否是否含有B【结构】。
2.判断【值】,值相同,继续判断左右子树的【值】
- 3.代码
function HasSubtree(pRoot1, pRoot2)
{
// write code here
if(!pRoot1 || !pRoot2) return false
return isSubTree(pRoot1, pRoot2) ||
HasSubtree(pRoot1.left, pRoot2) || // A的左子树是否含有B
HasSubtree(pRoot1.right, pRoot2) // A的右子树是否含有B
}
function isSubTree(root1, root2) {
if(!root2) return true
if(!root1) return false
if(root1.val == root2.val) { // 2.判断根结点的值是否相同
return isSubTree(root1.left, root2.left) && isSubTree(root1.right, root2.right) // 判断左右子树是否对应
} else {
return false
}
}
JZ33.二叉搜索树的后序遍历序列
- 1.思路
二叉搜索树:
若它的左子树不空,则左子树上所有结点的值【均小于】它的根节点的值; 若它的右子树不空,则右子树上所有结点的值【均大于】它的根结点的值; 它的左、右子树也分别为【二叉搜索树】。
后序遍历:左、右、根
输入:1 3 2 6 5,
【1 3 2】【6】【5】(左、右、根)
若最后P=j,true;
- 2.方法
1.递归结束条件,start>= end,定义移动的index 2.确保[start, right -1]所有的数都比根节点小的,就是左子树
3.检查[right, end]的值是否都比根节点大 4.index == end,说明是二叉搜索树。
- 3.代码
function VerifySquenceOfBST(sequence)
{
// write code here
function judge(arr, start, end){
// 递归结束的条件:当前数组的元素最多只有一个
if(start >= end){
return true
}
// 确保[start, right -1]所有的数都比根节点的值小的,就是左子树
var index = start;
while(arr[index] < arr[end]){
index++;
}
// 检查剩余元素是否都比根节点的值大
var right = index;
while(arr[index] > arr[end]){
index++;
}
return index==end && judge(arr, start,right-1)&& judge(arr,right, end-1);
}
if(sequence.length == 0){
return false
}
return judge(sequence, 0, sequence.length -1)
}
module.exports = {
VerifySquenceOfBST : VerifySquenceOfBST
};
JZ82 二叉树中和为某一值的路径(一)
给定一个二叉树root和一个值 sum ,判断是否有从根节点到叶子节点的节点值之和等于 sum 的路径。\
1.该题路径定义为从树的根结点开始往下一直到叶子结点所经过的结点
2.叶子节点是指没有子节点的节点
3.路径只能从父节点到子节点,不能从子节点到父节点
4.总节点数目为n
- 1.思路
- 2.方法
- 3.代码
26.二叉搜索树与双向链表
输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。如下图所示
- 1.思路
需要排序,所以用中序遍历:左根右。遍历到根节点时,左子树已经成为双向链表。 2.根节点加入到末尾,根的pre指向6,6的next指向10 3.指向右边根节点。
- 递归思想:把大问题转换为若干小问题;
- 由于JavaScript中并没有链表或者Tree这样的原生数据结构,都是通过对象模拟的,因此最终要返回的是指向双向链表首结点的指针;
- 将左子树构成双向链表,返回的是左子树的尾结点,将其连接到root的左边;
- 将右子树构成双向链表,将其追加到root结点之后,并返回尾结点;
- 向左遍历返回的链表至头结点处,即为所求双向链表的首结点
-
2.方法
-
3.代码
function Convert(pRootOfTree) {
if (!pRootOfTree) {
return null;
}
var head = null;
var tail = null; //用来标记转换好的双向链表头部和尾部
tail = ConvertNode(pRootOfTree, tail);
// head.left = tail;
head = tail;
while (head && head.left) {
head = head.left
}
return head;
}
function ConvertNode(cur, tail) {
if (!cur) {
return;
}
if (cur.left) {
tail = ConvertNode(cur.left, tail);
}
cur.left = tail;
if (tail) {
tail.right = cur;
}
tail = cur;
if (cur.right) {
tail = ConvertNode(cur.right, tail);
}
return tail;
}
module.exports = {
Convert : Convert
};