二叉树的知识点很多,在算法刷题中需要有想象力的数据结构了。主要是用链表存储,没有数组更容易理解。在刷二叉树相关算法时,需要注意以下几点:
-
掌握二叉树的基本概念:了解二叉树的基本概念,包括二叉树的定义、遍历方式(前序、中序、后序、层序遍历)、性质等。
-
熟练掌握二叉树的遍历算法:熟练掌握二叉树的前序、中序、后序遍历算法,以及它们的递归和迭代实现方式。
-
学习常见的二叉树算法题目:包括但不限于二叉树的最大深度、判断平衡二叉树、路径总和、对称二叉树等常见题目。
-
练习二叉树的递归和迭代实现:练习使用递归和迭代方式解决二叉树相关问题,加深对算法的理解和应用。
-
注意二叉树的性质和特点:了解二叉树的性质和特点,如完全二叉树、平衡二叉树、二叉搜索树等,可以更好地解决相关问题。
目录
简单介绍一下二叉树相关概念
什么是二叉树
二叉树是一种树形结构,每个节点最多有两个子节点(左子节点和右子节点)。根节点是位于树顶部的节点,叶子节点是没有子节点的节点。二叉树的每个节点最多有两个子节点,分别称为左子节点和右子节点。
二叉树相关术语
- 根节点:二叉树的顶部节点称为根节点。
- 叶子节点:没有子节点的节点称为叶子节点。
- 深度:从根节点到某个节点的唯一路径上的节点数称为该节点的深度。
- 高度:从某个节点到叶子节点的最长路径上的节点数称为该节点的高度。
- 子树:二叉树中每个节点都可以看作是根节点,它的左子树和右子树称为该节点的子树。
二叉树的遍历方式
- 前序遍历(Preorder Traversal):根节点 -> 左子树 -> 右子树
- 中序遍历(Inorder Traversal):左子树 -> 根节点 -> 右子树
- 后序遍历(Postorder Traversal):左子树 -> 右子树 -> 根节点
- 层序遍历(Level Order Traversal):逐层从上到下,从左到右遍历节点
二叉树的分类
- 满二叉树:每个节点要么没有子节点,要么有两个子节点。
- 完全二叉树:除了最后一层外,每一层的节点都是满的,且最后一层的节点靠左排列。
- 平衡二叉树:左右子树的高度差不超过1的二叉树。
- 二叉搜索树(BST):左子树上所有节点的值均小于根节点的值,右子树上所有节点的值均大于根节点的值。
二叉树的表示方式
- 链式存储结构:通过节点之间的引用关系来表示二叉树。
- 顺序存储结构:使用数组来表示二叉树,按照层序遍历的顺序存储节点。
二叉树的常见操作
- 插入节点:在二叉树中插入新节点,保持二叉树的结构特性。
- 删除节点:从二叉树中删除指定节点,保持二叉树的结构特性。
- 查找节点:在二叉树中查找指定值的节点。
- 判断是否为平衡二叉树:判断二叉树的左右子树高度差是否小于等于1,从而判断是否为平衡二叉树。
二叉树递归技巧
- 写出结束条件
- 不要把树复杂化,就当做树是三个节点,根节点,左子节点,右子节点
- 只考虑当前做什么,不用考虑下次应该做什么
- 每次调用应该返回什么
话不多说,上题目
二叉树的遍历
144. 二叉树的前序遍历
给你二叉树的根节点 root
,返回它节点值的 前序 遍历。
这里需要将遍历的结果存起来,需要定义一个数组。二叉树的遍历是一个递归的过程,考虑单独建一个函数,将arr作为入参传进去。对这个函数使用递归。最后返回arr。arr是一个引用类型,所以不需要函数返回值
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @return {number[]}
*/
var preorderTraversal = function (root) {
//定义数组存放遍历结果
let arr = [];
//将数组作为入参传进去
preorder(root, arr);
return arr;
};
function preorder(root, res) {
if (root) {
//前序遍历先存根节点
res.push(root.val);
//遍历左子树
preorder(root.left, res);
//遍历右子树
preorder(root.right, res);
}
}
或者用数组的concat方法拼接遍历结果
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @return {number[]}
*/
var preorderTraversal = function (root) {
if(!root)return [];
return [root.val].concat(preorderTraversal(root.left)).concat(preorderTraversal(root.right));
};
栈的形式
思路:从根节点开始,入栈。栈存放未遍历的节点。入栈顺序是先后后左。则出栈是先左后右。根节点在第一次出栈时已确定位置,只用考虑左右子树的入栈顺序即可
var preorderTraversal = function (root) {
if (!root) return [];
let arr = [];
let stack = [root];
while (stack.length) {
const o = stack.pop();
//出栈的顺序是遍历的顺序
arr.push(o.val);
//入栈顺序先后后左
o.right && stack.push(o.right);
//左子树后入栈,下次pop时先处理
o.left && stack.push(o.left);
}
return arr;
};
94. 二叉树的中序遍历
给定一个二叉树的根节点 root
,返回 它的 中序 遍历 。
思路:递归
先左,存根节点的val,在右
递归遍历时左右子树也会充当root,获取到val,所以只用考虑为空的时候
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @return {number[]}
*/
var inorderTraversal = function (root) {
if (!root) return [];
return inorderTraversal(root.left).concat(root.val).concat(inorderTraversal(root.right));
};
思路:用栈实现
遇到根节点先push进栈,去找它的左子树,一直找到最后一个左子树
这个时候就找到中序遍历的第一个节点了,也就是放在栈顶元素,将其pop出来。然后去找右子树。同理,找右子树也先找其左孩子节点。如果右节点为空,继续栈顶pop,每次pop的都是未遍历的节点。
嗯。。。这里需要手动画个树思考一下
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @return {number[]}
*/
var inorderTraversal = function (root) {
if (!root) return [];
const arr = [];
const stack = [];
let current = root;
while (current || stack.length) {
//每次遍历让current走到最左边
while (current) {
stack.push(current);
current = current.left;
}
//最左边没有了话弹出当前处于最左边的叶子节点
const o = stack.pop();
arr.push(o.val);
//弹出节点的右子树作为当前节点
current = o.right;
}
return arr;
};
注意:结束循环的条件是current没结束,或者栈里面还有没遍历的对象
145. 二叉树的后序遍历
给你一棵二叉树的根节点 root
,返回其节点值的 后序遍历 。
思路:递归版本的后序遍历也是很简单的,利用数组的concat方法可以不用再新增数组进行存储。先遍历左子树,在右子树,最后是根节点,放val值。
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @return {number[]}
*/
var postorderTraversal = function (root) {
if (!root) return [];
return postorderTraversal(root.left).concat(postorderTraversal(root.right)).concat(root.val);
};
递归遍历相当于维护了一个隐藏的栈,如果用栈来暂存节点怎么实现?
首先后序遍历,根节点是放在数组的最后一项,最先找到的是左子树最左边的节点。放入的顺序是【左右根左右根...根(root)】
查找节点的顺序是不是和前序遍历很像,只不过前序遍历先将根的val存起来,而后序遍历在左右子树查找完后再存。那如果换个角度思考,如果我遍历的时候遇到根节点将其放在未遍历节点的后面是不是就行了,其次是右节点放在未遍历前面,在然后是左节点放在未遍历前面。
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @return {number[]}
*/
var postorderTraversal = function (root) {
if (!root) return [];
let arr =[];//存放后序遍历的数组
let stack =[root];
//后序遍历入数组的顺序是左右根,按照前序遍历的思路入栈,但是存数组的时候从数组头部插入,而不是尾部
while(stack.length){
const o = stack.pop();
arr.unshift(o.val);
o.left && stack.push(o.left);
o.right && stack.push(o.right);
}
return arr;
};
push入栈的顺序是先左在右,这样在pop的时候右子树先出栈,进数组也是先进去。
这里arr用unshift方法每次在已遍历节点的头部插入当前遍历元素
102. 二叉树的层序遍历
给你二叉树的根节点 root
,返回其节点值的 层序遍历 。 (即逐层地,从左到右访问所有节点)。
思路:二叉树层序遍历,依次打印每层的节点信息。需要将未遍历的节点放入数组中,按照先入先出的顺序进行遍历。因此层序遍历必须结合队列来实现。队列存储的是当前所有未遍历节点的集合。左子树先入栈,其次右子树。在循环当前第n层时,第n-1层肯定已遍历完,因此通过队列的长度拿到当前层节点个数,这个长度需要提前保留,因为队列在当前层遍历中还会增加长度。这里用len表示当前层的节点数目。
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @return {number[][]}
*/
var levelOrder = function(root) {
//处理边界
if(!root) return [];
let queue = [root];//用队存储未遍历节点,左节点先入队
let res=[];//存储遍历结果
while(queue.length){//只要有未遍历左右子树节点存在,继续
let len = queue.length;//len当前层节点的个数,for循环结束的边界
let temp = [];
for(let i =0; i<len; i++){
const node = queue.shift();//按照入队的顺序,先入先出,左节点先出
temp.push(node.val);
node.left && queue.push(node.left);//如果出队的节点有左孩子,左子树先入队
node.right && queue.push(node.right);//右子树后入队
}
res.push(temp);//将当前层遍历结果push到结果数组中
}
return res;
};
106. 从中序与后序遍历序列构造二叉树
给定两个整数数组 inorder
和 postorder
,其中 inorder
是二叉树的中序遍历, postorder
是同一棵树的后序遍历,请你构造并返回这颗 二叉树 。
思路:利用中序遍历和后序遍历构建二叉树。中序遍历:左根右;后序遍历:左右根
通过后序遍历的最后一个数是不是可以判断二叉树的根节点,根据二叉树的根节点在中序遍历的位置,是不是可以把二叉树划分为左右子树。利用递归的特性构建左右子树。
那二叉树左右子树的后序遍历怎么得到呢?比如示例一:
找到根节点是3,3将中序遍历划分左子树中序遍历结果[9] 右子树中序遍历[15,20,7]。怎么去找左子树的后序遍历呢,我们肉眼可以区分,后序遍历是不是[9]?那为什么是[9]。
从左根右,左右根,这个规律可以看出来,后序遍历左右子树的结果是挨着的,就是[左子树的后序遍历]+[右子树的后序遍历]+根是不是一个完整的后序遍历结果?那么这个划分左右子树的index怎么找?
别急,在去看中序遍历根的位置,左根右,诶?后序遍历右子树第一个节点是不是和中序遍历根节点所在index是一样的啊?
所以我们找到了根所在中序遍历的index,就可以将中序、后序遍历的结果也能拆分成左右子树的中序、后序遍历结果
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {number[]} inorder
* @param {number[]} postorder
* @return {TreeNode}
*/
var buildTree = function (inorder, postorder) {
if (inorder.length === 0) return null;
//中序:左根右 后序遍历:左右根
//首先拿到根节点的value
let rootVal = postorder[postorder.length - 1];
let root = new TreeNode(rootVal);
//通过根节点去找中序遍历的index
const index = inorder.indexOf(rootVal);
//根据中序遍历的index,将中序和后序划分为左子树的中序、后序;右子树的中序、后序
//递归创建左右子树
root.left = buildTree(inorder.slice(0, index), postorder.slice(0, index));//slice(a,b)包前不包含后
root.right = buildTree(inorder.slice(index + 1), postorder.slice(index, postorder.length - 1));
return root;
};
由于题目明确说了,数组每个值都不同,根据根节点val找根所在中序遍历的index使用js的indexOf方法。找到index后根据slice方法复制原数组,将中序、后序划分为左右子树的遍历结果。
右子树的中序遍历结果是inorder.slice(index+1),因为要把根所在的index跳过。
右子树的后序遍历结果是postorder.slice(index,postorder.length-1),也是要把最后一个根节点跳过。
105. 从前序与中序遍历序列构造二叉树
给定两个整数数组 preorder
和 inorder
,其中 preorder
是二叉树的先序遍历, inorder
是同一棵树的中序遍历,请构造二叉树并返回其根节点。
思路:找到了根所在中序遍历的index,就可以将中序、后序遍历的结果也能拆分成左右子树的中序、先序遍历结果 。根左右,左根右。从中序遍历结果可以确定右子树的起始是rootIndex+1。先序中的右子树序列也是rootIndex+1。先序遍历确定左子树,因为左右子树的序列是挨着的,所以1-rootIndex+1,就是左子树在先序中的序列。
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {number[]} preorder
* @param {number[]} inorder
* @return {TreeNode}
*/
var buildTree = function (preorder, inorder) {
if (!preorder.length) return null;
const root = new TreeNode(preorder[0]);//创建一个根节点
const index = inorder.indexOf(root.val);//获取根节点在中序遍历的位置
root.left = buildTree(preorder.slice(1, index + 1), inorder.slice(0, index));//从中序遍历找出左节点遍历的序列构建左子树
root.right = buildTree(preorder.slice(index + 1), inorder.slice(index + 1));//递归构建右子树
return root;//返回构建的根
};
LCR 151. 彩灯装饰记录 III
一棵圣诞树记作根节点为 root
的二叉树,节点值为该位置装饰彩灯的颜色编号。请按照如下规则记录彩灯装饰结果:
- 第一层按照从左到右的顺序记录
- 除第一层外每一层的记录顺序均与上一层相反。即第一层为从左到右,第二层为从右到左。
思路:这题原来是叫从上到下打印二叉树。其实就是Z 之形,来回打印二叉树。中等题目,有点难度。正常来说,可以使用一个队列来辅助进行层序遍历。首先将根节点入队,然后进入循环,每次循环处理一层的节点。在循环中,首先获取当前层的节点个数,然后依次出队这些节点,并将它们的值存入当前层的数组。同时,将每个节点的左右子节点入队。最后,将当前层的数组存入结果数组。重复以上步骤,直到队列为空。但是这里要考虑相邻的两层的顺序不一样。对于逆序的需要翻转当前层,你可以正常push然后reverse,也可以在需要翻转的层用unshift从头插入实现。所以本题是二叉树的层序遍历+相邻层翻转即可实现
代码如下:通过isReverse标识当前层是否需要翻转。for循环处理当前层,通过len控制队列遍历范围。for循环中push入队的节点会在下次循环进入,不影响当前遍历。节点入队的顺序还是先左后右。只在收集当前层遍历结果时处理翻转的情况。for循环结束当前层的打印结果拿到,push到res中,并翻转isReverse的状态
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @return {number[][]}
*/
var decorateRecord = function (root) {
if (root === null) return []; //边界条件
const queue = [root]; //队列存放未遍历左右子树的节点
let ifReverse = false; //标记当前是奇数还是偶数
let res = []; //存储结果
while (queue.length) {
let temp = []; //某一层遍历结果
//记住上一个队列的长度,将其遍历完入temp数组
const len = queue.length;
for (let i = 0; i < len; i++) {
const node = queue.shift(); //左节点先出队头先出
if (ifReverse) { //判断从左到右插入,还是从右到左
temp.unshift(node.val);//左节点靠右排列
} else {
temp.push(node.val);//左节点正常靠左排列
}
//左右子树正常入队排在当前遍历节点的后面, 不影响当前循环
node.left && queue.push(node.left);
node.right && queue.push(node.right);
}
res.push(temp);
ifReverse = !ifReverse;
}
return res;
};
404. 左叶子之和
给定二叉树的根节点 root
,返回所有左叶子之和。
思路:利用二叉树的任意一种递归遍历方式,统计遍历过程中的左节点之和。关于左节点的判断,可以通过其父节点进行。对父节点进行如下判断
root.left要存在,并且root.left的左右子节点都不存在,那么root.left才是我们要找的左叶子节点。在统计sum时,不能放在递归函数里定义,因为这是个常量,非引用类型,在递归返回的时候会被恢复掉。
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @return {number}
*/
var sumOfLeftLeaves = function (root) {
let sum = 0;
function preTraversal(root) {
if (!root) return 0;//为null节点返回
if (root.left && !root.left.left && !root.left.right) {//核心:判断左叶子节点
sum += root.left.val;
}
preTraversal(root.left);//递归遍历左子树
preTraversal(root.right);//递归遍历右子树
}
preTraversal(root);
return sum;
};
2415. 反转二叉树的奇数层
给你一棵 完美 二叉树的根节点 root
,请你反转这棵树中每个 奇数 层的节点值。
- 例如,假设第 3 层的节点值是
[2,1,3,4,7,11,29,18]
,那么反转后它应该变成[18,29,11,7,4,3,1,2]
。
反转后,返回树的根节点。
完美 二叉树需满足:二叉树的所有父节点都有两个子节点,且所有叶子节点都在同一层。
节点的 层数 等于该节点到根节点之间的边数。
思路:看到针对每一层的处理,是不是可以相等层序遍历。因为层序遍历每次都能得到一层的所有节点。那么这题就转换为如何将同层节点的值进行翻转了。那么是不是可以用双端指针进行翻转。js交换地址可以用数组解构的方式快速交换。
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @return {TreeNode}
*/
var reverseOddLevels = function (root) {
if (!root) return root;
let isReverse = false;
let queue = [root];
while (queue.length) {
let len = queue.length;
//暂停,处理当前层的节点翻转现将队列中0到len-1的元素进行reverse
if (isReverse) {
let i = 0, j = len - 1;
while (i < j) {
[queue[i].val, queue[j].val] = [queue[j].val, queue[i].val];//用数组解构的方式交换值
i++;
j--;
}
}
//在处理层序遍历即可
for (let i = 0; i < len; i++) {
const node = queue.shift();
node.left && queue.push(node.left);
node.right && queue.push(node.right);
}
isReverse=!isReverse;
}
return root;
};
199. 二叉树的右视图
给定一个二叉树的 根节点 root
,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。
思路:很简单,利用层序遍历,记录每层的最后一个节点。其余节点正常进行层序入队操作
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @return {number[]}
*/
var rightSideView = function (root) {
if (!root) return [];
//层序遍历,记录每层的最右侧的节点
let res = [];
let queue = [root];
while (queue.length) {
let len = queue.length;
res.push(queue[len - 1].val);//记录当前层的最后一个元素
for (let i = 0; i < len; i++) {//层序遍历基操,从左到右加入子节点
const node = queue.shift();
node.left && queue.push(node.left);
node.right && queue.push(node.right);
}
}
return res;
};
114. 二叉树展开为链表
给你二叉树的根结点 root
,请你将它展开为一个单链表:
- 展开后的单链表应该同样使用
TreeNode
,其中right
子指针指向链表中下一个结点,而左子指针始终为null
。 - 展开后的单链表应该与二叉树 先序遍历 顺序相同。
思路:这个题很考察逻辑能力。观察示例一,想要构建这样的链表,右子树一定要先于左子树处理。否则,最后构建的指针指向的是尾部的6,而不是头部的5,那么头结点就丢失了。 假设我们希望每次能得到链表的最后一个节点,回退的时候处理之前的节点。那么我们处理的顺序应该是654321,右左根的后序遍历。 按照这个思想,用一个pre节点记录前一个节点处理结果。那么在6的时候pre=6,在5的时候,让5.right=pre,5.left清空。让5的right=pre是不是多此一举?不是,如果pre=5,处理节点是4,那么4.right=5,是不是很合理。那让left=null会不会丢失left的数据?其实不会,因为left也会优先root处理。比如在处理2的时候,3已经处理好,并且作为pre节点保存下来了。
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @return {void} Do not return anything, modify root in-place instead.
*/
var flatten = function (root) {
//右左根方式重构节点关系
let pre = null;
function help(node) {
if (!node) return;
help(node.right);
help(node.left);
//处理根逻辑
if (pre) {//如果尾节点有了,尾部已经串联好,将node的左右子树放心更改
node.right=pre;
node.left=null;
}
pre = node;//递归回退的时候第一个处理的节点是最右子树叶子节点,也就是新链表的尾节点
}
help(root);
};
特殊二叉树:二叉搜索树
二叉搜索树 (Binary Search Tree, BST) 是一种重要的数据结构,它具有以下特点:
- 左子树的所有节点值都小于根节点的值;
- 右子树的所有节点值都大于根节点的值;
- 左、右子树也是二叉搜索树。
这些特点使得二叉搜索树在算法中具有一些独特的优势和应用场景。以下是二叉搜索树在算法中的一些使用技巧:
- 中序遍历:由于二叉搜索树的中序遍历结果是一个有序的递增数组,因此可以利用这一特点来解决一些问题。例如,可以使用中序遍历来查找两个相邻元素的差值,或者查找第 k 大的元素等。有时候也可以用右根左的中序遍历方式,处理降序的逻辑!
- 值比较减少递归次数:由于二叉搜索树的节点值具有可比性,因此可以在递归的时候减少递归次数,只向左递归或向右递归。例如,可以使用值比较来查找一个目标值,或者统计大于或小于一个阈值的节点数等。
- 高效的插入和删除:二叉搜索树可以高效地插入和删除节点,时间复杂度为 O(log n)。因此,可以使用二叉搜索树来构建一个高效的数据结构,例如可以使用二叉搜索树来实现一个高效的字典或集合。
- 分治和递归:二叉搜索树可以使用分治和递归的方法来解决一些问题。例如,可以使用递归来计算二叉搜索树的高度,或者使用分治来计算二叉搜索树的节点数等。
700. 二叉搜索树中的搜索
给定二叉搜索树(BST)的根节点 root
和一个整数值 val
。
你需要在 BST 中找到节点值等于 val
的节点。 返回以该节点为根的子树。 如果节点不存在,则返回 null
。
思路:在二叉搜索树中进行搜索,考虑递归处理。只考虑三个节点,如果当前节点不存在,返回null。当前节点存在考虑三种情况
当前节点的值等于val返回root
当前节点值大于val,从左子树找,返回左子树查找结果
当前节点值小于val,从右子树找,返回右子树查找结果
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @param {number} val
* @return {TreeNode}
*/
var searchBST = function (root, val) {
if (!root) return null;
if (root.val === val) return root;
if (val < root.val) {
return searchBST(root.left, val);
} else {
return searchBST(root.right, val);
}
};
230. 二叉搜索树中第K小的元素
给定一个二叉搜索树的根节点 root
,和一个整数 k
,请你设计一个算法查找其中第 k
个最小元素(从 1 开始计数)。
思路:针对给定的二叉搜索树,我们可以使用中序遍历来找到第 k 个最小元素。由于二叉搜索树的中序遍历是有序的,因此第 k 个遍历到的节点就是第 k 个最小元素。
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @param {number} k
* @return {number}
*/
var kthSmallest = function (root, k) {
//中序遍历二叉搜索树,统计遍历的次数count
let count = 0;
let find;
function midTraversal(root) {
if (!root) return;
midTraversal(root.left);
//处理逻辑
count++;
if (count == k) {
find = root.val;
return;
}
midTraversal(root.right);
}
midTraversal(root);
return find;
};
如果二叉搜索树经常被修改(插入/删除操作)并且需要频繁地查找第 k 小的值,我们可以在每个节点中记录其左子树的节点数量。我们可以根据节点的左子树节点数量和当前 k 的大小来决定继续向左子树还是右子树遍历,从而快速定位第 k 个最小元素。
530. 二叉搜索树的最小绝对差
给你一个二叉搜索树的根节点 root
,返回 树中任意两不同节点值之间的最小差值 。
差值是一个正数,其数值等于两值之差的绝对值。
思路:看到二叉搜索树一定要想到这句话:中序遍历二叉搜索树等于遍历有序数组!
利用二叉树中序遍历的特性,可以将题目转换为从有序数组中找到最小的差值,最小差值怎么产生的?是不是在相邻元素中间啊?在有序数组里是不是可以双指针进行比较,pre永远记录cur的前一个元素。在二叉树里能否也进行双指针遍历呢,我们思考pre怎么让它从头开始?
是不是可以让pre=null强制它是第一个节点。cur在中序遍历的时候,一直让它递归左子树,左子树递归后,cur是不是得到了中序遍历的第一个的节点。OK,剩下的过程看代码就好啦
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @return {number}
*/
var getMinimumDifference = function (root) {
let min = Infinity;//min默认值取最大
let pre = null;//左指针默认最左侧节点中序遍历的第一个节点
function midTraversal(root) {
if (!root) return;
midTraversal(root.left);//中序递归遍历左子树
//处理逻辑
if (pre) {
min = Math.min(root.val - pre.val, min);//中序遍历的第二个节点开始统计差值
}
pre = root;//将当前的root复制给pre
midTraversal(root.right);//中序递归遍历右子树
}
midTraversal(root);
return min;
};
501. 二叉搜索树中的众数
给你一个含重复值的二叉搜索树(BST)的根节点 root
,找出并返回 BST 中的所有 众数(即,出现频率最高的元素)。
如果树中有不止一个众数,可以按 任意顺序 返回。
假定 BST 满足如下定义:
- 结点左子树中所含节点的值 小于等于 当前节点的值
- 结点右子树中所含节点的值 大于等于 当前节点的值
- 左子树和右子树都是二叉搜索树
思路:看到二叉搜索树一定要想到这句话:中序遍历二叉搜索树等于遍历有序数组,!
题目要找众数,根据二叉搜索树的特性,经过中序递归遍历可以得到一个有序的递增数组。对有序数组求众数是不是可以由下面两种方法
- 采用字典统计元素key和元素出现的次数value,然后根据value大小拍个序拿到对应的key组成的数组就行了。
- 或者使用双指针,遍历一次数组,找众数。因为众数会有多个,用count和maxCount和res进行配合,如果count=maxCount就当前值push进res,如果count>maxCount将res清空,push新的值。这样就能保证双指针只遍历一遍数组。
但是,我在中序遍历二叉树的时候,是不是就是从头遍历这个有序数组呢?我是不是不用遍历两边二叉树,一遍拿到有序数组,一遍遍历这个有序数组用双指针。本质是不是遍历了两次二叉树?
我是不是可以在中序遍历的时候处理这个双指针?递归的当前节点是cur,pre节点最开始是不是最左侧的节点。在递归过程中cur从有序数组的左向后移动,pre也可以根据cur的移动就行变换?Ok,直接看代码
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @return {number[]}
*/
var findMode = function (root) {
let pre = null, cur = root;
let count = 0, maxCount = 0;
let res = [];
function midTraversal(cur) {
if (!cur) return;//递归结束条件
midTraversal(cur.left);//中序遍历先遍历左子树
//处理逻辑,第一个处理节点一定是最左侧的节点
if (pre == null) {//pre 和cur充当了双指针
count = 1;
} else if (pre.val === cur.val) {//二叉搜索树的中序遍历是一个有序数组,相同元素肯定是挨着的
count++;
} else {
count = 1;
}
pre = cur;
//处理count
if (count == maxCount) {
res.push(cur.val);
} else if (count > maxCount) {
res = [];
res.push(cur.val);
maxCount=count;
}
midTraversal(cur.right);//中序遍历最后遍历右子树
}
midTraversal(root);
return res;
};
LCR 174. 寻找二叉搜索树中的目标节点
某公司组织架构以二叉搜索树形式记录,节点值为处于该职位的员工编号。请返回第 cnt
大的员工编号。
思路:前面都提到二叉搜索树的中序遍历是递增的有序数组,那反过来,是不是可以得到一个递减的数组?怎么遍历呢?
中序遍历是不是左根右。那我先遍历右子树,右根左,反方向的中序遍历,是不是得到降序排列的数组。如何得到count大的值呢。由于递归找的第一个节点是不是最大的那个,在递归回退的时候让count--就好了,count=0的时候就是递归结束,返回当前的root节点值
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @param {number} cnt
* @return {number}
*/
var findTargetNode = function (root, cnt) {
let count = cnt;
let target;
//二叉搜索树 右根左遍历 得到降序排列的序列
function traversal(root) {
if (!root) return;//递归结束条件1,叶子节点边缘
traversal(root.right);
//处理逻辑 第一个处理的节点一定是最大的节点,最右边的节点
count--;//通过count--的方式,从后向前找
if (count == 0) {
target = root.val;//递归结束条件2:找到了target
return;
}
traversal(root.left);
}
traversal(root);
return target;
};
538. 把二叉搜索树转换为累加树
给出二叉 搜索 树的根节点,该树的节点值各不相同,请你将其转换为累加树(Greater Sum Tree),使每个节点 node
的新值等于原树中大于或等于 node.val
的值之和。
提醒一下,二叉搜索树满足下列约束条件:
- 节点的左子树仅包含键 小于 节点键的节点。
- 节点的右子树仅包含键 大于 节点键的节点。
- 左右子树也必须是二叉搜索树。
思路:观察这个题目,最开始处理的是不是最右侧的节点,然后回溯向上处理。是不是可以用右根左这种中序遍历方式。先处理右子树,然后根,然后是左子树。在递归过程中就可以更改val值,然后用全局变量sum存储过程中的值,已更改的val就不用管了。递归结束后,从右到左,所有节点都会被遍历一遍,结果得到了累加。最左侧的节点一定是总和。
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @return {TreeNode}
*/
var convertBST = function (root) {
let sum = 0;
//中序遍历右中左得到降序排列的数组
function midTraversal(root) {
if (!root) return;//边界返回
midTraversal(root.right);
//处理逻辑,最开始找到的是最右侧的节点
root.val += sum;
sum = root.val;
midTraversal(root.left);
}
midTraversal(root);
return root;
};
98. 验证二叉搜索树
给你一个二叉树的根节点 root
,判断其是否是一个有效的二叉搜索树。
有效 二叉搜索树定义如下:
- 节点的左
子树
只包含 小于 当前节点的数。 - 节点的右子树只包含 大于 当前节点的数。
- 所有左子树和右子树自身必须也是二叉搜索树。
思路:这个题一开始想用递归做,但发现卡太多测试用例了。递归只能判断每个子节点都满足,却无法满足根节点,像下面的用例就不行。那如何一举遍历歼灭?是不是利用二叉搜索树中序遍历可以得到有序数组?那就中序遍历把。中序遍历比较节点必须要先生成数组吗,可不可以在递归过程中找到不符合条件的节点然后提前结束递归呢?其实是可以的,只要找到最左侧的节点pre,将其初始化为null,然后找到第一个非null的节点时,将pre=它就好啦。然后在递归回退的时候将pre不断更新。那么root和pre进行比较,如果pre>=root是不是就false了。pre和root表示中序遍历有序数组相邻的两个节点。
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @return {boolean}
*/
var isValidBST = function (root) {
//中序遍历
let pre = null;//中序遍历中的左指针,最开始指向最左侧的左节点
let flag = true;//默认是true,过程中只要是false了,就结束
function midTraversal(root) {
if (!root) return;
midTraversal(root.left);
//处理根节点
if (!pre) {
pre = root;
} else if (pre.val >= root.val) {
flag = false;
return;
}
pre = root;
midTraversal(root.right);
}
midTraversal(root);
return flag;
};
99. 恢复二叉搜索树
给你二叉搜索树的根节点 root
,该树中的 恰好 两个节点的值被错误地交换。请在不改变其结构的情况下,恢复这棵树 。
思路:利用二叉搜索树中序遍历的特性,在遍历中如果遇到了cur节点的值小于了pre节点的值,那么找到了第一个错误的节点。那交换的节点怎么找呢?
观察一下题目,示例1和2第一个错误的节点都是3,交换的节点都是在3后面遍历,且比3小的节点。那是不是只要找到比3小的节点就进行替换呢?我一开始也是这么想的,后来发现有测试用例失败,像下面这个,3是错误节点,跟谁交换,是不是1,看清,不是2。所以,与第一个错误节点交换的是后面比3小的最小的节点。在遍历过程中,只要比第一个错误的节点小就重新赋值。这样就避免了下面的情况。
代码如下:这里为了代码清除,定义了errorFirst第一次找到错误的节点,全局存储。定义了errorSecond,进行交换的节点。在一次中序遍历即可找到这两个节点,然后进行数值交换就行了。
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @return {void} Do not return anything, modify root in-place instead.
*/
var recoverTree = function (root) {
//一次中序遍历,先找第一个错误的节点,再从这个错误节点的后面节点中比错误节点小的节点
let pre = null;
let cur = root;
let errorFirst = null,
errorSecond = null;
function midTraversal(cur) {
if (!cur) return;
midTraversal(cur.left);
//处理逻辑
if (!errorFirst && pre && pre.val > cur.val) {//errorFirst只赋值一次
errorFirst = pre;
}
if (errorFirst && cur.val < errorFirst.val) {//找到errorFirst后再去找errorSecond
errorSecond = cur;//可以多次赋值,取最小的一个,最后赋值的就是最小的
}
pre = cur;
midTraversal(cur.right);
}
midTraversal(cur);
//找到两个出错的节点,进行交换
[errorFirst.val, errorSecond.val] = [errorSecond.val, errorFirst.val];
return root;
};
108. 将有序数组转换为二叉搜索树
给你一个整数数组 nums
,其中元素已经按 升序 排列,请你将其转换为一棵平衡二叉搜索树。
思路:观察这棵树,将有序数组转换为二叉搜索树,二叉树的根节点是不是数组的中间元素。二叉搜索树其实是跟快排类似,根节点充当了基准点。比基准小的在树的左边,比基准大的在树的右边。那么构建树的过程其实就是不断对数组进行二分的过程。是不是可以用递归进行处理。
前面说了,写递归不用考虑太多,只考虑三个节点,根和左右子节点。只关心根节点的构建过程,左右子节点采用递归处理。
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {number[]} nums
* @return {TreeNode}
*/
var sortedArrayToBST = function (nums) {
//边界条件
if (!nums.length) return null;
if (nums.length === 1) return new TreeNode(nums[0]);
//取中间的值做根节点
let mid = Math.floor(nums.length / 2);
//递归处理左右子树
return new TreeNode(nums[mid], sortedArrayToBST(nums.slice(0, mid)), sortedArrayToBST(nums.slice(mid + 1)));
};
235. 二叉搜索树的最近公共祖先
给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
例如,给定如下二叉搜索树: root = [6,2,8,0,4,7,9,null,null,3,5]
思路:先将p、q节点值区分出较小值pre的和较大值post。利用二叉搜索树的特性,根节点的左边比根节点小;根节点的右边比根节点大。找祖先,就找根节点就行了。如果一开始pre和post就分布在root的两端,那么root就是祖先,不用递归了。否则,利用中序遍历,依次将每个节点都作为root,尝试pre和post是否可以放在root的两端 。如果可以,则返回遍历过程中的root。
/**
* Definition for a binary tree node.
* function TreeNode(val) {
* this.val = val;
* this.left = this.right = null;
* }
*/
/**
* @param {TreeNode} root
* @param {TreeNode} p
* @param {TreeNode} q
* @return {TreeNode}
*/
var lowestCommonAncestor = function (root, p, q) {
//将p、q区分出大小,pre是两者较小的,post是两者较大的
const pre = p.val < q.val ? p : q;
const post = p.val < q.val ? q : p;
//一开始与根节点比,如果p、q分布在root的两端,只能是root节点
if (pre.val <= root.val && post.val >= root.val) {
return root;
}
//如果p、q在root的左侧或右侧,利用中序遍历二叉树。递归每个节点都当这个root。
//让p和q尝试分布在root两端
let target;
function midTraversal(root) {
if (!root) return;
midTraversal(root.left);
//处理逻辑
if (pre.val <= root.val && post.val >= root.val) {
target = root;//遍历过程中找到了target,结束
return;
}
midTraversal(root.right);
}
midTraversal(root);
return target;
};
653. 两数之和 IV - 输入二叉搜索树
给定一个二叉搜索树 root
和一个目标结果 k
,如果二叉搜索树中存在两个元素且它们的和等于给定的目标结果,则返回 true
。
思路:看这个图,二叉搜索树,左子树比当前节点小,右子树比当前节点大,像什么,是不是已经排序好的快排啊,那就给它合并为一个有序数组。234567。看出了什么,二叉树的中序遍历这不是。那对有序数组找两数之和,有哪些方法,有序,是不是可以双指针?如果是无序,只能是用字典喽。OK分析完题目:中序遍历+双指针。开写
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @param {number} k
* @return {boolean}
*/
var findTarget = function (root, k) {
//利用二叉搜索树的特性,使用中序遍历,左根右形成有序数组,在利用双指针求解
if (!root) return false;//处理边界
const array = inorderTraversal(root);
let left = 0, right = array.length - 1;
while (left < right) {//选两个元素,所以left和right不能相等
let sum = array[left] + array[right];
if (sum == k) {
return true;//结束
} else if (sum < k) {//两数之和小与目标,left右移动一个
left++;
} else {//两数之和大于目标,right向左移动一个
right--;
}
}
return false;//什么情况为false,就是left+1=right还没找到就是没有
};
//中序遍历二叉树
function inorderTraversal(root) {
if (!root) return [];
return inorderTraversal(root.left).concat(root.val).concat(inorderTraversal(root.right));
}
701. 二叉搜索树中的插入操作
给定二叉搜索树(BST)的根节点 root
和要插入树中的值 value
,将值插入二叉搜索树。 返回插入后二叉搜索树的根节点。 输入数据 保证 ,新值和原始二叉搜索树中的任意节点值都不同。
注意,可能存在多种有效的插入方式,只要树在插入后仍保持为二叉搜索树即可。 你可以返回 任意有效的结果 。
思路:从这题开始,就不能直接用中序遍历二叉搜索树了。而是想到二叉搜索树值比较可以根据节点值减少递归次数。比如只用向左找节点小的,或者只用向右找节点大的。
这里递归还是只考虑根,左右三种情况。我们假设,小与等于根节点的插在根的左边;大于根的插在右边;不存在的充当根返回。
插入节点考虑三种情况:
1.root不存在,创建一个节点,并返回该节点
2.插入节点的值小于等于root,将节点插入root.left,这里可以使用递归处理左子树。之后,返回root。
3.插入节点的值大于root,将节点插入root.right,这里也递归处理右子树。之后,返回root。
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @param {number} val
* @return {TreeNode}
*/
var insertIntoBST = function(root, val) {
if(!root) return new TreeNode(val);//root不存在,创建节点并返回
if(val<=root.val){
root.left = insertIntoBST(root.left,val);//节点在左子树中处理
return root;//返回root
}else{
root.right=insertIntoBST(root.right,val);//节点在右子树处理
return root;//返回root
}
};
669. 修剪二叉搜索树
给你二叉搜索树的根节点 root
,同时给定最小边界low
和最大边界 high
。通过修剪二叉搜索树,使得所有节点的值在[low, high]
中。修剪树 不应该 改变保留在树中的元素的相对结构 (即,如果没有被移除,原有的父代子代关系都应当保留)。 可以证明,存在 唯一的答案 。
所以结果应当返回修剪好的二叉搜索树的新的根节点。注意,根节点可能会根据给定的边界发生改变。
思路:二叉搜索树值比较可以根据节点值减少递归次数。比如只用向左找节点小的,或者只用向右找节点大的
在这题里,使用递归,只考虑三个节点。题目要求在[low,high]区间内的节点保留,删除比low小,比high大的节点。只考虑三个节点就三种情况
root.val比low还小,那么左子树不要,处理root.right就好了,返回处理后的root.right
root.val比high还大,那么右子树不要了,处理root.left,返回处理后的root.left
root.val在[low,high]区间内:递归处理左右子树,返回root
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @param {number} low
* @param {number} high
* @return {TreeNode}
*/
var trimBST = function (root, low, high) {
//递归
if (!root) return null;
if (root.val > high) {//右子树全部大于high在区间外,保留左子树
root.left = trimBST(root.left, low, high);//修剪左子树
return root.left;
} else if (root.val < low) {//左子树全部小与low,在区间外,保留右子树
root.right = trimBST(root.right, low, high);//修剪右子树
return root.right;//返回右子树
} else {//root在区间内,递归root的左右子树,并返回root
root.left = trimBST(root.left, low, high);
root.right = trimBST(root.right, low, high);
return root;
}
};
450. 删除二叉搜索树中的节点
给定一个二叉搜索树的根节点 root 和一个值 key,删除二叉搜索树中的 key 对应的节点,并保证二叉搜索树的性质不变。返回二叉搜索树(有可能被更新)的根节点的引用。
一般来说,删除节点可分为两个步骤:
- 首先找到需要删除的节点;
- 如果找到了,删除它。
思路:这道题难点在于,要使用递归,而不是中序遍历二叉树。还是考虑只有三个节点。
如果root不存在,返回null;如果root有值且就是要删除的值。
考虑以下几种情况(记住,递归只考虑当前最多只会有三个节点)
root是叶子节点,return null;
root左节点有值,右节点没值,return root.left。很合理,把root忽略,不就删除了root
root右节点有值,左节点没值,return root.right。同上
root的左右都右值。假设,将右子树替换为当前root并返回。那原来的左子树挂载哪里?首先是不是要挂载root.right的某个节点。如果root.right是一个叶子节点,比如上面实例节点4,那么节点2直接挂在节点4上。如果节点4下面还有其他节点呢?
举一个下面的例子,删除节点7,加上我们每次替换的都是右子树。节点9替换7,那么节点5挂在哪里。是不是在节点9后面找个节点挂上。首先5是比7小的,那5肯定挂载比5稍微大一点的节点。是不是9节点子树的最小的值。这里是8.
怎么找到8的?是不是对9进行找left。二叉树最左侧的节点值最小啊。
如果当前root.val不是key。根据root.val和key的大小,判断是递归左子树还是递归右子树
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @param {number} key
* @return {TreeNode}
*/
var deleteNode = function (root, key) {
if (!root) return null;//没有找到删除节点
if (root.val === key) {
//如果是叶子节点
if (!root.left && !root.right) {//删除叶子节点
return null;
}
if (root.left && !root.right) {//有左子树,右子树不存在
return root.left;
}
if (!root.left && root.right) {//左为空,右不为空
return root.right;
}
if (root.left && root.right) {//左右都右,假设取右子树替换当前节点,那root.left挂在哪里呢
let cur = root.right;
while (cur.left) {//如果右子树非叶子节点,找到右子树最左侧的节点,将root.left挂载上面
cur = cur.left;
}
cur.left = root.left;
return root.right;
}
} else if (root.val > key) {
root.left = deleteNode(root.left, key);//递归处理
} else {
root.right = deleteNode(root.right, key);//递归处理
}
return root;
};
二叉树比较
100. 相同的树
给你两棵二叉树的根节点 p
和 q
,编写一个函数来检验这两棵树是否相同。
如果两个树在结构上相同,并且节点具有相同的值,则认为它们是相同的。
思路:相同位置挨个比较 ;失败的时候很好考虑,即如果p和q有一个先结束,一个还没结束,说明节点个数不同,返回false;如果p和q相同节点上元素值不同也会返回false。难点在于什么时候结束?
这里可以反向思考,全部比较完,p和q同时比较完没有在比较的元素的时候,即p=null q=null。能走到这步,说明前面p和q不为空时元素都相等,返回true。
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} p
* @param {TreeNode} q
* @return {boolean}
*/
var isSameTree = function (p, q) {
//当前的p和q同时为空判断为相同
if (!p && !q) return true;
//如果两个树有一个缺失相应的节点,返回false
if (!p || !q) return false;
//如果两个节点都存在,但是值不相等,返回false
if (p.val != q.val) return false;
//递归p和q的左子树,以及p和q的右子树
return isSameTree(p.left, q.left) && isSameTree(p.right, q.right);
};
递归的思想只要将当前p和q整明白了,后面只是方法的递归调用罢了。
递归遍历p的left+q.left,拼接上递归调用p的right+q.right
572. 另一棵树的子树
给你两棵二叉树 root
和 subRoot
。检验 root
中是否包含和 subRoot
具有相同结构和节点值的子树。如果存在,返回 true
;否则,返回 false
。
二叉树 tree
的一棵子树包括 tree
的某个节点和这个节点的所有后代节点。tree
也可以看做它自身的一棵子树。
思路:比较root里是否有和subRoot一样的子树。题目给了两个例子,示例1是true,示例2是false。root里必须和subRoot的所有后代节点都一样,在示例2中,“2”这个阶段左子树0,而subRoot里没有,因此,subRoot不是root的子树。这就转换为比较两个树是否完全相等了。那root怎么处理呢,我们是不是可以考虑在遍历二叉树的时候将每个节点都当成是子树的根节点,依次去和subRoot进行比较啊。
因此本题的解决方案是:遍历二叉树+两个树相等比较
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @param {TreeNode} subRoot
* @return {boolean}
*/
var isSubtree = function (root, subRoot) {
let flag = false;
//任意一种遍历二叉树,遍历root,每个都作为根节点与subRoot判断是否为相同树
function preTraversal(root) {//采用前序遍历二叉树
if (isSameTree(root, subRoot)) {//判断逻辑
flag = true;
return;
};
root.left && preTraversal(root.left);//递归左右子树
root.right && preTraversal(root.right);
}
preTraversal(root);
return flag;
};
//判断两颗子树是否相同
function isSameTree(root1, root2) {
if (!root1 && !root2) return true;//两个树都是null,返回true
if (!root1 || !root2) return false;//有一个是null,返回false
if (root1.val != root2.val) return false;//都存在但val不等,返回false
return isSameTree(root1.left, root2.left) && isSameTree(root1.right, root2.right);//递归左右各比较
}
101. 对称二叉树
给你一个二叉树的根节点 root
, 检查它是否轴对称。
思路:跟上题类似,将数一分为二,左子树和右子树,比较左子树和右子树是否轴对称。
即左子树左节点==右子树右节点 && 左子树右节点==右子树左节点
什么情况下失败
- 左右节点一个存在,一个为nul;
- 左右节点同时存在,但val不相等。
什么情况下成功
左子树为右子树都为空,此时不需要递归,也就是叶子节点为true。
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
}
*/
/**
* @param {TreeNode} root
* @return {boolean}
*/
var isSymmetric = function (root) {
return checkSymmetric(root.left, root.right);
};
function checkSymmetric(leftSubtree, rightSubtree) {
// 如果左右子树都为null则对称
if (!leftSubtree && !rightSubtree) return true;
// 如果左右子树有一个缺失则非对称
if (!leftSubtree || !rightSubtree) return false;
// 节点都存在但值不同也返回false
if (leftSubtree.val != rightSubtree.val) return false;
// 否则左右子树都存在,继续递归判断左子树的左节点和右子树的右节点 以及左子树右节点和右子树左节点
return checkSymmetric(leftSubtree.left, rightSubtree.right) && checkSymmetric(leftSubtree.right, rightSubtree.left);
}
从根节点,一分为二,将左右子树看成是两个树进行对称比较。这里肯定是要创建一个函数来递归处理左右子树
226. 翻转二叉树
给你一棵二叉树的根节点 root
,翻转这棵二叉树,并返回其根节点。
思路:交换节点,根节点固定,交换左右
只用看示例2,只有三个节点的树。看下递归怎么解决
首先交换1和3,因为二叉树是指针串起来的,交换两个的地址指向就好了。
1变成3,需要先将3的信息暂存起来。
将暂存的3替换1
根节点交换完成,在递归处理根的左子树,右子树。
在递归处理,也就是第二次处理时,左子树的节点充当root,右子树的节点也会充当root
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @return {TreeNode}
*/
var invertTree = function (root) {
if (!root) return null;
let temp = root.left;
root.left = root.right;
root.right = temp;
invertTree(root.left);
invertTree(root.right);
return root;
};
617. 合并二叉树
想象一下,当你将其中一棵覆盖到另一棵之上时,两棵树上的一些节点将会重叠(而另一些不会)。你需要将这两棵树合并成一棵新二叉树。合并的规则是:如果两个节点重叠,那么将这两个节点的值相加作为合并后节点的新值;否则,不为 null 的节点将直接作为新二叉树的节点。
返回合并后的二叉树。
注意: 合并过程必须从两个树的根节点开始。
思路:两个树的比较,取并集。每个节点的逻辑都一样,很容易想到递归处理。返回一个root节点,考虑递归只把树看做三个节点。如果root1和root2都存在,那么新节点的root为两者的val和。如果root1和root2有只有一个有值,或者都为null,不用创建新节点,返回root1和root2中有值的节点即可,如果都没有,返回其中一个为null的。然后左右子树采用递归的方式。递归左子树,同时传root1.left,root2.left。右子树同理
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root1
* @param {TreeNode} root2
* @return {TreeNode}
*/
var mergeTrees = function (root1, root2) {
//递归只考虑三个节点,作用子树通过递归返回
if (root1 && root2) {
return new TreeNode(root1.val + root2.val, mergeTrees(root1.left, root2.left), mergeTrees(root1.right, root2.right));
}
return root1 ? root1 : root2;//包含了root1和root2有一个存在,和都不存在的情况
};
二叉树的深度、叶子节点、路径
104. 二叉树的最大深度
给定一个二叉树 root
,返回其最大深度。
二叉树的 最大深度 是指从根节点到最远叶子节点的最长路径上的节点数。
这道题如果考虑到两层节点,就会复杂很多。比如将左右节点高度都考虑进去。这样提交没问题,直到看了题解,NM,1的情况可以合并在判空的时候处理。
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @return {number}
*/
var maxDepth = function (root) {
if (!root) return 0;
if (!root.left && !root.right) return 1;
if (root.left && root.right) {
return 1 + Math.max(maxDepth(root.left), maxDepth(root.right));
}
if (root.left) return 1 + maxDepth(root.left);
return 1 + maxDepth(root.right);
};
如下是简洁版本返回左子树和右子树中高度较高的那个+1,如果不存在返回0
简化版本再次印证了递归的的技巧:只考虑根左右三个节点的树!!!如果左子树或右子树有孩子进入下次递归就行了
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @return {number}
*/
var maxDepth = function (root) {
if (!root) return 0;
return 1 + Math.max(maxDepth(root.left), maxDepth(root.right));
};
世界都清爽了。注意tips:尽量使用Math提供的方法,提示逼格,减少代码量
111. 二叉树的最小深度
给定一个二叉树,找出其最小深度。
最小深度是从根节点到最近叶子节点的最短路径上的节点数量。
说明:叶子节点是指没有子节点的节点。
思路:分析题目
- 如果同时存在左右子树,最小深度是左右子树的最小深度和+1
- 如果只存在左子树或右子树,最小深度等于左子树或右子树的深度+1
- 如果当前不为空,左右子树为空,返回1
- 如果是空则,返回0
其中2和3可以合并
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @return {number}
*/
var minDepth = function (root) {
if (!root) return 0;
if (root.left && root.right) {
return 1 + Math.min(minDepth(root.left), minDepth(root.right));
}
return 1 + minDepth(root.left || root.right);
};
110.平衡二叉树
给定一个二叉树,判断它是否是高度平衡的二叉树。
本题中,一棵高度平衡二叉树定义为:
一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1 。
思路:注意解题,题目说的每个节点的左右子树高度差不能超过1
继续利用递归处理。简单来说,先创建一个方法用于计算某个子树的高度。在将根节点的左右子树高度进行计算,如果根节点的左右子树高度差大于1则返回false。(只考虑根节点能通过90%的用例)
最后要判断,左右子树是否也满足高度相差1。像下面的示例,虽然根节点满足了,但是下面的2的左右高度差为2不满足。
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @return {boolean}
*/
var isBalanced = function (root) {
if (!root) return true;
if( Math.abs(getHeight(root.left) - getHeight(root.right)) > 1){
return false;
}
return isBalanced(root.left) && isBalanced(root.right);
};
function getHeight(node) {
if (!node) return 0;
return 1 + Math.max(getHeight(node.left), getHeight(node.right));
}
222. 完全二叉树的节点个数
给你一棵 完全二叉树 的根节点 root
,求出该树的节点个数。
完全二叉树 的定义如下:在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h
层,则该层包含 1~ 2h
个节点。
思路:这道题最笨的方法可能是递归找所有的节点,统计个数。
然而要利用这道题的结构,完全二叉树,我们在学习数据结构的时候,完全二叉树有个特性,就是高度如果为h,那么h-1层一定铺满,或者说h-1层一定是满二叉树。
这道题求节点总数,如果按层来看,不好处理。
换个角度,从中间砍一刀,一分为二,左右子树。
可以发现,完全二叉树就分两种
- 一种是左子树第h层铺满,但右子树没铺满
- 另一种是左子树h层有节点,而右子树h层没有
为什么这么看呢?
两种情况分别可以确定左子树的个数、右子树的个数
对于未确定的右子树或左子树可以采用递归方式处理
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @return {number}
*/
var countNodes = function (root) {
if (!root) return 0;
let leftHeight = getHeight(root.left);
let rightHeight = getHeight(root.right);
//左右高度相等,左子树是满二叉树,个数=2^高度-1 别忘了根节点+1
if (leftHeight == rightHeight) {
return Math.pow(2, leftHeight) + countNodes(root.right);
}
//左子树大于右子树,右子树是满二叉树,个数=2^右子树的高度-1 加上根节点+1
return Math.pow(2, rightHeight) + countNodes(root.left);
};
function getHeight(node) {
if (!node) return 0;
return 1 + Math.max(getHeight(node.left), getHeight(node.right));
}
257. 二叉树的所有路径
给你一个二叉树的根节点 root
,按 任意顺序 ,返回所有从根节点到叶子节点的路径。
叶子节点 是指没有子节点的节点。
思路:使用深度优先搜索(DFS)算法来遍历二叉树,并在遍历的过程中记录从根节点到叶子节点的路径。深度搜索递归结束条件是找到叶子节点,将路径信息打印到result中。
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @return {string[]}
*/
var binaryTreePaths = function (root) {
if (!root) return [];
let result = [];
dfs(root, [], result);
return result;
};
//深度遍历递归方式,携带路径信息,result结果信息
function dfs(root, path, result) {
if (!root) return;
path.push(root.val);
if (!root.left && !root.right) {
//碰到叶子节点结束递归
result.push(path.join('->'));
} else {
//进行递归,将当前路径信息带入,注意浅拷贝当前路径信息
dfs(root.left, path.slice(), result);
dfs(root.right, path.slice(), result);
}
}
1022. 从根到叶的二进制数之和
给出一棵二叉树,其上每个结点的值都是 0
或 1
。每一条从根到叶的路径都代表一个从最高有效位开始的二进制数。
- 例如,如果路径为
0 -> 1 -> 1 -> 0 -> 1
,那么它表示二进制数01101
,也就是13
。
对树上的每一片叶子,我们都要找出从根到该叶子的路径所表示的数字。
返回这些数字之和。题目数据保证答案是一个 32 位 整数。
思路:在二叉树里天然适配回溯,这道题难点在于如何求二进制和。其实我们先获取的是二进制的高位,由高位向低位的过程,其实是高位不断*2的过程。只要将当前节点的路径和在往下走的时候*2就能不断计算十进制的结果。
在回溯的时候还是用path表示当前路径。注意回溯的参数path之后还有用,所以使用path<<1的方法不直接改变path,传表达式的结果给回溯的参数。
关于回溯的leetcode刷题技巧可以参考我的另一篇博客:
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @return {number}
*/
var sumRootToLeaf = function (root) {//利用二进制左移的特性求和
let sum = 0;
//回溯 path当前路径和 当前节点
function backTracking(path, root) {
if (!root) {
return;//递归结束边界
}
//path逻辑
path += root.val;
if (!root.left && !root.right) {//叶子节点,path为路径二进制总和
sum += path;
}
backTracking(path << 1, root.left);//回溯的时候传path左移后的值,注意不要修改当前path值
backTracking(path << 1, root.right);//回溯左右子树
}
backTracking(0, root);
return sum;
};
1372. 二叉树中的最长交错路径
给你一棵以 root
为根的二叉树,二叉树中的交错路径定义如下:
- 选择二叉树中 任意 节点和一个方向(左或者右)。
- 如果前进方向为右,那么移动到当前节点的的右子节点,否则移动到它的左子节点。
- 改变前进方向:左变右或者右变左。
- 重复第二步和第三步,直到你在树中无法继续移动。
交错路径的长度定义为:访问过的节点数目 - 1(单个节点的路径长度为 0 )。
请你返回给定树中最长 交错路径 的长度。
思路:可以用递归,或者用回溯,用回溯比较好理解。递归处理二叉树更像是得到root的结果。因为这里的max是在递归过程中产生的。不一定是根节点。回溯在过程中找到值。
回溯的难点在于什么时候累加上路径?在尝试后,得到答案是在下一次处理左右子树的时候加上1。
递归左子树还是右子树?其实每个节点都需要递归左右子树,因为你不确定哪个是最大的。这里要记录从左来的还是从右来的。因为来源决定了路径长度。
如果flagLeft表示从左来的:
当前path+1,正常是不是递归右子树,因为我从左边来的;
但是我也可以递归左子树,那我的path就清空为0,因为我不要前面的路径了;
如果flagRight表示从右边来的,是不是同理。既可以遍历左子树,也可以遍历右子树。
回溯结束的条件是目标节点不存在,此时路径结束,将路径path与全局max比较,保留最大的。
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @return {number}
*/
var longestZigZag = function (root) {
//递归求左右子树中最大的长度,在过程中用max保存最大的
let max = 0;
if (!root || (!root.left && !root.right)) return 0;
const flagLeft = 1, flagRight = 2;
//回溯不需要返回值,在递归过程中处理
function backTrack(path, root, flag) {
if (!root) {//回溯结束条件,目标节点不存在,计算当前路径长度
max = Math.max(max, path);//与max进行比较
return;
}
if (flag == flagLeft) {//从左侧来的,当前应遍历右侧
backTrack(1 + path, root.right, flagRight);//保留path
backTrack(0, root.left, flagLeft);//强行从左侧,清空path信息,从0开始
} else {//从右侧来的,当前应遍历左侧
backTrack(1 + path, root.left, flagLeft);//保留path
backTrack(0, root.right, flagRight);//强行从右侧,清空path,从0开始
}
}
backTrack(0,root.left,flagLeft);//当前不统计,找到下一个节点在+1
backTrack(0,root.right,flagRight);
return max;
};
是不是很简单,用flagLeft和flagRight标记从哪边过来的。回溯只要写终止条件,过程中递归就行了。不需要返回值。
437. 路径总和 III
给定一个二叉树的根节点 root
,和一个整数 targetSum
,求该二叉树里节点值之和等于 targetSum
的 路径 的数目。
路径 不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的(只能从父节点到子节点)。
思路:这个题如果求的是从根节点出发,找路径和为targetSum的是不是就好处理,一个递归就行了。但是这里是所有节点都可以作为路径的起始点,所以路径的起始点要递归。因此本题可拆成两个部分:递归每个节点当路径的起始点+每个起始点开始找路径和为targetSum的路径是否存在。
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @param {number} targetSum
* @return {number}
*/
var pathSum = function (root, targetSum) {
let count = 0;
function dfs(node, target) {
if (!node) return;
check(node, target);//将每个根节点作为path的起点,找是否有路径等于targetSum的
dfs(node.left, target);//没有,以root.left为path的起点
dfs(node.right, target);//递归root.right
}
function check(node, target) {//以node为根,递归统计path路径和
if (!node) return;
if (node.val === target) {
count++;
}
check(node.left, target - node.val);
check(node.right, target - node.val);
}
dfs(root, targetSum);
return count;
};
236. 二叉树的最近公共祖先
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
使用二叉树的递归遍历来查找两个指定节点的最近公共祖先。具体思想如下:
- 从根节点开始递归遍历整棵二叉树。
- 如果当前节点为空或等于其中一个指定节点(p或q),则返回当前节点。
- 递归地在左子树和右子树中查找指定节点p和q的最近公共祖先。
- 如果左子树和右子树分别找到了指定节点p和q,说明当前节点就是它们的最近公共祖先,直接返回当前节点。
- 如果只在左子树或右子树中找到了指定节点p和q,则返回找到的节点。
- 最终返回的节点即为两个指定节点的最近公共祖先。
这种递归的思想能够有效地在二叉树中查找两个指定节点的最近公共祖先,并且保证最近公共祖先的深度尽可能大
/**
* Definition for a binary tree node.
* function TreeNode(val) {
* this.val = val;
* this.left = this.right = null;
* }
*/
/**
* @param {TreeNode} root
* @param {TreeNode} p
* @param {TreeNode} q
* @return {TreeNode}
*/
var lowestCommonAncestor = function (root, p, q) {
function help(root, p, q) {//在根节点中查找左右节点是否存在p和q,如果左子树和右子树
if (!root || root == p || root == q) {//如果根就是pq其中的节点
return root;//返回根
}
let left = help(root.left, p, q);//先递归左右子树
let right = help(root.right, p, q);
if (left && right) {//如果左右各找到一个,说明pq分布在两边,root就是最近的公共祖先
return root;
}
return left || right;//否则返回左左子树同时找到pq的结果
}
return help(root, p, q);
};