前端视角学习《数据结构与算法》,包括数据结构的理解,算法解题技巧,Leetcode 经典题解,欢迎讨论!
🐧 前情提要
阅读本章节你会了解到以下内容:
- 树的理解
- 二叉树的遍历
- 二叉树的属性
- 二叉树的操作
- LeetCode 刷题公式
- LeetCode 题目分类
1. 树的理解
(1)数据结构
树是一种数据结构,是由 n 个节点组成的具有层次关系的集合。结构图很像一棵倒置的树,根节点在最上层,叶子节点依次排列在下面逐层。
// 最常用的二叉树的数据结构表示
const TreeNode = function(val){
this.val = val
this.left = null
this.right = null
}
(2)树的特性
- 每个节点都有有限个子节点,或没有子节点
- 树中仅有根节点没有父节点
- 除根节点外,其他节点只有一个父节点
- 树中没有环路
(3)树的概念
- 根节点:树中仅有一个根节点,没有父节点的节点被称为根节点 root,理解为树的源头
- 父节点:一个节点有子节点,则称该节点为其子节点的父节点 parent node(相对概念)
- 子节点:一个父节点可能同时拥有多个子节点 children node (相对概念)
- 兄弟节点:相同父节点的节点,之间互称为兄弟节点 sibling node
- 叶子节点:子树最底层,没有子节点的节点 leaf node
- 深度:树中的任意节点n,即从根节点到节点n的唯一路径长度,根节点的深度为0
- 树的深度:从根节点到叶子节点的所有路径中,最长的那条路径的长度 depth
- 高度:树中的任意节点n,到其叶子节点的最长路径的长度,叶子节点的高度为0 height
- 节点的度:一个节点含有的子树的个数,称为该节点的度
- 树的度:一棵树中,所有节点的度的最大值,称为该树的度 level
- 节点值:当前节点对应的数值 root.val
(4)树的种类
树数据结构中可能种类较多,仅说明以下几个前端经常用到的
- 二叉树:每个节点最多含有两个子树
- 平衡二叉树:每个节点的子树高度差不大于1的二叉树
- 完美二叉树:对于一棵二叉树,除了其最底层节点外,其余各层的节点的数目都达到最大值
- 二叉搜索树:左子树上的所有节点值,均小于它的根节点值; 右子树上的所有节点值,均大于它的根节点值;
- N叉树:每个节点含有n个子树
2.树的遍历
对于树来说,最需要掌握的就是对树的遍历,很多延伸都是在遍历的前提下进行的,以下列举常用对树的遍历的方式:
- 二叉树的前序、中序、后序遍历
- 二叉树的层序遍历
- 二叉搜索树的遍历
- N叉树的前序、后序、层序遍历
(1)二叉树的前序、中序、后序遍历
利用 DFS 深度优先遍历 + 递归,依次遍历左子树、右子树,前序、中序、后序的区别在于,在什么遍历的位置处理填充值
- 前序遍历:Pre-Order Traversal 先访问根节点,再访问子树节点
- 中序遍历:In-Order Traversal 先访问左子树,再访问根节点,最后访问右子树
- 后序遍历:Post-Order Traversal 先访问子树节点,再访问根节点
const orderTraversal = function(root) {
let ret = []
const dfs = function(node) {
if(root === null) return
/* ret.push(root.val) 前序 */
root.left && dfs(root.left)
ret.push(root.val) // 中序
root.right && dfs(root.right)
/* ret.push(root.val) 后序 */
}
dfs(root)
return ret
}
(2)二叉树的层序遍历
利用 BFS 广度优先遍历 + 队列先进先出特性;len 记录树中每层节点的数量,curLevel 数组集合记录每层节点的值;分别遍历左子树、右子树依次入队列
层序遍历:Level-Order Traversal 先访问离根节点最近的节点,逐层访问树
const levelTaversal = function(root) {
let ret = []
if(root === null) return ret
let queue = []
queue.push(root)
while(queue.length) {
let len = queue.length
let curLevel = []
while(len>0) {
let node = queue.shift()
curLevel.push(node.val)
node.left && queue.push(node.left)
node.right && queue.push(node.right)
len--
}
ret.push(curLevel)
}
return ret
}
3. 二叉树的属性
二叉树的属性,包括最小深度、最大深度、树的直径等。以下是解题思路:
(1)二叉树的直径
- 先理解二叉树直径的概念,任意两个节点路径长度的最大值
- 涉及到路径,最先想到用 DFS 这种遍历方式
- 返回值是一个整数,创建 ret 记录路径的最大值
- 考虑 root === null 时,二叉树的深度为0
- 依次遍历左右子树,并记录左、右子树的深度
- 最后返回 +1 算上根节点
const diameter = function(root) {
let ret = 0
const dfs = function(root) {
if(root === null) return ret
const left = dfs(root.left)
const right = dfs(root.right)
ret = Math.max(ret, left + right)
return Math.max(left, right) + 1
}
dfs(root)
return ret
}
(2)二叉树的最大深度
最大深度,意思是从根节点到叶子节点,最长路径上的所有节点数 转换成逻辑语言,理解为左子树的深度、右子树的深度的最大值 + 1根节点
const maxDepth = function(root) {
if(root === null) return 0
return Math.max(maxDepth(root.left), maxDepth(root.right)) + 1
}
(3)二叉树的最小深度
最小深度,意识是从根节点到叶子节点,最短路径上的所有节点数 转换成逻辑语言,理解为左子树的深度、右子树的深度的最小值 + 1根节点
const minDepth = function(root) {
if(root === null) return 0
let queue = [root]
let depth = 1
while(queue.length) {
let len = queue.length
while(len--) {
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)
}
depth++
}
return root
}
4. 二叉树的操作
二叉树的操作,包括翻转二叉树、合并二叉树...
(1)翻转二叉树
递归的一般思路是,先找递归的子问题,再考虑终止条件
const invertTree = function(root) {
if(root === null) return root
[root.left, root.right] = [invertTree(root.right), invertTree(root.left)]
return root
}
(2)合并二叉树
const mergeTree = function(root1, root2) {
if(root1 === null) return root2
if(root2 === null) return root1
let ret = new TreeNode(root1.val + root2.val)
ret.left = mergeTree(root1.left, root2.left)
ret.right = mergeTree(root1.right, root2.right)
return ret
}
5. leetcode 刷题公式
以下是做树题目能够用到的最基本的公式。刷题时会发现基本都是这两种公式的变形,先理解树的概念,思考做题时可以用到哪些方式解题。需要注意的是,某类题目刷到一定量时,要停下来总结下相似点,还有什么更好的解题思路么... 而不是一直盲目的追求刷题的数量,刷 500 道题不是目的,遇到题了能解出来,能优化解题思路,才是更重要的
解题思路步骤:
- 读题,清楚并能转换题目需求,涉及到树的基本概念要理解,不断积累解题思路
- 考虑是利用 DFS 还是 BFS 解题,区别在于遍历方式不同。有时可能两种都可以解出来,前期用你能理解的解题思路做,能理解挺重要的。随着后期接触的多了,可以深入了解不同的解题思路和方式,选择更高效复杂度更低的
- 确定之后,把对应的公式默写出来
- 看返回值,考虑是否需要创建 ret,还是可以直接修改当前树,作为返回值
- 递归的边界条件,以及 root === null 时返回值
- 细节处理,具体根据题目要求转换成逻辑代码
(1)DFS 深度优先遍历 + 递归
let ret = []
const dfs = function(root) {
if(root === null) return
ret.push(root.val)
root.left && dfs(root.left)
root.right && dfs(root.right)
}
dfs(root)
return ret
(2)BFS 广度优先遍历 + 队列
let ret = []
if(root === null) return ret
let queue = [root]
while(queue.length) {
let len = queue.length
let curLevel = []
while(len > 0) {
let node = queue.shift()
curLevel.push(root.val)
node.left && queue.push(node.left)
node.right && queue.push(node.right)
len--
}
}
return ret
6. LeetCode 题目分类
集中分类刷 LeetCode 树相关题目时,可以按照如下建议顺序来做题
- 二叉树的遍历
- 94.二叉树的中序遍历
- 144.二叉树的前序遍历
- 145.二叉树的后序遍历
- 102.二叉树的层序遍历 - 自顶向下
- 107.二叉树的层序遍历II - 自底向上
- 199.二叉树的层序遍历 - 右视图
- 513.二叉树的左下角的值 - 左视图
- 637.二叉树的每层平均值
- 515.二叉树每层的最大值
- 429.N叉树的层序遍历
- 589.N叉树的前序遍历
- 590.N叉树的后序遍历
- 98.验证二叉搜索树
- 99.恢复二叉搜索树
- 700.二叉搜索树中的搜索
- 701.二叉搜索树中插入值
- 230.二叉搜索树中第k小的元素
- 108.数组转换二叉搜索树
- 109.链表转换二叉搜索树
- 112.二叉树的路径总和
- 113.二叉树的路径总和II
- 257.二叉树的所有路径
- 二叉树的操作
- 100.相同二叉树
- 101.对称二叉树
- 226.翻转二叉树
- 617.合并二叉树
- 116.填充二叉树
- 二叉树的属性
- 111.二叉树的最小深度
- 104.二叉树的最大深度
- 543.二叉树的直径
- 236.二叉树的最近祖先
- 235.二叉搜索树的最近祖先