数据结构(六)树JS实现
树的特点
1.相关术语
- 节点的度(Degree):节点的子树个数个数
- 树的度:树中所有节点里最大的度
- 叶节点(Leaf):度数为0的节点
- 父节点(Parent):有子树的节点,相对其子节点
- 子节点(Child):有父节点的节点,相对其父节点
- 兄弟节点(Sibling):具有相同父节点的子节点
- 路径和路径长度:树从根节点到最远叶节点之间拥有的边数
- 节点的层次(Level):根节点在1层,其他热议节点的层数是其父节点的层数加一
- 树的深度(Depth):树中所有节点里最大层数就是这棵树的深度
2.优点
- 树是数组和链表的折中方案,既有数组的查询速度,又有链表的插入和删除的速度
3.树的表示方式
- 普通的表示方法
- 左儿子-右兄弟表示法
二叉树
树中每个节点最多有2个的子节点
二叉树的五种形态
二叉树特性
完美二叉树
完全二叉树
二叉树的存储方式
-
数组
从上到下,从左到右
*链表
每个节点都包含左节点和右节点的引用
二叉搜索树 (Binary Search Tree)
性质
- 非空左子树所有键值小于根节点的键值
- 非空右子树所有键值大于根节点的键值
- 左,右子树本身也是二叉搜索树
- 相对较小的值总是保存在左节点上,相对较大的值总是保存在右节点上
- 查询效率较高
插入
- 插入操作可以分解成每个节点与传入节点键值进行比较,大于传入键值则向左子节点查询,反之向右查询直到节点键值为null,可以利用递归方式
insert(key) {
//1.创建一个node对象
let newNode = new this.Node(key)
//2.判断root是否为空
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)
}
} else {
//向右插入
if (node.right == null) {
node.right = newNode
} else {
this.insertNode(node.right, newNode)
}
}
}
遍历
- 先序遍历: 递归函数每次先处理节点键值,再查询左节点查询,最后查询右节点
preOrder(handler) {
this.preOrderNode(this.root, handler)
}
preOrderNode(node, handler) {
if (node !== null) {
handler(node.key)
this.preOrderNode(node.left, handler)
this.preOrderNode(node.right, handler)
}
}
- 中序遍历:递归函数 = left-> node.key -> right
middleOrder(handlder) {
this.middleOrderNode(this.root, handlder)
}
middleOrderNode(node, handlder) {
if (node !== null) {
this.middleOrderNode(node.left, handlder)
handlder(node.key)
this.middleOrderNode(node.right, handlder)
}
}
-
后序遍历:left-> right -> node.key
postOrder(handlder) { this.postOrderNode(this.root, handlder) } postOrderNode(node, handlder) { if (node !== null) { this.postOrderNode(node.left, handlder) this.postOrderNode(node.right, handlder) handlder(node.key) } }
最值查找
- 最大值
max(root = this.root) {
//扩展根据传入节点查找其下最值,默认等于根节点,允许传入节点,返回相关数组
// let root = this.root
while (root.right !== null) {
root = root.right
}
return [root, root.key] //0-> root,1->key
}
- 最小值
min(root = this.root) {
// let root = this.root
while (root.left !== null) {
root = root.left
}
return [root, root.key] //0-> root,1->key
}
删除
分三种情况
-
- 删除的是叶子节点
-
- 删除的节点有一个子节点
- 3.删除的节点拥有超过一个子节点
1.查找要删除的点
/*
*搜索函数根据传入的键值找出对应节点
*return 数组node[] 0-> parent, 1->child, 2->isLeftChild
*/
searchNode(key) {
let node = [null, this.root, false] //0-> parent, 1->child, 2->isLeftChild
let root = this.root
while (node[1] !== null) {
if (node[1].key > key) {
node[0] = node[1]
node[2] = true
node[1] = node[1].left
} else if (node[1].key < key) {
node[0] = node[1]
node[2] = false
node[1] = node[1].right
} else if (node[1].key == key) {
return node
}
}
return false
}
let nodeArray = this.searchNode(key)
2.判断节点情况
remove(key) {
//1.查询要删除的节点
let nodeArray = this.searchNode(key)
if (nodeArray == false) {
return null
} else if (nodeArray[1].left == null && nodeArray[1].right == null) {
//1.删除叶子节点
if (nodeArray[2] == true) {
nodeArray[0].left = null
} else {
nodeArray[0].right = null
}
} else if (nodeArray[1].left == null || nodeArray[1].right == null) {
//2.删除的节点只有一个子节点
if (nodeArray[2] == true) {
if (nodeArray[1].left == null) {
nodeArray[0].left = nodeArray[1].right
} else {
nodeArray[0].left = nodeArray[1].left
}
} else {
if (nodeArray[1].left == null) {
nodeArray[0].right = nodeArray[1].right
} else {
nodeArray[0].right = nodeArray[1].left
}
}
} else {
//3.删除的节点有超过一个子节点,左子树找最大,右子树找最小 。默认查左子树
let max = this.getBest(nodeArray[1].left, true)[1] //找出其最大的节点
let maxNode = this.searchNode(max) //找出最大节点的父节点
if (nodeArray[0] == null) {
//更改节点指向的顺序很重要
this.root = maxNode[1]
maxNode[0].right = maxNode[1].left
maxNode[1].right = nodeArray[1].right
maxNode[1].left = nodeArray[1].left
return true
}
if (nodeArray[2] == true) {
//当要删除的节点属于左节点
nodeArray[0].left = maxNode[1]
maxNode[1].left =
nodeArray[1].left.key == maxNode[1].key
? null
: nodeArray[1].left //判断
maxNode[1].right = nodeArray[1].right
maxNode[0].right = maxNode[1].left
} else {
nodeArray[0].right = maxNode[1]
maxNode[1].left =
nodeArray[1].left.key == maxNode[1].key
? null
: nodeArray[1].left
maxNode[1].right = nodeArray[1].right
maxNode[0].right = maxNode[1].left
}
}
}
getBest(node, isLeft) {
//根据节点返回最大或最小节点
if (isLeft) {
return this.max(node)
} else {
return this.min(node)
}
}
二叉平衡树
- 需求:二叉搜索树在插入一系列键值连续的节点时,会造成二叉树左右分布不均匀和深度增加,使查询等操作的时间复杂度下降到O(N),
- 解决方案
AVL树
红黑树
通过规定并保持一些性质保证二叉树的分布均匀
特点
- 1.节点是红色或者黑色
- 2.根节点是黑色
- 3.每个叶子节点都是黑色(NIL)
- 4.每个红色节点都有两个黑色子节点(从叶子节点到根的路径上不能拥有两个连续的红色节点)
- 5.任一节点到其各个叶子节点之间的黑色节点的数目相同
- 在根到所有叶子节点的路径中,最长路径的长度不能是最短路径的两倍
- 根据性质4任意路径不能有连续的两个红色节点
- 根据性质5所有路径都具有相同数目的黑色节点,推出特点6
在插入一个新的节点时,如何保持红黑树的特点
- 变色 – 左旋转 – 右旋转
- 旋转:
插入操作
设要插入的节点为N(node),父节点P,祖父节点G,叔叔节点U
-
情况一:N位于树的根上没有父节点
直接将那个红色换黑色即可
-
情况二:N的父节点P是黑色
不变
-
情况三:P->红,U->红,G->黑
红红黑 ->黑黑红
-
情况四:P->红,U->黑,N为左孩子
P->黑,G->红,以G为根右旋转
-
情况五:P->红,U->黑,N为右孩子
以P为根进行左旋转 -> N变黑色 ->G变红色 ->以G为根进行右旋转
实例