树-BST基本实现

之前的数组, 栈, 链表, 队列等都是顺序数据结构, 这里来介绍一个非顺序数据结构, 树. 树在处理有层级相关的数据时非常有用, 还有在存储数据如数据库查询实现等场景也是高频使用.

作为一种分层数据的抽象模型, 在现实中最常见的例子是族谱, 公司组织架构图等.

我个人觉得树, 图等非顺序的数据结构是不太好直观理解的, 它的实现都需要用到函数的递归. 就关于函数递归调用栈搞不清楚的话, 那也就很难理解代码了.

树的基本内容包括:

  • 树相关的术语
  • 创建二叉搜索树
  • 树的三种遍历
  • 添加和移除节点
  • AVL平衡树

树的核心术语

  • 节点: 树的每个元素都称为 "节点". 节点的祖先包括父节点, 祖父节点等; 节点的后代包括子节点, 孙节点等
  • 根节点: 一颗树顶部最上层有个唯一的根节点元素, 称为根节点
  • 内/外部节点: 至少有一个子节点的称为 "内部节点", 否则称 "外部节点 或 叶节点"
  • 边: 节点和节点之间的 "连线" 成为边, 类似链表中的指针
  • 子树: 节点和其子节点的集合, 构成一颗子树
  • 节点深度: 节点深度指其祖先节点的数量
  • 树高度: 所有节点深度的最大值

搜索二叉树

二叉树中的节点最多只能有两个子节点: 左子节点 和 右子节点.

这种只有2个路径的结构可以让我们写出更高效的数据插入, 查询, 删除节点的算法. 比如二分法, 关系型数据库的查询设计等, 就使用非常高频.

二叉搜索树 (BST) 是二叉树的一种, 强制要求:

  • 比父节点小的值, 存左侧子节点
  • 比父节点大的值, 存右侧子节点
// 节点类
class Node {
    constructor(key) {
        this.key = key     // 节点值
        this.left = null   // 左节点指针
        this.right = null  // 右节点指针
    }
}

// 二叉搜索树类
class BSTtree {
    constructor() {
        this.root = null  // 根节点对象
    }
}

主要实现的方法是有:

  • insert(key): 向树中插入一个新的键
  • search(key): 搜索 key, 如果存在返回 true, 否则返回 false
  • inOrderTraverse(): 中序遍历所有节点
  • preOrderTraverse(): 先序遍历所有节点
  • postOrderTraverse(): 后序遍历所有节点
  • min() : 返回树中最小值
  • max(): 返回树中最大值
  • remove(key): 从树中移除某个键

BST - 插入键

这里我们先来实现一个辅助的方法 insertNode(node, key) .

它的作用是帮我们找到新节点应该插入到哪个最合适的层级位置, 因此它是一个 递归函数.

**当插入的 key 小于 node.key 时: **

  • 如果 node.left 是叶子节点时, 则直接插入
  • 否则就递归往左下层子树查找

当插入的 key 大于 node 时:

  • 如果 node.right 是叶子节点时, 则直接插入
  • 否则就递归往右下层子树查找
// 从某节点开始, 插入新的 key
insertNode(node, key) {
    if (key < node.key) {
        // 左子树处理
        if (node.left == null) {
            node.left = new Node(key)
        } else {
            this.insertNOde(node.left, key)
        }
    } else {
        // 右子树处理
        if (node.right == null) {
            node.right = new Node(key)
        } else {
            this.insertNode(node.right, key)
        }
    }
}

这样插入的逻辑就是变成从根节点开始进行判断:

  • 根节点无值, 则新增的 key 节点就是根节点
  • 根节点有值, 则将根节点开始进行 insertNode(root, key) 找到合适位置插入
// 插入一个 key
insert(key) {
    if this.root == null {
        // 空树的话那就是根节点
        this.root = new Node(key)
    } else {
        // 有值则找到合适位置插入
        this.insertNode(this.root, key)
    }
}

二叉树遍历

二叉树的遍历方式基本就3种, 先序遍历, 后序遍历, 中序遍历.

先序步骤: 根 => 左 => 右

应用: 复制树, 序列化树, 前缀表达式解析.

中序步骤:左 => 根 => 右

应用: 排序, 中缀表达式解析 (a + b * c)

后序步骤:左 => 右 => 根

应用: 释放内存, 后缀表达式解析 (栈), 修改树.

      1
     / \
    2   3
   /   / \
  4   5   6

先序, 遍历的结果是: 1, 2, 4, 3, 5, 6

后序, 遍历的结果是: 4, 2, 5, 6, 3, 1

中序, 遍历的结果是: 4, 2, 1, 5, 3, 6

先序遍历

即按照 根 -> 左 -> 右 的顺序, 先会访问节点本身(父节点), 然后再访问左侧节点, 最后右侧节点.

这里对于节点访问处理可以传递一个通用的 回调函数 callback , 同时用一个辅助方法 preOrderTraverseNode(node, callback) 来接受一个节点和回调函数.

// 辅助方法: 节点遍历
preOrderTraverseNode(node, callback) {
    // 基本情况
    if (node == null) return 
    
    // 递归情况: 先访问自身
    callback(node.key)
    // 再访问左树, 再是右树
    this.preOrderTraverseNode(node.left, callback)
    this.preOrderTraverseNode(node.right, callback)
}

// 先序遍历: 从根节点开始输入
preOrderTraverse(callback) {
    this.preOrderTraver(this.root, callback)
}

中序遍历

即按照 左 -> 根 -> 右 的顺序, 先访问子节点(叶子), 然后再访问根节点, 最后右侧节点.

同样节点访问处理可以传递一个通用的 回调函数 callback , 同时用一个辅助方法 inOrderTraverseNode(node, callback) 来接受一个节点和回调函数.

// 辅助方法: 节点遍历
inOrderTraverseNode(node, callback) {
    // 基本情况
    if (node == null) return 
    
    // 递归情况:  先左侧到叶子节点
    this.inOrderTraverse(node.left, callback)
    // 访问节点
    callback(node.key)
    // 再是右侧节点
    this.inOrderTraverse(node.right, callback)
}

// 中序遍历
inOrderTraverse(callback) {
    this.inOrderTraverse(this.root, callback)
}

后序遍历

即按照 左 -> 右 -> 根 的顺序, 先访问子节点(叶子), 然后再访问根节点, 最后右侧节点.

同样节点访问处理可以传递一个通用的 回调函数 callback , 同时用一个辅助方法 postOrderTraverseNode(node, callback) 来接受一个节点和回调函数.

// 辅助方法: 节点遍历
postOrderTraverseNode(node, callback) {
    // 基本情况
    if (node == null) return 
    
    // 递归情况:
    this.postOrderTraverse(node.left, callback)
    this.postOrderTraverse(node.right, callback)
    // 访问节点
    callback(node.key)
    
}

// 后序遍历
postOrderTraverse(callback) {
    this.postOrderTraverse(this.root, callback)
}

最大最小值

// BST 树的最小值
min() {
    return this.minNode(this.root)
}

minNode(node) {
    let current = node 
    // 因为是 BST 树, 小的值一定在左侧, 从树一层层往下到底就行
    while (current != null && current.left != null) {
        current = current.left
    }
    return current
}


// BST 树的最大值
max() {
    return this.maxNode(this.root)
}

maxNode(node) {
    let current = node 
    while (current != null && current.right != null) {
        current = current.right
    }
    return current.key
}

树中查询值

因为是 BST 树, 左侧深度递归查找就是越来越小, 右侧深度递归查找的值就越来越大, 否则就基本条件, 没有找到喽.

// 查询值
searchNode(node, key) {
    // 基本情况, 没有找到就返回 false
    if (node == null) return false 
    
    // 若 key 小于当前节点, 则继续左侧深度递归搜索
    if (key < node.key) return this.searchNode(node.left, key)
    
    // 若 key 大于当前节点, 则继续右侧深度递归搜索
    else if (key > node.key) return this.searchNode(node.right, key)
    
    // 找到就返回
    else return false 
}

树中移除值

这个算是非常复杂的逻辑了, 既要分析不同场景, 不同场景下又要进行递归, 就很难搞.

首先要做的就是要通过对子树分析, 来确定将被删节点的位置,

然后根据将要被删的节点位置, 分情况进行讨论处理.

* 如果不为 null,我们需要在树中找到要移除的键

* 如果要找的键比当前节点的值小, 就沿着树的左边找到下一个节点

* 如果要找的键比当前节点的值大, 就那么就沿着树的右边找到下一个节点

也就是说我们要分析它的子树。如果我们找到了要找的键(键和 node.key 相等),就需要处理三种不同的情况:

  • 移除一个叶节点 (无腿)
  • 移除一个有左侧 或者 右侧子节点的节点 (一条腿)
  • 移除有2个子节点的节点 (2条腿)
// 移除BST树中的节点
removeNode(node, key) {
    if (node == null) return null 
    
    if (key < node.key) {
        // 从左子树深度递归查找
        node.left = this.removeNode(node.left, key)
        return node
        
    } else if (key > node.key) {
        // 从右子树深度递归查找
        node.right = this.removeNode(node.right, key)
        return node 
        
    } else {
        // 找到了 key 的位置, 即 key == node.key 时候, 分3中情况讨论
        
        // 情况-01: 移除一个叶节点
        if (node.left == null && node.right == null) {
            node = null
            // 通过返回 null 将父节点的指针指向 null
            return node  
        }
        
        // 情况-02: 移除一个有左或右节点的节点(1条腿)
        // 跳过子节点, 直接将父节点指向其子节点
        if (node.left == null) {
            node = node.right
            return node
            
        } else if (node.right == null) {
            node = node.left
            return node
        }
        
        // 情况03 移除有两个子节点的节点 (2条腿)
        
        // a. 找到它右子树中最小节点, 下下层元素 grandson
        // b. 用 grandson 去替换掉被删节点的值 (改了键, 则移除了)
        // c. 继续将右侧子树的最小节点移除 (删掉重复键节点)
        // d. 向父节点返回更新的引用
        const grandson = this.minNode(node.right)
        node.key = grandson.key
        node.right = this.removeNode(node.right, grandson.key)
        return node 
    }
}

测试

// 测试
const tree = new BSTtree()
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);


// 中序遍历
function callback(value) {
  console.log('callback-log: ', value);
}

// 中序遍历
console.log('中序遍历:');
tree.inOrderTraverse(callback)
console.log();

// 先序遍历
console.log('先序遍历:');
tree.preOrderTraverse(callback)
console.log();
// 后序遍历
console.log('后序遍历:');
tree.postOrderTraverse(callback)

// 树的最小值
console.log();
console.log('树的最小值是: ', tree.min());

// 树的最大值
console.log();
console.log('树的最大值是: ', tree.max());

// 查找值
console.log();
console.log(tree.search(1));
console.log(tree.search(8));

// 删除值
console.log('删除值 25', tree.remove(25));
tree.inOrderTraverse(callback)

console.log('删除值 15', tree.remove(15));
tree.inOrderTraverse(callback)

输出:

PS F:\algorithms> node .\bst_tree.js
中序遍历:
callback-log:  3
callback-log:  5
callback-log:  6
callback-log:  7
callback-log:  8
callback-log:  9
callback-log:  10
callback-log:  11
callback-log:  12
callback-log:  13
callback-log:  14
callback-log:  15
callback-log:  18
callback-log:  20
callback-log:  25

先序遍历:
callback-log:  11
callback-log:  7
callback-log:  5
callback-log:  3
callback-log:  6
callback-log:  9
callback-log:  8
callback-log:  10
callback-log:  15
callback-log:  13
callback-log:  12
callback-log:  14
callback-log:  20
callback-log:  18
callback-log:  25

后序遍历:
callback-log:  3
callback-log:  6
callback-log:  5
callback-log:  8
callback-log:  10
callback-log:  9
callback-log:  7
callback-log:  12
callback-log:  14
callback-log:  13
callback-log:  18
callback-log:  25
callback-log:  20
callback-log:  15
callback-log:  11

树的最小值是:  Node { key: 3, left: null, right: null }

树的最大值是:  25

false
true
删除值 25 undefined
callback-log:  3
callback-log:  5
callback-log:  6
callback-log:  7
callback-log:  8
callback-log:  9
callback-log:  10
callback-log:  11
callback-log:  12
callback-log:  13
callback-log:  14
callback-log:  15
callback-log:  18
callback-log:  20

删除值 15 
callback-log:  3
callback-log:  5
callback-log:  6
callback-log:  7
callback-log:  8
callback-log:  9
callback-log:  10
callback-log:  11
callback-log:  12
callback-log:  13
callback-log:  14
callback-log:  18
callback-log:  20

PS F:\algorithms>

至此, 关于树, 二叉搜索树的基本实现就到这里啦.

posted @ 2024-08-12 21:07  致于数据科学家的小陈  阅读(7)  评论(0编辑  收藏  举报