ES6实现二叉树、二叉搜索树(BST)、自平衡树

本文介绍了二叉树的基本概念,详细讲解了二叉搜索树(BST)的特性,并使用ES6语法实现了BST的插入、中序遍历、先序遍历、后序遍历和搜索等操作。接着探讨了自平衡树——AVL树的原理,包括平衡因子计算和四种旋转操作(RR、LL、LR、RL),以及如何在插入节点时保持树的平衡。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

树的相关术语

  • 根节点:位于树顶部的节点叫作根节点,它没有父节点,树中的每个元素都叫作节点。
  • 内部节点:至少有一个子节点称为内部节点
  • 外部节点或叶节点:没有子元素的节点称为外部节点或叶节点
  • 节点的深度:节点的深度取决于它的祖先的节点的数量。
  • 树的高度:取决于所有节点深度的最大值

二叉树与二叉搜索树(BST)

  • 二叉树:二叉树中的节点最多只能有两个子节点:一个是左侧子节点,另一个是右侧子节点。
  • 二叉搜索树:是二叉树的一种,但是它只允许你在左侧节点存储(比父节点)小的值, 在右侧节点存储(比父节点)大(或者等于)的值

ES6语法实现二叉搜索树(BST)

首先创建一个BinarySearchTree

需要实现的一些方法如下:

  • insert(key):向树中插入一个新的键。
  • inOrderTraverse:通过中序遍历方式遍历所有节点,对树进行排序操作。
  • preOrderTraverse:通过先序遍历方式遍历所有节点,一个应用是打印一个结构化的文档。
  • postOrderTraverse:通过后序遍历方式遍历所有节点,后序遍所有节点,一个应用是计算一个目录和它的子目录中所有文件所占空间的大小。
  • search(key):在树中查找一个键,如果节点存在,则返回true;如果不存在,则返回 false。
  • min:返回树中最小的值/键。
  • max:返回树中最大的值/键。
  • remove(key):从树中移除某个键。
class BinarySearchTree {
    constructor() {
        this.root = null
    }
    insert(key) {
        this.root = insertNode(this.root, key)
    }
    search(key) {
        return searchNode(this.root, key)
    }
    // 中序遍历所有节点,对树进行排序操作
    inOrderTraverse(cb) {
        inOrderTraverseNode(this.root, cb)
    }
    // 先序遍历所有节点,一个应用是打印一个结构化的文档
    preOrderTraverse(cb) {
        preOrderTraverseNode(this.root, cb)
    }
    // 后序遍所有节点,一个应用是计算一个目录和它的子目录中所有文件所占空间的大小
    postOrderTraverse(cb) {
        postOrderTraverseNode(this.root, cb)
    }
    // 返回数中最小的值/键
    min() {
        return minNode(this.root)
    }
    // 返回树中最大的值/键
    max() {
        return maxNode(this.root)
    }
    // 移除某个键
    remove(key) {
        removeNode(this.root, key)
    }
}
insert 向树中插入一个键的功能
// 创建树节点
class Node {
    constructor(key) {
        this.key = key
        this.left = null
        this.right = null
    }
}

// 递归插入节点
function insertNode(node, element) {
    if (node === null) {
        node = new Node(element)
    } else if (element < node.key) {
        node.left = insertNode(node.left, element)
    } else if (element > node.key) {
        node.right = insertNode(node.right, element)
    }
    return node
}

const tree = new BinarySearchTree()
tree.insert(11);
tree.insert(7);
tree.insert(15);
tree.insert(5);
tree.insert(3);
tree.insert(9);
tree.insert(8);
tree.insert(10);
tree.insert(13);
tree.insert(12);
tree.insert(14);
tree.insert(20);
tree.insert(18);
tree.insert(25); 
tree.insert(6)

console.log(JSON.stringify(tree.root,null,2))
// 打印结果如下
{
  "key": 7,
  "left": {
    "key": 5,
    "left": { "key": 3, "left": null, "right": null },
    "right": { "key": 6, "left": null, "right": null }
  },
  "right": {
    "key": 15,
    "left": {
      "key": 9,
      "left": { "key": 8, "left": null, "right": null },
      "right": {
        "key": 10,
        "left": null,
        "right": {
          "key": 13,
          "left": { "key": 12, "left": null, "right": null },
          "right": { "key": 14, "left": null, "right": null }
        }
      }
    },
    "right": {
      "key": 20,
      "left": { "key": 18, "left": null, "right": null },
      "right": { "key": 25, "left": null, "right": null }
    }
  }
}
inOrderTraverse 中序遍历操作(left==>selft==>right)
// 中序遍历 left==>self==>right的顺序访问所有节点
function inOrderTraverseNode(node, cb) {
    if (node) {
        inOrderTraverseNode(node.left, cb)
        cb(node.key)
        inOrderTraverseNode(node.right, cb)
    }
}

tree.inOrderTraverse((key) => {
	console.log(key)
})

// 打印结果如下
3
5
6
7
8
9
10
11
12
13
14
15
18
20
25
preOrderTraverse 先序遍历(self==>left==>right)
// 先序遍历 self==>left==>right顺序访问所有节点
function preOrderTraverseNode(node, cb) {
    if (node) {
        cb(node.key)
        preOrderTraverseNode(node.left, cb)
        preOrderTraverseNode(node.right, cb)
    }
}

tree.inOrderTraverse((key) => {
	console.log(key)
})

// 打印结果如下
11
7
5
3
6
9
8
10
15
13
12
14
20
18
25
postOrderTraverse 后序遍历(left==>right==>self)
// 后序遍历 left==>right==>self顺序访问所有节点
function postOrderTraverseNode(node, cb) {
    if (node) {
        postOrderTraverseNode(node.left, cb)
        postOrderTraverseNode(node.right, cb)
        cb(node.key)
    }
}

tree.postOrderTraverse((key) => { 
    console.log(key)
})
// 打印结果
3
6
5
8
10
9
7
12
14
13
18
25
20
15
11

search搜索树中的值
  • 搜索最小值

    // 寻找最小的节点
    function minNode(node) {
        if (!node) return null
        while (node.left) {
            node = node.left
        }
        return node.key
    }
    
  • 搜索最大值

    // 寻找最大的节点
    function maxNode(node) {
        if (!node) return null
        while (node.right) {
            node = node.right
        }
        return node.key
    }
    
  • 搜索特定值

    // 搜索节点
    function searchNode(node, key) {
        if (!node || !key) return null
        if (node.key > key) {
            return searchNode(node.left, key)
        } else if (node.key < key) {
            return searchNode(node.right, key)
        } else {
            return true
        }
    }
    
移除一个节点
  • 移除一个叶节点

    在这里插入图片描述

  • 移除有一个左侧或右侧子节点的节点

在这里插入图片描述

  • 移除有两个子节点的节点
    在这里插入图片描述
// 找到最小的节点
function findMinNode(node) {
    while (node && node.left) {
        node = node.left
    }
    return node
}

// 移除节点
function removeNode(node, key) {
    if (!node) return null
    if (node.key > key) {
        node.left = removeNode(node.left, key)
        return node
    } else if (node.key < key) {
        node.right = removeNode(node.right, key)
        return node
    } else {
        // 一个叶节点
        if (node.left === null && node.right === null) {
            node = null
            return node
        }

        // 一个只有一个子节点的节点
        if (node.left === null) {
            node = node.right
            return node
        } else if (node.right === null) {
            node = node.left
            return node
        }

        // 一个有两个子节点的节点
        // 1、找到右侧子树最小的节点 
        // 2、并用这个节点去更新这个节点的值 
        // 3、同时把右侧子树中的最小节点移除
        // 4、最后,向它的父节点返回更新后节点的引用

        const aux = findMinNode(node.right)
        node.key = aux.key
        node.right = removeNode(node.right, aux.key)
        return node
    }
}

自平衡树(AVL树)

AVL树是一种自平衡二叉搜索树,意思是任何一个节点左右两侧子树的高度之差最多为1。也就是说这种树会在添加或移除节点时尽量试着成为一棵完全树。

二叉搜索树存在一个问题:取决于你添加的节点数,树的一条边可能会非常深;也就是说,树的一 条分支会有很多层,而其他的分支却只有几层

解决办法:

  • 右----右(RR):向左的单旋转
    在这里插入图片描述
    假设向上图的树插入节点90,这会造成树失衡(节点50-Y高度为-2),因此需要恢复树的平衡。 下面是我们执行的操作:

    1. 与平衡操作相关的节点有三个(X、Y、Z),将节点X置于节点Y(平衡因子为2)所在 的位置(行{1})

    2. 节点X的右子树保持不变

    3. 将节点Y的右子节点置为节点X的左子节点Z(行{2})

    4. 将节点X的左子节点置为节点Y(行{3})

  • 左----左(LL):向右的单旋转
    在这里插入图片描述
    假设向上图的树插入节点5,这会造成树失衡(节点50 Y高度为+2),需要恢复树的平衡。下 面是我们执行的操作:

    1. 与平衡操作相关的节点有三个(X、Y、Z),将节点X置于节点Y(平衡因子为+2)所在 的位置(行{1})
    2. 节点X的左子树保持不变
    3. 将节点Y的左子节点置为节点X的右子节点Z(行{2})
    4. 将节点X的右子节点置为节点Y(行{3})
  • 左----右(LR):向右的双旋转
    在这里插入图片描述
    假设向上图的树插入节点35,这会造成树失衡(节点50 Y高度为+2),需要恢复树的平衡。下 面是我们执行的操作:

    1. 将节点X置于节点Y(平衡因子为+2)所在的位置

    2. 将节点Y的左子节点置为节点X的右子节点

    3. 将节点Z的右子节点置为节点X的左子节点

    4. 将节点X的右子节点置为节点Y

    5. 将节点X的左子节点置为节点Z

      基本上,就是先做一次RR旋转,再做一次LL旋转

  • 右----左(RL):向左的双旋转
    在这里插入图片描述
    假设向上图的树插入节点75,这会造成树失衡(节点70 Y高度为2),需要恢复树的平衡。下 面是我们执行的操作:

    1. 将节点X置于节点Y(平衡因子为2)所在的位置
    2. 将节点Z的左子节点置为节点X的右子节点
    3. 将节点Y的右子节点置为节点X的左子节点
    4. 将节点X的左子节点置为节点Y
    5. 将节点X的右子节点置为节点Z

计算平衡因子

在AVL树中,需要对每个节点计算右子树高度(hr)和左子树高度(hl)的差值,该值(hr-hl)应为0、1或1。如果结果不是这三个值之一,则需要平衡该AVL树。这就是平衡因子的概念。

下图举例说明了一些树的平衡因子(所有的树都是平衡的):
在这里插入图片描述

// 计算节点树的高度
function heightNode(node) {
    if (node === null) {
        return -1
    } else {
        return Math.max(heightNode(node.left), heightNode(node.right)) + 1
    }
}

在插入节点时,计算平衡因子,做一下平衡操作,让我们来改造一下insert方法

// 左-左(LL):向右的单旋转
function rotationLL(node) {
    var temp = node.left
    node.left = temp.right
    temp.right = node
    return temp
}

// LR 先向左旋转,转换为左-左,然后再右旋转
function rotationLR(node) {
    node.left = rotationRR(node.left)
    // return node
        return rotationLL(node)
}
// RL 先向右旋转,转换为右-右,然后再左旋转
function rotationRL(node) {
    node.right = rotationLL(node.right)
    return rotationRR(node)
}

// 递归用于插入树节点
// BST存在一个问题:取决于你添加的节点数,树的一条边可能会非常深;也就是说,树的一
// 条分支会有很多层,而其他的分支却只有几层,这会在需要在某条边上添加、移除和搜索某个节点时引起一些性能问题
function insertNode(node, element) {
    if (node === null) {
        node = new Node(element)
    } else if (element < node.key) {
        node.left = insertNode(node.left, element)
        // 二叉树平衡
        if(node.left !== null) {
            if(heightNode(node.left) - heightNode(node.right) > 1) {
                if(element < node.left.key) {
                    // 左-左(LL):向右的单旋转
                    node = rotationLL(node)
                } else {
                    // LR 先向左旋转,转换为左-左,然后再右旋转
                    node = rotationLR(node)
                }
            }
        }
    } else if (element > node.key) {
        node.right = insertNode(node.right, element)
        // 二叉树平衡
        if(node.right !== null) {
            if(heightNode(node.right) - heightNode(node.left) > 1) {
                if(element > node.right.key) {
                    //  右-右(RR):向左的单旋转
                    node = rotationRR(node)
                } else {
                    // RL 先向右旋转,转换为右-右,然后再左旋转
                    node = rotationRL(node)
                }
            }
        }
    }

    return node
}) {
            if(heightNode(node.right) - heightNode(node.left) > 1) {
                if(element > node.right.key) {
                    //  右-右(RR):向左的单旋转
                    node = rotationRR(node)
                } else {
                    // RL 先向右旋转,转换为右-右,然后再左旋转
                    node = rotationRL(node)
                }
            }
        }
    }

    return node
}

参考:学习JavaScript数据结构与算法.第2版

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值