二叉树种类
- 满二叉树:如果一棵二叉树只有度为0的结点和度为2的结点,并且度为0的结点在同一层上,则这棵二叉树为满二叉树。深度为k,有2^k-1个节点。
- 完全二叉树:除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h 层,则该层包含 1~ 2^(h-1) 个节点。
- 二叉搜索树:前面介绍的树,都没有数值的,而二叉搜索树是有数值的。若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;它的左、右子树也分别为二叉排序。二叉搜索树是一个有序树。
- 平衡二叉搜索树:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
二叉树的存储方式
链式存储方式就用指针, 顺序存储的方式就是用数组。顺序存储的元素在内存是连续分布的,而链式存储则是通过指针把分布在各个地址的节点串联一起。
二叉树的遍历方式
- 深度优先遍历(前中后,其实指的就是中间节点的遍历顺序)
- 前序遍历(递归法,迭代法) =》LeetCode144.二叉树的前序遍历
- 中序遍历(递归法,迭代法) =》LeetCode94.二叉树的中序遍历
- 后序遍历(递归法,迭代法) =》LeetCode145.二叉树的后序遍历
- 广度优先遍历
- 层次遍历(迭代法)
二叉树的定义
//JavaScript版本
function TreeNode(val, left, right){
this.val = (val === undefined? 0:val)
this.left = (left === undefined? null:left)
this.right = (right === undefined? null:right)
}
二叉树的递归遍历
递归算法的三要素
-
确定递归函数的参数和返回值: 确定哪些参数是递归的过程中需要处理的,那么就在递归函数里加上这个参数, 并且还要明确每次递归的返回值是什么进而确定递归函数的返回类型。
-
确定终止条件: 写完了递归算法, 运行的时候,经常会遇到栈溢出的错误,就是没写终止条件或者终止条件写的不对,操作系统也是用一个栈的结构来保存每一层递归的信息,如果递归没有终止,操作系统的内存栈必然就会溢出。
-
确定单层递归的逻辑: 确定每一层递归需要处理的信息。在这里也就会重复调用自己来实现递归的过程。
-
//前序遍历 LeetCode144 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; } //中序遍历 LeetCode94 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; } //后序遍历 LeetCode145 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; }
递归也是用栈实现的,基本上大部分的递归都用栈的思想
二叉树的迭代遍历(非递归法重点掌握)
用栈实现
前序遍历是中左右,每次先处理的是中间节点,那么先将根节点放入栈中,然后将右孩子加入栈,再加入左孩子。中序遍历是左中右,先访问的是二叉树顶部的节点,然后一层一层向下访问,直到到达树左面的最底部,再开始处理节点(也就是在把节点的数值放进result数组中),这就造成了处理顺序和访问顺序是不一致的。
不能通过改一点前序遍历代码顺序就把中序遍历搞出来。在使用迭代法写中序遍历,就需要借用指针的遍历来帮助访问节点,栈则用来处理节点上的元素。
后序遍历,先序遍历是中左右,后续遍历是左右中,那么我们只需要调整一下先序遍历的代码顺序,就变成中右左的遍历顺序,然后在反转result数组,输出的结果顺序就是左右中了。
迭代普通写法
前序遍历:
// 入栈 右 -> 左
// 出栈 中 -> 左 -> 右
var preorderTraversal = function(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;
};
中序遍历:
// 入栈 左 -> 右
// 出栈 左 -> 中 -> 右
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;
};
后序遍历:
// 入栈 左 -> 右
// 出栈 中 -> 右 -> 左 结果翻转
var postorderTraversal = function(root, res = []) {
if (!root) {
return res;
}
const stack = [root];
let cur = null;
while(stack.length) {
cur = stack.pop();
res.push(cur.val);
cur.left && stack.push(cur.left);
cur.right && stack.push(cur.right);
}
return res.reverse();
};
迭代统一写法(前序和中序稍微改一下就行,但是不好理解)
使用栈的话,无法同时解决访问节点(遍历节点)和处理节点(将元素放进结果集)不一致的情况。那就需要将访问的节点放入栈中,把要处理的节点也放入栈中但是要做标记。如何标记呢,就是要处理的节点放入栈之后,紧接着放入一个空指针作为标记。 这种方法也可以叫做标记法。
将访问的节点直接加入到栈中,但如果是处理的节点则后面放入一个空节点, 这样只有空节点弹出的时候,才将下一个节点放进结果集。
//前序遍历:遍历顺序=》根左右,进栈顺序=》右左根
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;
};
二叉树的morris遍历(非递归法重点掌握)
后续补上
二叉树的层序遍历
层序遍历,即逐层地,从左到右访问所有节点
需要借用一个辅助数据结构即队列来实现,队列先进先出,符合一层一层遍历的逻辑,而用栈先进后出适合模拟深度优先遍历也就是递归的逻辑,而这种层序遍历方式就是图论中的广度优先遍历。
//层序遍历用【迭代法】
var levelOrder = function(root) {
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++) {
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;
};
<扩展>用递归实现 以后补上
思路:相对于102.二叉树的层序遍历,就是最后把result数组反转一下就可以了。
var levelOrderBottom = function(root) {
let res = [], queue = [];
queue.push(root);
while (queue.length && root !== null) {
let curLevel = []; // 存放当前层级节点数组
let length = queue.length; // 计算当前层级节点数量
while (length -- ) {
let node = queue.shift();
curLevel.push(node.val); // 把当前层节点存入curLevel数组
node.left && queue.push(node.left); // 把下一层级的左右节点存入queue队列
node.right && queue.push(node.right);
}
res.unshift(curLevel); // 从数组前头插入值,避免最后反转数组,减少运算时间
}
return res;
};
思路:层序遍历的时候,判断是否遍历到单层的最后面的元素,如果是,就放进result数组中,随后返回result就可以了。
var rightSideView = function(root) {
let res = [], queue = [];
queue.push(root);
while(queue.length && root !== null) {
let length = queue.length;
while (length -- ){
let node = queue.shift();
if (!length) { // length长度为0的时候表明到了层级最后一个节点
res.push(node.val);
}
node.left && queue.push(node.left);
node.right && queue.push(node.right);
}
}
return res;
};
var averageOfLevels = function(root) {
let res = [], queue = [];
queue.push(root);
while (queue.length && root !== null) {
let length = queue.length; //每一层节点个数
let sum = 0; //sum记录每一层的和
for (let i = 0; i < length; i++){
let node = queue.shift();
sum += node.val;
node.left && queue.push(node.left);
node.right && queue.push(node.right);
}
res.push(sum/length); //每一层的平均值存入数组res
}
return res;
};
var levelOrder = function(root) {
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);
//每一层可能有2个以上,所以不再使用node.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);
while(queue.length && root !== null) {
let max = queue[0].val; //设置max初始值就是队列的第一个元素
let length = queue.length;
while (length --) {
let node = queue.shift();
max = max > node.val ? max : node.val;
node.left && queue.push(node.left);
node.right && queue.push(node.right);
}
res.push(max);
}
return res;
};
LeetCode116:填充每个节点的下一个右侧节点指针(完美二叉树)
思路:本题依然是层序遍历,只不过在单层遍历的时候记录一下本层的头部节点,然后在遍历的时候让前一个节点指向本节点就可以了
var connect = function(root) {
if (root === null) {
return root;
};
let queue = [root];
while(queue.length) {
let length = queue.length;
for (let i = 0; i < length; i++) {
let node = queue.shift();
if (i < length-1) {
node.next = queue[0];
}
node.left && queue.push(node.left);
node.right && queue.push(node.right);
}
}
return root;
};
LeetCode117:填充每个节点的下一个右侧节点指针II(二叉树)
代码随想录的代码运行不了
思路:使用迭代法的话,使用层序遍历是最为合适的,因为最大的深度就是二叉树的层数,和层序遍历的方式极其吻合。
var maxDepth = function(root) {
if (root === null) {
return 0;
};
let queue = [root];
let height = 0;
while (queue.length) {
let length = queue.length;
height ++;
for (let i = 0; i < length; i++) {
let node = queue.shift();
node.left && queue.push(node.left);
node.right && queue.push(node.right);
}
}
return height;
};
var minDepth = function(root) {
if (root === null) {
return 0;
};
let queue = [root];
let depth = 0;
while (queue.length) {
let n = queue.length;
depth ++;
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;
};
翻转二叉树
只要把每一个节点的左右孩子翻转一下,就可以达到整体翻转的效果。这道题目使用前序遍历和后序遍历都可以,唯独中序遍历不方便,因为中序遍历会把某些节点的左右孩子翻转了两次!建议拿纸画一画,就理解了。那么层序遍历可以不可以呢?依然可以的!只要把每一个节点的左右孩子翻转一下的遍历方式都是可以的!
way1:递归
var invertTree = function(root) { //递归元素一:确定参数和返回值
if (!root) { //递归元素二:终止条件
return null;
};
//递归要素三:确定单层递归逻辑
const rightNode = root.right; //交换左右节点
root.right = invertTree(root.left);
root.left = invertTree(rightNode);
return root;
};
way2:迭代(统一写法)的前序遍历
var invertTree = function(root) {
//先定义节点交换函数
const inverNode = function(root, left, right) {
let temp = left;
left = right;
right = temp;
root.left = left;
root.right = right;
}
let stack = [];
if (root === null) {
return root;
}
stack.push(root);
while(stack.length) {
let node = stack.pop();
if(node !== null) {
node.right && stack.push(node.right);
node.left && stack.push(node.left);
stack.push(node);
stack.push(null);
} else {
node = stack.pop();
inverNode(node, node.left, node.right);
}
}
return root;
};
way3:层序遍历
var invertTree = function(root) {
//先定义节点交换函数
const inverNode = function(root, left, right) {
let temp = left;
left = right;
right = temp;
root.left = left;
root.right = right;
}
let queue = [];
if (root === null) {
return root;
}
queue.push(root);
while (queue.length) {
let length = queue.length;
while (length --) {
let node = queue.shift();
inverNode(node, node.left, node.right);
node.left && queue.push(node.left);
node.right && queue.push(node.right);
}
}
return root;
};
对称二叉树
判断对称二叉树要比较的是哪两个节点,要比较的可不是左右节点!
对于二叉树是否对称,要比较的是根节点的左子树与右子树是不是相互翻转的,理解这一点就知道了其实我们要比较的是两个树(这两个树是根节点的左右子树),所以在递归遍历的过程中,也是要同时遍历两棵树。
本题遍历只能是“后序遍历”,因为我们要通过递归函数的返回值来判断两个子树的内侧节点和外侧节点是否相等。
正是因为要遍历两棵树而且要比较内侧和外侧节点,所以准确的来说是一个树的遍历顺序是左右中,一个树的遍历顺序是右左中。
// 递归
var isSymmetric = function(root) {
// 使用递归遍历左右子树 递归三部曲
// 1. 确定递归的参数 root.left root.right和返回值true false
const compareNode = function(left, right) {
// 2. 确定终止条件 空的情况
if(left === null && right !== null || left !== null && right === null) {
return false;
} else if(left === null && right === null) {
return true;
} else if(left.val !== right.val) {
return false;
}
// 3. 确定单层递归逻辑
let outSide = compareNode(left.left, right.right);
let inSide = compareNode(left.right, right.left);
return outSide && inSide;
}
if(root === null) {
return true;
}
return compareNode(root.left, root.right);
};
// 队列
var isSymmetric = function(root) {
// 迭代方法判断是否是对称二叉树
// 首先判断root是否为空
if(root === null) {
return true;
}
let queue = [];
queue.push(root.left);
queue.push(root.right);
while(queue.length) {
let leftNode = queue.shift(); //左节点
let rightNode = queue.shift(); //右节点
if(leftNode === null && rightNode === null) {
continue;
}
if(leftNode === null || rightNode === null || leftNode.val !== rightNode.val) {
return false;
}
queue.push(leftNode.left); //左节点左孩子入队
queue.push(rightNode.right); //右节点右孩子入队
queue.push(leftNode.right); //左节点右孩子入队
queue.push(rightNode.left); //右节点左孩子入队
}
return true;
};
// 栈
var isSymmetric = function(root) {
// 迭代方法判断是否是对称二叉树
// 首先判断root是否为空
if(root === null) {
return true;
}
let stack = [];
stack.push(root.left);
stack.push(root.right);
while(stack.length) {
let rightNode = stack.pop(); //左节点
let leftNode=stack.pop(); //右节点
if(leftNode === null && rightNode === null) {
continue;
}
if(leftNode === null || rightNode === null || leftNode.val !== rightNode.val) {
return false;
}
stack.push(leftNode.left); //左节点左孩子入队
stack.push(rightNode.right); //右节点右孩子入队
stack.push(leftNode.right); //左节点右孩子入队
stack.push(rightNode.left); //右节点左孩子入队
}
return true;
};
<扩展>
- 100.相同的树
- 572.另一个树的子树