二叉搜索树
假设有一万个数,需要查找某个数存在不存在
按照以往的方法,暴力循环
let arr = new Array(10000)
for(let i = 0; i < 10000; i++) {
arr[i] = Math.floor(Math.random() * 10000)
}
let num = 0
function search(arr, target) {
for (let i = 0; i < arr.length; i++) {
num += 1
if (arr[i] == target) return true
}
return false
}
console.log(search(arr, 1000)) // false / true
console.log(num) // 10000 / 7260
可以看出,这样写,循环了非常多次,这是非常浪费性能的
- 如果一个算法的性能很烂的话,有两个方面的原因
- 数据结构很烂
- 算法不对
很明显上方的算法没有什么问题,就是比较嘛。那么问题就只能出现在数据结构上了,这个数据结构很烂!
二叉搜索树
这是一颗二叉树
这颗二叉树有排序效果,左子树的节点都比当前节点小,右子树的节点都比当前节点大
构建二叉搜索树
- 任选一个数字做根节点
- 将剩下的数与节点比较,比节点小的放左边,比节点大的放右边
- 重复第2步

代码实现
interface INodeType {
value: number;
left: INodeType | null;
right: INodeType | null;
}
type INode = INodeType | null;
class BuildSearchTree {
private arr: number[]
readonly root: INode
constructor() {
this.arr = this.createArr()
this.root = this.init()
}
// 初始化数组
private createArr(): number[] {
let arr: number[] = new Array(10000)
for (let i = 0; i < 10000; i++) {
arr[i] = Math.floor(Math.random() * 10000)
}
return arr
}
private node(value): INode {
return {
value,
left: null,
right: null,
}
}
/**
* 连接节点
* @param root 根节点
* @param num 需要连接的数
*/
private addNode(root: INode, num: number): void {
if (root == null || root.value == num) return // 如果这个数存在,则不作处理
if (root.value < num) { // 大的数放右边
if (root.right == null) root.right = this.node(num)
this.addNode(root.right, num)
} else { // 小的数放左边
if (root.left == null) root.left = this.node(num)
this.addNode(root.left, num)
}
}
/**
* 创建二叉搜索树
*/
private init(): INode {
if (this.arr == null || this.arr.length == 0) return null
let root: INode = this.node(arr[0]) // 选定数组第0位作为根节点
for (let i = 0; i < this.arr.length; i++) {
this.addNode(root, this.arr[i])
}
return root
}
}
const root = new BuildSearchTree().root
二叉搜索树创建好了之后,搜索其实很简单,很像前序遍历
/**
* 二叉树搜索
* @param root 根节点
* @param target 目标数
*/
let num2 = 0
function searchByTree(root: INode, target: number): boolean{
if(root == null) return false
num2 += 1
if (root.value == target) return true
if (root.value < target) return searchByTree(root.right, target)
if (root.value > target) return searchByTree(root.left, target)
}
console.log(searchByTree(root, 1000)) // false
console.log(num2) // 15
console.log(search(arr, 1000)) // false
console.log(num) // 10000
从循环次数上面来看,二叉搜索树的效果简直完爆嘛,二叉搜索树的强大之处,不言而喻。
虽然现在的性能看起来已经很强大了,但是不要忘了,前序遍历的循环次数是受二叉树层数影响的,层数越少,遍历的次数也就越少。也就是说,如果能把这颗二叉树尽量构造成平衡二叉树,那么就还能提升性能,用计算机科学的话来说,就是还未到性能的极致。
优化二叉搜索树 - 平衡二叉树
平衡二叉树概念
- 根节点的左子树与右子树的高度差不超过1
- 这棵树的每个子树都符合第一条
判断二叉树是否平衡
获取二叉树的深度 从上往下一层一层判断。不平衡就停止,平衡则继续向下判断
class Pingheng {
// 获取二叉树深度
public static getDeep(root: INode): number {
if (root == null) return 0
let leftDeep = this.getDeep(root.left),
rightDeep = this.getDeep(root.right);
return Math.max(leftDeep, rightDeep) + 1; // 当前还有一层, 所以要 + 1
}
// 判断是否是平衡二叉树
public static isBlance(root: INode): boolean {
if (root == null) return true;
let leftDeep = this.getDeep(root.left),
rightDeep = this.getDeep(root.right);
if (Math.abs(leftDeep - rightDeep) > 1) {
// 差值大于1 不平衡
return false;
} else {
return this.isBlance(root.right) && this.isBlance(root.left);
}
}
}
二叉树的单旋(左单旋,右单旋)
某一节点不平衡,如果左边浅,右边深,进行左单旋。 反之亦然
上面的类里面加一点方法
class Change extends Pingheng {
// 左单旋
protected static leftRotate(root: INode): INode {
// 找到新根
let newRoot = root.right
// 找到变化分支
let changeTree = root.right.left
// 当前旋转节点的右孩子为变化分支
root.right = changeTree
// 新根的左孩子为旋转节点
newRoot.left = root
// 返回新根
return newRoot
}
// 右单旋
protected static rightRotate (root: INode):INode {
// 找到新根
let newRoot: INode = root.left
// 找到变化分支
let changeTree = root.left.right
// 当前旋转节点的左孩子为变化分支
root.left = changeTree
// 新根的右孩子为旋转节点
newRoot.right = root
// 返回新根
return newRoot
}
// 旋转二叉树
public static change(root: INode): INode {
if (this.isBlance(root)) return root;
if (root.left != null) root.left = this.change(root.left)
if (root.right != null) root.right = this.change(root.right)
let leftDeep = this.getDeep(root.left)
let rightDeep = this.getDeep(root.right)
if (Math.abs(leftDeep - rightDeep) < 2) {
return root
} else if (leftDeep > rightDeep) { // 左边深, 右单旋
return this.rightRotate(root)
} else { // 右边深, 左单旋
return this.leftRotate(root)
}
}
}
二叉树的双旋(右左双旋, 左右双旋)
- 当要对某个节点进行左单旋时: 如果变化分支是唯一的最深分支,要先对新根进行右单旋,然后进行左单旋,这样的旋转叫做右左双旋
- 当要对某个节点进行右单旋时: 如果变化分支是唯一的最深分支,要先对新根进行左单旋,然后进行右单旋,这样的旋转叫做左右双旋
class Shuangxuan extends Change {
public static change(root: INode): INode {
if (!root) { return null; }
if (this.isBlance(root) || (root.right == null && root.left == null)) {
return root;
}
if (root.left != null) {
root.left = this.change(root.left);
}
if (root.right != null) {
root.right = this.change(root.right);
}
const leftDeep = this.getDeep(root.left);
const rightDeep = this.getDeep(root.right);
if (Math.abs(leftDeep - rightDeep) < 2) {
return root;
} else if (leftDeep > rightDeep) { // 左边深, 右单旋
const changeTreeDeep = this.getDeep(root.right && root.right.left),
noChangeTreeDeep = this.getDeep(root.right && root.right.right);
if (changeTreeDeep > noChangeTreeDeep) {
root.left = this.rightRotate(root.left as INodeType);
}
return this.rightRotate(root);
} else { // 右边深, 左单旋
const changeTreeDeep = this.getDeep(root.right && root.right.left),
noChangeTreeDeep = this.getDeep(root.right && root.right.right);
if (changeTreeDeep > noChangeTreeDeep) {
root.right = this.rightRotate(root.right as INodeType);
}
return this.leftRotate(root);
}
}
}
二叉树的双旋
前面经过了二叉树的单旋,左右双旋,右左双旋,二叉树依旧有可能不平衡。那就还需要考虑一种情况:如果变化分支的深度比旋转节点的另一侧高度差距超过2,那么单旋之后依旧不平衡。
那再改造一下change方法
class DoubleRotate extends Change {
public static change(root: INode): INode {
if (!root) { return null; }
if (this.isBlance(root) || (root.right == null && root.left == null)) {
return root;
}
if (root.left != null) {
root.left = this.change(root.left);
}
if (root.right != null) {
root.right = this.change(root.right);
}
const leftDeep = this.getDeep(root.left);
const rightDeep = this.getDeep(root.right);
if (Math.abs(leftDeep - rightDeep) < 2) {
return root;
} else if (leftDeep > rightDeep) { // 左边深, 右单旋
const changeTreeDeep = this.getDeep(root.right && root.right.left),
noChangeTreeDeep = this.getDeep(root.right && root.right.right);
if (changeTreeDeep > noChangeTreeDeep) {
root.left = this.rightRotate(root.left as INodeType);
}
let newRoot = this.rightRotate(root);
if (newRoot) {
newRoot.right = this.change(newRoot.right);
}
newRoot = this.change(newRoot);
return newRoot;
} else { // 右边深, 左单旋
const changeTreeDeep = this.getDeep(root.right && root.right.left),
noChangeTreeDeep = this.getDeep(root.right && root.right.right);
if (changeTreeDeep > noChangeTreeDeep) {
root.right = this.rightRotate(root.right as INodeType);
}
let newRoot = this.leftRotate(root);
if (newRoot) {
newRoot.left = this.change(newRoot.left);
}
newRoot = this.change(newRoot);
return newRoot;
}
}
}
234树
思考一下
影响二叉平衡树的性能的点是什么
- 在于二叉平衡搜索树的叉只有两个,导致在节点铺满时也有很多层。
- 如果一个节点存多个数,可以提升空间性能
- 树的层级越少,查找的效率越高
怎么样能使二叉平衡排序树的层数变少
- 如果不是二叉,层数会更少
叉越多,层数越小,但是叉阅读,树的结构就越复杂,树的叉最多为4层最好
234树
-
希望一颗树,最多有四个叉(度最大为4)
-
234树的子节点永远在最后一层,
-
234树永远是平衡的(每一个路径的高度都相同)
-
达成的效果
- 分支变多了,层数变少了
- 节点中存的树变多了,节点变少了
- 因为分支变多了, 所以复杂度上升了
期望
- 希望对二三四树进行简化
- 简化为二叉树
- 依旧保留多叉
- 单节点依旧保留存放多个值
由此出现了红黑树
红黑树
to be continue--