js二叉搜索树以及先序、中序、后序、层次遍历

二叉搜索树

文章代码及一些解释性词语来自:https://www.bilibili.com/video/BV1x7411L7Q7?p=73,视频中少了一些内容,这里我都做了一些我自己的解释

满足二叉树搜索树的条件:

  1. 非空左子树中任意一个节点的值都要小于根节点的值
  2. 右子节点中任意一个节点的值都要大于根节点的值
  3. 左右子树本身也都是二叉搜索树

如:

在这里插入图片描述

在这个二叉树中,因为6作为5的左子树,不能比5要大,所以6应该出现在5的右子树中

在这里插入图片描述

在这个二叉树中,则才是正确的二叉搜索树。

有的人会问,这两个不是一样的吗,注意:在二叉树中,严格区分左右节点,在图中就应该表示为向右的箭头指向右节点,反之则指向左节点。

3.1 了解二叉树的遍历

下面的遍历都已下图作为参考:

在这里插入图片描述

  • 先序遍历

我用一句话概括就是:根左右

什么是根左右呢?即:首先从全局看,是有一个二叉树的,但是对于7这个节点来看,7可以看做是其子节点的根节点,以此类推。那么根左右的意思就是,每次遍历,都先遍历根节点、其次是左子节点、最后是右子节点。那么对于上面图中的例子,从大到小看,首先根是11

遍历顺序:11、

之后就是遍历左子节点。由于左子节点也是一个二叉树,他自身就是根节点,所以第二个遍历到的应该是7

遍历顺序:11、7

依次类推:11、7、5、3

之后由于三到了叶子节点了,遍历完之后,则需要回退到又5组成的二叉树,由于5的左节点都遍历完了,下面就需要遍历5的右子节点,由于没有,所以已经回退到7组成的二叉树,你要知道,每次回退上来,都代表着左子节点已经遍历完了,然后就是遍历右子节点,由于右子节点9她自身也是一个二叉树,所以他也要遵循根左右的顺序继续遍历,结果如下:

遍历顺序:11、7、5、3、9、8、10

到这里为止,全局的左子树已经遍历完了,下面就需要遍历全局的右子树了,操作还是依据根左右的方式,最终的结果如下:

遍历结果:11, 7, 5, 3, 9, 8, 10, 15, 13, 12, 14, 20, 18, 19, 25

  • 中序遍历

我用一句话概括就是:左根右

方法跟上面是一样的,区别只在于先遍历左还是根还是右的区别,结果如下:

3, 5, 7, 8, 9, 10, 11, 12, 13, 14, 15, 18, 19, 20, 25

在中序遍历中,你会发现,对于二叉搜索树而言 ,中序遍历就是升序排序

  • 后序遍历

我用一句话概括就是:左右根

结果为:3, 5, 8, 10, 9, 7, 12, 14, 13, 19, 18, 25, 20, 15, 11

  • 层次遍历

层次遍历没什么好说的,就是一层层的从左往右遍历,结果为:

11, 7, 15, 5, 9, 13, 20, 3, 8, 10, 12, 14, 18, 25, 19

了解了这些遍历之后,我们用代码来写一个 二叉搜索树

3.2 编写二叉搜索树

  1. 写出一个架构:
function BinarySearchTree() {
  // 用于创建一个新的节点,每个节点都有一个指向左子节点的指针和一个指向右子节点的指针,还有一个保存自己值的key
  function Node(key) {
    this.key = key
    this.left = null
    this.right = null
  }
  // 指向根节点
  this.root = null
}
  1. 定义一个插入的方法,传入一个key,在内部创建一个节点,并添加到指定位置
// 递归方法
function insertNode(node, newNode) {
    if (newNode.key < node.key) {
        if (!node.left) {
            node.left = newNode
        } else {
            insertNode(node.left, newNode)
        }
    } else {
        if (!node.right) {
            node.right = newNode
        } else {
            insertNode(node.right, newNode)
        }
    }
}
BinarySearchTree.prototype.insert = function (key) {
  // 1.创建一个新节点
  let newNode = new Node(key)
  // 2.判断根节点是否有元素,如果没有则表示这是插入的第一个元素,反之则需要找到指定位置插入
  if (!this.root) {
    this.root = newNode
  } else {
    let current = this.root
    // 方式二:调用一个递归函数
    insertNode(current, newNode)
  }
}
  1. 根据key,查找到指定的元素,有则返回true,没有则返回false
BinarySearchTree.prototype.search = function (key) {
  if (typeof key !== "number") return false
  let current = this.root
  while (current) {
    if (key < current.key) {
      current = current.left
    } else if (key > current.key) {
      current = current.right
    } else {
      return true
    }
  }
  return false
}
  1. 先序、中序、后序遍历
// 调用的递归方法
/**
 * 递归遍历先、中、后序的函数
 * node:需要进行某种遍历的树结构
 * type:按某种方式进行遍历。如:0 => 先序遍历,1 => 中序遍历,2 => 后序遍历
 */
function ergodic(node, type = 1) {
    if (!node) return
    let arr = [], left = [], right = [], root = []
    root.push(node.key)
    if (node.left) {
        left.push(...ergodic(node.left, type))
    }
    if (node.right) {
        right.push(...ergodic(node.right, type))
    }
    if (type === 0) {
        arr.push(...root, ...left, ...right)
    } else if (type === 2) {
        arr.push(...left, ...right, ...root)
    } else if (type === 1) {
        arr.push(...left, ...root, ...right)
    }
    return arr
}
// 中序遍历
BinarySearchTree.prototype.inOrderTraverse = function () {
  return ergodic(this.root)
}
// 先序遍历
BinarySearchTree.prototype.preOrderTraverse = function () {
  return ergodic(this.root, 0)
}
// 后序遍历
BinarySearchTree.prototype.postOrderTraverse = function () {
  return ergodic(this.root, 2)
}
  1. 查找最小值
BinarySearchTree.prototype.min = function () {
  // 方式一:
  // return ergodic(this.root)[0]
  // 方式二:
  let current = this.root
  while (current.left) {
    current = current.left
  }
  return current.key
}
  1. 查找最大值
BinarySearchTree.prototype.max = function () {
  // 方式一:
  /*let arr = ergodic(this.root)
  return arr[arr.length - 1]*/
  // 方式二:
  let current = this.root
  while (current.right) {
    current = current.right
  }
  return current.key
}
  1. 根据key删除某一个节点

删除操作有点复杂,我们细细说:

首先删除有三种情况:

在这里插入图片描述

  1. 删除的节点刚好是叶子节点,则可以直接删除即可,伪代码如下:
// 找到需要被删除的节点的父节点,假如要删除的key为3,那么则需要找到5即可,通过判断5的left或者right指向的哪个节点的key为3,则删除即可
  1. 删除的节点有一个子节点,则需要将被删除节点的父节点的left或者right指向被删除节点的子节点
// 比如要删除5这个节点,则需要将7的left指向5的left
// 如果3是在5的右子树,则需要将7的left指向5的right
// 反之,如果是在7的右子树中删除一个只有一个子节点的元素,那就是改变的7的right即可
  1. 删除的节点有两个子节点
// 这种情况就比较复杂了,首先,假如我们需要删除一个7这个元素,则面对他的左右两个子树,需要选择一个合适的节点替换到7这个位置,同时还要保证删除后的结果还是一个二叉搜索树。假如我们在7的左子树中找,即在3和5中选一个替换,则只能选择5。如果用右子树中找一个替换,则在8/9/10中,只能是8
// 如果替换15呢,则我们如果在左子树中找,只能是14;而在右子树中,只能是18
---------------------------
// 下面我们来找一下规律,如果你仔细发现的话,你会看出来,其实就是如果在左子树中找的话,找的绝对是里面最大的值,而在右子树中找的话,找的绝对是里面最小的值。
----------------------------
// 其次,找到被替换的节点之后,还需要处理的就是该节点他的子节点要怎么处理?
// 其实很简单,如果是在右子节点中找的话,因为被替换的节点是右子节点树中最大的值,这代表则,这个被替换的元素肯定right指向为null,因为right指向的节点是要比他要大的,既然他已经是最大的了,就说明他的right指向为null,根据这个判断条件,我们就能知道被替换的元素肯定是一个只有一个子节点的元素。在左节点树中就是相反的,被删除的节点的left肯定指向为null,因为他自己已经是最小的了。所以我们只需要找到待替换节点的父节点,让该父节点指向待替换节点的left或者right就可以了。具体是left还是right,则看你是让左子树中节点替换还是右子树中节点替换。

说了那么多,上代码:

BinarySearchTree.prototype.remove = function (key) {
    if (typeof key !== "number") return false
    let current = this.root
    let parent = null
    // 在循环中找到需要被删除的元素,保存在current中,parent为current的父节点,注意,如果key直接等于根节点的话,parent=null
    while (current.key !== key) {
        parent = current
        if (key < current.key) {
            current = current.left
        } else if (key > current.key) {
            current = current.right
        }
        if (!current) return false
    }
    // 下面就是找到了节点需要删除的情况
    // 1:删除的节点是叶子节点,则直接使用parent删除
    if (!current.left && !current.right) {
        // 1.1:如果该节点是根节点,同时也是叶子节点
        if (current === this.root) {
            this.root = null
        }
        // 1.2:如果该节点是parent的左孩子节点
        else if (parent.left.key === key) {
            parent.left = null
        }
        // 1.3:如果该节点是parent的右孩子节点
        else if (parent.right.key === key) {
            parent.right = null
        }
    }
    // 2:删除的节点有一个子节点
    else if (!current.left || !current.right) {
        if (parent.left.key === key) {
            parent.left = current.left? current.right : current.left
        }
        else if (parent.right.key === key) {
            parent.right = current.left? current.right : current.left
        }
    }
    // 3:删除的节点有两个子节点
    else if (current.left && current.right) {
        // 3.1:如果要删除的节点是根节点,同时也有两个子节点,区别对待根节点是因为如果是根节点,则parent为null
        if (current === this.root) {
            let child = current.left
            let parentChild = null
            while (child.right) {
                parentChild = child
                child = child.right
            }
            // parentChild为待替换节点的父节点,child为带替换节点
            parentChild.right = child.left
            this.root = child
            child.left = current.left
            child.right = current.right
        }
        // 3.2:不是根节点,而是其他的普通的有两个子节点的节点
        else {
            let child = current.left
            let parentChild = null
            while (child.right) {
                parentChild = child
                child = child.right
            }
            // 到这里说明找到了被替换的子节点了
            // parentChild为待替换节点的父节点,parent为被替换节点的父节点,child为带替换节点
            parentChild.right = child.left
            parent.left.key === current.key? parent.left = child : parent.right = child
            child.left = current.left
            child.right = current.right
        }
    }
    return true
}

到这里,二叉搜索树的相关代码就完成了,如果有兴趣的话您可以测试一下。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值