本篇介绍的是,前端经常遇到的数据类型树类结构,以前端的视角来学习树类算法,切勿生搬硬套
1 基本概念
【树的基本概念图】
2. 二叉树
如果用数据对象表示就是这样:
const binaryTtree = {
left:{
left:null,
right:null
};
right:{
left:null,
right:null
};
}
上面就是最简单明了的二叉树表示方式:
这里强调几个点:
- 度为二的树和二叉树不是一回事,度为二的树至少3个节点,二叉树可以为空
- 如果二叉树的度为一,那么必有左子树而无右子树
- 一个子节点如果有双亲,则其双亲编号为
Math.floor(i/2)
(编号以根节点为1为基准),若有左孩子则为2i
,有右孩子则为2i+1
2.1 二叉树的存储结构
【二叉树】
- 使用数组存储 二叉树的数据对象,使用数组存储在规范的教材上都称它为顺序存储。
如果节点编号是以i=1
为基准。那么就将该标号的树节点存入数组下标为i-1
的空间中。没有存入树节点的下标就记为0
或者null
,怎么理解呢?上图的二叉树使数组存储就是这样,[{data:0},{data:1},{data:2},null,null,{data:5},{data:6}]
- 使用对象链式存储 链式存储就是前面说的二叉树的数据对象表示方式
2.2 二叉树的遍历
【二叉树遍历】
下面是基于上图用对象链表示的二叉树,其中
left
是指其左孩子,right
指其右孩子,data
为存储的数据
const binaryTree = {
data:'A',
right:{
left:{
left:null,
right:null,
data:'C'
}
},
left:{
data:'B',
left:{
data:'D',
left:{
data:'F',
left:null,
right:null
},
right:{
data:'G',
left:{
data:'H',
right:null,
left:null
},
left:{
data:'I',
left:null,
right:null
}
}
},
right:{
data:'E',
left:null,
right:null
}
}
}
在遍历树当中,非常适合使用递归算法
- 前序遍历
意思就是先访问根节点,然后遍历左子树,再遍历右子树;上面的二叉树,经前序遍历的结果就是
A->B->D->F>G->H->I->E->C
const PreOrder = (bitree)=>{
const recored = [];
const Order = (bitree)=>{
if(bitree !== null){
recored.push(bitree.data)
Order(bitree.left);
Order(bitree.right);
}
}
Order(bitree)
console.log(recored)
}
PreOrder(binaryTree)
用栈将递归改为循环也是一个很重要的知识点,因此前序遍历的循环如下
const PreOrder = function(root) {
const queue = [];
const record = [];
let p = root;
const time = setInterval(()=>{
if(!p && queue.length === 0){
clearInterval(time)
}
if(p){
record.push(p.val)
queue.push(p)
p = p.left
}else{
const treeNode = queue.pop();
p = treeNode?treeNode.right:null
}
},1000)
};
PreOrder(binaryTtrees)
上面是用了js的定时器来代替了while语句,方便阅读的同学自己试的时候,看打印结果,增强印象
- 中序遍历
先遍历左子树,在访问根节点,在遍历右子树;其遍历结果就是
F->D->H->G->I->B->E->A->C
const inOrder = (bitree) =>{
const recored = [];
const Order = (bitree)=>{
if(bitree !== null){
Order(bitree.left);
recored.push(bitree.data);
Order(bitree.right)
}
}
Order(bitree)
console.log(recored)
}
inOrder(binaryTree)
- 后序遍历
先遍历左子树,后遍历右子树,最后访问根节点;其遍历结果就是
F->H->I->G->D->E->B->C->A
const PostOrder = (bitree)=>{
const recored = [];
const Order = (bitree)=>{
if(bitree !== null){
Order(bitree.left);
Order(bitree.right);
recored.push(bitree.data);
}
}
Order(bitree)
console.log(recored)
}
PostOrder(binaryTree)
- 层次遍历 要进行层次遍历,需要借助于队列,先将二叉树的根节点入队,然后出队访问该节点。如果它有左子树,则将左子树根入队列,如果它有右子树,则将他的右子树根节点入队列。然后出队,对出队节点访问一直到队列为空;
上图,其层次遍历的结果为:A->B->C->D->E->F->G->H->I
const LevelOrder = (bitree)=>{
const recored = [];
const queueRecored = [];
// 根节点数据入队
queueRecored.unshift(bitree);
while(queueRecored.length !== 0){
const treeNode = queueRecored.shift();
recored.push(treeNode.data);
if(treeNode.left!==null){
queueRecored.push(treeNode.left)
}
if(treeNode.right!==null){
queueRecored.push(treeNode.right)
}
}
console.log(recored)
}
LevelOrder(binaryTree)
2.3 二叉树的构建
由遍历构建二叉树,因为篇幅有限,这里就简单说明一下
- 前序序列 + 中序序列 可以确定一个唯一的二叉树
- 后序序列 + 中序序列
- 层次序列 + 中序序列
3. 树
3.1 树的存储方式
- 双亲表示法
一般用的不多,而且是采用数组存储,每个节点设置一个伪指针,指示双亲节点的位置,如下:
const Tree = [
{
data:'A',
index:-1 // 表示根节点
},
{
data:'B',
index:0, // 表示父节点为数据域为A的节点
},
{
data:'C',
index:0
},
{
data:'D',
index:0
}
]
- 孩子表示法 这个其实就很容易理解,也是我们前端遇到的最多的一种;其方式就是将每个节点都用单链表链接起来形成一个线性结构
const Tree = {
data:'A',
child:[
{
data:'B'
child:[
{
data:'D',
child:[]
}
]
},
{
data:'C',
child:[]
}
]
}
- 孩子兄弟表示法 孩子兄弟表示法,又叫二叉树表示法。如下图 【孩子兄弟表示法】
const Tree = {
data:'A',
sibling:null,
child:{
data:'B',
sibling:{
data:"C",
sibling:{
data:'D',
child:null,
sibling:null
},
child:{
data:'F',
child:null,
sibling:null
}
},
child:{
data:'E',
child:null,
sibling:null
}
}
}
3.2 树的遍历方式
- 先根遍历
先根遍历,就是访问根节点,然后从左到右访问各个子树
这里我们选取孩子兄弟表示法进行先根遍历(react的fiber树就是兄弟孩子表示法,只是他做了一个加强,就是给每个节点增设了一个parent域指向了其父节点)
const ProOrderTree = (tree)=>{
const record = [];
const parentNodeTree = [];
const OrederTree = (tree)=>{
if(tree !== null){
// 访问节点
console.log(tree,'访问节点')
record.push(tree.data)
if(tree.child !== null){
// 如果存在child,就继续递归
// 因为这里没有给node节点增设parent域,因此需要记录parent
parentNodeTree.push(tree);
// console.log(parentNodeTree)
OrederTree(tree.child)
}else{
// 如果不存在,就遍历该节点的兄弟
if(tree.sibling){
OrederTree(tree.sibling)
}else{
const node = parentNodeTree.pop();
OrederTree(node.sibling)
}
}
}
}
OrederTree(tree)
console.log(record)
}
ProOrderTree(Tree)
- 后根遍历 后根遍历就是先从左到右访问各个子树,然后访问根节点,具体实现篇幅有限,不在赘述。
这里也同样选取孩子兄弟表示法进行后根遍历,自己尝试写写吧~
4.实战练习(leetCode)
4.1 验证二叉搜索树
给定一个二叉树,判断其是否是一个有效的二叉搜索树。
假设一个二叉搜索树具有如下特征:
- 节点的左子树只包含小于当前节点的数。
- 节点的右子树只包含大于当前节点的数。
- 所有左子树和右子树自身必须也是二叉搜索树。
力扣LeetCode
const binaryTtrees = {
val:5,
left:{
val:4,
left:null,
right:null
},
right:{
val:6,
left:{
val:3,
left:null,
right:null
},
right:{
val:7,
left:null,
right:null
}
}
}
基本思路:
- 因为二叉树适合递归,因此就构造一个递归函数order,同时外面命一个
result = true
的变量,先默认他是二叉搜索树 - 这个order接受一个tree,按照前面学过的前序遍历,如果tree不为空,就
order(tree.left);order(tree.right)
- 因为是要比较,因此order必须增设2个参数一个是
lv表示是作为左子树父节点值
;rv表示是作为右子树父节点的值
- 根据上面二叉搜索树的定义,很快就能得出当:
- tree.val 大于 左子树父节点的值是不合理的
- tree.val 小于 右子树父节点的值也是不合理的
const isValidBST = (root)=>{
let result = true;
const Order = (tree,lv=null,rv=null) =>{
if(tree !== null){
const value = tree.val;
// console.log(value,lv,rv)
if(lv!=null && value>=lv ){
result = false
return
}
if(rv!==null && value <= rv){
result = false
return
}
Order(tree.left,value,rv)
Order(tree.right,lv,value)
}
}
Order(root)
console.log(result)
return result
}
isValidBST(binaryTtrees)
扩展 如果熟悉二叉搜索树的性质,其实这个题也特别简单,要知道二叉搜索树的一个很重要的性质就是 对二叉搜索树进行中序遍历,总会得到一个从小到大的一个排列
4.2 路径总和
给你二叉树的根节点root
和一个表示目标和的整数targetSum
,判断该树中是否存在 根节点到叶子节点 的路径,这条路径上所有节点值相加等于目标和targetSum
。
叶子节点 是指没有子节点的节点。
const binaryTtrees = {
val:5,
left:{
val:4,
left:null,
right:null
},
right:{
val:6,
left:{
val:3,
left:null,
right:null
},
right:{
val:7,
left:null,
right:null
}
}
}
看到这个题,一下子就想到用前序遍历 对前面的前序遍历进行改造,就能得到本题的结果。
const hasPathSum = function(root, targetSum) {
const recored = [];
const sum = (arry)=>{
let add = 0;
arry.forEach( item=>{
add += item
})
return add
}
const PreOrder = (root,path=[])=>{
if(root){
const route = [..path,root.val]
// 这样就是查询所有路径
if(root.left === null && root.right === null && sum(route) === targetSum){
recored.push(route)
}
PreOrder(root.left,route);
PreOrder(root.right,route);
}
}
PreOrder(root,[])
console.log(recored)
};
hasPathSum(binaryTtrees)