前言
准备根据修言老师的小册子结合leetcode上剑指offer的二叉树学习这一部分。
先序、后序、中序、层序遍历的非递归实现
在我的理解里,树就是一种特殊的图。先序对应了图的深度遍历,而层序对应了图的广度遍历。
先序和后序的递归实现方式还是比较好理解,非递归实现就自己手动去实现一个递归栈。
我初学习数据结构是跟着浙大的mooc,其中对于树的遍历有一个很好理解的解释:
按照叉号读出来的顺序是前序遍历,正好是第一次经过该节点
按照星号读出来的顺序是中序遍历,正好是第二次经过该节点
按照三角形读出来的顺序是后序遍历,正好是第三次经过该节点
ok 画不多上~上代码
先序遍历
需要关注的是对stack数组的一个入栈顺序。
众所周知(x)先序是 root => left => right 的顺序~ 所以入栈的时候要先right 再 left 确保弹栈的时候总是left先被访问到。
/**
* @param {TreeNode} root
* @return {number[]}
*/
const preorderTraversal = function(root) {
// 定义结果数组
const res = []
// 处理边界条件
if(!root) {
return res
}
// 初始化栈结构
const stack = []
// 首先将根结点入栈
stack.push(root)
// 若栈不为空,则重复出栈、入栈操作
while(stack.length) {
// 将栈顶结点记为当前结点
const cur = stack.pop()
// 当前结点就是当前子树的根结点,把这个结点放在结果数组的尾部
res.push(cur.val)
// 若当前子树根结点有右孩子,则将右孩子入栈
if(cur.right) {
stack.push(cur.right)
}
// 若当前子树根结点有左孩子,则将左孩子入栈
if(cur.left) {
stack.push(cur.left)
}
}
// 返回结果数组
return res
};
后序遍历
欸嘿,其实后序遍历和先序遍历还是很像滴!
众所周知(x)后序是 left => right => root 的顺序
但是,我们遍历的时候总是会先访问到root。
所以有个很好的办法,之前我们都是在res数组的尾部新增一个val。后序的时候就可以改成在res数组的头部新增一个val。(等于把res从一个队列变成了一个栈
相对应的,原本考虑到出栈,总是让right压在栈底,left放在栈顶。
在后序遍历时这个思路就要反一下了,我们应该先压入left ,再压入 right~
这也right会被率先弹出,又压入res数组中~ res 的顺序就变成了: left => right => root
/**
* @param {TreeNode} root
* @return {number[]}
*/
const postorderTraversal = function(root) {
// 定义结果数组
const res = []
// 处理边界条件
if(!root) {
return res
}
// 初始化栈结构
const stack = []
// 首先将根结点入栈
stack.push(root)
// 若栈不为空,则重复出栈、入栈操作
while(stack.length) {
// 将栈顶结点记为当前结点
const cur = stack.pop()
// 当前结点就是当前子树的根结点,把这个结点放在结果数组的头部
res.unshift(cur.val)
// 若当前子树根结点有左孩子,则将左孩子入栈
if(cur.left) {
stack.push(cur.left)
}
// 若当前子树根结点有右孩子,则将右孩子入栈
if(cur.right) {
stack.push(cur.right)
}
}
// 返回结果数组
return res
};
中序遍历
中序遍历相比前两个比较相似的遍历方式呢,就显得有些格格不入(x)
众所周知(x)中序是 left => root => right 的顺序
代码思路:
while(栈非空 || 指针非空):
all的左子树入栈; //包括根节点
出栈访问;
指针指向右结点这也的好处是,在出栈访问的时候,实际访问的是root点。然后,right才会入栈,确保了遍历的顺序~
/**
* @param {TreeNode} root
* @return {number[]}
*/
const inorderTraversal = function(root) {
// 定义结果数组
const res = []
// 初始化栈结构
const stack = []
// 用一个 cur 结点充当游标
let cur = root
// 当 cur 不为空、或者 stack 不为空时,重复以下逻辑
while(cur || stack.length) {
// 这个 while 的作用是把寻找最左叶子结点的过程中,途径的所有结点都记录下来
while(cur) {
// 将途径的结点入栈
stack.push(cur)
// 继续搜索当前结点的左孩子
cur = cur.left
}
// 取出栈顶元素
cur = stack.pop()
// 将栈顶元素入栈
res.push(cur.val)
// 尝试读取 cur 结点的右孩子
cur = cur.right
}
// 返回结果数组
return res
};
层序遍历
修言老师在这里引了一题,关于层序遍历。我觉得层序遍历还是比较简单滴,直接看代码就很好理解了~
题目描述:给你一个二叉树,请你返回其按 层序遍历 得到的节点值。 (即逐层地,从左到右访问所有节点)。
/**
* @param {TreeNode} root
* @return {number[][]}
*/
const levelOrder = function(root) {
// 初始化结果数组
const res = []
// 处理边界条件
if(!root) {
return res
}
// 初始化队列
const queue = []
// 队列第一个元素是根结点
queue.push(root)
// 当队列不为空时,反复执行以下逻辑
while(queue.length) {
// level 用来存储当前层的结点
const level = []
// 缓存刚进入循环时的队列长度,这一步很关键,因为队列长度后面会发生改变
const len = queue.length
// 循环遍历当前层级的结点
for(let i=0;i<len;i++) {
// 取出队列的头部元素
const top = queue.shift()
// 将头部元素的值推入 level 数组
level.push(top.val)
// 如果当前结点有左孩子,则推入下一层级
if(top.left) {
queue.push(top.left)
}
// 如果当前结点有右孩子,则推入下一层级
if(top.right) {
queue.push(top.right)
}
}
// 将 level 推入结果数组
res.push(level)
}
// 返回结果数组
return res
};
剑指offer
最近和周同学在按tag刷剑指offer。进度是每天一道题。最近发现lc手机版贼好用,坐地铁和中午午休的时间都可以用手机刷题~
剑指 Offer 27. 二叉树的镜像
这题在没看修言老师的小册子前就做完了。
思路也很简单:递归的方式,遍历树中的每一个结点,并将每一个结点的左右孩子进行交换。
/**
* Definition for a binary tree node.
* function TreeNode(val) {
* this.val = val;
* this.left = this.right = null;
* }
*/
/**
* @param {TreeNode} root
* @return {TreeNode}
*/
var mirrorTree = function(root) {
//定义递归边界
if(root) {
//交换左右子树
let tmp = root.left
root.left = root.right
root.right = tmp
//递归
mirrorTree(root.left)
mirrorTree(root.right)
return root
}else {
return root
}
};
剑指 Offer 28. 对称的二叉树
也是递归的一道题。
因为是判断对称,从根节点开始看,左边是不是等于右边。
如果等于的话,就判断:
1.左节点的左节点 是否等于 右节点的右节点
2.左节点的右节点 是否等于 右节点的左节点
var isSymmetric = function(root) {
return check(root, root)
};
const check = (leftPtr, rightPtr) => {
// 如果只有根节点,返回true
if (!leftPtr && !rightPtr) return true
// 如果左右节点只存在一个,则返回false
if (!leftPtr || !rightPtr) return false
return leftPtr.val === rightPtr.val && check(leftPtr.left, rightPtr.right) && check(leftPtr.right, rightPtr.left)
}
剑指 Offer 37. 序列化二叉树
这题我用了层序遍历
//序列化
var serialize = function(root) {
let res=[]
let queue=[root]
while(queue.length){
let node=queue.shift()
if(node){
res.push(node.val)
queue.push(node.left)
queue.push(node.right)
}else{
res.push('X')
}
}
return res.join(',')
};
//反序列化
var deserialize = function(data) {
if (data == 'X') return null;
// 序列化字符串split成数组
const list = data.split(',');
// 获取首项,构建根节点
const root = new TreeNode(list[0]);
// 根节点推入队列
const queue = [root];
// 初始指向list第二项
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);
}
// 一次考察一对儿子,指针加2
cursor += 2;
}
return root;
};