基本概念
树是有限个结点的集合,如果这个有限是大于0的话,那么必然存在一个特殊的根节点,其余节点都在挂在它下面,不一定是直接挂在根节点上。这些结点本身和挂在它们身上的节点集合也是树。
也就是树中有树,这个概念似乎在无限递归,用自己来解释自己,的确如此。不过需要注意的是不同子树的节点是不会连接在一起的,每个节点只和它的上一级和下一集的节点有关联
下面 是一颗合法的树
而下面的就不是一棵树,因为它不符合不同子树节点不会连接在一起,这一准则。 用链表来理解,可以说,树是不会形成闭环的(视觉上的)
树的节点可以用链表的节点来理解,但是树和链表是两种不同的结构。
树的节点代表集合, 边(通常是有向边)代表关系
二叉树同样每个节点只需要存储 数据(值) 和 它的子节点的引用(指针域)。
二叉树就是每个节点最多有两个子节点的树,也就是说,最多的情况下。 无极生太极,太极生两仪,两仪生四象,四象生八卦。这不就是二进制吗。
这种每层都是满的二叉树既是满二叉树,又是完全二叉树。
完全二叉树就是除了最后一层,每层都是满的,最后一层的节点又全部集中在左侧。完全二叉树的一个重要特性就是可以根据当前节点的索引算出对应父子节点的索引,也就是说,可以不用存储子节点的指针,还能逆推出父节点。这样,用数组就能建立一颗完全二叉树, 虽然它在存储上是连续的数组, 但是在我们逻辑层面可以是一棵树。
下面是二叉树的js简单实现,和基本遍历方法,递归版。树天生就是要用递归的。
递归版
class TreeNode {
constructor(val){
this.val = val
this.left = null
this.right = null
}
}
// 前序就是 根左右
function preOrder(node){
// 一个二叉树节点如果存在那它肯定不会是原始值0 false ,不存在正常情况下应该是null,这个要看TreeNode的具体实现
if (!node) return
// do something
// console.log(node.val)
//
preOrder(node.left)
preOrder(node.right)
}
// 中序就是 左根右
function inOrder(node){
// 一个二叉树节点如果存在那它肯定不会是原始值0 false ,不存在正常情况下应该是null,这个要看TreeNode的具体实现
inOrder(node.left)
if (!node) return
// do something
// console.log(node.val)
//
inOrder(node.right)
}
// 后序就是 左右根
function sufOrder(node){
// 一个二叉树节点如果存在那它肯定不会是原始值0 false ,不存在正常情况下应该是null,这个要看TreeNode的具体实现
sufOrder(node.left)
sufOrder(node.right)
if (!node) return
// do something
// console.log(node.val)
//
}
// 层序遍历
// 层序遍历如果把下一层的 节点 return出去 是不是就算是转迭代了 可暂停 ,递归很多时候是不能暂停的 因为外层还没执行完 就开始执行内层
function depOrder(node){
// 需要一个辅助函数
const help = (nodes, cb)=> {
if (!nodes || !nodes.length) return
const temp = [] // 用于收集下一层的节点
for ( let node of nodes) {
node.left && temp.push(node.left)
node.right && temp.push(node.right)
// do something
// console.log(node.val);
cb(node)
}
help(temp)
}
let res =[]
help([node], (node)=> {res.push(node.val)})
}
迭代版
补上迭代版的遍历。下面是一种通用的模拟栈的迭代方式。需要两个栈,一个存储状态(也就是程序走到哪了),另一个存储数据(可认为是当前函数执行栈应该有的数据)。
理解这个迭代写法的重点在于模拟系统执行递归程序的过程, 比如当前处理的是根节点, 前序遍历的情况,下一步应该去找左节点 , 左节点下一步应该去找右节点。但是每次数据栈存入新的节点时,下一步就变成了输出根节点。就跟递归调用自身一样, 每次调用函数就又回到函数开始的地方。
中序也是一样, 先找左节点, 找到了 下一个应该是 根, 如果左节点存在,就会将左节点压入数据栈, 这样 下一个就应该是 左节点的左节点 所以要再加一个状态进栈。
后序,一模一样。
一定不能忘的就是 ,找到一个新的节点时,等于重新开始了一个函数的执行。
待,一个节点的左右都访问过了,那么这个节点就可以弹栈了。 也就是在遍历顺序的最后的那个访问过,就弹栈。 这三种遍历,最后一个都是右,所以右节点访问了之后弹栈就行了。
0 1 2
*/
var preorderTraversal = function(root) {
if(!root) return []
let res=[],s1=[],s2=[]
s1.push(root)
s2.push(0)
while(s1.length){
let status = s2.pop()
let root = s1[s1.length -1]
switch (status) {
case 0:
s2.push(1) ;
res.push(root.val)
break;
case 1:
s2.push(2)
if(root.left){
s1.push(root.left)
s2.push(0)
}
break;
case 2:
s1.pop()
if(root.right){
s1.push(root.right)
s2.push(0)
}
break;
default:
break;
}
}
return res
}
/* n叉树的前序遍历 关键是状态码的管理, -1根节点 n 第n个子节点 这里弹栈的条件是 n溢出children的length */
var preorder = function (root) {
if (!root) return []
const res = [], s1 = [], s2 = [];
s1.push(root);
s2.push(-1);
while (s1.length) {
const status = s2.pop();
const node = s1[s1.length - 1];
switch (status) {
case -1:
/* -1的下一步就是0 */
s2.push(0)
res.push(node.val);
break;
default:
/* n的下一步就是n+1 */
if (!node.children || status >= node.children.length) {
s1.pop()
continue
}
s2.push(status + 1)
if (node.children[status]) {
s1.push(node.children[status])
s2.push(-1)
}
break;
}
}
// console.log(res)
return res
};