数据结构(十二)之二叉搜索树

如需转载, 请咨询作者, 并且注明出处.
有任何问题, 可以关注我的微博: coderwhy, 或者添加我的微信: 372623326

前面, 我们学习了关于树的一些概念以及比较重要的二叉树的特性.

现在, 我们为二叉树再增加一个限制, 那么就可以形成一个二叉搜索树.

一. 二叉搜索树的概念

我们先来简单理解一下什么是二叉搜索树.

什么是二叉搜索树?

  • 二叉搜索树(BST,Binary Search Tree),也称二叉排序树或二叉查找树

  • 二叉搜索树是一颗二叉树, 可以为空;如果不为空,满足以下性质:

    • 非空左子树的所有键值小于其根结点的键值。
    • 非空右子树的所有键值大于其根结点的键值。
    • 左、右子树本身也都是二叉搜索树。
  • 下面哪些是二叉搜索树, 哪些不是?

    img
  • 二叉搜索树的特点:

    • 二叉搜索树的特点就是相对较小的值总是保存在左结点上, 相对较大的值总是保存在右结点上.
    • 那么利用这个特点, 我们可以做什么事情呢?
    • 查找效率非常高, 这也是二叉搜索树中, 搜索的来源.

二叉搜索树的操作

  • 二叉搜索树有哪些常见的操作呢?
    • insert(key):向树中插入一个新的键。
    • search(key):在树中查找一个键,如果结点存在,则返回true;如果不存在,则返回false
    • inOrderTraverse:通过中序遍历方式遍历所有结点。
    • preOrderTraverse:通过先序遍历方式遍历所有结点。
    • postOrderTraverse:通过后序遍历方式遍历所有结点。
    • min:返回树中最小的值/键。
    • max:返回树中最大的值/键。
    • remove(key):从树中移除某个键。

二. 二叉搜索树的实现

现在, 我们通过代码来实现二叉搜索树.

创建二叉搜索树

  • 我们像封装其他数据结构一样, 先来封装一个BinarySearchTree的类

    // 创建BinarySearchTree
    function BinarySerachTree() {
        // 创建结点构造函数
        function Node(key) {
            this.key = key
            this.left = null
            this.right = null
        }
        
        // 保存根的属性
        this.root = null
        
        // 二叉搜索树相关的操作方法
    }
    
  • 代码解析:

    • 封装BinarySearchTree的构造函数.
    • 还需要封装一个用于保存每一个结点的类Node.
      • 该类包含三个属性: 结点对应的key, 指向的左子树, 指向的右子树
    • 对于BinarySearchTree来说, 只需要保存根结点即可, 因为其他结点都可以通过根结点找到.

向树中插入数据

  • 我们两个部分来完成这个功能.

  • 外界调用的insert方法

    // 向树中插入数据
    BinarySerachTree.prototype.insert = function (key) {
        // 1.根据key创建对应的node
        var newNode = new Node(key)
        
        // 2.判断根结点是否有值
        if (this.root === null) {
            this.root = newNode
        } else {
            this.insertNode(this.root, newNode)
        }
    }
    
  • 代码解析:

    • 首先, 根据传入的key, 创建对应的Node.
    • 其次, 向树中插入数据需要分成两种情况:
      • 第一次插入, 直接修改根结点即可.
      • 其他次插入, 需要进行相关的比较决定插入的位置.
    • 在代码中的insertNode方法, 我们还没有实现, 也是我们接下来要完成的任务.
  • 插入非根结点

    BinarySerachTree.prototype.insertNode = function (node, newNode) {
        if (newNode.key < node.key) { // 1.准备向左子树插入数据
            if (node.left === null) { // 1.1.node的左子树上没有内容
                node.left = newNode
            } else { // 1.2.node的左子树上已经有了内容
                this.insertNode(node.left, newNode)
            }
        } else { // 2.准备向右子树插入数据
            if (node.right === null) { // 2.1.node的右子树上没有内容
                node.right = newNode
            } else { // 2.2.node的右子树上有内容
                this.insertNode(node.right, newNode)
            }
        }
    }
    
  • 代码解析:

    • 插入其他节点时, 我们需要判断该值到底是插入到左边还是插入到右边.
    • 判断的依据来自于新节点的key和原来节点的key值的比较.
      • 如果新节点的newKey小于原节点的oldKey, 那么就向左边插入.
      • 如果新节点的newKey大于原节点的oldKey, 那么就向右边插入.
    • 代码的1序号位置, 就是准备向左子树插入数据. 但是它本身又分成两种情况
      • 情况一(代码1.1位置): 左子树上原来没有内容, 那么直接插入即可.
      • 情况二(代码1.2位置): 左子树上已经有了内容, 那么就一次向下继续查找新的走向, 所以使用递归调用即可.
    • 代码的2序号位置, 和1序号位置几乎逻辑是相同的, 只是是向右去查找.
      • 情况一(代码2.1位置): 左右树上原来没有内容, 那么直接插入即可.
      • 情况二(代码2.2位置): 右子树上已经有了内容, 那么就一次向下继续查找新的走向, 所以使用递归调用即可.
  • 测试代码: 如果按照下面的代码插入, 最后形成什么样的树呢?

    // 测试代码
    var bst = new BinarySerachTree()
    
    // 插入数据
    bst.insert(11)
    bst.insert(7)
    bst.insert(15)
    bst.insert(5)
    bst.insert(3)
    bst.insert(9)
    bst.insert(8)
    bst.insert(10)
    bst.insert(13)
    bst.insert(12)
    bst.insert(14)
    bst.insert(20)
    bst.insert(18)
    bst.insert(25)
    
  • 形成的树:

    img
  • 如果这个时候, 我新插入一个数据6, 那么插入的位置和顺序应该怎样的呢?

    bst.insert(6)
    
  • 新的树:

    img

遍历二叉搜索树

  • 前面, 我们向树中插入了很多的数据, 为了能很多的看到测试结果. 我们先来学习一下树的遍历.
    • 注意: 这里我们学习的树的遍历, 针对所有的二叉树都是适用的, 不仅仅是二叉搜索树.
  • 树的遍历:
    • 遍历一棵树是指访问树的每个结点(也可以对每个结点进行某些操作, 我们这里就是简单的打印)
    • 但是树和线性结构不太一样, 线性结构我们通常按照从前到后的顺序遍历, 但是树呢?
    • 应该从树的顶端还是底端开始呢? 从左开始还是从右开始呢?
    • 二叉树的遍历常见的有三种方式: 先序遍历/中序遍历/后续遍历. (还有程序遍历, 使用较少, 可以使用队列来完成)
先序遍历
  • 遍历过程为:

    • ①访问根结点;
    • ②先序遍历其左子树;
    • ③先序遍历其右子树。
  • 遍历过程:

    img
  • 遍历的代码实现

    BinarySerachTree.prototype.preOrderTraversal = function (handler) {
        this.preOrderTranversalNode(this.root, handler)
    }
    
    BinarySerachTree.prototype.preOrderTranversalNode = function (node, handler) {
        if (node !== null) {
            // 1.打印当前经过的节点
            handler(node.key)
            // 2.遍历所有的左子树
            this.preOrderTranversalNode(node.left, handler)
            // 3.遍历所有的右子树
            this.preOrderTranversalNode(node.right, handler)
        }
    }
    
  • 测试代码:

    // 测试前序遍历结果
    var resultString = ""
    bst.preOrderTraversal(function (key) {
        resultString += key + " "
    })
    alert(resultString) // 11 7 5 3 6 9 8 10 15 13 12 14 20 18 25
    
  • 代码解析:

    • 遍历树最好用的办法就是递归, 因为每个节点都可能有自己的子节点, 所以递归调用是最好的方式.
    • 在先序遍历中, 我们在经过节点的时候, 会先将该节点打印出来.
    • 然后, 我们会遍历节点的左子树, 再然后遍历节点的右子树.
  • 代码先序遍历图解:

    img
中序遍历
  • 遍历过程为:

    • ①中序遍历其左子树;
    • ②访问根结点;
    • ③中序遍历其右子树。
  • 遍历过程:

    img
  • 遍历的代码实现:

    // 中序遍历
    BinarySerachTree.prototype.inOrderTraversal = function (handler) {
        this.inOrderTraversalNode(this.root, handler)
    }
    
    BinarySerachTree.prototype.inOrderTraversalNode = function (node, handler) {
        if (node !== null) {
            this.inOrderTraversalNode(node.left, handler)
            handler(node.key)
            this.inOrderTraversalNode(node.right, handler)
        }
    }
    
  • 测试代码:

    // 测试中序遍历结果
    resultString = ""
    bst.inOrderTraversal(function (key) {
        resultString += key + " "
    })
    alert(resultString) // 3 5 6 7 8 9 10 11 12 13 14 15 18 20 25 
    
  • 代码解析:

    • 先从最左边开始, 进行中序遍历.
    • 依次向右移动, 最后遍历最右边.
    • 可以根据代码和图片解析来查看. (这里不太好描述, 但是一图胜千言, 大家多看一下图片)
  • 代码中序遍历图解:

    img
后序遍历
  • 遍历过程为:

    • ①后序遍历其左子树;
    • ②后序遍历其右子树;
    • ③访问根结点。
  • 遍历过程:

    img
  • 遍历的代码实现:

    // 后续遍历
    BinarySerachTree.prototype.postOrderTraversal = function (handler) {
        
    }
    
    BinarySerachTree.prototype.postOrderTraversalNode = function (node, handler) {
        if (node !== null) {
            this.postOrderTraversalNode(node.left, handler)
            this.postOrderTraversalNode(node.right, handler)
            handler(node.key)
        }
    }
    
  • 测试代码:

    // 测试后续遍历结果
    resultString = ""
    bst.postOrderTraversal(function (key) {
        resultString += key + " "
    })
    alert(resultString) // 3 6 5 8 10 9 7 12 14 13 18 25 20 15 11 
    
  • 后续遍历

    • 先遍历左子树上的节点, 再遍历右子树上的节点, 最后遍历根节点. (仔细查看图片和代码)
  • 代码后续遍历图解:

    img

最大值&最小值

  • 在二叉搜索树中搜索最值是一件非常简单的事情, 其实用眼睛看就可以看出来了.

    img
  • 下面, 我们通过代码来实现一下.

  • 获取最大值&最小值:

    // 获取最大值和最小值
    BinarySerachTree.prototype.min = function () {
        var node = this.root
        while (node.left !== null) {
            node = node.left
        }
        return node.key
    }
    
    BinarySerachTree.prototype.max = function () {
        var node = this.root
        while (node.right !== null) {
            node = node.right
        }
        return node.key
    }
    
  • 代码解析:

    • 代码也是比较简单的:
      • 代码依次向左找到最左边的结点就是最小值,
      • 代码依次向右找到最右边的结点就是最大值.
    • 也可以使用递归来实现, 不过这里就没有什么必要了, 递归反而增加代码的复杂度.
  • 代码测试:

    // 获取最值
    alert(bst.min()) // 3
    alert(bst.max()) // 25
    

搜索特定的值

  • 二叉搜索树不仅仅获取最值效率非常高, 搜索特定的值效率也非常高.

    // 搜搜特定的值
    BinarySerachTree.prototype.search = function (key) {
        return this.searchNode(this.root, key)
    }
    
    BinarySerachTree.prototype.searchNode = function (node, key) {
        // 1.如果传入的node为null那么, 那么就退出递归
        if (node === null) {
            return false
        }
    
        // 2.判断node节点的值和传入的key大小
        if (node.key > key) { // 2.1.传入的key较小, 向左边继续查找
            return this.searchNode(node.left, key)
        } else if (node.key < key) { // 2.2.传入的key较大, 向右边继续查找
            return this.searchNode(node.right, key)
        } else { // 2.3.相同, 说明找到了key
            return true
        }
    }
    
  • 代码解析:

    • 这里我们还是使用了递归的方式. 待会儿我们来写一个非递归的实现.
    • 递归必须有退出条件, 我们这里是两种情况下退出.
      • node === null, 也就是后面不再有节点的时候.
      • 找到对应的key, 也就是node.key === key的时候.
    • 在其他情况下, 根据node.的key和传入的key进行比较来决定向左还是向右查找.
      • 如果node.key > key, 那么说明传入的值更小, 需要向左查找.
      • 如果node.key < key, 那么说明传入的值更大, 需要向右查找.
  • 测试代码:

    // 查找特定的值
    alert(bst.search(10)) // true
    alert(bst.search(21)) // false
    
  • 非递归代码实现:

    BinarySerachTree.prototype.search = function (key) {
        var node = this.root
        while (node !== null) {
            if (node.key > key) {
                node = node.left
            } else if (node.key < key) {
                node = node.right
            } else {
                return true
            }
        }
        return false
    }
    
  • 递归or循环?

    • 其实递归和循环之间可以相互转换.
    • 大多数情况下, 递归调用可以简化代码, 但是也会增加空间的复杂度.
    • 循环空间复杂度较低, 但是代码会相对复杂.
    • 可以根据实际的情况自行选择, 不需要套死必须使用某种方式.

三. 二叉搜索树的删除

二叉搜索树的删除有些复杂, 为了大家更加清晰的理解其中的原理, 我单独讲解这部分内容.

删除节点的思路

  • 删除节点要从查找要删的节点开始, 找到节点后, 需要考虑三种情况:

    • 该节点是也结点(没有字节点, 比较简单)
    • 该节点有一个子节点(也相对简单)
    • 该节点有两个子节点.(情况比较复杂, 我们后面慢慢道来)
  • 我们先从查找要删除的节点入手

    // 删除结点
    BinarySerachTree.prototype.remove = function (key) {
        // 1.定义临时保存的变量
        var current = this.root
        var parent = this.root
        var isLeftChild = true
    
        // 2.开始查找节点
        while (current.key !== key) {
            parent = current
            if (key < current.key) {
                isLeftChild = true
                current = current.left
            } else {
                isLeftChild = false
                current = current.right
            }
    
            // 如果发现current已经指向null, 那么说明没有找到要删除的数据
            if (current === null) return false
        }
    
        return true
    }
    
  • 代码解析:

    • 在上面的代码序号1位置中, 我们先保存了一些临时变量.
      • current: 用于一会儿找到的要删除的节点对应的node.
      • parent: 用于保存current节点的父节点. 因为如果current有子节点, 那么在删除current节点的时候, 必然需要将parent的left或者right指向它的某一个子节点. 所以需要保存起来current的parent. (树中的节点关系不能向上的, 和链表非常相似)
      • isLeftChild: boolean类型,它用户记录我们是在current是在父节点的左侧还是右侧, 以便到时候设置parent的left或者right
    • 在上面的代码序号2位置中, 开始查找对应的key.
      • 还是之前的思路, 依次向下找到节点, 同时记录current/parent/isLeftChild这些变量
      • 如果遍历到current === null, 那么说明在二叉搜索树中没有该key, 直接返回false即可.
      • 如果找到, 后面就需要我们进一步考虑更加复杂的情况了.

情况一: 没有子节点

  • 情况一: 没有子节点.

    • 这种情况相对比较简单, 我们需要检测current的left以及right是否都为null.
    • 都为null之后还要检测一个东西, 就是是否current就是根, 都为null, 并且为跟根, 那么相当于要清空二叉树(当然, 只是清空了根, 因为只有它).
    • 否则就把父节点的left或者right字段设置为null即可.
  • 图解过程:

    • 如果只有一个单独的根, 直接删除即可

      img
    • 如果是叶结点, 那么处理方式如下:

      img
  • 代码实现如下:

    // 3.删除的结点是叶结点
    if (current.left === null && current.right === null) {
        if (current == this.root) {
            this.root == null
        } else if (isLeftChild) {
            parent.left = null
        } else {
            parent.right = null
        }
    }
    
  • 代码解析:

    • 首先, 判断是否是叶结点. 通过current的left&right是否为null
    • 上面条件成立, 再判断current是否是根结点: 回答是, 那么就将this.root = null即可.
    • 如果不是根, 再判断是左结点, 还是右结点, 以便于将parent的left或者right设置为null

情况二: 一个子节点

  • 情况二: 有一个子节点

    • 这种情况也不是很难.
    • 要删除的current结点, 只有2个连接(如果有两个子结点, 就是三个连接了), 一个连接父节点, 一个连接唯一的子节点.
    • 需要从这三者之间: 爷爷 - 自己 - 儿子, 将自己(current)剪短, 让爷爷直接连接儿子即可.
    • 这个过程要求改变父节点的left或者right, 指向要删除节点的子节点.
    • 当然, 在这个过程中还要考虑是否current就是根.
  • 图解过程:

    • 如果是根的情况, 大家可以自己画一下, 比较简单, 这里不再给出.
    • 如果不是根, 并且只有一个子节点的情况.
    img
  • 代码实现如下:

    // 4.删除有一个子节点的节点
    else if (current.right === null) {
        if (current == this.root) {
            this.root = current.left
        } else if (isLeftChild) {
            parent.left = current.left
        } else {
            parent.right = current.left
        }
    } else if (current.left === null) {
        if (current == this.root) {
            this.root = current.right
        } else if (isLeftChild) {
            parent.left = current.right
        } else {
            parent.right = current.right
        }
    }
    
  • 代码解析:

    • 首先, 我们需要判断是current的left还是right为null. 因为这样才能决定, 只有我们从current中取儿子的时候, 取的是current.left还是current.right来给别的地方赋值.
    • 三种情况:
      • current是根节点, 那么直接将this.root = son.
      • current不是根节点, 是父节点的left节点, 那么parent.left = son.
      • current不是根节点, 是父节点的right节点, 那么parent.right = son.
    • 分析清楚的话, 还比较简单.

情况三: 两个子节点

  • 情况三: 有两个子节点.

    • 事情变得非常复杂, 也非常有趣了.
  • 我们先来思考一下我提出的一些问题:

    img
  • 先来, 我们来总结一下删除有两个节点的规律:

    • 如果我们要删除的节点有两个子节点, 甚至子节点还有子节点, 这种情况下我们需要从下面的子节点中找到一个节点, 来替换当前的节点.
    • 但是找到的这个节点有什么特征呢? 应该是current节点下面所有节点中最接近current节点的.
      • 要么比current节点小一点点, 要么比current节点大一点点.
      • 总结你最接近current, 你就可以用来替换current的位置.
    • 这个节点怎么找呢?
      • 比current小一点点的节点, 一定是current左子树的最大值.
      • 比current大一点点的节点, 一定是current右子树的最小值.
    • 前驱&后继
      • 而在二叉搜索树中, 这两个特别的节点, 有两个特比的名字.
      • 比current小一点点的节点, 称为current节点的前驱.
      • 比current大一点点的节点, 称为current节点的后继.
    • 也就是为了能够删除有两个子节点的current, 要么找到它的前驱, 要么找到它的后继.
    • 所以, 接下来, 我们先找到这样的节点(前驱或者后继都可以, 我这里以找后继为例)
  • 寻找后继的代码实现:

    // 找后继的方法
    BinarySerachTree.prototype.getSuccessor = function (delNode) {
        // 1.使用变量保存临时的节点
        var successorParent = delNode
        var successor = delNode
        var current = delNode.right // 要从右子树开始找
    
        // 2.寻找节点
        while (current != null) {
            successorParent = successor
            successor = current
            current = current.left
        }
    
        // 3.如果是删除图中15的情况, 还需要如下代码
        if (successor != delNode.right) {
            successorParent.left = successor.right
            successor.right = delNode.right
        }
        
        return successor
    }
    
  • 代码解析:

    • 代码是根据传入的delNode来寻找后继节点.
    • 本身代码比较简单, 但是后面有一个序号3的代码, 相对较难理解.
    • 我们这里先不做讨论, 先把找到后继后进行的操作写完, 再后头理解这段代码.
    • 序号3: TODO
  • 找到后继后的处理代码:

    // 5.删除有两个节点的节点
    else {
        // 1.获取后继节点
        var successor = this.getSuccessor(current)
        
        // 2.判断是否是根节点
        if (current == this.root) {
            this.root = successor
        } else if (isLeftChild) {
            parent.left = successor
        } else {
            parent.right = successor
        }
        
        // 3.将删除节点的左子树赋值给successor
        successor.left = current.left
    }
    
  • 代码解析:

    • 序号1: 调用刚才封装的方法, 获取后继节点.
    • 序号2: 判断三种情况:
      • 情况一: 是根节点, 那么this.root = successor. 并且successor的left应该等于current的left
      • 情况二: 是父节点的左结点, parent.left = successor, 并且successor的left应该等于current的left
      • 情况三: 是父节点的右结点, parent.right = successor, 并且successor的left应该等于current的left
    • 需要3: 就是将successor.left = current.left从判断中抽取出来.
  • 回头头看TODO的情况

    • 上面的代码实现, 对于删除9是适用的. 做法就是将7节点的left 赋值为 10. 10节点的left应该赋值为8即可.
    • 但是, 对于删除15我们还缺少什么呢?
      • 已经完成: 11的left指向了18, 18的right指向了13.
      • 没有完成: 19怎么办? 20这个左子树怎么办?
      • 很明显, 19应该放在20的左边, 20应该放在18的右边.
      • 19放在20的左边代码: successorParent.left = successor.right
      • 20放在18的右边代码: successor.right = delNode.right
    • 搞定, 收工!!!

删除节点完整代码

  • 最后, 还是给出完整代码

    // 删除结点
    BinarySerachTree.prototype.remove = function (key) {
        // 1.定义临时保存的变量
        var current = this.root
        var parent = this.root
        var isLeftChild = true
    
        // 2.开始查找节点
        while (current.key !== key) {
            parent = current
            if (key < current.key) {
                isLeftChild = true
                current = current.left
            } else {
                isLeftChild = false
                current = current.right
            }
    
            // 如果发现current已经指向null, 那么说明没有找到要删除的数据
            if (current === null) return false
        }
    
        // 3.删除的结点是叶结点
        if (current.left === null && current.right === null) {
            if (current == this.root) {
                this.root == null
            } else if (isLeftChild) {
                parent.left = null
            } else {
                parent.right = null
            }
        }
    
        // 4.删除有一个子节点的节点
        else if (current.right === null) {
            if (current == this.root) {
                this.root = current.left
            } else if (isLeftChild) {
                parent.left = current.left
            } else {
                parent.right = current.left
            }
        } else if (current.left === null) {
            if (current == this.root) {
                this.root = current.right
            } else if (isLeftChild) {
                parent.left = current.right
            } else {
                parent.right = current.right
            }
        }
    
        // 5.删除有两个节点的节点
        else {
            // 1.获取后继节点
            var successor = this.getSuccessor(current)
    
            // 2.判断是否是根节点
            if (current == this.root) {
                this.root = successor
            } else if (isLeftChild) {
                parent.left = successor
            } else {
                parent.right = successor
            }
    
            // 3.将删除节点的左子树赋值给successor
            successor.left = current.left
        }
    
        return true
    }
    
    // 找后继的方法
    BinarySerachTree.prototype.getSuccessor = function (delNode) {
        // 1.使用变量保存临时的节点
        var successorParent = delNode
        var successor = delNode
        var current = delNode.right // 要从右子树开始找
    
        // 2.寻找节点
        while (current != null) {
            successorParent = successor
            successor = current
            current = current.left
        }
    
        // 3.如果是删除图中15的情况, 还需要如下代码
        if (successor != delNode.right) {
            successorParent.left = successor.right
            successor.right = delNode.right
        }
        
        return successor
    }
    

删除节点的回顾

  • 看到这里, 你就会发现删除节点相当棘手.
  • 实际上, 因为它非常复杂, 一些程序员都尝试着避开删除操作.
    • 他们的做法是在Node类中添加一个boolean的字段, 比如名称为isDeleted.
    • 要删除一个节点时, 就将此字段设置为true.
    • 其他操作, 比如find()在查找之前先判断这个节点是不是标记为删除.
    • 这样相对比较简单, 每次删除节点不会改变原有的树结构.
    • 但是在二叉树的存储中, 还保留着那些本该已经被删除掉的节点.
  • 上面的做法看起来很聪明, 其实是一种逃避.
    • 这样会造成很大空间的浪费, 特别是针对数据量较大的情况.
    • 而且, 作为程序员要学会通过这些复杂的操作, 锻炼自己的逻辑, 而不是避重就轻.

四. 二叉搜索树完整代码

  • 最后, 我们还是给出二叉搜索树的完整代码

    // 创建BinarySearchTree
    function BinarySerachTree() {
        // 创建节点构造函数
        function Node(key) {
            this.key = key
            this.left = null
            this.right = null
        }
    
        // 保存根的属性
        this.root = null
    
        // 二叉搜索树相关的操作方法
        // 向树中插入数据
        BinarySerachTree.prototype.insert = function (key) {
            // 1.根据key创建对应的node
            var newNode = new Node(key)
    
            // 2.判断根节点是否有值
            if (this.root === null) {
                this.root = newNode
            } else {
                this.insertNode(this.root, newNode)
            }
        }
    
        BinarySerachTree.prototype.insertNode = function (node, newNode) {
            if (newNode.key < node.key) { // 1.准备向左子树插入数据
                if (node.left === null) { // 1.1.node的左子树上没有内容
                    node.left = newNode
                } else { // 1.2.node的左子树上已经有了内容
                    this.insertNode(node.left, newNode)
                }
            } else { // 2.准备向右子树插入数据
                if (node.right === null) { // 2.1.node的右子树上没有内容
                    node.right = newNode
                } else { // 2.2.node的右子树上有内容
                    this.insertNode(node.right, newNode)
                }
            }
        }
    
        // 获取最大值和最小值
        BinarySerachTree.prototype.min = function () {
            var node = this.root
            while (node.left !== null) {
                node = node.left
            }
            return node.key
        }
    
        BinarySerachTree.prototype.max = function () {
            var node = this.root
            while (node.right !== null) {
                node = node.right
            }
            return node.key
        }
    
        // 搜搜特定的值
        /*
        BinarySerachTree.prototype.search = function (key) {
            return this.searchNode(this.root, key)
        }
    
        BinarySerachTree.prototype.searchNode = function (node, key) {
            // 1.如果传入的node为null那么, 那么就退出递归
            if (node === null) {
                return false
            }
    
            // 2.判断node节点的值和传入的key大小
            if (node.key > key) { // 2.1.传入的key较小, 向左边继续查找
                return this.searchNode(node.left, key)
            } else if (node.key < key) { // 2.2.传入的key较大, 向右边继续查找
                return this.searchNode(node.right, key)
            } else { // 2.3.相同, 说明找到了key
                return true
            }
        }
        */
        BinarySerachTree.prototype.search = function (key) {
            var node = this.root
            while (node !== null) {
                if (node.key > key) {
                    node = node.left
                } else if (node.key < key) {
                    node = node.right
                } else {
                    return true
                }
            }
            return false
        }
    
        // 删除节点
        BinarySerachTree.prototype.remove = function (key) {
            // 1.获取当前的node
            var node = this.root
            var parent = null
    
            // 2.循环遍历node
            while (node) {
                if (node.key > key) {
                    parent = node
                    node = node.left
                } else if (node.key < key) {
                    parent = node
                    node = node.right
                } else {
                    if (node.left == null && node.right == null) {
    
                    }
                }
            }
        }
    
        BinarySerachTree.prototype.removeNode = function (node, key) {
            // 1.如果传入的node为null, 直接退出递归.
            if (node === null) return null
    
            // 2.判断key和对应node.key的大小
            if (node.key > key) {
                node.left = this.removeNode(node.left, key)
    
            }
        }
    
        // 删除结点
        BinarySerachTree.prototype.remove = function (key) {
            // 1.定义临时保存的变量
            var current = this.root
            var parent = this.root
            var isLeftChild = true
    
            // 2.开始查找节点
            while (current.key !== key) {
                parent = current
                if (key < current.key) {
                    isLeftChild = true
                    current = current.left
                } else {
                    isLeftChild = false
                    current = current.right
                }
    
                // 如果发现current已经指向null, 那么说明没有找到要删除的数据
                if (current === null) return false
            }
    
            // 3.删除的结点是叶结点
            if (current.left === null && current.right === null) {
                if (current == this.root) {
                    this.root == null
                } else if (isLeftChild) {
                    parent.left = null
                } else {
                    parent.right = null
                }
            }
    
            // 4.删除有一个子节点的节点
            else if (current.right === null) {
                if (current == this.root) {
                    this.root = current.left
                } else if (isLeftChild) {
                    parent.left = current.left
                } else {
                    parent.right = current.left
                }
            } else if (current.left === null) {
                if (current == this.root) {
                    this.root = current.right
                } else if (isLeftChild) {
                    parent.left = current.right
                } else {
                    parent.right = current.right
                }
            }
    
            // 5.删除有两个节点的节点
            else {
                // 1.获取后继节点
                var successor = this.getSuccessor(current)
    
                // 2.判断是否是根节点
                if (current == this.root) {
                    this.root = successor
                } else if (isLeftChild) {
                    parent.left = successor
                } else {
                    parent.right = successor
                }
    
                // 3.将删除节点的左子树赋值给successor
                successor.left = current.left
            }
    
            return true
        }
    
        // 找后继的方法
        BinarySerachTree.prototype.getSuccessor = function (delNode) {
            // 1.使用变量保存临时的节点
            var successorParent = delNode
            var successor = delNode
            var current = delNode.right // 要从右子树开始找
    
            // 2.寻找节点
            while (current != null) {
                successorParent = successor
                successor = current
                current = current.left
            }
    
            // 3.如果是删除图中15的情况, 还需要如下代码
            if (successor != delNode.right) {
                successorParent.left = successor.right
                successor.right = delNode.right
            }
        }
    
        // 遍历方法
        // 先序遍历
        BinarySerachTree.prototype.preOrderTraversal = function (handler) {
            this.preOrderTranversalNode(this.root, handler)
        }
    
        BinarySerachTree.prototype.preOrderTranversalNode = function (node, handler) {
            if (node !== null) {
                handler(node.key)
                this.preOrderTranversalNode(node.left, handler)
                this.preOrderTranversalNode(node.right, handler)
            }
        }
    
        // 中序遍历
        BinarySerachTree.prototype.inOrderTraversal = function (handler) {
            this.inOrderTraversalNode(this.root, handler)
        }
    
        BinarySerachTree.prototype.inOrderTraversalNode = function (node, handler) {
            if (node !== null) {
                this.inOrderTraversalNode(node.left, handler)
                handler(node.key)
                this.inOrderTraversalNode(node.right, handler)
            }
        }
    
        // 后续遍历
        BinarySerachTree.prototype.postOrderTraversal = function (handler) {
            this.postOrderTraversalNode(this.root, handler)
        }
    
        BinarySerachTree.prototype.postOrderTraversalNode = function (node, handler) {
            if (node !== null) {
                this.postOrderTraversalNode(node.left, handler)
                this.postOrderTraversalNode(node.right, handler)
                handler(node.key)
            }
        }
    }
    
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容