【数据结构与算法】JavaScript实现二叉搜索树

文章目录

      • 一、二叉搜索树的封装
        • 1.插入数据
        • 2.遍历数据
          • 2.1.先序遍历
          • 2.2.中序遍历
          • 2.3.后续遍历
        • 3.查找数据
          • 3.1.查找最大值&最小值
          • 3.2.查找特定值
        • 4.删除数据
          • 4.1.情况1:没有子节点
          • 4.2.情况2:有一个子节点
          • 4.3.情况3:有两个子节点
          • 4.4.完整实现
        • 5.二叉搜索树完整封装
      • 二、平衡树

一、二叉搜索树的封装

二叉树搜索树的基本属性

如图所示:二叉搜索树有四个最基本的属性:指向节点的(root),节点中的(key)、左指针(right)、右指针(right)。

在这里插入图片描述

所以,二叉搜索树中除了定义root属性外,还应定义一个节点内部类,里面包含每个节点中的left、right和key三个属性:

    //封装二叉搜索树function BinarySearchTree(){//节点内部类function Node(key){this.key = keythis.left = nullthis.right = null}//属性this.root = null}

二叉搜索树的常见操作:

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

实现思路:

  • 首先根据传入的key创建节点对象;
  • 然后判断根节点是否存在,不存在时通过:this.root = newNode,直接把新节点作为二叉搜索树的根节点。
  • 若存在根节点则重新定义一个内部方法insertNode()用于查找插入点。
     //insert方法:对外向用户暴露的方法BinarySearchTree.prototype.insert = function(key){//1.根据key创建节点let newNode = new Node(key)//2.判断根节点是否存在if (this.root == null) {this.root = newNode//根节点存在时}else {this.insertNode(this.root, newNode)}}

内部方法insertNode()的实现思路:

根据比较传入的两个节点,一直查找新节点适合插入的位置,直到成功插入新节点为止。

当newNode.key < node.key向左查找:

  • 情况1:当node无左子节点时,直接插入:
  • 情况2:当node有左子节点时,递归调用insertNode(),直到遇到无左子节点成功插入newNode后,不再符合该情况,也就不再调用insertNode(),递归停止。

在这里插入图片描述

当newNode.key >= node.key向右查找,与向左查找类似:

  • 情况1:当node无右子节点时,直接插入:
  • 情况2:当node有右子节点时,依然递归调用insertNode(),直到遇到传入insertNode方法的node无右子节点成功插入newNode为止:

在这里插入图片描述

insertNode()代码实现:

      //内部使用的insertNode方法:用于比较节点从左边插入还是右边插入BinarySearchTree.prototype.insertNode = function(node, newNode){//当newNode.key < node.key向左查找
/*----------------------分支1:向左查找--------------------------*/      if(newNode.key < node.key){//情况1:node无左子节点,直接插入
/*----------------------分支1.1--------------------------*/if (node.left == null) {node.left = newNode//情况2:node有左子节点,递归调用insertNode(),直到遇到无左子节点成功插入newNode后,不再符合该情况,也就不再调用insertNode(),递归停止。
/*----------------------分支1.2--------------------------*/}else{this.insertNode(node.left, newNode)}//当newNode.key >= node.key向右查找
/*-----------------------分支2:向右查找--------------------------*/        }else{//情况1:node无右子节点,直接插入
/*-----------------------分支2.1--------------------------*/ if(node.right == null){node.right == newNode//情况2:node有右子节点,依然递归调用insertNode(),直到遇到无右子节点成功插入newNode为止
/*-----------------------分支2.2--------------------------*/ }else{this.insertNode(node.right, newNode)}}}

过程详解:

为了更好理解以下列二叉搜索树为例:

在这里插入图片描述

想要上述的二叉搜索树(蓝色)中插入数据10:

  • 先把key = 10 传入insert方法,由于存在根节点 9,所以直接调用insetNode方法,传入的参数:node = 9,newNode = 10;
  • 由于10 > 9,进入分支2,向右查找适合插入的位置;
  • 由于根节点 9 的右子节点存在且为 13 ,所以进入分支2.2,递归调用insertNode方法,传入的参数:node = 13,newNode = 10;
  • 由于 10 < 13 ,进入分支1,向左查找适合插入的位置;
  • 由于父节点 13 的左子节点存在且为11,所以进入分支1.2,递归调用insertNode方法,传入的参数:node = 11,newNode = 10;
  • 由于 10 < 11,进入分支1,向左查找适合插入的位置;
  • 由于父节点 11 的左子节点不存在,所以进入分支1.1,成功插入节点 10 。由于不符合分支1.2的条件所以不会继续调用insertNode方法,递归停止。

测试代码:

    //测试代码//1.创建BinarySearchTreelet bst = new BinarySearchTree()//2.插入数据bst.insert(11);bst.insert(7);bst.insert(15);bst.insert(5);bst.insert(9);console.log(bst);

应得到下图所示的二叉搜索树:

在这里插入图片描述

测试结果

在这里插入图片描述

2.遍历数据

这里所说的树的遍历不仅仅针对二叉搜索树,而是适用于所有的二叉树。由于树结构不是线性结构,所以遍历方式有多种选择,常见的三种二叉树遍历方式为:

  • 先序遍历;
  • 中序遍历;
  • 后序遍历;

还有层序遍历,使用较少。

2.1.先序遍历

先序遍历的过程为:

  • 首先,遍历根节点;
  • 然后,遍历其左子树;
  • 最后,遍历其右子树;

在这里插入图片描述

如上图所示,二叉树的节点遍历顺序为:A -> B -> D -> H -> I -> E -> C -> F -> G。

代码实现:

	  //先序遍历//掺入一个handler函数方便之后对得到的key进行处理BinarySearchTree.prototype.preOrderTraversal = function(handler){this.preOrderTraversalNode(this.root, handler)}//封装内部方法,对某个节点进行遍历BinarySearchTree.prototype.preOrderTraversalNode = function(node,handler){if (node != null) {//1.处理经过的节点handler(node.key)
/*----------------------递归1----------------------------*///2.遍历左子树中的节点this.preOrderTraversalNode(node.left, handler)
/*----------------------递归2----------------------------*///3.遍历右子树中的节点this.preOrderTraversalNode(node.right, handler)}}

过程详解:

以遍历以下二叉搜索树为例:

在这里插入图片描述

首先调用preOrderTraversal方法,在方法里再调用preOrderTraversalNode方法用于遍历二叉搜索树。在preOrderTraversalNode方法中,递归1负责遍历左子节点,递归2负责遍历右子节点。先执行递归1,执行过程如下图所示:

记:preOrderTraversalNode() 为 A()

在这里插入图片描述

可以看到一共递归调用了4次方法A,分别传入11、7、5、3,最后遇到null不满足 node != null 条件结束递归1;注意此时只是执行完最开始的递归1,并没有执行递归2,并且递归1执行到null停止后要一层层地往上返回,按顺序将调用的函数压出函数调用栈。

关于函数调用栈:之前的四次递归共把4个函数压入了函数调用栈,现在递归执行完了一层层地把函数压出栈。

值得注意的是:每一层函数都只是执行完了递归1,当返回到该层函数时,比如A(3)要继续执行递归2遍历二叉搜索树中的右子节点;

在执行递归2的过程中会不断调用方法A,并依次执行递归1和递归2,以此类推直到遇到null不满足 node != null 条件为止,才停止递归并一层层返回,如此循环。同理A(5)层、A(7)层、A(11)层都要经历上述循环,直到将二叉搜索树中的节点全部遍历完为止。

具体过程如下图所示:

在这里插入图片描述

测试代码:

    //测试代码//1.创建BinarySearchTreelet bst = new BinarySearchTree()//2.插入数据bst.insert(11);bst.insert(7);bst.insert(15);bst.insert(5);bst.insert(3);bst.insert(9);bst.insert(8);bst.insert(10);bst.insert(13);bst.insert(12);bst.insert(14);bst.insert(20);bst.insert(18);bst.insert(25);bst.insert(6);//3.测试遍历let resultString = ""//掺入处理节点值的处理函数bst.preOrderTraversal(function(key){resultString += key + "->"})alert(resultString)

应输出这样的顺序:11 -> 7 -> 5 -> 3 -> 6 -> 9 -> 8 -> 10 -> 15 -> 13 ->12 -> 14 -> 20 -> 18 -> 25 。

测试结果:

在这里插入图片描述

2.2.中序遍历

实现思路:与先序遍历原理相同,只不过是遍历的顺序不一样了。

  • 首先,遍历其左子树;
  • 然后,遍历根(父)节点;
  • 最后,遍历其右子树;

代码实现:

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

过程详解:

遍历的顺序应如下图所示:

在这里插入图片描述

首先调用midOrderTraversal方法,在方法里再调用midOrderTraversalNode方法用于遍历二叉搜索树。先使用递归1遍历左子树中的节点;然后,处理父节点;最后,遍历右子树中的节点。

测试代码:

  //测试代码//1.创建BinarySearchTreelet bst = new BinarySearchTree()//2.插入数据bst.insert(11);bst.insert(7);bst.insert(15);bst.insert(5);bst.insert(3);bst.insert(9);bst.insert(8);bst.insert(10);bst.insert(13);bst.insert(12);bst.insert(14);bst.insert(20);bst.insert(18);bst.insert(25);bst.insert(6);	//3.测试中序遍历let resultString2 =""bst.midOrderTraversal(function(key){resultString2 += key + "->"})alert(resultString2)

输出节点的顺序应为:3 -> 5 -> 6 -> 7 -> 8 -> 9 -> 10 -> 11 -> 12 -> 13 -> 14 -> 15 -> 18 -> 20 -> 25 。

测试结果:

在这里插入图片描述

2.3.后续遍历

实现思路:与先序遍历原理相同,只不过是遍历的顺序不一样了。

  • 首先,遍历其左子树;
  • 然后,遍历其右子树;
  • 最后,遍历根(父)节点;

代码实现:

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

过程详解:

遍历的顺序应如下图所示:

在这里插入图片描述

首先调用postOrderTraversal方法,在方法里再调用postOrderTraversalNode方法用于遍历二叉搜索树。先使用递归1遍历左子树中的节点;然后,遍历右子树中的节点;最后,处理父节点。

测试代码:

    //测试代码//1.创建BinarySearchTreelet bst = new BinarySearchTree()//2.插入数据bst.insert(11);bst.insert(7);bst.insert(15);bst.insert(5);bst.insert(3);bst.insert(9);bst.insert(8);bst.insert(10);bst.insert(13);bst.insert(12);bst.insert(14);bst.insert(20);bst.insert(18);bst.insert(25);bst.insert(6);//3.测试后序遍历let resultString3 =""bst.postOrderTraversal(function(key){resultString3 += key + "->"})alert(resultString3)

输出节点的顺序应为:3 -> 6 -> 5 -> 8 -> 10 -> 9 -> 7 -> 12 -> 14 -> 13 -> 18 -> 25 -> 20 -> 15 -> 11 。

测试结果:

在这里插入图片描述

**总结:**以遍历根(父)节点的顺序来区分三种遍历方式。比如:先序遍历先遍历根节点、中序遍历第二遍历根节点、后续遍历最后遍历根节点。

3.查找数据
3.1.查找最大值&最小值

在二叉搜索树中查找最值非常简单,最小值在二叉搜索树的最左边,最大值在二叉搜索树的最右边。只需要一直向左/右查找就能得到最值,如下图所示:

在这里插入图片描述

代码实现:

      //寻找最大值BinarySearchTree.prototype.max = function () {//1.获取根节点let node = this.root//2.定义key保存节点值let key = null//3.依次向右不断查找,直到节点为nullwhile (node != null) {key = node.keynode = node.right}return key}//寻找最小值BinarySearchTree.prototype.min = function(){//1.获取根节点let node = this.root//2.定义key保存节点值let key = null//3.依次向左不断查找,直到节点为nullwhile (node != null) {key = node.keynode = node.left}return key}

测试代码:

   //测试代码//1.创建BinarySearchTreelet bst = new BinarySearchTree()//2.插入数据bst.insert(11);bst.insert(7);bst.insert(15);bst.insert(5);bst.insert(3);bst.insert(9);bst.insert(8);bst.insert(10);bst.insert(13);bst.insert(12);bst.insert(14);bst.insert(20);bst.insert(18);bst.insert(25);bst.insert(6);//4.测试最值console.log(bst.max());console.log(bst.min());

测试结果:

在这里插入图片描述

3.2.查找特定值

查找二叉搜索树当中的特定值效率也非常高。只需要从根节点开始将需要查找节点的key值与之比较,若node.key < root则向左查找,若node.key > root就向右查找,直到找到或查找到null为止。这里可以使用递归实现,也可以采用循环来实现。

实现代码:

     //查找特定的keyBinarySearchTree.prototype.search = function(key){//1.获取根节点let node = this.root//2.循环搜索keywhile(node != null){if (key < node.key) {//小于根(父)节点就往左边找node = node.left//大于根(父)节点就往右边找}else if(key > node.key){node = node.right}else{return true}} return false}

测试代码:

    //测试代码//1.创建BinarySearchTreelet bst = new BinarySearchTree()//2.插入数据bst.insert(11);bst.insert(7);bst.insert(15);bst.insert(5);bst.insert(3);bst.insert(9);bst.insert(8);bst.insert(10);bst.insert(13);bst.insert(12);bst.insert(14);bst.insert(20);bst.insert(18);bst.insert(25);bst.insert(6);//3.测试搜索方法console.log(bst.search(24));//falseconsole.log(bst.search(13));//trueconsole.log(bst.search(2));//false

测试结果:

在这里插入图片描述

4.删除数据

实现思路:

**第一步:**先找到需要删除的节点,若没找到,则不需要删除;

首先定义变量current用于保存需要删除的节点、变量parent用于保存它的父节点、变量isLeftChild保存current是否为parent的左节点,这样方便之后删除节点时改变相关节点的指向。

实现代码:

 		//1.1.定义变量let current = this.rootlet parent = nulllet isLeftChild = true//1.2.开始寻找删除的节点while (current.key != key) {parent = current// 小于则往左查找if (key < current.key) {isLeftChild = truecurrent = current.left} else{isLeftChild = falsecurrent = current.rigth}//找到最后依然没有找到相等的节点if (current == null) {return false}}//结束while循环后:current.key = key

**第二步:**删除找到的指定节点,后分3种情况:

  • 删除叶子节点;
  • 删除只有一个子节点的节点;
  • 删除有两个子节点的节点;
4.1.情况1:没有子节点

没有子节点时也有两种情况:

当该叶子节点为根节点时,如下图所示,此时current == this.root,直接通过:this.root = null,删除根节点。

在这里插入图片描述

当该叶子节点不为根节点时也有两种情况,如下图所示:

在这里插入图片描述

若current = 8,可以通过:parent.left = null,删除节点8;

若current = 10,可以通过:parent.right = null,删除节点10;

代码实现:

        //情况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}}
4.2.情况2:有一个子节点

有六种情况分别是:

当current存在左子节点时(current.right == null):

  • 情况1:current为根节点(current == this.root),如节点11,此时通过:this.root = current.left,删除根节点11;
  • 情况2:current为父节点parent的左子节点(isLeftChild == true),如节点5,此时通过:parent.left = current.left,删除节点5;
  • 情况3:current为父节点parent的右子节点(isLeftChild == false),如节点9,此时通过:parent.right = current.left,删除节点9;

在这里插入图片描述

当current存在右子节点时(current.left = null):

  • 情况4:current为根节点(current == this.root),如节点11,此时通过:this.root = current.right,删除根节点11。
  • 情况5:current为父节点parent的左子节点(isLeftChild == true),如节点5,此时通过:parent.left = current.right,删除节点5;
  • 情况6:current为父节点parent的右子节点(isLeftChild == false),如节点9,此时通过:parent.right = current.right,删除节点9;

在这里插入图片描述

实现代码:

        //情况2:删除的节点有一个子节点//当current存在左子节点时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}//当current存在右子节点时} else if(current.left == null){if (current == this.root) {this.root = current.rigth} else if(isLeftChild) {parent.left = current.right} else{parent.right = current.right} }
4.3.情况3:有两个子节点

这种情况十分复杂,首先依据以下二叉搜索树,讨论这样的问题:

在这里插入图片描述

删除节点9

在保证删除节点9后原二叉树仍为二叉搜索树的前提下,有两种方式:

  • 方式1:从节点9的左子树中选择一合适的节点替代节点9,可知节点8符合要求;
  • 方式2:从节点9的右子树中选择一合适的节点替代节点9,可知节点10符合要求;

在这里插入图片描述

删除节点7

在保证删除节点7后原二叉树仍为二叉搜索树的前提下,也有两种方式:

  • 方式1:从节点7的左子树中选择一合适的节点替代节点7,可知节点5符合要求;
  • 方式2:从节点7的右子树中选择一合适的节点替代节点7,可知节点8符合要求;

在这里插入图片描述

删除节点15

在保证删除节点15后原树二叉树仍为二叉搜索树的前提下,同样有两种方式:

  • 方式1:从节点15的左子树中选择一合适的节点替代节点15,可知节点14符合要求;
  • 方式2:从节点15的右子树中选择一合适的节点替代节点15,可知节点18符合要求;

在这里插入图片描述

相信你已经发现其中的规律了!

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

若用current表示需要删除的节点,则合适的节点指的是:

  • current左子树中比current小一点点的节点,即current左子树中的最大值
  • current右子树中比current大一点点的节点,即current右子树中的最小值

前驱&后继

在二叉搜索树中,这两个特殊的节点有特殊的名字:

  • 比current小一点点的节点,称为current节点的前驱。比如下图中的节点5就是节点7的前驱;
  • 比current大一点点的节点,称为current节点的后继。比如下图中的节点8就是节点7的后继;

在这里插入图片描述

代码实现:

  • 查找需要被删除的节点current的后继时,需要在current的右子树中查找最小值,即在current的右子树中一直向左遍历查找;
  • 查找前驱时,则需要在current的左子树中查找最大值,即在current的左子树中一直向右遍历查找。

下面只讨论查找current后继的情况,查找前驱的原理相同,这里暂不讨论。

4.4.完整实现
      //删除节点BinarySearchTree.prototype.remove = function(key){
/*------------------------------1.寻找要删除的节点---------------------------------*///1.1.定义变量current保存删除的节点,parent保存它的父节点。isLeftChild保存current是否为parent的左节点let current = this.rootlet parent = nulllet isLeftChild = true//1.2.开始寻找删除的节点while (current.key != key) {parent = current// 小于则往左查找if (key < current.key) {isLeftChild = truecurrent = current.left} else{isLeftChild = falsecurrent = current.right}//找到最后依然没有找到相等的节点if (current == null) {return false}}//结束while循环后:current.key = key/*------------------------------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}}//情况2:删除的节点有一个子节点//当current存在左子节点时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}//当current存在右子节点时} 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} }//情况3:删除的节点有两个子节点else{//1.获取后继节点let successor = this.getSuccessor(current)//2.判断是否根节点if (current == this.root) {this.root = successor}else if (isLeftChild){parent.left = successor}else{parent.right = successor}//3.将后继的左子节点改为被删除节点的左子节点successor.left = current.left}}//封装查找后继的方法BinarySearchTree.prototype.getSuccessor = function(delNode){//1.定义变量,保存找到的后继let successor = delNodelet current = delNode.rightlet successorParent = delNode//2.循环查找current的右子树节点while(current != null){successorParent = successorsuccessor = currentcurrent = current.left}//3.判断寻找到的后继节点是否直接就是删除节点的right节点if(successor != delNode.right){successorParent.left = successor.rightsuccessor.right = delNode.right }return successor}

测试代码:

   //测试代码//1.创建BinarySearchTreelet bst = new BinarySearchTree()//2.插入数据bst.insert(11);bst.insert(7);bst.insert(15);bst.insert(5);bst.insert(3);bst.insert(9);bst.insert(8);bst.insert(10);bst.insert(13);bst.insert(12);bst.insert(14);bst.insert(20);bst.insert(18);bst.insert(25);bst.insert(6);bst.insert(19);//3.测试删除代码//删除没有子节点的节点bst.remove(3)bst.remove(8)bst.remove(10)//删除有一个子节点的节点bst.remove(5)bst.remove(19)//删除有两个子节点的节点bst.remove(9)bst.remove(7)bst.remove(15)//遍历二叉搜索树并输出let resultString = ""bst.midOrderTraversal(function(key){resultString += key + "->"})alert(resultString)

测试结果:

在这里插入图片描述

可见三种情况的节点都被成功删除了。

5.二叉搜索树完整封装
    //封装二叉搜索树function BinarySearchTree(){//节点内部类function Node(key){this.key = keythis.left = nullthis.right = null}//属性this.root = null//方法//一.插入数据:insert方法:对外向用户暴露的方法BinarySearchTree.prototype.insert = function(key){//1.根据key创建节点let newNode = new Node(key)//2.判断根节点是否存在if (this.root == null) {this.root = newNode//根节点存在时}else {this.insertNode(this.root, newNode)}}//内部使用的insertNode方法:用于比较节点从左边插入还是右边插入BinarySearchTree.prototype.insertNode = function(node, newNode){//当newNode.key < node.key向左查找if(newNode.key < node.key){//情况1:node无左子节点,直接插入if (node.left == null) {node.left = newNode//情况2:node有左子节点,递归调用insertNode(),直到遇到无左子节点成功插入newNode后,不再符合该情况,也就不再调用insertNode(),递归停止。}else{this.insertNode(node.left, newNode)}//当newNode.key >= node.key向右查找}else{//情况1:node无右子节点,直接插入if(node.right == null){node.right = newNode//情况2:node有右子节点,依然递归调用insertNode(),直到遇到无右子节点成功插入newNode为止}else{this.insertNode(node.right, newNode)}}}//二.树的遍历//1.先序遍历//掺入一个handler函数对得到的key进行处理BinarySearchTree.prototype.preOrderTraversal = function(handler){this.preOrderTraversalNode(this.root, handler)}//封装内部方法,对某个节点进行遍历BinarySearchTree.prototype.preOrderTraversalNode = function(node,handler){if (node != null) {//1.处理经过的节点handler(node.key)//2.遍历经过节点的左子节点this.preOrderTraversalNode(node.left, handler)//3.遍历经过节点的右子节点this.preOrderTraversalNode(node.right, handler)}}//2.中序遍历BinarySearchTree.prototype.midOrderTraversal = function(handler){this.midOrderTraversalNode(this.root, handler)}BinarySearchTree.prototype.midOrderTraversalNode = function(node, handler){if (node != null) {//1.遍历左子树中的节点this.midOrderTraversalNode(node.left, handler)//2.处理节点handler(node.key)//3.遍历右子树中的节点this.midOrderTraversalNode(node.right, handler)}}//3.后序遍历BinarySearchTree.prototype.postOrderTraversal = function(handler){this.postOrderTraversalNode(this.root, handler)}BinarySearchTree.prototype.postOrderTraversalNode = function(node, handler){if (node != null) {//1.遍历左子树中的节点this.postOrderTraversalNode(node.left, handler)//2.遍历右子树中的节点this.postOrderTraversalNode(node.right, handler)//3.处理节点handler(node.key)}}//三.寻找最值//寻找最大值BinarySearchTree.prototype.max = function () {//1.获取根节点let node = this.root//2.定义key保存节点值let key = null//3.依次向右不断查找,直到节点为nullwhile (node != null) {key = node.keynode = node.right}return key}//寻找最小值BinarySearchTree.prototype.min = function(){//1.获取根节点let node = this.root//2.定义key保存节点值let key = null//3.依次向左不断查找,直到节点为nullwhile (node != null) {key = node.keynode = node.left}return key}//查找特定的keyBinarySearchTree.prototype.search = function(key){//1.获取根节点let node = this.root//2.循环搜索keywhile(node != null){if (key < node.key) {//小于根(父)节点就往左边找node = node.left//大于根(父)节点就往右边找}else if(key > node.key){node = node.right}else{return true}} return false}//四.删除节点BinarySearchTree.prototype.remove = function(key){
/*------------------------------1.寻找要删除的节点---------------------------------*///1.1.定义变量current保存删除的节点,parent保存它的父节点。isLeftChild保存current是否为parent的左节点let current = this.rootlet parent = nulllet isLeftChild = true//1.2.开始寻找删除的节点while (current.key != key) {parent = current// 小于则往左查找if (key < current.key) {isLeftChild = truecurrent = current.left} else{isLeftChild = falsecurrent = current.right}//找到最后依然没有找到相等的节点if (current == null) {return false}}//结束while循环后:current.key = key/*------------------------------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}}//情况2:删除的节点有一个子节点//当current存在左子节点时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}//当current存在右子节点时} 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} }//情况3:删除的节点有两个子节点else{//1.获取后继节点let successor = this.getSuccessor(current)//2.判断是否根节点if (current == this.root) {this.root = successor}else if (isLeftChild){parent.left = successor}else{parent.right = successor}//3.将后继的左子节点改为被删除节点的左子节点successor.left = current.left}}//封装查找后继的方法BinarySearchTree.prototype.getSuccessor = function(delNode){//1.定义变量,保存找到的后继let successor = delNodelet current = delNode.rightlet successorParent = delNode//2.循环查找current的右子树节点while(current != null){successorParent = successorsuccessor = currentcurrent = current.left}//3.判断寻找到的后继节点是否直接就是删除节点的right节点if(successor != delNode.right){successorParent.left = successor.rightsuccessor.right = delNode.right }return successor}}

二、平衡树

二叉搜索树的缺陷:

当插入的数据是有序的数据,就会造成二叉搜索树的深度过大。比如原二叉搜索树右 11 7 15 组成,如下图所示:

在这里插入图片描述

当插入一组有序数据:6 5 4 3 2就会变成深度过大的搜索二叉树,会严重影响二叉搜索树的性能。

在这里插入图片描述

非平衡树

  • 比较好的二叉搜索树,它的数据应该是左右均匀分布的;
  • 但是插入连续数据后,二叉搜索树中的数据分布就变得不均匀了,我们称这种树为非平衡树
  • 对于一棵平衡二叉树来说,插入/查找等操作的效率是O(logN)
  • 而对于一棵非平衡二叉树来说,相当于编写了一个链表,查找效率变成了O(N);

树的平衡性

为了能以较快的时间O(logN)来操作一棵树,我们需要保证树总是平衡的:

  • 起码大部分是平衡的,此时的时间复杂度也是接近O(logN)的;
  • 这就要求树中每个节点左边的子孙节点的个数,应该尽可能地等于右边的子孙节点的个数;

常见的平衡树

  • AVL树:是最早的一种平衡树,它通过在每个节点多存储一个额外的数据来保持树的平衡。由于AVL树是平衡树,所以它的时间复杂度也是O(logN)。但是它的整体效率不如红黑树,开发中比较少用。
  • 红黑树:同样通过一些特性来保持树的平衡,时间复杂度也是O(logN)。进行插入/删除等操作时,性能优于AVL树,所以平衡树的应用基本都是红黑树。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/198977.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

成为AI产品经理——回归模型评估(MSE、RMSE、MAE、R方)

分类问题的评估是看实际类别和预测类别是否一致&#xff0c;它的评估指标主要有混淆矩阵、AUC、KS。回归问题的评估是看实际值和预测值是否一致&#xff0c;它的评估指标包括MAE、MSE、RMSE、R方。 如果我们预测第二天某支股票的价格&#xff0c;给一个模型 y1.5x&#xff0c;…

查询绑定了所有id的name

1、如图&#xff0c;绑定了所有id的有A,B两个name 2、第一种Sql及效率 explain SELECT name,count(id) as count from test GROUP BY name HAVING count(id)(SELECT count(DISTINCT id) from test); 3、第二种sql及效率 explain select * from (SELECT name,count(id) as co…

SQL自学通之简介

目录 一、SQL 简史 二、数据库简史 1、Dr. Codds 对关系型数据库系统的十二条规则 2、设计数据库的结构 3、数据库的前景 4、对于什么是客户机/服务器型电脑系统 BernardH.Boar的定义如下&#xff1a; 5、交互式语言 6、易于实现 7、SQL 总览 三、流行的 SQL 开发工具…

MFC 绘制单一颜色圆形、渐变颜色边框圆形、渐变填充圆形以及绘制三角函数正弦函数曲线.

MFC 绘制三种不同圆形以及绘制正弦函数曲线 本文使用visual Studio MFC 平台实现绘制单一颜色圆形、渐变颜色边框圆形、渐变填充圆形以及绘制三角函数正弦函数曲线. 关于基础工程的创建请参考 01-Visual Studio 使用MFC 单文档工程绘制单一颜色直线和绘制渐变颜色的直线 02-vis…

【【FPGA 之 MicroBlaze XADC 实验】】

FPGA 之 MicroBlaze XADC 实验 Vivado IP 核提供了 XADC 软核&#xff0c;XADC 包含两个模数转换器&#xff08;ADC&#xff09;&#xff0c;一个模拟多路复用器&#xff0c;片上温度和片上电压传感器等。我们可以利用这个模块监测芯片温度和供电电压&#xff0c;也可以用来测…

【五分钟】熟练使用np.interp函数函数(干货!!!)

引言 np.interp函数可对一维数组进行线性插值。具体来说&#xff0c;如果已知xp和yp&#xff0c;并且xp和yp具有确定的对应关系(比如ypsin(xp))&#xff0c;那么对于新的一维数组x&#xff0c;可通过np.interp函数得到数组y&#xff0c;y是基于 xp和yp的映射关系 通过【线性插…

Spring之AOP理解与应用

1. AOP的认识 面向切面编程&#xff1a;基于OOP基础之上新的编程思想&#xff0c;OOP面向的主要对象是类&#xff0c;而AOP面向的主要对象是切面&#xff0c;在处理日志、安全管理、事务管理等方面有非常重要的作用。AOP是Spring中重要的核心点&#xff0c;AOP提供了非常强…

面了一个测试工程师要求月薪26K,总感觉他背了很多面试题

最近有朋友去华为面试&#xff0c;面试前后进行了20天左右&#xff0c;包含4轮电话面试、1轮笔试、1轮主管视频面试、1轮hr视频面试。 据他所说&#xff0c;80%的人都会栽在第一轮面试&#xff0c;要不是他面试前做足准备&#xff0c;估计都坚持不完后面几轮面试。 其实&…

Nacos 安装下载

Nacos是阿里巴巴的产品&#xff0c;现在是SpringCloud中的一个组件。相比Eureka功能更加丰富&#xff0c;在国内受欢迎程度较高。 1、Windows安装 开发阶段采用单机安装即可 1.1 下载安装包 在Nacos的GitHub页面&#xff0c;提供有下载链接&#xff0c;可以下载编译好的Nac…

uniapp到底用什么ui框架最合适-关于uni-app的ui库、ui框架、ui组件

文章目录 直接看答案关于uni-app的ui库、ui框架、ui组件组件的概念扩展组件的选择uni ui如何使用uni ui 综上&#xff0c;官方对组件的使用建议是&#xff1a;附录&#xff1a;其他全端兼容ui库参考文章&#xff1a; 直接看答案 如果想自己纯手写&#xff0c;直接用内置组件。…

it统一运维平台怎么样?有可以推荐的品牌吗?

随着互联网化&#xff0c;随着信息化的不断发展&#xff0c;企业IT系统的规模和复杂性也在日益增加。在这个背景下&#xff0c;IT统一运维平台就应用而生了。它以一种全面、集成的方式管理企业IT资源&#xff0c;从而提高效率、降低成本、改善服务&#xff0c;为企业提供更快更…

「词令」2023年12月5日支付宝蚂蚁庄园今日问题答案是什么?12.5蚂蚁庄园今日问题正确答案

问题&#xff1a;我国哪个城市有“四面荷花三面柳&#xff0c;一城山色半城湖”的美誉&#xff1f; 选项&#xff1a;A、济南 B、泉州 答案&#xff1a;济南 解析&#xff1a;一城山色&#xff1a;千佛山在城南&#xff0c;为附近最高的山&#xff0c;全城人都能看到它苍翠的…

Linux last命令教程:如何查看用户的登录和注销历史(附案例详解和注意事项)

Linux last命令介绍 last命令在Linux中用于显示自文件/var/log/wtmp创建以来所有用户的登录和注销列表。可以给出一个或多个用户名作为参数&#xff0c;以显示他们的登录&#xff08;和注销&#xff09;时间和主机名。 Linux last命令适用的Linux版本 last命令在大多数Linux…

Comprehension from Chaos: Towards Informed Consent for Private Computation

目录 笔记后续的研究方向摘要引言 Comprehension from Chaos: Towards Informed Consent for Private Computation CCS 2023 笔记 本文探讨了用户对私有计算的理解和期望&#xff0c;其中包括多方计算和私有查询执行等技术。该研究进行了 22 次半结构化访谈&#xff0c;以调查…

14、SQL注入——HTTP文件头注入

文章目录 一、HTTP Header概述1.1 HTTP工作原理1.2 HTTP报文类型1.3 较重要的HTTP Header内容 二、HTTP Header注入2.1 HTTP Header注入的前提条件2.2 常见的HTTP Header注入类型 一、HTTP Header概述 1.1 HTTP工作原理 1.2 HTTP报文类型 &#xff08;1&#xff09;请求报文 …

软件设计模式原则(五)接口隔离原则

顾名思义&#xff0c;该原则说的是&#xff1a;客户端不应该依赖它不需要的接口。一个类对另一个类的依赖应该建立在最小的接口上。 一.定义 核心思想&#xff1a; 使用多个专门的接口比使用单一的总接口要好。一个类对另外一个类的依赖性应当是建立在最小的接口上的。一个接口…

博客文章SEO:提升博客排名和吸引更多读者的方法来啦!

互联网发展到现在&#xff0c;搜索引擎优化&#xff08;SEO&#xff09;一直发挥着不可替代的作用。搜索引擎的流量往往更加定向&#xff0c;来自搜索引擎的流量转化率更高&#xff0c;可以帮助企业更好地实现销售和推广目标。因此&#xff0c;通过合理的SEO策略&#xff0c;你…

市场调研:中国南美白对虾养殖面积达到17.75万公顷

南美白对虾(学名:Litopenaeus Vannamei )是对虾科、滨对虾属动物。成体最长达23厘米&#xff0c;甲壳较薄&#xff0c;正常体色为青蓝色或浅青灰色&#xff0c;全身不具斑纹。步足常呈白垩状&#xff0c;故有白肢虾之称。南美白对虾额角尖端的长度不超出第1触角柄的2节&#xf…

从浅入深掌握进阶结构体(C语言)

前言 这一期我们将继续讲解结构体的知识&#xff0c;还没有看过上一期的小伙伴一定要赶紧去学习哦。 上一期&#xff0c;冲鸭&#xff01; 那么话不多说我们开始今天的学习吧&#xff01; 文章目录 1,结构体的自引用2,匿名结构体3,位段4,结构体的传参5,尾声 1,结构体的自引用 …

基因组注释流程

一边学习&#xff0c;一边总结&#xff0c;一边分享&#xff01; 详细教程请访问&#xff1a; 组学分析流程 本期分析流程 Hisat2-SamtoolsTrinity_GG_denovoPASA … 本期教程文章 题目&#xff1a;Genomic insights into local adaptation and future climate-induced vu…