「这是我参与2022首次更文挑战的第4天,活动详情查看:2022首次更文挑战」。
为什么会有树这种数据结构?
单链表的访问操作太慢了,要访问中间或者后面的结点,必须从头结点一个一个往后面去访问。
这时双向链表出现了,它不仅有后继结点,还有前驱结点,让链表的访问更加快速。
这时有人突发奇想,为什么链表不能有多个next指针呢,让结点分叉出来,于是就出现了树这种数据结构。
所以树可以被理解为特殊化了的链表。
树的基本概念
数据结构中的树,是对现实世界中树的简化:把树根抽象为“根结点”,树枝抽象为“边”,树枝的两个端点抽象为“结点”,树叶抽象为“叶子结点”。抽象后的树结构如下:
把这棵抽象后的树颠倒一下,就得到了计算机中的树结构:
树有以下定义:
- 树的层次计算规则:根结点所在的那一层记为第一层,其子结点所在的就是第二层,以此类推。
- 结点和树的“高度”计算规则:叶子结点高度记为1,每向上一层高度就加1,逐层向上累加至目标结点时,所得到的的值就是目标结点的高度。树中结点的最大高度,称为“树的高度”。
- “度”的概念:一个结点开叉出去多少个子树,被记为结点的“度”。比如我们上图中,根结点的“度”就是3。
- “叶子结点”:叶子结点就是度为0的结点。在上图中,最后一层的结点的度全部为0,所以这一层的结点都是叶子结点。
二叉树
指树中节点的度不大于2的树,每个结点最多只有两个 next 指针
二叉树的代码实现
在 JS 中,二叉树使用对象来定义。它的结构分为三块:
- 数据域
- 左侧子结点(左子树根结点)的引用
- 右侧子结点(右子树根结点)的引用
function TreeNode (val, left, right) {
this.val = (val === undefined ? 0 : val)
this.left = (left === undefined ? null : left)
this.right = (right === undefined ? null : right)
}
const node = new TreeNode(1)
二叉搜索树
科学家们发明了树这种数据结构,本质是要解决实际问题,如果二叉树的两个指针指向的是很乱的数据,其实是不高效的数据结构,实际应用场景不多。
所以他们设计了二叉搜索树,让左子树比右子树的数据小,这样搜索起来会更加高效。
二叉搜索树,又称有序⼆叉树,排序⼆叉树,是指⼀棵空树或者具有下列性质的⼆叉树:
-
若任意节点的左⼦树不空,则左⼦树上所有结点的值均⼩于它的根结点的值;
-
若任意节点的右⼦树不空,则右⼦树上所有结点的值均⼤于它的根结点的值;
-
任意节点的左、右⼦树也分别为⼆叉查找树。
二叉搜索树查找的效率和二分查找是一样的,但插入和删除的效率比数组高。
操作 | 数组 | 二叉搜索树 |
---|---|---|
查找 | O(logn) | O(logn) |
插入 | O(n) | O(logn) |
删除 | O(n) | O(logn) |
但二叉搜索树如果不平衡,最坏情况下访问、插入和排序的时间复杂度都是 O(n)。
实际上科学家们早就解决了这个问题,真实业务场景中,数据库用的是更好的数据结构,先贴出来。
leetcode 树之初体验
二叉树的中序遍历
题目描述:给定一个二叉树的根节点
root
,返回它的 中序 遍历。
- 先序遍历:根结点 -> 左子树 -> 右子树
- 中序遍历:左子树 -> 根结点 -> 右子树
- 后序遍历:左子树 -> 右子树 -> 根结点
在这三种顺序中,根结点的遍历分别被安排在了首要位置、中间位置和最后位置。
所谓的“先序”、“中序”和“后序”,“先”、“中”、“后”其实就是指根结点的遍历时机。
代码实现
const inorderTraversal = function (root) {
const res = []
const inorder = (root) => {
if (!root) { // 递归边界(基线条件)
return false
}
inorder(root.left) // 左子树 -> 根结点 -> 右子树
res.push(root.val)
inorder(root.right)
}
inorder(root)
return res
}
时间复杂度:O(n)
空间复杂度:O(n),空间复杂度取决于递归的栈深度,而栈深度在二叉树为一条链的情况下会达到 O(n) 的级别。
相同的树
题目描述:给你两棵二叉树的根节点
p
和q
,编写一个函数来检验这两棵树是否相同。
如果两个树在结构上相同,并且节点具有相同的值,则认为它们是相同的。
代码实现
const isSameTree = function (p, q) {
if (!p && !q) { // p 和 q 都为空,两棵树相等
return true
}
if (!p || !q) { // p 和 q 任意一个为空,两棵树不相等
return false
}
if (p.val !== q.val) { // p 和 q 的 val 不相同,两棵树不相等
return false
}
// 递归比较左子树和右子树
return isSameTree(p.left, q.left) && isSameTree(p.right, q.right)
}
时间复杂度:O(min(m,n)),其中m 和 n 分别是两个二叉树的节点数。对两个二叉树同时进行深度优先搜索,只有当两个二叉树中的对应节点都不为空时才会访问到该节点,因此被访问到的节点数不会超过较小的二叉树的节点数。
空间复杂度:O(min(m,n)),其中 m 和n 分别是两个二叉树的节点数。空间复杂度取决于递归调用的层数,递归调用的层数不会超过较小的二叉树的最大高度,最坏情况下,二叉树的高度等于节点数。
对称二叉树
题目描述:给你一个二叉树的根节点
root
, 检查它是否轴对称。
代码实现
const isSymmetric = function (root) {
const isMirror = (l, r) => {
if (!l && !r) { // l 和 r 都为空,是对称的
return true
}
if (!l || !r) { // l 和 r 有一个为空,不对称
return false
}
if (l.val !== r.val) { // l 和 r 值不相等,不对称
return false
}
// 递归判断左右子树
return isMirror(l.left, r.right) && isMirror(l.right, r.left)
}
return isMirror(root, root)
}
时间复杂度:O(n)
空间复杂度:O(n)
二叉树的最大深度
题目描述:给定一个二叉树,找出其最大深度。
二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。
代码实现
const maxDepth = function (root) {
if (!root) { // 根结点为空,直接返回 0
return 0
} else {
const l = maxDepth(root.left) // 递归拿到左子树和右子树的最大深度,取 max,最后加上根结点的1层
const r = maxDepth(root.right)
return Math.max(l, r) + 1
}
}
时间复杂度:O(n)
空间复杂度:O(height),其中 height 表示二叉树的高度。
小结
- 树不过是被特殊化了的链表而已,知道其诞生原因,就没什么难的。
- 平时一般写的算法题都是二叉树或者二叉搜索树。
- 真实的业务场景中,比如数据库,用的效率更高的树。
js 中树这个数据结构太重要了,DOM就是树形结构 ,平时开发的很多组件,比如菜单,下拉选择等也要支持树形结构,了解这些基本概念还是很有用的。