文章目录
1. 树结构与其他数据结构的优缺点
-
数组
优点:
- 数组在查询和修改某个值时效率比较高,因为是根据下标直接查找
- 如果是通过数据来寻找对应的位置,查找效率为O(N)这样的效率很低,也可以先给数组进行排序,在进行二分法查找效率为O(
logN
),但是给数组排序也要对应的时间
缺点:
- 数组在进行插入和删除操作时,效率会很低,因为数组前面的数据增加或减少时,数组后面的每一项数据的下标都要改变
-
链表
优点:
- 链表在进行插入和删除操作时要比数组效率高很多,因为只需要当前数据和前面后面的数据断开引用或改变引用指向(指的是添加)就可以了
缺点:
- 链表在查询和修改对应值时效率会很低,因为需要从头或从尾部开始寻找
- 其实链表在插入和删除时效率也并不是特别高,因为它还是要先查找在去做对应的操作,只是效率相对数组而言要高很多
-
哈希表
优点:
- 哈希表在进行增、删、改、查时效率都非常高
缺点:
- 哈希表空间利用率不高,为了提升效率会浪费一定的空间
- 哈希表底层是数组实现的,但是哈希表是无序的不能被遍历
- 不能快速的找出最大或最小的这些特殊值
-
树结构
树结构的介绍:
- 首先我们不能说树结构比其他数据结构都要好,各有所长吧
- 但是树结构综合了上面的数据结构的优先(当然不能取代他们的优点,只是综合),并且也弥补了上面数据结构的缺点
- 有些场景使用树结构会比较方便,树结构是非线性的,可以表示一对多的关系,比如文件的目录结构,公司的等级分化
2. 树的术语
- 树(Tree)
- n(n>=0)个子节点构成的有限集合
- 当n = 0是,及为空树
- 对于一个非空树(n>0),他具备以下的性质
- 树中有一个根(Root)的特殊节点,用r表示
- 其余节点可以分为m(m>0)个互不相交的有限集
T1
、T2
、…,Tm
,其中每一个集合又是一颗树,称为原来树的子树(SubTree
)
- 相关术语
- 父节点与子节点(Parent与Child):如果有两个节点A和B,A的子节点是B的话,那么B的父节点就是A
- 节点的度(Degree):表示当前节点的子节点个数
- 树的度:表示这个树下面所有的节点中最大的度
- 叶节点(Leaf):度为0的节点,表示它已经没有子节点了
- 兄弟节点(Sibling):表示拥有同一个父节点的节点们
- 路径和路径长度:假如有三个节点,A是根节点,B是A的子节点,C是B的子节点,那么A到C的路径就为A-B、B-C,长度就是全部的路径(路径只会向下找)
- 节点的层次(Level):表示当前节点的层次,根节点在1层,他的子节点们在2层,以此类推每下一层就加1
- 树的深度(Depth):树中所有节点中最大的层次就是这棵树的深度
3. 树结构的表现方式
所有树的本质都可以使用二叉树模拟出来
比如一个根,它有三个子节点,可以每个节点都用子节点和兄弟节点来表示,这样一个根的三个子节点中,他们都有兄弟节点这个属性,都可以找到自己的兄弟,这个表示太抽象了需要画图来表示
4. 认识二叉树
-
二叉树中每个节点最多只能有两个节点
二叉树不仅仅是因为简单,也是因为基本上所有的树,都可以被二叉树模拟出来
-
二叉树可以为空,也就是没有节点。如果不为空那么他的两个节点,称为左子节点和右子节点,这两个节点就是兄弟节点
-
二叉树有五种形态
- 空树
- 只有自己本身
- 只有左子节点
- 只有右子节点
- 左子节点和右子节点都有
-
二叉树的特征
- 一个二叉树第 i 层的最大节点数为:2^(i - 1),i >= 1
- 深度为 k 的二叉树的最大全部节点为:2^k - 1,k >= 1
n0
=n2
+ 1:n0
表示叶节点(度为0)的个数,n2
表示度为2节点的个数
-
完美二叉树(满二叉树):除叶节点外,每一层节点都有两个节点
-
完全二叉树:除二叉树最后一层外,其他各层的节点数都达到最大个数。且最后一层从左向右的叶节点连续存在,只能缺右侧若干节点
5. 二叉树的存储
- 数组存储:数组一般只能存储完全二叉树或完美二叉树,因为在使用数组存储是由左向右、由上向下去存储的,如果不是完美或完全树的话,存储会浪费大量空间
- 链表存储:二叉树最常见的存储方式,每一个节点都有两个节点的引用,子左节点和子右节点
6. 二叉搜索树
6.1 认识二叉搜索树
- 二叉搜索树(
BST
,Binary Search Tree ),也称二叉排序树或二叉查找树 - 二叉搜索树是一颗二叉树,可以为空
- 如果不为空要满足以下条件
- 非空左子树的所有键值小于其根节点的键值
- 非空右子树的所有键值大于其根节点的键值
- 左、右子树本身也都是二叉搜索树
- 二叉搜索树的查询数据就是二分法的思想,二叉搜索树也是有序的
6.2 二叉搜索树增、删、改、查实现过程
-
添加、修改
添加和修改的方法是一样的,当没有key时就是添加,有key时修改value即可
在添加时,先判断根节点是否为空,为空添加给它,不为空使用递归查找深层的节点
判断要添加的key,与当前节点的key做对比,如果小于向left走,大于向right走,直到找到null然后把这个数据添加给它
// 1.insert(key, value):向树中添加或修改某个数据
// 提示:没有key就是添加,有相同key就是修改
insert(key, value) {
// 创建子节点
let newNode = new Node(key, value)
if (this.root == null) {
this.root = newNode
} else {
this.insertNode(this.root, newNode)
}
}
// 需要递归添加的节点
insertNode(node, newNode) {
// 向左走
if (node.key > newNode.key) {
// 查看左子节点是否为空,为空就可以添加
// 不为空就需要递归继续向下层找
if (node.left == null) {
node.left = newNode
} else {
this.insertNode(node.left, newNode)
}
// key相等,修改value
} else if (node.key == newNode.key) {
node.value = newNode.value
// 向右走
} else {
if (node.right == null) {
node.right = newNode
} else {
this.insertNode(node.right, newNode)
}
}
}
-
查询
查询使用的循环,和递归的原理一样
当key小于当前节点时,向左走,右边也一样,匹配到key返回value,如果找到最后还没有返回代表没有该值,返回false即可
// 2.search(key):在树中查找一个键,存在返回value,不存在返回false
search(key) {
let node = this.root
if (node == null) return false
while (node != null) {
// 如果传入的key比当前树的key要小,那么就指向左子树
if (key < node.key) {
node = node.left
// 找到了就返回value
} else if (node.key == key) {
return node.value
// 否则指向右子树
} else {
node = node.right
}
}
return false
}
-
先序遍历、中序遍历、后序遍历
代码中注释比较详细,看注释或者看下面的图都可以
// 3.preOrderTraverse():先序遍历
// (1).访问根节点
// (2).先序遍历所有左子树
// (3).先序遍历所有右子树
// (4).循环2、3操作
// 先序遍历是先查找根节点,在依次先查找左子树
// 如果左子树中还有左子树,也会先查找,直到左子树(多个左子树)中没有左子节点
// 就会返回返回最深层的上一层去查找右子树,在右子树中也会先查找它的左子节点
preOrderTraverse() {
let resutlStr = this.preOrderTraverseNode(this.root)
let resultArr = resutlStr.split(",")
// 因为遍历到最后一个节点时,也会返回并拼上逗号
// 所以变成数组后会多一位空项
resultArr.pop()
return resultArr
}
// preOrderTraverseNode():先序递归查找
preOrderTraverseNode(node) {
let resultStr = ""
if (node != null) {
// 把排序好的数据用逗号拼接
// 这个递归的返回是从内向外的,这里采用的是+=所以每当返回一个值,就会拼接到这个数的后面
// 所以排序好之后,就是从头开始到最深处
resultStr = node.key + ","
// 先查找全部左子节点
resultStr += this.preOrderTraverseNode(node.left)
// 先查找全部右子节点
resultStr += this.preOrderTraverseNode(node.right)
}
return resultStr
}
// 4.inOrderTraverse():中序遍历
// (1).中序遍历左子树
// (2).访问根节点
// (3).中序遍历右子树
// (4).循环1、3操作
// 中序遍历中的其中一个树节点,一定是先把左子树查找完才会查找右子树
// 注意:中序遍历因为是先找左在找右,所以返回的key一定是顺序的
// 因为使用的是递归,左子节点全部找完才会向上层查找
inOrderTraverse() {
let resultStr = this.inOrderTraverseNode(this.root)
let resultArr = resultStr.split(",")
resultArr.pop()
return resultArr
}
// inOrderTraverseNode():中序递归查找
inOrderTraverseNode(node) {
let resultStr = ""
if (node != null) {
resultStr += this.inOrderTraverseNode(node.left)
resultStr += node.key + ","
resultStr += this.inOrderTraverseNode(node.right)
}
return resultStr
}
// 5.postOrderTraverse():后序遍历
// (1).后序遍历左子树
// (2).后序遍历右子树
// (3).循环1、2操作
// (4).访问根节点
// 后序遍历,一个树节点先遍历(都是从最深层开始查找)左子节点
// 在遍历右子节点,当一个树的子节点遍历完才会返回它本身
postOrderTraverse() {
let resultStr = this.postOrderTraverseNode(this.root)
let resultArr = resultStr.split(",")
resultArr.pop()
return resultArr
}
// postOrderTraverseNode():后序递归查找
postOrderTraverseNode(node) {
let resultStr = ""
if (node != null) {
resultStr += this.postOrderTraverseNode(node.left)
resultStr += this.postOrderTraverseNode(node.right)
resultStr += node.key + ","
}
return resultStr
}
-
删除
删除是二叉搜索树中最难的一部分,以下会具体说明
- 当删除的节点为叶节点时(它没有子节点)
- 在删除之前要先找到这个节点,找的方法和查询的一样,但是这里需要提三个变量,要删除的节点、要删除节点的父节点、一个布尔值来判断,要删除的节点为当前父节点的左子节点还是右子节点,以便删除
- 当删除的节点为父节点时,符合叶节点的情况,表示只有一个根节点,直接为null即可
- 找到不为父节点的叶节点后,把它的父节点的左子节点或右子节点直接为null即可(根据刚才的变量名来确定是左是右)
- 当删除的节点只有一个子节点时
- 当被删除节点只有左子节点时,为根节点时表示根节点只有一个左子树,直接把左子树覆盖根节点即可
- 为子节点时,先确定这个子节点是它父节点的左还是右,如果是左就把父节点的left,指向它子节点的左,这样中间这个要删的节点就断了联系,浏览器会垃圾回收
- 当被删除节点只有右子节点时,原理一样
- 当删除的节点有两个子节点时
- 有一个规律,就是被删除的节点为两个节点时,需要来找新的节点来代替它,在被删除节点的左子树中找最大,右子树中最小的来代替,都可以,这样不会打乱二叉搜索树的结构,这两个节点称为前驱和后继,这里采用的是后继,原理都一样只是相反就可以了
- 这里需要额外添加三个变量,一个是后继节点的左儿子,它会一直寻找left只到找到null,一个是后继节点,后继节点的左儿子为null时代表已经找到了删除节点右子树中最小的一个节点了(后继节点),还有一个就是后继节点的父节点,方便改指向
- 这里需要把后继的右子树赋给,指向它父节点的左节点,因为这个后继节点的左节点是一定为空的,它作为代替节点它后面的数据也不能丢失,接着他父节点left的引用,在二叉搜索树中一个树的根节点是一定会比他所有左子节点大的,所以这点不用担心,再把删除节点的右子节点等于后继的右节点
- 如果后继节点为删除节点的右子节点,那么就代表这个后继节点的父节点就是删除节点,就不能让删除节点的右子树给它的父左节点,这样会把删除节点的左子树覆盖,会好的办法就是这里不动,类似于一个节点的做法,在这个后继节点下,他没有左节点就代表它就是最小值的,这种情况比较特殊,这里后继替换后的右节点已经处理完了
- 把后继右边的数据指向处理完后,就来更改后继左边的指向和原删除父节点指向的问题
- 如果原删除节点为根节点时,把原删除节点的左节点等于后继的左节点
- 如果原删除节点不为根时,就通过父节点和删除节点是父节点的左子还是右子,来确定哪个父去指向它,然后在把原删除节点的左节点等于后继的左节点就完成了
- 当删除的节点为叶节点时(它没有子节点)
// 8.remove(key):通过键删除某个数据
remove(key) {
// 当前节点
let node = this.root
// 当前节点的上一层节点
let prev = null
// 记录是上一层节点的左子节点还是右子节点,以便删除
let isLeftChild = true
// 寻找要删除的节点
while (node.key != key) {
prev = node
if (key < node.key) {
node = node.left
// 进入左节点
isLeftChild = true
} else {
node = node.right
// 进入右节点
isLeftChild = false
}
// 表示没要找到要删除的节点
if (node == null) return false
}
// 删除节点
// 1.删除的是叶子节点(没有子节点)
if (node.left == null && node.right == null) {
// 表示只有一个根节点,一个子节点都没有
if (node == this.root) {
this.root = null
return true
}
// 表示要删除的这个节点是,它父节点的左子节点
if (isLeftChild) {
prev.left = null
// 反之
} else {
prev.right = null
}
// 2.删除的有一个子节点
// 在此之前,有一个条件是node的left和right都为空
// 所以当左为空时,右边一定不为空
} else if (node.left == null) {
// 当要删除的是根节点,并且根节点只有一个子节点时
if (node == this.root) {
this.root = node.right
return true
}
// 进入的是左节点,把当前节点直接指向他的,的左子节点的右子节点
// 这样中间要删除的那个节点就断了联系
if (isLeftChild) {
prev.left = node.right
} else {
prev.right = node.right
}
// 当右为空,左不为空时
} else if (node.right == null) {
if (node == this.root) {
this.root = node.left
return true
}
if (isLeftChild) {
prev.left = node.left
} else {
prev.right = node.left
}
// 3.删除的有两个子节点
// 删除这个节点,应该找所有子节点中,key值最接近它的来替换
// 也就是左子树中最大的(前驱),或右子树中最小的(后继)
} else {
// 获取后继节点,因为要替换删除的节点
let successor = this.getSuccessor(node)
// 如果删除的为根节点,直接让后继等于它
// 左子树右子树同理
if (this.root == node) {
this.root = successor
} else if (isLeftChild) {
prev.left = successor
} else {
prev.right = successor
}
successor.left = node.left
}
return true
}
// getSuccessor(delNode):寻找后继节点
getSuccessor(delNode) {
// 要删除的节点
let parent = delNode
// 后继节点
let successor = delNode
// 后继节点的所有右节点
let rightChild = successor.right
while (rightChild != null) {
// 三个相连节点
parent = successor
successor = rightChild
rightChild = rightChild.left
}
// 后继节点不能等于要删除节点的右子节点
// 如果等于就需要把左子节点给连接过来
if (delNode.right != successor) {
parent.left = successor.right
successor.right = delNode.right
}
// 返回这个后继节点
return successor
}
6.3 二叉搜索树的封装(全部代码)
// 内部类,存储每一个节点
class Node {
constructor(key, value) {
// 节点自身的key(索引)
this.key = key
// 表示自身的数据
this.value = value
// 指向左子节点
this.left = null
// 指向右子节点
this.right = null
}
}
// 二叉搜索树类
class BinarySerachTree {
constructor() {
// 根节点
this.root = null
}
// 1.insert(key, value):向树中添加或修改某个数据
// 提示:没有key就是添加,有相同key就是修改
insert(key, value) {
// 创建子节点
let newNode = new Node(key, value)
if (this.root == null) {
this.root = newNode
} else {
this.insertNode(this.root, newNode)
}
}
// 需要递归添加的节点
insertNode(node, newNode) {
// 向左走
if (node.key > newNode.key) {
// 查看左子节点是否为空,为空就可以添加
// 不为空就需要递归继续向下层找
if (node.left == null) {
node.left = newNode
} else {
this.insertNode(node.left, newNode)
}
// key相等,修改value
} else if (node.key == newNode.key) {
node.value = newNode.value
// 向右走
} else {
if (node.right == null) {
node.right = newNode
} else {
this.insertNode(node.right, newNode)
}
}
}
// 2.search(key):在树中查找一个键,存在返回value,不存在返回false
search(key) {
let node = this.root
if (node == null) return false
while (node != null) {
// 如果传入的key比当前树的key要小,那么就指向左子树
if (key < node.key) {
node = node.left
// 找到了就返回value
} else if (node.key == key) {
return node.value
// 否则指向右子树
} else {
node = node.right
}
}
return false
}
// 3.preOrderTraverse():先序遍历
// (1).访问根节点
// (2).先序遍历所有左子树
// (3).先序遍历所有右子树
// (4).循环2、3操作
// 先序遍历是先查找根节点,在依次先查找左子树
// 如果左子树中还有左子树,也会先查找,直到左子树(多个左子树)中没有左子节点
// 就会返回返回最深层的上一层去查找右子树,在右子树中也会先查找它的左子节点
preOrderTraverse() {
let resutlStr = this.preOrderTraverseNode(this.root)
let resultArr = resutlStr.split(",")
// 因为遍历到最后一个节点时,也会返回并拼上逗号
// 所以变成数组后会多一位空项
resultArr.pop()
return resultArr
}
// preOrderTraverseNode():先序递归查找
preOrderTraverseNode(node) {
let resultStr = ""
if (node != null) {
// 把排序好的数据用逗号拼接
// 这个递归的返回是从内向外的,这里采用的是+=所以每当返回一个值,就会拼接到这个数的后面
// 所以排序好之后,就是从头开始到最深处
resultStr = node.key + ","
// 先查找全部左子节点
resultStr += this.preOrderTraverseNode(node.left)
// 先查找全部右子节点
resultStr += this.preOrderTraverseNode(node.right)
}
return resultStr
}
// 4.inOrderTraverse():中序遍历
// (1).中序遍历左子树
// (2).访问根节点
// (3).中序遍历右子树
// (4).循环1、3操作
// 中序遍历中的其中一个树节点,一定是先把左子树查找完才会查找右子树
// 注意:中序遍历因为是先找左在找右,所以返回的key一定是顺序的
// 因为使用的是递归,左子节点全部找完才会向上层查找
inOrderTraverse() {
let resultStr = this.inOrderTraverseNode(this.root)
let resultArr = resultStr.split(",")
resultArr.pop()
return resultArr
}
// inOrderTraverseNode():中序递归查找
inOrderTraverseNode(node) {
let resultStr = ""
if (node != null) {
resultStr += this.inOrderTraverseNode(node.left)
resultStr += node.key + ","
resultStr += this.inOrderTraverseNode(node.right)
}
return resultStr
}
// 5.postOrderTraverse():后序遍历
// (1).后序遍历左子树
// (2).后序遍历右子树
// (3).循环1、2操作
// (4).访问根节点
// 后序遍历,一个树节点先遍历(都是从最深层开始查找)左子节点
// 在遍历右子节点,当一个树的子节点遍历完才会返回它本身
postOrderTraverse() {
let resultStr = this.postOrderTraverseNode(this.root)
let resultArr = resultStr.split(",")
resultArr.pop()
return resultArr
}
// postOrderTraverseNode():后序递归查找
postOrderTraverseNode(node) {
let resultStr = ""
if (node != null) {
resultStr += this.postOrderTraverseNode(node.left)
resultStr += this.postOrderTraverseNode(node.right)
resultStr += node.key + ","
}
return resultStr
}
// 6.min():返回树中最小的键
min() {
let node = this.root
// 这里需要一个变量来代表树的上一层
// 因为当找到null的时候停止,我就找不到他上一层的数据了,上一层才是想要的数据
let prev = null
// 当左子树不为空的时候一直向左查找
// 因为二叉搜索树最左边的子节点一定是最小的
while (node != null) {
prev = node
node = node.left
}
return prev.key
}
// 7.max():返回树中最大的键
max() {
let node = this.root
let prev = null
while (node != null) {
prev = node
node = node.right
}
return prev.key
}
// 8.remove(key):通过键删除某个数据
remove(key) {
// 当前节点
let node = this.root
// 当前节点的上一层节点
let prev = null
// 记录是上一层节点的左子节点还是右子节点,以便删除
let isLeftChild = true
// 寻找要删除的节点
while (node.key != key) {
prev = node
if (key < node.key) {
node = node.left
// 进入左节点
isLeftChild = true
} else {
node = node.right
// 进入右节点
isLeftChild = false
}
// 表示没要找到要删除的节点
if (node == null) return false
}
// 删除节点
// 1.删除的是叶子节点(没有子节点)
if (node.left == null && node.right == null) {
// 表示只有一个根节点,一个子节点都没有
if (node == this.root) {
this.root = null
return true
}
// 表示要删除的这个节点是,它父节点的左子节点
if (isLeftChild) {
prev.left = null
// 反之
} else {
prev.right = null
}
// 2.删除的有一个子节点
// 在此之前,有一个条件是node的left和right都为空
// 所以当左为空时,右边一定不为空
} else if (node.left == null) {
// 当要删除的是根节点,并且根节点只有一个子节点时
if (node == this.root) {
this.root = node.right
return true
}
// 进入的是左节点,把当前节点直接指向他的,的左子节点的右子节点
// 这样中间要删除的那个节点就断了联系
if (isLeftChild) {
prev.left = node.right
} else {
prev.right = node.right
}
// 当右为空,左不为空时
} else if (node.right == null) {
if (node == this.root) {
this.root = node.left
return true
}
if (isLeftChild) {
prev.left = node.left
} else {
prev.right = node.left
}
// 3.删除的有两个子节点
// 删除这个节点,应该找所有子节点中,key值最接近它的来替换
// 也就是左子树中最大的(前驱),或右子树中最小的(后继)
} else {
// 获取后继节点,因为要替换删除的节点
let successor = this.getSuccessor(node)
// 如果删除的为根节点,直接让后继等于它
// 左子树右子树同理
if (this.root == node) {
this.root = successor
} else if (isLeftChild) {
prev.left = successor
} else {
prev.right = successor
}
successor.left = node.left
}
return true
}
// getSuccessor(delNode):寻找后继节点
getSuccessor(delNode) {
// 要删除的节点
let parent = delNode
// 后继节点
let successor = delNode
// 后继节点的左儿子
let leftChild = successor.right
while (rightChild != null) {
// 三个相连节点
parent = successor
successor = leftChild
leftChild = leftChild.left
}
// 后继节点不能等于要删除节点的右子节点
// 如果等于就需要把左子节点给连接过来
if (delNode.right != successor) {
parent.left = successor.right
successor.right = delNode.right
}
// 返回这个后继节点
return successor
}
}
let BST = new BinarySerachTree()
BST.insert(11, "root")
BST.insert(7, "子节点1")
BST.insert(15, "子节点1")
BST.insert(5, "子节点2")
BST.insert(3, "子节点3")
BST.insert(9, "子节点2")
BST.insert(8, "子节点3")
BST.insert(10, "子节点3")
BST.insert(13, "子节点2")
BST.insert(12, "子节点3")
BST.insert(14, "子节点3")
BST.insert(20, "子节点2")
BST.insert(18, "子节点3")
BST.insert(25, "子节点3")
// console.log(BST.preOrderTraverse());
// console.log(BST.inOrderTraverse());
// console.log(BST.postOrderTraverse());
// console.log(BST.min());
// console.log(BST.max());
// console.log(BST.search(15));
console.log(BST.remove(20));
console.log(BST.inOrderTraverse());
console.log(BST);
6.4 二叉搜索树的优缺点
- 优点:
- 二叉搜索树的曾、删、改、查效率都比较高,效率为O(log
N
)
- 二叉搜索树的曾、删、改、查效率都比较高,效率为O(log
- 缺点:
- 要知道,二叉搜索树的效率是根据深度来判断的
- 如果我最开始插入一个100的key,然后我在依次插入99、98、97…1,以这样的顺序去插入的话,它所有的数据都会添加到对应的左子树上,就相等于链表了(非平衡树),这样它的效率就从O(log
N
)变为O(N)了
7. 树的平衡
- 为了能较快的时间O(log
N
)来操作一棵树,我们需要保证树总是平衡的 - 至少大部分是平衡的,那么时间复杂度也是接近O(log
N
)的 - 也就是每个的左子节点的子孙个数,尽可能与右边的相等
- 常见的平衡树有哪些?
AVL
树:AVL
树是最早的一种平衡树,它有些办法保持树的平衡(每个节点多存储了一个额外的数据)- 因为
AVL
树是平衡的,所以时间复杂度也是O(logN
) - 但是,每次插入、删除操作相当于红黑树效率都不高,所以整体效率不如红黑树
- 红黑树:
- 红黑树也是通过一些特性来保持树的平衡
- 因为是平衡树,所以时间复杂度也是在O(log
N
) - 而且插入、删除等操作,红黑树的性能要优于
AVL
树,所以现在平衡树的应用基本都是红黑树