数据结构与算法 — 二叉搜索树 及其操作与实现、二叉搜索树的缺陷

目录

一、二叉搜索树

二、二叉搜索树的操作及实现

  1.插入数据

  2.遍历二叉搜索树

        (1).先序遍历

        (2).中序遍历

        (3).后序遍历

  3.最大值和最小值 

  4.搜索特定的值 

  5.二叉搜索树的删除

        (1).查找到要删除的节点

        (2).删除的节点是叶子节点

        (3).删除的节点有一个子节点

        (4).删除的节点有两个子节点

三、二叉搜索树的缺陷


一、二叉搜索树

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

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

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

举个例子:下图中第一个不是二叉搜索树,第二三个符合二叉搜索树的特征

 

二叉搜索树的特点:

        相对较小的值总是保存在左结点上,相对较大的值总是保存在右结点上。查找效率非常高,这也是二叉搜索树中“搜索”一词的来源。

 

二、二叉搜索树的操作及实现

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

  1.插入数据

    //插入数据:对外给用户调用的方法
    BinarySerachTree.prototype.insert = function(key){
        //1.根据key创建节点
        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){//向左查找
            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)
            }
        }
    }

  2.遍历二叉搜索树

        此处树的遍历,针对所有的二叉树都是适用的,不仅仅是二叉搜索树。

        树的遍历:

         遍历一棵树是指访问树的每个结点(也可以对每个结点进行某些操作)。二叉树的遍历常见的有三种方式: 先序遍历、中序遍历、后续遍历。(还有程序遍历, 使用较少, 可以使用队列来完成)

        (1).先序遍历

        遍历过程为:

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

        遍历过程:


 

代码实现: 

    //1.先序遍历
    BinarySerachTree.prototype.preOrderTraversal = function (handler) {
        this.preOrderTraversalNode(this.root, handler)
    }
    BinarySerachTree.prototype.preOrderTraversalNode = function (node, handler) {
        if (node !== null) {
            //1.处理经过的节点
            handler(node.key)

            //2.处理经过节点的左子节点
            this.preOrderTraversalNode(node.left, handler)

            //3.处理经过节点的右子节点
            this.preOrderTraversalNode(node.right, handler)
        }
    }

 测试代码:

    //测试代码
    var pkq = new BinarySerachTree()
    pkq.insert(11)
    pkq.insert(3)
    pkq.insert(6)
    pkq.insert(21)
    pkq.insert(17)
    pkq.insert(5)

    //测试遍历
    var resultString = ""
    pkq.preOrderTraversal(function(key) {
        resultString += key + " "
    })
    alert(resultString)

        (2).中序遍历

        遍历过程为:

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

        遍历过程:

 代码实现:

    //2.中序遍历
    BinarySerachTree.prototype.midOrderTraversal = function(handler){
        this.midOrderTraversalNode(this.root,handler)
    }
    BinarySerachTree.prototype.midOrderTraversalNode = function(node,handler){
        if(node !== null){
            //1.处理左子树中的节点
            this.midOrderTraversalNode(node.left,handler)
            //2.处理节点
            handler(node.key)
            //3.处理右子树中的节点
            this.midOrderTraversalNode(node.right,handler)
        }
    }

测试代码:

    //2.中序遍历
    var resultString = ""
    pkq.midOrderTraversal(function(key) {
        resultString += key + " "
    })
    alert(resultString)

        (3).后序遍历

        遍历过程为:

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

        遍历过程:

代码实现:

    //3.后序遍历
    BinarySerachTree.prototype.postOrderTraversal = function(handler){
        this.postOrderTraversalNode(this.root,handler)
    }
    BinarySerachTree.prototype.postOrderTraversalNode = function(node,handler){
        if(node !== null){
            //1.处理左子树中的节点
            this.postOrderTraversalNode(node.left,handler)
            //2.处理右子树中的节点
            this.postOrderTraversalNode(node.right,handler)
            //3.处理节点
            handler(node.key)
        }
    }

测试代码:

    //3.后序遍历
    resultString = ""
    pkq.postOrderTraversal(function(key) {
        resultString += key + " "
    })
    alert(resultString)

  3.最大值和最小值 

        依次向左找到最左边的结点就是最小值,代码依次向右找到最右边的结点就是最大值。

    //寻找最值
    //1.寻找最大值
    BinarySerachTree.prototype.max = function(){
        //1.获取根节点
        var node = this.root

        //2.依次向右不断地查找,直到节点为null
        var key = null
        while(node !== null){
            key = node.key
            node = node.right
        }

        return key
    }
    //2.寻找最小值
    BinarySerachTree.prototype.min = function(){
        //1.获取根节点
        var node = this.root

        //2.依次向左不断地查找,直到节点为null
        var key = null
        while(node !== null){
            key = node.key
            node = node.left
        }

        return key
    }

  4.搜索特定的值 

        二叉搜索树获取最值和搜索特定的值的效率都非常高。下面分别使用递归方法和循环方法来实现。关于如何选择递归方法和循环方法,其实递归和循环之间可以相互转换。大多数情况下,递归调用可以简化代码,但是也会增加空间的复杂度。循环空间复杂度较低,但是代码会相对复杂。

递归方法:

    //搜索特定的值
    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
        }
    }

循环方法:

    //搜索某一个特定的值,用for循环完成
    BinarySerachTree.prototype.search = function(key){
        //1.获取根节点、
        var node = this.root
        //2.循环搜索key
        while(node !== null){
            if(key < node.key){
                node = node.left
            }else if(key > node.key){
                node = node.right
            }else{
                return true
            }
        }
        return false
    }

  5.二叉搜索树的删除

        删除节点要先查找节点,有三种情况:

        1.该节点是叶子结点

        2.该节点有一个子节点

        3.该节点有两个子节点

 

        (1).查找到要删除的节点

    BinarySerachTree.prototype.remove = function(key){
        //1.寻找要删除的节点
        //1.1.定义变量,保存一些信息
        var current = this.root
        var parent = null
        var isLeftChild = true

        //1.2.开始寻找删除的节点
        while(current.key != key){
            parent = current
            if(key<current.key){
                isLeftChild = true
                current = current.left
            }else{
                isLeftChild = false
                current = current.right
            }

            //某种情况:已经找到了最后的节点,依然没有找到==key
            if(current == null) return false
        }

 

        (2).删除的节点是叶子节点

        //2.1.删除的节点是叶子节点(没有子节点)
        if(current.left == null && current.right == null){
            if(current == this.root){
                this.root = null
            }else if(isLeftChild){
                parent.left = null
            }else{
                parent.right = null
            }
        }

        (3).删除的节点有一个子节点

        //2.2.删除的节点有一个子节点
        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
            }
        }

        (4).删除的节点有两个子节点

        删除有两个节点的规律:

        如果要删除的节点有两个子节点,甚至子节点还有子节点,这种情况下需要从下面的子节点中找到一个节点,来替换当前的节点。

        要找到的这个节点应该是current节点下面所有节点中最接近current节点的。要么比current节点小一点点,要么比current节点大一点点。

        比current小一点点的节点,一定是current左子树的最大值;比current大一点点的节点,一定是current右子树的最小值

        在二叉搜索树中,这两个特别的节点,有两个特别的名字:前驱、后继。比current小一点点的节点,称为current节点的前驱;比current大一点点的节点,称为current节点的后继。

所以,先找到这样的节点(前驱或者后继)

寻找后继的代码实现:

    //找后继的方法
    BinarySerachTree.prototype.getSuccssor = function (delNode) {
        //1.定义变量,保存找到的后继
        var successor = delNode
        var current = delNode.right
        var successorParent = delNode
        //2.循环查找
        while (current != null) {
            successorParent = successor
            successor = current
            current = current.left
        }
        //3.判断寻找的后继节点是否直接就是delNode的right节点
        if (successor != delNode.right) {
            successorParent.left = successor.right
            successor.right = delNode.right
        }
        return successor
    }

删除有两个子节点的代码:

        //2.3.删除的节点有两个子节点
        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
        }

三、二叉搜索树的缺陷

        二叉搜索树可以快速地找到给定关键字的数据项,并且可以快速的插入和删除数据项,但是它有一个缺陷,例如,在一棵初始化为9,8,12的二叉树中插入7,6,5,4,3时,就会造成不均匀分布。

        较好的二叉搜索树数据应该是左右均匀分布的,分布得不均匀的树,称之为非平衡树。对于一棵平衡二叉树来讲,查找和插入等效率是O(logN),对于一棵非平衡二叉树,相当于一个链表,查找效率变成了O(N)

        常见的平衡树有AVL树红黑树

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值