热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

如何利用JavaScript实现二叉搜索树

这篇文章主要给大家介绍了关于如何利用JavaScript实现二叉搜索树的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

计算机科学中最常用和讨论最多的数据结构之一是二叉搜索树。这通常是引入的第一个具有非线性插入算法的数据结构。二叉搜索树类似于双链表,每个节点包含一些数据,以及两个指向其他节点的指针;它们在这些节点彼此相关联的方式上有所不同。二叉搜索树节点的指针通常被称为“左”和“右”,用来指示与当前值相关的子树。这种节点的简单 Javascript 实现如下:

var node = {
  value: 125,
  left: null,
  right: null
};

从名称中可以看出,二叉搜索树被组织成分层的树状结构。第一个项目成为根节点,每个附加值作为该根的祖先添加到树中。但是,二叉搜索树节点上的值是唯一的,根据它们包含的值进行排序:作为节点左子树的值总是小于节点的值,右子树中的值都是大于节点的值。通过这种方式,在二叉搜索树中查找值变得非常简单,只要你要查找的值小于正在处理的节点则向左,如果值更大,则向右移动。二叉搜索树中不能有重复项,因为重复会破坏这种关系。下图表示一个简单的二叉搜索树。

上图表示一个二叉搜索树,其根的值为 8。当添加值 3 时,它成为根的左子节点,因为 3 小于 8。当添加值 1 时,它成为 3 的左子节点,因为 1 小于 8(所以向左)然后 1 小于3(再向左)。当添加值 10 时,它成为跟的右子节点,因为 10 大于 8。不断用此过程继续处理值 6,4,7,14 和 13。此二叉搜索树的深度为 3,表示距离根最远的节点是三个节点。

二叉搜索树以自然排序的顺序结束,因此可用于快速查找数据,因为你可以立即消除每个步骤的可能性。通过限制需要查找的节点数量,可以更快地进行搜索。假设你要在上面的树中找到值 6。从根开始,确定 6 小于 8,因此前往根的左子节点。由于 6 大于 3,因此你将前往右侧节点。你就能找到正确的值。所以你只需访问三个而不是九个节点来查找这个值。

要在 Javascript 中实现二叉搜索树,第一步要先定义基本接口:

function BinarySearchTree() {
  this._root = null;
}

BinarySearchTree.prototype = {

  //restore constructor
  constructor: BinarySearchTree,

  add: function (value){
  },

  contains: function(value){
  },

  remove: function(value){
  },

  size: function(){
  },

  toArray: function(){
  },

  toString: function(){
  }

};

基本接与其他数据结构类似,有添加和删除值的方法。我还添加了一些方便的方法,size(),toArray()和toString(),它们对 Javascript 很有用。

要掌握使用二叉搜索树的方法,最好从 contains() 方法开始。contains() 方法接受一个值作为参数,如果值存在于树中则返回 true,否则返回 false。此方法遵循基本的二叉搜索算法来确定该值是否存在:

BinarySearchTree.prototype = {

  //more code

  contains: function(value){
    var found    = false,
      current   = this._root

    //make sure there's a node to search
    while(!found && current){

      //if the value is less than the current node's, go left
      if (value  current.value){
        current = current.right;

      //values are equal, found it!
      } else {
        found = true;
      }
    }

    //only proceed if the node was found
    return found;
  },

  //more code

};

搜索从树的根开始。如果没有添加数据,则可能没有根,所以必须要进行检查。遍历树遵循前面讨论的简单算法:如果要查找的值小于当前节点则向左移动,如果值更大则向右移动。每次都会覆盖 current 指针,直到找到要找的值(在这种情况下 found 设置为 true)或者在那个方向上没有更多的节点了(在这种情况下,值不在树上)。

在 contains() 中使用的方法也可用于在树中插入新值。主要区别在于你要寻找放置新值的位置,而不是在树中查找值:

BinarySearchTree.prototype = {

  //more code

  add: function(value){
    //create a new item object, place data in
    var node = {
        value: value,
        left: null,
        right: null
      },

      //used to traverse the structure
      current;

    //special case: no items in the tree yet
    if (this._root === null){
      this._root = node;
    } else {
      current = this._root;

      while(true){

        //if the new value is less than this node's value, go left
        if (value  current.value){

          //if there's no right, then the new node belongs there
          if (current.right === null){
            current.right = node;
            break;
          } else {
            current = current.right;
          }   

        //if the new value is equal to the current one, just ignore
        } else {
          break;
        }
      }
    }
  },

  //more code

};

在二叉搜索树中添加值时,特殊情况是在没有根的情况。在这种情况下,只需将根设置为新值即可轻松完成工作。对于其他情况,基本算法与 contains() 中使用的基本算法完全相同:新值小于当前节点向左,如果值更大则向右。主要区别在于,当你无法继续前进时,这就是新值的位置。所以如果你需要向左移动但没有左侧节点,则新值将成为左侧节点(与右侧节点相同)。由于不存在重复项,因此如果找到具有相同值的节点,则操作将停止。

在继续讨论 size() 方法之前,我想深入讨论树遍历。为了计算二叉搜索树的大小,必须要访问树中的每个节点。二叉搜索树通常会有不同类型的遍历方法,最常用的是有序遍历。通过处理左子树,然后是节点本身,然后是右子树,在每个节点上执行有序遍历。由于二叉搜索树以这种方式排序,从左到右,结果是节点以正确的排序顺序处理。对于 size() 方法,节点遍历的顺序实际上并不重要,但它对 toArray() 方法很重要。由于两种方法都需要执行遍历,我决定添加一个可以通用的 traverse() 方法:

BinarySearchTree.prototype = {

  //more code

  traverse: function(process){

    //helper function
    function inOrder(node){
      if (node){

        //traverse the left subtree
        if (node.left !== null){
          inOrder(node.left);
        }      

        //call the process method on this node
        process.call(this, node);

        //traverse the right subtree
        if (node.right !== null){
          inOrder(node.right);
        }
      }
    }

    //start with the root
    inOrder(this._root);
  },

  //more code

};

此方法接受一个参数 process,这是一个应该在树中的每个节点上运行的函数。该方法定义了一个名为 inOrder() 的辅助函数用于递归遍历树。注意,如果当前节点存在,则递归仅左右移动(以避免多次处理 null )。然后 traverse() 方法从根节点开始按顺序遍历,process() 函数处理每个节点。然后可以使用此方法实现size()、toArray()、toString():

BinarySearchTree.prototype = {

  //more code

  size: function(){
    var length = 0;

    this.traverse(function(node){
      length++;
    });

    return length;
  },

  toArray: function(){
    var result = [];

    this.traverse(function(node){
      result.push(node.value);
    });

    return result;
  },

  toString: function(){
    return this.toArray().toString();
  },

  //more code

};

size() 和 toArray() 都调用 traverse() 方法并传入一个函数来在每个节点上运行。在使用 size()的情况下,函数只是递增长度变量,而 toArray() 使用函数将节点的值添加到数组中。toString()方法在调用 toArray() 之前把返回的数组转换为字符串,并返回  。

删除节点时,你需要确定它是否为根节点。根节点的处理方式与其他节点类似,但明显的例外是根节点需要在结尾处设置为不同的值。为简单起见,这将被视为 Javascript 代码中的一个特例。

删除节点的第一步是确定节点是否存在:

BinarySearchTree.prototype = {

  //more code here

  remove: function(value){

    var found    = false,
      parent   = null,
      current   = this._root,
      childCount,
      replacement,
      replacementParent;

    //make sure there's a node to search
    while(!found && current){

      //if the value is less than the current node's, go left
      if (value  current.value){
        parent = current;
        current = current.right;

      //values are equal, found it!
      } else {
        found = true;
      }
    }

    //only proceed if the node was found
    if (found){
      //continue
    }

  },
  //more code here

};

remove()方法的第一部分是用二叉搜索定位要被删除的节点,如果值小于当前节点的话则向左移动,如果值大于当前节点则向右移动。当遍历时还会跟踪 parent 节点,因为你最终需要从其父节点中删除该节点。当 found 等于 true 时,current 的值是要删除的节点。

删除节点时需要注意三个条件:

  1. 叶子节点
  2. 只有一个孩子的节点
  3. 有两个孩子的节点

从二叉搜索树中删除除了叶节点之外的内容意味着必须移动值来对树正确的排序。前两个实现起来相对简单,只删除了一个叶子节点,删除了一个带有一个子节点的节点并用其子节点替换。最后一种情况有点复杂,以便稍后访问。

在了解如何删除节点之前,你需要知道节点上究竟存在多少个子节点。一旦知道了,你必须确定节点是否为根节点,留下一个相当简单的决策树:

BinarySearchTree.prototype = {

  //more code here

  remove: function(value){

    var found    = false,
      parent   = null,
      current   = this._root,
      childCount,
      replacement,
      replacementParent;

    //find the node (removed for space)

    //only proceed if the node was found
    if (found){

      //figure out how many children
      childCount = (current.left !== null ? 1 : 0) + 
             (current.right !== null ? 1 : 0);

      //special case: the value is at the root
      if (current === this._root){
        switch(childCount){

          //no children, just erase the root
          case 0:
            this._root = null;
            break;

          //one child, use one as the root
          case 1:
            this._root = (current.right === null ? 
                   current.left : current.right);
            break;

          //two children, little work to do
          case 2:

            //TODO

          //no default

        }    

      //non-root values
      } else {

        switch (childCount){

          //no children, just remove it from the parent
          case 0:
            //if the current value is less than its 
            //parent's, null out the left pointer
            if (current.value 

处理根节点时,这是一个覆盖它的简单过程。对于非根节点,必须根据要删除的节点的值设置 parent 上的相应指针:如果删除的值小于父节点,则 left 指针必须重置为 null(对于没有子节点的节点)或删除节点的 left 指针;如果删除的值大于父级,则必须将 right 指针重置为 null 或删除的节点的 right指针。

如前所述,删除具有两个子节点的节点是最复杂的操作。下图是儿茶搜索树的一种表示。

根为 8,左子为 3,如果 3 被删除会发生什么?有两种可能性:1(3 左边的孩子,称为有序前身)或4(右子树的最左边的孩子,称为有序继承者)都可以取代 3。

这两个选项中的任何一个都是合适的。要查找有序前驱,即删除值之前的值,请检查要删除的节点的左子树,并选择最右侧的子节点;找到有序后继,在删除值后立即出现的值,反转进程并检查最左侧的右子树。其中每个都需要另一次遍历树来完成操作:

BinarySearchTree.prototype = {

  //more code here

  remove: function(value){

    var found    = false,
      parent   = null,
      current   = this._root,
      childCount,
      replacement,
      replacementParent;

    //find the node (removed for space)

    //only proceed if the node was found
    if (found){

      //figure out how many children
      childCount = (current.left !== null ? 1 : 0) + 
             (current.right !== null ? 1 : 0);

      //special case: the value is at the root
      if (current === this._root){
        switch(childCount){

          //other cases removed to save space

          //two children, little work to do
          case 2:

            //new root will be the old root's left child
            //...maybe
            replacement = this._root.left;

            //find the right-most leaf node to be 
            //the real new root
            while (replacement.right !== null){
              replacementParent = replacement;
              replacement = replacement.right;
            }

            //it's not the first node on the left
            if (replacementParent !== null){

              //remove the new root from it's 
              //previous position
              replacementParent.right = replacement.left;

              //give the new root all of the old 
              //root's children
              replacement.right = this._root.right;
              replacement.left = this._root.left;
            } else {

              //just assign the children
              replacement.right = this._root.right;
            }

            //officially assign new root
            this._root = replacement;

          //no default

        }    

      //non-root values
      } else {

        switch (childCount){

          //other cases removed to save space

          //two children, a bit more complicated
          case 2:

            //reset pointers for new traversal
            replacement = current.left;
            replacementParent = current;

            //find the right-most node
            while(replacement.right !== null){
              replacementParent = replacement;
              replacement = replacement.right;
            }

            replacementParent.right = replacement.left;

            //assign children to the replacement
            replacement.right = current.right;
            replacement.left = current.left;

            //place the replacement in the right spot
            if (current.value 

具有两个子节点的根节点和非根节点的代码几乎相同。此实现始终通过查看左子树并查找最右侧子节点来查找有序前驱。遍历是使用 while 循环中的 replacement 和 replacementParent 变量完成的。replacement中的节点最终成为替换 current 的节点,因此通过将其父级的 right 指针设置为替换的 left 指针,将其从当前位置移除。对于根节点,当 replacement 是根节点的直接子节点时,replacementParent 将为 null,因此 replacement 的 right 指针只是设置为 root 的 right 指针。最后一步是将替换节点分配到正确的位置。对于根节点,替换设置为新根;对于非根节点,替换被分配到原始 parent 上的适当位置。

说明:始终用有序前驱替换节点可能导致不平衡树,其中大多数值会位于树的一侧。不平衡树意味着搜索效率较低,因此在实际场景中应该引起关注。在二叉搜索树实现中,要确定是用有序前驱还是有序后继以使树保持适当平衡(通常称为自平衡二叉搜索树)。

总结

到此这篇关于如何利用Javascript实现二叉搜索树的文章就介绍到这了,更多相关Javascript二叉搜索树内容请搜索以前的文章或继续浏览下面的相关文章希望大家以后多多支持!


推荐阅读
  • 一、Hadoop来历Hadoop的思想来源于Google在做搜索引擎的时候出现一个很大的问题就是这么多网页我如何才能以最快的速度来搜索到,由于这个问题Google发明 ... [详细]
  • 学习SLAM的女生,很酷
    本文介绍了学习SLAM的女生的故事,她们选择SLAM作为研究方向,面临各种学习挑战,但坚持不懈,最终获得成功。文章鼓励未来想走科研道路的女生勇敢追求自己的梦想,同时提到了一位正在英国攻读硕士学位的女生与SLAM结缘的经历。 ... [详细]
  • 生成式对抗网络模型综述摘要生成式对抗网络模型(GAN)是基于深度学习的一种强大的生成模型,可以应用于计算机视觉、自然语言处理、半监督学习等重要领域。生成式对抗网络 ... [详细]
  • 云原生边缘计算之KubeEdge简介及功能特点
    本文介绍了云原生边缘计算中的KubeEdge系统,该系统是一个开源系统,用于将容器化应用程序编排功能扩展到Edge的主机。它基于Kubernetes构建,并为网络应用程序提供基础架构支持。同时,KubeEdge具有离线模式、基于Kubernetes的节点、群集、应用程序和设备管理、资源优化等特点。此外,KubeEdge还支持跨平台工作,在私有、公共和混合云中都可以运行。同时,KubeEdge还提供数据管理和数据分析管道引擎的支持。最后,本文还介绍了KubeEdge系统生成证书的方法。 ... [详细]
  • CSS3选择器的使用方法详解,提高Web开发效率和精准度
    本文详细介绍了CSS3新增的选择器方法,包括属性选择器的使用。通过CSS3选择器,可以提高Web开发的效率和精准度,使得查找元素更加方便和快捷。同时,本文还对属性选择器的各种用法进行了详细解释,并给出了相应的代码示例。通过学习本文,读者可以更好地掌握CSS3选择器的使用方法,提升自己的Web开发能力。 ... [详细]
  • “你永远都不知道明天和‘公司的意外’哪个先来。”疫情期间,这是我们最战战兢兢的心情。但是显然,有些人体会不了。这份行业数据,让笔者“柠檬” ... [详细]
  • 生成对抗式网络GAN及其衍生CGAN、DCGAN、WGAN、LSGAN、BEGAN介绍
    一、GAN原理介绍学习GAN的第一篇论文当然由是IanGoodfellow于2014年发表的GenerativeAdversarialNetworks(论文下载链接arxiv:[h ... [详细]
  • [译]技术公司十年经验的职场生涯回顾
    本文是一位在技术公司工作十年的职场人士对自己职业生涯的总结回顾。她的职业规划与众不同,令人深思又有趣。其中涉及到的内容有机器学习、创新创业以及引用了女性主义者在TED演讲中的部分讲义。文章表达了对职业生涯的愿望和希望,认为人类有能力不断改善自己。 ... [详细]
  • 解决Cydia数据库错误:could not open file /var/lib/dpkg/status 的方法
    本文介绍了解决iOS系统中Cydia数据库错误的方法。通过使用苹果电脑上的Impactor工具和NewTerm软件,以及ifunbox工具和终端命令,可以解决该问题。具体步骤包括下载所需工具、连接手机到电脑、安装NewTerm、下载ifunbox并注册Dropbox账号、下载并解压lib.zip文件、将lib文件夹拖入Books文件夹中,并将lib文件夹拷贝到/var/目录下。以上方法适用于已经越狱且出现Cydia数据库错误的iPhone手机。 ... [详细]
  • sklearn数据集库中的常用数据集类型介绍
    本文介绍了sklearn数据集库中常用的数据集类型,包括玩具数据集和样本生成器。其中详细介绍了波士顿房价数据集,包含了波士顿506处房屋的13种不同特征以及房屋价格,适用于回归任务。 ... [详细]
  • 我有使用googledrivepdf的共享链接,我必须使用angular7下载pdf文件。我已经完成了自欺 ... [详细]
  • Itwasworkingcorrectly,butyesterdayitstartedgiving401.IhavetriedwithGooglecontactsAPI ... [详细]
  • ZSI.generate.Wsdl2PythonError: unsupported local simpleType restriction ... [详细]
  • 本文介绍了Perl的测试框架Test::Base,它是一个数据驱动的测试框架,可以自动进行单元测试,省去手工编写测试程序的麻烦。与Test::More完全兼容,使用方法简单。以plural函数为例,展示了Test::Base的使用方法。 ... [详细]
  • 推荐系统遇上深度学习(十七)详解推荐系统中的常用评测指标
    原创:石晓文小小挖掘机2018-06-18笔者是一个痴迷于挖掘数据中的价值的学习人,希望在平日的工作学习中,挖掘数据的价值, ... [详细]
author-avatar
xin新的
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有