Day13-二叉树-理论-leetcode144,145,94,102,107,199,637,429,515,116,117,104,111
二叉树
理论
二叉树的种类
-
满二叉树,如果一颗二叉树只有度为0的节点和度为2的节点,且度为0的节点都在同一层上,则这颗二叉树为满二叉树。深度为k,有2^k-1个节点的二叉树。k是深度,除了叶子节点,每个节点都有2个节点
-
完全二叉树,除了底层以外,其它层都是满的,底层是从左到右是连续的。若最底层为第 h 层(h从1开始),则该层包含 1~ 2^(h-1) 个节点。
-
二叉搜索树,节点上的元素有一定顺序,二叉搜索树是一个有序树。
- 若它的左子树不空,则左子树上所有节点的值均小于它的根节点的值;
- 若它的右子树不空,则右子树上的所有节点的值均大于它的根节点的值;
- 它的左右子树分别为二叉排序树;
-
平衡二叉树,左子树和右子树的高度的绝对值不能超过1,
-
满二叉树一定是完全二叉树,
二叉树存储方式
-
链式存储,一般用这个,指针,链表
-
线式存储,顺序存储,数组。
-
顺序存储的元素在内存是连续分布的,而链式存储则是通过指针把分布在各个地址的节点串联一起。
-
二叉树其实就是一个链表,
二叉树的遍历方式
-
二叉树主要有两种遍历方式:
- 深度优先遍历:先往深走,遇到叶子节点再往回走。
- 广度优先遍历:一层一层的去遍历。
-
深度优先遍历,
-
朝一个方向一直去搜,一直搜到终点了,再回退,再换下一个方向,再一直往下走,一直搜到终点,再回退,一条路跑到黑,然后再回头换方向,
-
前序遍历、中序遍历、后续遍历都是深度优先遍历;
-
一般递归方式实现,用递归能实现,借助栈使用递归的过程也可实现;迭代法也可实现,即非递归;每一个递归方法都可以用迭代法实现,
-
前序遍历,根节点在前面,即中左右;
-
中序遍历,根节点在中间,即左中右;
-
后序遍历,根节点在后面,即左右中;
-
这里前中后,其实指的就是中间节点的遍历顺序,只要大家记住 前中后序指的就是中间节点的位置就可以了。
-
-
广度优先遍历,
- 一层一层的去遍历(二叉树里遍历方式),或者一圈一圈的去遍历(图论里遍历方式),
- 层序遍历是广度优先遍历,用一个队列来实现对二叉树一层一层的搜索,
- 迭代法
二叉树的定义
// 使用class定义
class TreeNode {
constructor(value) {
this.value = value // 节点值
this.left = null // 左子节点
this.right = null // 右子节点
}
}
// 使用构造函数定义
function TreeNode (value) {
this.value = value
this.left = null
this.right = null
}
function TreeNode(val, left, right) {
this.val = (val===undefined ? 0 : val)
this.left = (left===undefined ? null : left)
this.right = (right===undefined ? null : right)
}
递归遍历
- 确定递归函数的参数和返回值
- 确定终止条件
- 确定单层递归的逻辑
-
通过简单题目把方法论确定下来,有了方法论,后面才能应付复杂的递归。
-
每次写递归,都按照这三要素来写,可以保证大家写出正确的递归算法!
- 确定递归函数的参数和返回值: 确定哪些参数是递归的过程中需要处理的,那么就在递归函数里加上这个参数, 并且还要明确每次递归的返回值是什么进而确定递归函数的返回类型。
- 确定终止条件: 写完了递归算法, 运行的时候,经常会遇到栈溢出的错误,就是没写终止条件或者终止条件写的不对,操作系统也是用一个栈的结构来保存每一层递归的信息,如果递归没有终止,操作系统的内存栈必然就会溢出。
- 确定单层递归的逻辑: 确定每一层递归需要处理的信息。在这里也就会重复调用自己来实现递归的过程。
- 前提条件,二叉树定义
function TreeNode(val, left, right) {
this.val = (val===undefined ? 0 : val)
this.left = (left===undefined ? null : left)
this.right = (right===undefined ? null : right)
}
- 二叉树的前序遍历
// 前序遍历
var preorderTraversal = function(root){
let res = []
const dfs = function(root) {
if (root === null) return
res.push(root.val)
// 递归左子树
dfs(root.left)
// 递归右子树
dfs(root.right)
}
dfs(root)
return res
}
// 前序遍历
var preorderTraversal = function(root) {
return root ? [
root.val,
...preorderTraversal(root.left),
...preorderTraversal(root.right)
]: []
};
- 二叉树的后序遍历
// 后序遍历
var postorderTraversal = function(root) {
let res = []
const dfs = function(root) {
if(root === null) {
return
}
dfs(root.left)
dfs(root.right)
res.push(root.val)
}
dfs(root)
return res
}
// 后序遍历
var postorderTraversal = function(root) {
return root ? [
...postorderTraversal(root.left),
...postorderTraversal(root.right),
root.val
]:[]
};
- 二叉树的中序遍历
// 中序遍历
var inorderTraversal = function(root) {
let res = []
const dfs = function(root) {
if (root === null) {
return
}
dfs(root.left)
res.push(root.val)
dfs(root.right)
}
dfs(root)
return res
}
// 中序遍历
var inorderTraversal = function(root) {
return root ? [
...inorderTraversal(root.left),
root.val,
...inorderTraversal(root.right)
] : []
};
非递归
- 用栈模拟递归的过程
- 遍历节点和处理节点顺序不一致,想办法弄成一致
前序遍历
- 前序遍历是中左右,每次先处理的是中间节点,那么先将根节点放入栈中,然后将右孩子加入栈,再加入左孩子。
- 为什么要先加入 右孩子,再加入左孩子呢? 因为这样出栈的时候才是中左右的顺序。
- 前序遍历的顺序是中左右,先访问的元素是中间节点,要处理的元素也是中间节点,所以刚刚才能写出相对简洁的代码,因为要访问的元素和要处理的元素顺序是一致的,都是中间节点。
- 前序遍历中访问节点(遍历节点)和处理节点(将元素放进result数组中)可以同步处理,但是中序就无法做到同步!
// 前序遍历
// 入栈:右=》左
// 出栈:中=》左=》右
function preorderTraversal(root, res = []) {
if (!root) return res
const stack = [root]
let cur = null
while(stack.length) {
cur = stack.pop()
res.push(cur.val)
cur.right && stack.push(cur.right)
cur.left && stack.push(cur.left)
}
return res
}
中序遍历
- 中序遍历是左中右,先访问的是二叉树顶部的节点,然后一层一层向下访问,直到到达树左面的最底部,再开始处理节点(也就是在把节点的数值放进result数组中),这就造成了处理顺序和访问顺序是不一致的。
- 使用迭代法写中序遍历,就需要借用指针的遍历来帮助访问节点,栈则用来处理节点上的元素。
// 入栈 左=》右
// 出栈 左=》中=》右
var inorderTraversal = function(root, res = []) {
const stack = []
let cur = root
while(stack.length || cur) {
if (cur) {
stack.push(cur)
// 左
cur = cur.left
} else {
// 弹出 中
cur = stack.pop()
res.push(cur.val)
// 右
cur = cur.right
}
}
return res;
}
后序遍历
- 先序遍历是中左右,后序遍历是左右中,那么我们只需要调整一下先序遍历的代码顺序,就变成中右左的遍历顺序,然后在反转result数组,输出的结果顺序就是左右中了
// 入栈 左=》右
// 出栈 中=》右=》左 结果翻转
var postorderTraversal = function(root, res=[]) {
if (!root) return res
const stack = [root]
let cur = null
do {
cur = stack.pop()
res.push(cur.val)
cur.left && stack.push(cur.left)
cur.right && stack.push(cur.right)
} while(stack.length)
return res.reverse()
}
统一迭代法
-
将访问的节点放入栈中,把要处理的节点也放入栈中但是要做标记。
-
如何标记呢?
-
方法一:就是要处理的节点放入栈之后,紧接着放入一个空指针作为标记。 这种方法可以叫做空指针标记法。
-
方法二:加一个 boolean 值跟随每个节点,false (默认值) 表示需要为该节点和它的左右儿子安排在栈中的位次,true 表示该节点的位次之前已经安排过了,可以收割节点了。 这种方法可以叫做boolean 标记法,样例代码见下文C++ 和 Python 的 boolean 标记法。 这种方法更容易理解,在面试中更容易写出来。
// 前序遍历:中左右
// 压栈顺序:右左中
var preorderTraversal = function(root, res = []) {
const stack = [];
if (root) stack.push(root);
while(stack.length) {
const node = stack.pop();
if(!node) {
res.push(stack.pop().val);
continue;
}
if (node.right) stack.push(node.right); // 右
if (node.left) stack.push(node.left); // 左
stack.push(node); // 中
stack.push(null);
};
return res;
};
// 中序遍历:左中右
// 压栈顺序:右中左
var inorderTraversal = function(root, res = []) {
const stack = [];
if (root) stack.push(root);
while(stack.length) {
const node = stack.pop();
if(!node) {
res.push(stack.pop().val);
continue;
}
if (node.right) stack.push(node.right); // 右
stack.push(node); // 中
stack.push(null);
if (node.left) stack.push(node.left); // 左
};
return res;
};
// 后续遍历:左右中
// 压栈顺序:中右左
var postorderTraversal = function(root, res = []) {
const stack = [];
if (root) stack.push(root);
while(stack.length) {
const node = stack.pop();
if(!node) {
res.push(stack.pop().val);
continue;
}
stack.push(node); // 中
stack.push(null);
if (node.right) stack.push(node.right); // 右
if (node.left) stack.push(node.left); // 左
};
return res;
};
层序遍历
-
层序遍历一个二叉树。就是从左到右一层一层的去遍历二叉树。
-
借助队列,帮助保存每一层遍历的元素。队列先进先出,符合一层一层遍历的逻辑,而用栈先进后出适合模拟深度优先遍历也就是递归的逻辑。
-
size,记录本层里的元素个数,后面遍历层数不同时,size也会变化,所以需要提前记录,遍历完一层后,立刻去记录下一层里面的数量
-
二叉树层序遍历的模板
var levelOrder = function(root) {
// 二叉树的层序遍历
// res 用于保存每一层的节点值(二维数组)。
// queue 用于辅助层序遍历,存储当前待访问的节点
let res = [], queue = []
// 先把根节点加入队列,作为遍历的起点
queue.push(root)
// 如果根节点为空,直接返回空数组。
if (root === null) return res
// 主循环:只要队列不为空就继续遍历
// 只要队列里有节点,就说明还有未遍历的层。
while(queue.length !== 0) {
// 记录当前层级点数,当前队列的长度就是当前层的节点数。
let length = queue.length
// 存放每一层的节点
let curLevel = []
// 遍历当前层的所有节点
for (let i = 0; i < length; i++) {
// queue.shift() 取出队首节点。
let node = queue.shift()
// curLevel.push(node.val) 把当前节点的值加入本层结果。
curLevel.push(node.val)
// 存放当前层下一层的节点
// 如果有左子节点,加入队列;如果有右子节点,也加入队列。这样下一轮 while 循环时,队列里就是下一层的所有节点。
node.left && queue.push(node.left)
node.right && queue.push(node.right)
}
// 把每一层的结果放到结果数组中,将当前层的结果加入总结果
res.push(curLevel)
}
// 返回每一层节点值组成的二维数组。
return res
}
题目
- 二叉树的层序遍历
-
给你一个二叉树,请你返回其按 层序遍历 得到的节点值。 (即逐层地,从左到右访问所有节点)。
-
思路:
-
- 准备一个队列
-
队列用于存储每一层待访问的节点,先进先出,适合一层一层遍历。
-
- 将根节点加入队列
-
如果根节点为空,直接返回空数组。
-
- 循环遍历队列
-
只要队列不为空,就说明还有未遍历的节点。
-
- 记录当前层的节点数量
-
每次循环开始时,记录队列长度(即当前层的节点数)。
-
- 遍历当前层的所有节点
-
用 for 循环遍历当前层的节点数次,每次弹出队首节点,保存其值到当前层数组。
-
如果该节点有左子节点,加入队列;有右子节点,也加入队列。
-
- 保存当前层的结果
-
当前层遍历完后,把本层节点值数组加入总结果数组。
-
- 重复步骤3-6,直到队列为空
-
所有节点都被访问过,遍历结束。
-
- 返回结果
-
返回每一层节点值组成的二维数组。
var levelOrder = function(root) {
let res = [], queue = []
queue.push(root)
if (root === null) {
return res
}
while(queue.length !== 0) {
let len = queue.length
let curLevel = []
for (let i = 0; i< len; i++) {
let node = queue.shift()
curLevel.push(node.val)
node.left && queue.push(node.left)
node.right && queue.push(node.right)
}
res.push(curLevel)
}
return res
};
- 二叉树的层序遍历 II
-
给你二叉树的根节点 root ,返回其节点值 自底向上的层序遍历 。 (即按从叶子节点所在层到根节点所在的层,逐层从左向右遍历)
-
思路:
-
将层序遍历模版结果翻转
var levelOrderBottom = function(root) {
let res = [], queue = []
queue.push(root)
if (root === null) {
return res
}
while(queue.length !== 0) {
let len = queue.length
let curLevel = []
for (let i = 0; i< len; i++) {
let node = queue.shift()
curLevel.push(node.val)
node.left && queue.push(node.left)
node.right && queue.push(node.right)
}
res.push(curLevel)
}
return res.reverse()
};
- 二叉树的右视图
-
给定一个二叉树的 根节点 root,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。
-
思路:
-
层序遍历的时候,判断是否遍历到单层的最后面的元素,如果是,就放进result数组中,随后返回result就可以了。
var rightSideView = function(root) {
// 二叉树右视图,只需要把每一层最后一个节点存储到res数组
let res = [], queue = []
queue.push(root)
while(queue.length && root !== null) {
// 记录当前层级节点个数
let len = queue.length
while(len--) {
let node = queue.shift()
// len长度为0时表明到了层级最后一个节点
if (!len) {
res.push(node.val)
}
node.left && queue.push(node.left)
node.right && queue.push(node.right)
}
}
return res
}
- 二叉树的层平均值
-
给定一个非空二叉树, 返回一个由每层节点平均值组成的数组。
-
思路
-
先记录每一层的数据curLevel,再遍历计算每一层的平均值,再推入总结果res中
var averageOfLevels = function(root) {
let res = [], queue = []
queue.push(root)
if (root === null) {
return res
}
while(queue.length) {
let len = queue.length
let curLevel = []
for (let i = 0; i < len; i++) {
let node = queue.shift()
curLevel.push(node.val)
node.left && queue.push(node.left)
node.right && queue.push(node.right)
}
let sum = 0;
for(let val of curLevel) {
sum = sum + val
}
let average = sum / len;
res.push(average)
}
return res
};
var averageOfLevels = function(root) {
let res = [],
queue = [];
queue.push(root);
while (queue.length) {
// 每一层节点个数;
// lengthLevel用于控制 while 循环,表示当前层还剩多少节点要处理。每处理一个节点就 lengthLevel--,直到为 0,说明这一层处理完了。
let lengthLevel = queue.length,
// len用于记录当前层的节点总数,用于后面计算平均值。因为在 while 循环中 queue.length 会变化(节点不断被 shift 出队),所以要提前保存下来,后面才能正确计算平均值。
let len = queue.length
//sum记录每一层的和;
let sum = 0;
while (lengthLevel--) {
const node = queue.shift();
sum += node.val;
// 队列存放下一层节点
node.left && queue.push(node.left);
node.right && queue.push(node.right);
}
// 求平均值
res.push(sum / len);
}
return res;
};
- N 叉树的层序遍历
-
树的序列化输入是用层序遍历,每组子节点都由 null 值分隔(参见示例)。
-
思路
-
不再区分左右节点,而是孩子节点,遍历孩子节点,推入queue队列中
var levelOrder = function(root) {
//每一层可能有2个以上,所以不再使用node.left node.right
let res = [], queue = [];
queue.push(root);
while(queue.length && root!==null) {
//记录每一层节点个数还是和二叉树一致
let length = queue.length;
//存放每层节点 也和二叉树一致
let curLevel = [];
while(length--) {
let node = queue.shift();
curLevel.push(node.val);
//这里不再是 ndoe.left node.right 而是循坏node.children
for(let item of node.children){
item && queue.push(item);
}
}
res.push(curLevel);
}
return res;
};
- 在每个树行中找最大值
-
需要在二叉树的每一行中找到最大的值。
-
思路
-
层序遍历,取每一层的最大值
var largestValues = function(root) {
let res = [], queue = []
queue.push(root)
if (root === null) {
return res
}
while(queue.length) {
let len = queue.length
let curLevel = []
for (let i = 0; i < len; i++) {
let node = queue.shift()
curLevel.push(node.val)
node.left && queue.push(node.left)
node.right && queue.push(node.right)
}
let max = -Infinity;;
for(let val of curLevel) {
max = Math.max(max, val)
}
res.push(max)
}
return res
};
var largestValues = function (root) {
let res = [],
queue = [];
queue.push(root);
if (root === null) {
return res;
}
while (queue.length) {
let lengthLevel = queue.length,
// 初始值设为负无穷大
max = -Infinity;
while (lengthLevel--) {
const node = queue.shift();
// 在当前层中找到最大值
max = Math.max(max, node.val);
// 找到下一层的节点
node.left && queue.push(node.left);
node.right && queue.push(node.right);
}
res.push(max);
}
return res;
};
- 填充每个节点的下一个右侧节点指针
- 给定一个完美二叉树,其所有叶子节点都在同一层,每个父节点都有两个子节点。二叉树定义如下:
struct Node {
int val;
Node *left;
Node *right;
Node *next;
}
-
填充它的每个 next 指针,让这个指针指向其下一个右侧节点。如果找不到下一个右侧节点,则将 next 指针设置为 NULL。
-
初始状态下,所有 next 指针都被设置为 NULL。
-
思路
-
- 队列实现层序遍历
- 每次 while 循环处理一层,for 循环遍历当前层的所有节点。
-
- 设置 next 指针
- 如果当前节点不是本层最后一个节点(i < n-1),就让它的 next 指向队列中的下一个节点(即本层下一个节点)。
- 如果是本层最后一个节点,则 next 默认就是 null,不用手动设置。
-
- 加入下一层节点
- 把当前节点的左右孩子(如果有)加入队列,供下一层遍历。
var connect = function(root) {
if (root === null) return root; // 特判,空树直接返回
let queue = [root]; // 用队列辅助层序遍历,初始只放根节点
while (queue.length) { // 只要队列不空,就说明还有节点要处理
let n = queue.length; // 当前层的节点数
for (let i = 0; i < n; i++) { // 遍历当前层的所有节点
let node = queue.shift(); // 弹出队首节点,记录本层的头部节点
// 不是本层最后一个节点时,next指向队列下一个节点
if (i < n-1) {
node.next = queue[0];
}
// 把下一层的左右孩子加入队列
node.left && queue.push(node.left);
node.right && queue.push(node.right);
}
}
// 返回修改后的树
return root;
};
- 填充每个节点的下一个右侧节点指针 II
- 给定一个二叉树:
struct Node {
int val;
Node *left;
Node *right;
Node *next;
}
-
填充它的每个 next 指针,让这个指针指向其下一个右侧节点。如果找不到下一个右侧节点,则将 next 指针设置为 NULL 。
-
初始状态下,所有 next 指针都被设置为 NULL 。
-
思路
-
- 队列实现层序遍历
- 每次 while 循环处理一层,for 循环遍历当前层的所有节点。
-
- 设置 next 指针
- 如果当前节点不是本层最后一个节点(i < n-1),就让它的 next 指向队列中的下一个节点(即本层下一个节点)。
- 如果是本层最后一个节点,则 next 默认就是 null,不用手动设置。
-
- 加入下一层节点
- 把当前节点的左右孩子(如果有)加入队列,供下一层遍历。
var connect = function(root) {
if (root === null) {
return null;
}
let queue = [root];
while (queue.length > 0) {
let n = queue.length;
for (let i=0; i<n; i++) {
let node = queue.shift();
if (i < n-1) node.next = queue[0];
if (node.left != null) queue.push(node.left);
if (node.right != null) queue.push(node.right);
}
}
return root;
};
- 二叉树的最大深度
-
给定一个二叉树,找出其最大深度。
-
二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。
-
说明: 叶子节点是指没有子节点的节点。
-
思路
-
层级排序模版得到的数组的长度
var maxDepth = function(root) {
var res = []
var queue = []
queue.push(root)
if (root === null) return 0
while(queue.length) {
let len = queue.length
let curLevel = []
for(let i = 0; i< len; i++) {
let node = queue.shift()
curLevel.push(node.val)
node.left && queue.push(node.left)
node.right && queue.push(node.right)
}
res.push(curLevel)
}
return res.length
};
var maxDepth = function (root) {
// 二叉树的 最大深度 是指从根节点到最远叶子节点的最长路径上的节点数。
let max = 0,
queue = [root];
if (root === null) {
return max;
}
while (queue.length) {
max++;
let length = queue.length;
while (length--) {
let node = queue.shift();
node.left && queue.push(node.left);
node.right && queue.push(node.right);
}
}
return max;
};
- 二叉树的最小深度
-
给定一个二叉树,找出其最小深度。
-
最小深度是从根节点到最近叶子节点的最短路径上的节点数量。
-
说明:叶子节点是指没有子节点的节点。
-
思路
-
只有当左右孩子都为空的时候,才说明遍历的最低点了。如果其中一个孩子为空则不是最低点
-
- 层序遍历(BFS):一层一层地遍历二叉树。
-
- 遇到第一个叶子节点就返回:最小深度就是第一个被遍历到的叶子节点所在的层数。
-
- 每层遍历时,深度加一:每进入一层,depth++。
-
- 队列辅助:用队列保存每一层的节点,依次出队、入队
var minDepth = function(root) {
if (root === null) return 0; // 空树深度为0
let queue = [root];// 队列初始化,根节点入队
let depth = 0; // 记录当前深度
while (queue.length) { // 只要队列不空,就继续遍历
let n = queue.length; // 当前层节点数
depth++; // 每进入一层,深度+1
for (let i=0; i<n; i++) {
let node = queue.shift(); // 弹出队首节点
// 如果遇到第一个叶子节点(左右孩子都为空),直接返回当前深度
if (node.left === null && node.right === null) {
return depth;
}
// 左右孩子不为空的入队,等待下一层遍历
node.left && queue.push(node.left);;
node.right && queue.push(node.right);
}
}
return depth;
};