查找基本分类如下:

  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
}
}

详细教你实现BST(二叉排序树)的更多相关文章

  1. DNS域欺骗攻击详细教程之Linux篇

    .DNS域欺骗攻击原理 DNS欺骗即域名信息欺骗是最常见的DNS安全问题.当一 个DNS服务器掉入陷阱,使用了来自一个恶意DNS服务器的错误信息,那么该DNS服务器就被欺骗了.DNS欺骗会使那些易受攻 ...

  2. Oracle 集群】ORACLE DATABASE 11G RAC 知识图文详细教程之ORACLE集群概念和原理(二)

    ORACLE集群概念和原理(二) 概述:写下本文档的初衷和动力,来源于上篇的<oracle基本操作手册>.oracle基本操作手册是作者研一假期对oracle基础知识学习的汇总.然后形成体 ...

  3. 【Oracle 集群】ORACLE DATABASE 11G RAC 知识图文详细教程之RAC 工作原理和相关组件(三)

    RAC 工作原理和相关组件(三) 概述:写下本文档的初衷和动力,来源于上篇的<oracle基本操作手册>.oracle基本操作手册是作者研一假期对oracle基础知识学习的汇总.然后形成体 ...

  4. 【Oracle 集群】ORACLE DATABASE 11G RAC 知识图文详细教程之RAC 特殊问题和实战经验(五)

    RAC 特殊问题和实战经验(五) 概述:写下本文档的初衷和动力,来源于上篇的<oracle基本操作手册>.oracle基本操作手册是作者研一假期对oracle基础知识学习的汇总.然后形成体 ...

  5. 【Oracle 集群】11G RAC 知识图文详细教程之RAC在LINUX上使用NFS安装前准备(六)

    RAC在LINUX上使用NFS安装前准备(六) 概述:写下本文档的初衷和动力,来源于上篇的<oracle基本操作手册>.oracle基本操作手册是作者研一假期对oracle基础知识学习的汇 ...

  6. 【转】【Oracle 集群】ORACLE DATABASE 11G RAC 知识图文详细教程之RAC 特殊问题和实战经验(五)

    原文地址:http://www.cnblogs.com/baiboy/p/orc5.html   阅读目录 目录 共享存储 时间一致性 互联网络(或者私有网络.心跳线) 固件.驱动.升级包的一致性 共 ...

  7. 【转】【Oracle 集群】ORACLE DATABASE 11G RAC 知识图文详细教程之RAC 工作原理和相关组件(三)

    原文地址:http://www.cnblogs.com/baiboy/p/orc3.html 阅读目录 目录 RAC 工作原理和相关组件 ClusterWare 架构 RAC 软件结构 集群注册(OC ...

  8. 【转】Oracle 集群】ORACLE DATABASE 11G RAC 知识图文详细教程之ORACLE集群概念和原理(二)

      阅读目录 目录 Oracle集群概念和原理 RAC概述 RAC 集成集群件管理 RAC 的体系结构 RAC 的结构组成和机制 RAC 后台进程 RAC 共享存储 RAC 数据库和单实例数据库的区别 ...

  9. 【转】【Oracle 集群】11G RAC 知识图文详细教程之RAC在LINUX上使用NFS安装前准备(六)

    原文地址:http://www.cnblogs.com/baiboy/p/orc6.html 阅读目录 目录 介绍 下载软件 操作系统安装 Oracle安装先决条件 创建共享磁盘 参考文献 相关文章 ...

随机推荐

  1. 【LeetCode415】Add Strings

    题目描述: 解决思路: 此题较简单,和前面[LeetCode67]方法一样. Java代码: public class LeetCode415 { public static void main(St ...

  2. javaScript真值和假值以及相等操作符

    真值和假值 相等操作符(==和===) 下面分析一下不同类型的值用相等操作符(==)比较后的结果 toNumber 对不同 类型返回的结果如下: toPrimitive 对不同类型返回的结果如下: = ...

  3. win7-x64上MySql的初次安装

    1.官网:https://dev.mysql.com/downloads/mysql/下载对应的zip包 2.将包解压缩到本地,如:F:\mysql\mysql-8.0.15-winx64 3.配置环 ...

  4. Delphi XE7调用C++动态库出现乱码问题回顾

    事情源于有个客户需使用我们C++的中间件动态库来跟设备连接通讯,但是传入以及传出的字符串指针格式都不正确(出现乱码或是被截断),估计是字符编码的问题导致.以下是解决问题的过程: 我们C++中间件动态库 ...

  5. STM32单片机复位后GPIO电平状态

    stm32单片机gpio共有八种工作模式,如下图: stm32单片机是一个低功耗的处理器,当复位以后,gpio默认是高阻状态,也就是浮空输入.这样的好处是: 1.降低了单片机的功耗 2.把gpio模式 ...

  6. Linux--find命令和 xargs命令组合

    find 查找文件的命令,并可以做出相应的处理 命令格式: find filename [选项][-print -exec -ok ...] 选项参数: 1.-name :按照文件名称查找,可以提前c ...

  7. [原][osgearth]OE地形平整代码解读

    在FlatteningLayer文件的createHeightField函数中:使用的github在2017年1月份的代码 if (!geoms.getComponents().empty()) { ...

  8. P4427 [BJOI2018]求和

    P4427 [BJOI2018]求和 同[TJOI2018]教科书般的扭曲虚空 懒得写了(雾 #include<bits/stdc++.h> #define il inline #defi ...

  9. jQuery js 格式化数字

    写程序与的时候,有些地方需要js或者jQuery取值,然后将50000000.00格式化成50,000,000.00这种形式: 首先创建formatCurrency.js,代码如下: function ...

  10. express的web server设置流程

    对于express的设置,一直是拿来就用,只知其然,今天查了一下文档,记录详细过程如下. 1.实现基本常用功能需要的模块path 用来处理路径字符串拼接,设置模板路径和静态资源路径时使用cookie- ...