手把手教你实现一个完整的BST(超级详细)

查找基本分类如下:

  1. 线性表的查找

    • 顺序查找
    • 折半查找
    • 分块查找
  2. 树表的查找
    • 二叉排序树
    • 平衡二叉树
    • B树
    • B+树
  3. 散列表的查找

今天介绍二叉排序树

二叉排序树 ( Binary Sort Tree ) 又称为二叉查找树,它是一种对排序和查找都很有用的特殊二叉树。

1. 二叉排序树的定义



二叉排序树是具有如下性质的二叉树:

  1. 若它的左子树不为空,则左子树上所有节点的值均小于它的根节点的值。
  2. 若它的右子树不为空,则右子树上的所有节点的值均大于它的根节点的值。
  3. 它的左子树、右子树也均为二叉排序树。

二叉排序树是递归定义的。所以可以得出二叉排序树的一个重要性质:中序遍历一棵二叉排序树时可以得到一个节点值递增的有序序列

若中序遍历上图二叉树,则可以得到一个按数值大小排序的递增序列:3,12,24,37,45,53,61,78,90,100

2. 创建一个二叉排序树



二叉树是由节点构成,所以我们需要一个Node类,node实例保存当前节点的数据,以及保存左右节点的指针,还可以输出当前节点数据。

  class Node {
    constructor(data, leftNode, rightNode) {
      this.data = data
      this.leftNode = leftNode
      this.rightNode = rightNode
    }
    print () {
      return this.data
    }
  }

二叉排序树有一个根节点,根节点存储了根节点的数据,左右子节点的地址,还有相应的实例方法,提供插入、遍历、查找等操作。

  class BST {
    constructor() {
      this.root = null
    }

    insert (data) {...}
    preOrder () {...}
    inOrder () {...}
    postOrder () {...}
    ...
  }

3. 二叉排序树的插入



我们要根据二叉排序树树的性质来决定insert的data的位置

  1. 若当前是一棵空树,则将插入的数据作为根节点
  2. 若不是空树,循环遍历二叉排序树的节点
    • 若当前遍历的节点的data大于要插入的data,则将下一个要遍历的节点赋值为当前遍历的节点的左节点,进行下一层循环,直到叶子节点为止,将data作为叶子节点的左节点
    • 若当前遍历的节点的data小于要插入的data,则将下一个要遍历的节点赋值为当前遍历的节点的右节点,进行下一层循环,直到叶子节点为止,将data作为叶子节点的右节点

还是代码直观

  function insert (data) {
    if (this.find(data)) {
        return
    }

    var node = new Node(data, null, null)
    if (this.root == null) {
      this.root = node
    } else {
      var currentNode = this.root
      var parentNode
      while (currentNode) {
        parentNode = currentNode
        if (data < currentNode.data) {
          currentNode = currentNode.leftNode
          if (currentNode == null) {
            parentNode.leftNode = node
            break
          }
        } else {
          currentNode = currentNode.rightNode
          if (currentNode == null) {
            parentNode.rightNode = node
            break
          }
        }
      }
    }
  }

4. 递归遍历二叉排序树



简单,贴下代码,重点在非递归遍历

  class BST {
    constructor() {
      this.data = null
    }

    preOrder () {
      preOrderFn(this.root)
    }
    inOrder () {
      inOrderFn(this.root)
    }
    postOrder () {
      postOrderFn(this.root)
    }
  }

  function preOrderFn (node) {
    if (node) {
      console.log(node.print())
      preOrderFn(node.leftNode)
      preOrderFn(node.rightNode)
    }
  }
  function inOrderFn (node) {
    if (node) {
      inOrderFn(node.leftNode)
      console.log(node.print())
      inOrderFn(node.rightNode)
    }
  }
  function postOrderFn (node) {
    postOrderFn (node.leftNode)
    postOrderFn (node.rightNode)
    console.log(node.print())
  }

5.非递归中序遍历二叉排序树



中序遍历的非递归算法最简单,后序遍历的非递归算法最难,所以先介绍中序遍历。

非递归遍历一定要用到栈。

  class Stack {
    constructor() {
      this.arr = []
    }
    pop () {
      return this.arr.shift()
    }
    push (data) {
      this.arr.unshift(data)
    }
    isEmpty () {
      return this.arr.length == 0
    }
  }

我们一点一点写想,中序遍历,肯定是要先找到左子树最下面的节点吧?想不明白就好好想想。

  function inOrderWithoutRecursion (root) {
    var parentNode = root
    var stack = new Stack()

    // 一直遍历到左子树的最下面,将一路遍历过的节点push进栈中
    while (parentNode) {
      stack.push(parentNode)
      parentNode = parentNode.leftNode
    }
  }

这里为什么要先让遍历过的节点入栈呢?中序遍历,先遍历左节点,再根节点,最后是右节点,所以我们需要保存一下根节点,以便接下来访问根节点和借助根节点来访问右节点。

1.现在我们已经到了左子树的最下面的节点了,这时它是一个叶子节点。通过遍历,它也在栈中而且是在栈顶,所以就可以访问它的data了,然后访问根节点的data,最后将parentNode指向根节点的右节点,访问右节点。

如图

按我上面说的话,代码应该是这个样子的。

    parentNode = stack.pop()
    console.log(parentNode.data)
    parentNode = stack.pop()
    console.log(parentNode.data)
    parentNode = parentNode.rightNode

2.但是还有一种情况呢?如果左子树最下面的节点没有左节点,只有右节点呢?也就是说如果这个节点不是叶子节点呢?那么就直接访问根节点的data,再将parentNode指向根节点的右节点,访问右节点。对吧?

如图

那现在代码又成了这个样子。

    parentNode = stack.pop()
    console.log(parentNode.data)
    parentNode = parentNode.rightNode

那么怎么统一格式呢?之前我们说到当parentNode不存在时就需要出栈了,那我们可以把左子树最下面的节点也就是第一种情况时的叶子节点看作一个根节点,继续访问它的右节点,因为它是一个叶子节点,所以右节点为null,所以就又执行了一次出栈操作。这时候代码就可以统一了,好好想一想,有点抽象。

统一后的代码就是情况2的代码

    parentNode = stack.pop()
    console.log(parentNode.data)
    parentNode = parentNode.rightNode

如果上面的都理解了的话,就很简单了,贴代码

  function inOrderWithoutRecursion (root) {
    if (!root)
      return

    var parentNode = root
    var stack = new Stack()

    while (parentNode || !stack.isEmpty()) {

      // 一直遍历到左子树的最下面,将一路遍历过的节点push进栈中
      while (parentNode) {
        stack.push(parentNode)
        parentNode = parentNode.leftNode
      }
      // 当parentNode为空时,说明已经达到了左子树的最下面,可以出栈操作了
      if (!stack.isEmpty()) {
        parentNode = stack.pop()
        console.log(parentNode.data)
        // 进入右子树,开始新一轮循环
        parentNode = parentNode.rightNode
      }
    }
  }

优化

  function inOrderWithoutRecursion (root) {
    if (!root)
      return

    var parentNode = root
    var stack = new Stack()

    while (parentNode || !stack.isEmpty()) {

      // 一直遍历到左子树的最下面,将一路遍历过的节点push进栈中
      if (parentNode) {
        stack.push(parentNode)
        parentNode = parentNode.leftNode
      }
      // 当parentNode为空时,说明已经达到了左子树的最下面,可以出栈操作了
      else {
        parentNode = stack.pop()
        console.log(parentNode.data)
        // 进入右子树,开始新一轮循环
        parentNode = parentNode.rightNode
      }
    }
  }

6.非递归先序遍历二叉排序树



有了中序遍历的基础,掌握先序遍历就不难了吧?先序就是到了根节点就打印出来,然后将节点入栈,然后左子树,基本与中序类似,想想就明白。

直接贴最终代码

  function PreOrderWithoutRecursion (root) {
    if (!root)
      return

    var parentNode = root
    var stack = new Stack()

    while (parentNode || !stack.isEmpty()) {

      // 一直遍历到左子树的最下面,一边打印data,将一路遍历过的节点push进栈中
      if (parentNode) {
        console.log(parentNode.data)
        stack.push(parentNode)
        parentNode = parentNode.leftNode
      }
      // 当parentNode为空时,说明已经达到了左子树的最下面,可以出栈操作了
      else {
        parentNode = stack.pop()
        // 进入右子树,开始新一轮循环
        parentNode = parentNode.rightNode
      }
    }
  }

7.非递归后序遍历二叉排序树



后序遍历中,一个根节点被访问的前提是,右节点不存在或者右节点已经被访问过

后序遍历难点在于:判断右节点是否被访问过。

  • 如果右节点不存在或者右节点已经被访问过,则访问根节点
  • 如果不符合上述条件,则跳过根节点,去访问右节点

我们可以使用一个变量来保存上一个访问的节点,如果是当前访问的节点的右节点就是上一个访问过的节点,证明右节点已经被访问过了,可以去访问根节点了。

这里需要注意的一点是:节点Node是一个对象,如果用==比较的话,返回的永远是false,所以我们比较的是node的data属性。

代码在这里

function PostOrderWithoutRecursion (root) {
    if (!root)
      return

    var parentNode = root
    var stack = new Stack()
    var lastVisitNode = null

    while (parentNode || !stack.isEmpty()) {
      if (parentNode) {
        stack.push(parentNode)
        parentNode = parentNode.leftNode
      }
      else {
        parentNode = stack.pop()
        // 如果当前节点没有右节点或者是右节点被访问过,则访问当前节点
        if (!parentNode.rightNode || parentNode.rightNode.data == lastVisitNode.data) {
          console.log(parentNode.data)
          lastVisitNode = parentNode
        }
        // 访问右节点
        else {
          stack.push(parentNode)
          parentNode = parentNode.rightNode
          while (parentNode) {
            parentNode = parentNode.leftNode
          }
        }
      }
    }
  }

8.二叉排序树的查找



写查找是为了删除节点做准备。

1.查找给定值

很简单,根据要查找的数据和根节点对比,然后遍历左子树或者右子树就好了。

  find (data) {
    var currentNode = this.root
    while (currentNode) {
      if (currentNode.data == data) {
        return currentNode
      } else if (currentNode.data > data) {
        currentNode = currentNode.leftNode
      } else {
        currentNode = currentNode.rightNode
      }
    }
    return null
  }

2.查找最大值

很简单,直接找到最右边的节点就是了

  getMax () {
    var currentNode = this.root
    while (currentNode.rightNode) {
      currentNode = currentNode.rightNode
    }
    return currentNode.data
  }

3.查找最小值

一样

  getMax () {
    var currentNode = this.root
    while (currentNode.leftNode) {
      currentNode = currentNode.leftNode
    }
    return currentNode.data
  }

9.二叉排序树的删除



删除很重要,说下逻辑:

首先从二叉排序树的根节点开始查找关键字为key的待删节点,如果树中不存在此节点,则不做任何操作;

否则,假设待删节点为delNode,其父节点为delNodeParentdelNodeLeftdelNodeRight分别为待删节点的左子树、右子树。

可设delNodedelNodeParent的左子树(右子树情况类似)。 分下面三种情况考虑

1.若delNode为叶子节点,即delNodeLeftdelNodeRight均为空。删除叶子节点不会破坏整棵树的结构,则只需修改delNodeParent的指向即可。

delNodeParent.leftNode = null

2.若delNode只有左子树delNodeLeft或者只有右子树delNodeRight,此时只要令delNodeLeft或者delNodeRight直接成为待删节点的父节点的左子树即可。

delNodeParent.leftNode = delNode.leftNode

(或者delNodeParent.leftNode = delNode.rightNode)

3.若delNode左子树和右子树均不为空,删除delNode之后,为了保持其他元素之间的相对位置不变,可以有两种处理办法

  • delNode的左子树为delNodeParent的左子树,而delNode的右子树为delNode的左子树中序遍历的最后一个节点(令其为leftBigNode,即左子树中最大的节点,因为要符合二叉树的性质,仔细想一想)的右子树

    delNodeParent.leftNode = delNode.leftNode

    leftBigNode.rightNode = delNode.rightNode

  • delNode的直接前驱(也就是左子树中最大的节点,令其为leftBigNode)替代delNode,然后再从二叉排序树中删除它的直接前驱(或直接后继,原理类似)。当以直接前驱替代delNode时,由于leftBigNode只有左子树(否则它就不是左子树中最大的节点),则在删除leftBigNode之后,只要令leftBigNode的左子树为双亲leftBigNodeParent的右子树即可。

    delNode.data = leftBigNode.data

    leftBigNodeParent.rightNode = leftBigNode.leftNode

画了三张图片来理解下:

删除节点P之前:

第一种方式删除后:

第二种方式删除后:

显然,第一种方式可能增加数的深度,而后一种方法是以被删节点左子树中最大的节点代替被删的节点,然后从左子树中删除这个节点。此节点一定没有子树(同上,否则它就不是左子树中最大的节点),这样不会增加树的高度,所以常采用这种方案,下面的算法也使用这种方案。

代码注释很清除,好好理解下,这块真的不好想

  deleteNode (data) {
    /********************** 初始化 **************************/
    var delNode = this.root,
        delNodeParent = null
    /************ 从根节点查找关键字为data的节点 ***************/
    while (delNode) {
      if (delNode.data == data) break
      delNodeParent = delNode // 记录被删节点的双亲节点
      if (delNode.data > data) delNode = delNode.leftNode // 在被删节点左子树中继续查找
      else delNode = delNode.rightNode  // 在被删节点的右子树中继续查找
    }
    if (!delNode) { // 没找到
      return
    }
    /**
     * 三种情况
     * 1.被删节点既有左子树,又有右子树
     * 2.被删节点只有右子树
     * 3.被删节点只有左子树
    **/
    var leftBigNodeParent = delNode
    if (delNode.leftNode && delNode.rightNode) { // 被删节点左右子树都存在
      var leftBigNode = delNode.leftNode
      while (leftBigNode.rightNode) { // 在被删节点的左子树中寻找其前驱节点,即最右下角的节点,也就是左子树中数值最大的节点
        leftBigNodeParent = leftBigNode
        leftBigNode = leftBigNode.rightNode // 走到右尽头
      }
      delNode.data = leftBigNode.data // 令被删节点的前驱替代被删节点
      if (leftBigNodeParent.data != delNode.data) {
        leftBigNodeParent.rightNode = leftBigNode.leftNode // 重接被删节点的前驱的父节点的右子树
      } else {
        leftBigNodeParent.leftNode = leftBigNode.leftNode // 重接被删节点的前驱的父节点的左子树
      }
    } else if (!delNode.leftNode) {
      delNode = delNode.rightNode // 若被删节点没有左子树,只需重接其右子树
    } else if (!delNode.rightNode) {
      delNode = delNode.leftNode // 若被删节点没有右子树,只需重接其左子树
    }
    /********* 将被删节点的子树挂接到其父节点的相应位置 **********/
    if (!delNodeParent) {
      this.root = delNode // 若被删节点是根节点
    } else if (leftBigNodeParent.data == delNodeParent.data) {
      delNodeParent.leftNode = delNode // 挂接到父节点的左子树位置
    } else {
      delNodeParent.rightNode = delNode // 挂接到父节点的右子树位置
    }
  }

10.其他方法



1.复制二叉排序树

这一块我先用了递归,后来想到,BST是个对象,直接深度克隆就好了。。。不说了

2.二叉排序树深度

递归递归递归

  class BST {
    constructor() {
      this.root = null
    }
    depth () {
      return depthFn(this.root)
    }
  }

  function depthFn (node) {
    if (!node) {
      return 0
    } else {
      var leftDepth = depthFn(node.leftNode)
      var rightDepth = depthFn(node.rightNode)
      if (leftDepth > rightDepth)
        return (leftDepth + 1)
      else
        return (rightDepth + 1)
    }
  }

3.二叉排序树节点个数

递归递归递归

  class BST {
    constructor() {
      this.root = null
    }
    nodeCount () {
      return nodeCount(this.root)
    }
  }
  function nodeCount(node) {
    if (!node) {
      return 0
    } else {
      return nodeCount(node.leftNode) + nodeCount(node.rightNode) + 1
    }
  }
时间: 2024-10-22 00:42:09

手把手教你实现一个完整的BST(超级详细)的相关文章

手把手教你打造一个Material Design风格的App(二)

--接上文. 3.1添加ToolBar(ActionBar) 添加ToolBar非常简单,你需要做的仅仅是为toolbar创建一个单独的layout布局,如果你想在哪里展示toolbar,只要在对应布局里将toolbar的布局文件include进来即可. (8)在res-->layout文件夹下创建一个名为toolbar.xml的文件,然后在里面添加一个android.support.v7.widget.Toolbar元素,这样就创建了一个具有特定高度和主题的toolbar. toolbar.x

手把手教你打造一个Material Design风格的App(三)

--接上文. 3.2添加抽屉导航 添加导航抽屉跟Android 5.0之前是一样的,只是以前我们使用ListView来作为菜单容器,现在我们则使用Material Design风格的RecyclerView. (14)在你工程的java文件夹中,创建3个名为activity.adapter.model的包,将MainActivity.java移到activtiy包中,这样做使得你的代码可以很好地组织和管理. (15)打开位于app模块下的build.gradle文件并添加如下依赖.添加完依赖之后

手把手教你打造一个Material Design风格的App(一)

你应该听说过Android的Material Design,它是在Android 5.0(Lollipop)版本引入的.在Material Design中还引入了很多新东西,比如Material Theme,新的小部件,自定义的阴影,矢量图片及自定义动画等.如果你之前没有用过Material Design,那么本文将是一个很好的入门教程. 在这篇教程中,我们将会学习Material Design开发的基本步骤,即编写自定义的主题以及使用RecyclerView来实现抽屉导航. 通过下面的两个链接

手把手教你画一个 逼格满满圆形水波纹loadingview Android

才没有完结呢o( ̄︶ ̄)n .大家好,这里是番外篇. 拜读了爱哥的博客,又学到不少东西.爱哥曾经说过: 要站在巨人的丁丁上. 那么今天,我们就站在爱哥的丁丁上来学习制作一款自定义view(开个玩笑,爱哥看到别打我). 转载请注明出处:http://blog.csdn.net/wingichoy/article/details/50523713 上一篇 带领大家做了一款炫酷的loading动画view 手把手带你做一个超炫酷loading成功动画view  不知道大家跟着做了一遍没有呢? 在开始之

手把手教你写一个java的orm(完)

生成sql:select 上一篇讲了怎样生成一个sql中where的一部分,之后我们要做事情就简单很多了,就只要像最开始一样的生成各种sql语句就好了,之后只要再加上我们需要的条件,一个完整的sql就顺利的做好了. 现在我们开始写生成查询语句的sql.一个查询语句大致上是这样的: SELECT name, id, create_date, age, mark, status FROM user 这里可以看出来,一个基础的查询语句基本上就是一个 SELECT 后面加上需要查询的字段,跟上 FROM

手把手教你构建一个音视频小程序

欢迎大家前往腾讯云+社区,获取更多腾讯海量技术实践干货哦~ 本文由腾讯视频云终端团队发表于云+社区专栏 腾讯云提供了全套技术文档和源码来帮助您快速构建一个音视频小程序,但是再好的源码和文档也有学习成本,为了尽快的能调试起来,我们还提供了一个免费的一键部署服务:您只需轻点几下鼠标,就可以在自己的账号下获得一个音视频小程序,同时附送一台拥有独立域名的测试服务器,让您可以在 5 分钟内快速构建出自己的测试环境. 通过微信公众平台授权登录腾讯云 打开 微信公众平台 注册并登录小程序,按如下步骤操作: 单

手把手教你写一个RN小程序!

时间过得真快,眨眼已经快3年了! 1.我的第一个App 还记得我14年初写的第一个iOS小程序,当时是给别人写的一个单机的相册,也是我开发的第一个完整的app,虽然功能挺少,但是耐不住心中的激动啊,现在我开始学react native,那么现在对于react native也算是有所了解了,就用网上的接口开发一个小程序,现在带大家来写这个程序!接口是用看知乎的API,简简单单的只有get,可以从这里入门,也算是带大家入门吧,过后我会把源代码放在我的github上,前期项目肯定特别简陋,后面慢慢来优

手把手教你实现一个引导动画

前言 最近看了一些文章,知道了实现引导动画的基本原理,所以决定来自己亲手做一个通用的引导动画类. 我们先来看一下具体的效果:点这里 原理 通过维护一个Modal实例,使用Modal的mask来隐藏掉页面的其他元素. 根据用户传入的需要引导的元素列表,依次来展示元素.展示元素的原理:通过cloneNode来复制一个当前要展示元素的副本,通过当前元素的位置信息来展示副本,并且通过z-index属性来让其在ModalMask上方展示.大致代码如下: const newEle = target.clon

手把手教你实现一个 Vue 进度条组件!

最近在个人的项目中,想对页面之间跳转的过程进行优化,想到了很多文档或 npm 等都用到的页面跳转进度条,于是便想自己去实现一个,特此记录. 来看下 npm 搜索组件时候的效果: so 下面咱们一起动手实现一下呗.  定义使用方式  想实现一个组件的前提,一定要想好你的需求是什么,还要自己去定义一个舒服的使用方法,这其中也是有原则的,对使用者来说,使用方式越简单越好.那么对应的代价就是写这个组件的复杂度会变高. 我想要的使用方式是这样的:可以在任意的地方去调用到这个方法,可以随时控制其状态.  看