五、树结构

五、树结构

5.1 树结构简介

树的定义

QLOO}AP.jpg

  • 树(Tree): n(n≥0)个结点构成的有限集合

    • n=0时,称为空树

    • 对于任一棵非空树(n> 0),它具备以下性质:

    • 树中有一个称为“**根(Root)”的特殊结点,用 r **表示;

    • 其余结点可分为m(m>0)个互不相交的有限集T1,T2,… ,Tm

其中 :

每个集合本身又是一棵树,称为原来树的“子树(SubTree)

注意:

  • 子树之间不可以相交
  • 除了根结点外,每个结点有且仅有一个父结点
  • 一棵N个结点的树有N-1条边

树的术语

  • 节点的度(Degree):节点的子树个数,比如节点B的度为2

  • 树的度:树的所有节点中最大的度数,如上图树的度为2

  • 叶节点(Leaf)度为0的节点(也称为叶子节点),如上图的H,I等;

  • 父节点(Parent):度不为0的节点称为父节点,如上图节点B是节点D和E的父节点;

  • 子节点(Child):若B是D的父节点,那么D就是B的子节点

  • 兄弟节点(Sibling):具有同一父节点的各节点彼此是兄弟节点,比如上图的B和C,D和E互为兄弟节点

  • 路径和路径长度:路径指的是一个节点到另一节点的通道,路径所包含边的个数称为路径长度,比如A->H的路径长度为3

  • 节点的层次(Level):规定根节点在1层,其他任一节点的层数是其父节点的层数加1。如B和C节点的层次为2

  • 树的深度(Depth):树种所有节点中的最大层次是这棵树的深度,如上图树的深度为4

树的优点

  • 数组:

    • 优点:

      • 数组的主要优点是根据下标值访问效率会很.

      • 但是如果我们希望根据元素来查找对应的位置呢?

      • 比较好的方式是先对数组进行排序, 再进行二分查找.

    • 缺点:

      • 需要先对数组进行排序, 生成有序数组, 才能提高查找效率.

      • 另外数组在插入和删除数据时, 需要有大量的位移操作(插入到首位或者中间位置的时候), 效率很低.

  • 链表:

    • 优点:

      • 链表的插入和删除操作效率都很高.
    • 缺点:

      • 查找效率很低, 需要从头开始依次访问链表中的每个数据项, 直到找到.

      • 而且即使插入和删除操作效率很高, 但是如果要插入和删除中间位置的数据, 还是需要重头先找到对应的数据.

  • 哈希表:

    • 优点:

      • 我们学过哈希表后, 已经发现了哈希表的插入/查询/删除效率都是非常高的

      • 但是哈希表也有很多缺点.

    • 缺点:

      • 空间利用率不高, 底层使用的是数组, 并且某些单元是没有被利用的.

      • 哈希表中的元素是无序的, 不能按照固定的顺序来遍历哈希表中的元素.

      • 不能快速的找出哈希表中的最大值或者最小值这些特殊的值.

  • 树结构:

    • 我们不能说树结构比其他结构都要好, 因为每种数据结构都有**自己特定的应用场景.

**

  • 但是树确实也综合了上面的数据结构的优点(当然优点不足于盖过其他数据结构, 比如效率一般情况下没有哈希表高), 并且也弥补了上面数据结构的缺点.

  • 而且为了模拟某些场景, 我们使用树结构会更加方便. 比如**文件的目录结构. **

树结构的表示方法

树可以有很多种表示的方式

  • 最普通的表示方式:

![L)X3PG}{J8XT4[7K@1_8AB.jpg](https://img-blog.csdnimg.cn/img_convert/952207835475d3c68e80eba4fc32ef3a.jpeg#clientId=ud7979db5-dca3-4&crop=0&crop=0&crop=1&crop=1&from=drop&id=ub65ea189&margin=[object Object]&name=L)X3PG}{J8XT4[7K@1_8AB.jpg&originHeight=402&originWidth=1200&originalType=binary&ratio=1&rotation=0&showTitle=false&size=31867&status=done&style=none&taskId=udd055088-e5a9-4ec7-85b4-9a033416369&title=)

  • 儿子-兄弟表示法:

![]1H]QGD[7NUK~AHFQB~A.jpg](https://img-blog.csdnimg.cn/img_convert/53fa2d44d12fe6e1e4018e6b545d064b.jpeg#clientId=ud7979db5-dca3-4&crop=0&crop=0&crop=1&crop=1&from=drop&id=u79173f57&margin=[object Object]&name=]_1H]QGD_[7NUK~AHFQB~A.jpg&originHeight=443&originWidth=1200&originalType=binary&ratio=1&rotation=0&showTitle=false&size=32002&status=done&style=none&taskId=ufa6cdaa3-c86e-425a-a8cd-f66e1677d25&title=)

//节点A
Node{
  //存储数据
  this.data = data
  //统一只记录左边的子节点
  this.leftChild = B
  //统一只记录右边的第一个兄弟节点
  this.rightSibling = null
}

//节点B
Node{
  this.data = data
  this.leftChild = E
  this.rightSibling = C
}

//节点F
Node{
  this.data = data
  this.leftChild = null
  this.rightSibling = null
}

  • 儿子-兄弟表示法旋转:

![9 [ Y 46 [ R R [Y46[RR [Y46[RRZVCFWEV[DA_A.jpg

总结出的规律

  • 所有的树本质上都可以使用二叉树模拟出来
  • 所以在学习的过程中,二叉树非常重要

5.2 二叉树

二叉树简介

  • 如果树中每个节点最多只能有两个子节点,这样的树就成为**“二叉树”**

  • 前面, 我们已经提过二叉树的重要性, 不仅仅是因为简单, 也因为几乎上所有的树都可以表示成二叉树的形式

二叉树的概念

  • 二叉树的定义

    • 二叉树可以为空, 也就是没有结点.

    • 不为空,则它是由根结点和称为其左子树TL和右子树TR的两个不相交的二叉树组成。

  • 二叉树有五种形态


  • 注意c和d是不同的二叉树, 因为二叉树是有左右之分的.

![U3}P6H([@E%KP9O[PJ3VW)5.jpg](https://img-blog.csdnimg.cn/img_convert/4ab14be65a2b159678c97e4b5c1d59ef.jpeg#clientId=u17d54ebf-7058-4&crop=0&crop=0&crop=1&crop=1&from=drop&id=udff539ff&margin=[object Object]&name=U3}P6H([@E%KP9O[PJ3VW)5.jpg&originHeight=289&originWidth=1200&originalType=binary&ratio=1&rotation=0&showTitle=false&size=19517&status=done&style=none&taskId=u44af94c8-2524-4b11-9819-f7f1077b547&title=)

二叉树的特性

  • 二叉树有几个比较重要的特性, 在笔试题中比较常见:

  • 一个二叉树第 i 层的最大结点数为:2^(i-1), i >= 1;

  • 深度为k的二叉树有最大结点总数为: 2^k - 1, k >= 1;

  • 对任何非空二叉树 T,若n0表示叶结点的个数、n2是度为2的非叶结点个数,那么两者满足关系n0 = n2 + 1

Y%T2.jpg

特殊的二叉树

  • ** 完美二叉树(Perfect Binary Tree) , 也称为满二叉树(Full Binary Tree)**

    • 在二叉树中, 除了最下一层的叶结点外, 每层节点都有2个子结点, 就构成了满二叉树.

![[IY(6QFP3{TI6MW0C1I3ATG.jpg](https://img-blog.csdnimg.cn/img_convert/04a79be974ea8ddbac9446cbf2c2613f.jpeg#clientId=u17d54ebf-7058-4&crop=0&crop=0&crop=1&crop=1&from=drop&id=u3e4c3bfc&margin=[object Object]&name=[IY(6QFP3{TI6MW0C1I3ATG.jpg&originHeight=537&originWidth=901&originalType=binary&ratio=1&rotation=0&showTitle=false&size=40430&status=done&style=none&taskId=ub0f868a0-53ca-4327-9cd1-85d4cee568a&title=)

  • 完全二叉树(Complete Binary Tree)

    • 除二叉树最后一层外, 其他各层的节点数都达到最大个数.

    • 且最后一层从左向右的叶结点连续存在, 只缺右侧若干节点.

    • 完美二叉树是特殊的完全二叉树.


下面不是完全二叉树, 因为D节点还没有右结点, 但是E节点就有了左右结点.

![3X46HD9D88Q@G6PU{PFM09C.jpg](https://img-blog.csdnimg.cn/img_convert/82d3c89d4ffe1c5dabdaa571d9fb9250.jpeg#clientId=u17d54ebf-7058-4&crop=0&crop=0&crop=1&crop=1&from=drop&id=u36da9345&margin=[object Object]&name=3X46HD9D88Q@G6PU{PFM09C.jpg&originHeight=540&originWidth=882&originalType=binary&ratio=1&rotation=0&showTitle=false&size=30715&status=done&style=none&taskId=u8174b5ac-7d52-4925-abb8-5169c9b7346&title=)

二叉树的数据存储

二叉树的存储常见的方式是数组和链表

  • 使用数组存储

    • 完全二叉树:按从上至下,从左到右顺序存储

![%SJNV6A)RV1K]H2{@~@ZC6J.jpg](https://img-blog.csdnimg.cn/img_convert/d2e3bc446c5ff29f0bb59dc9ec4586ef.jpeg#clientId=u17d54ebf-7058-4&crop=0&crop=0&crop=1&crop=1&from=drop&id=ubabc0a7c&margin=[object Object]&name=%SJNV6A)RV1K]H2{@~@ZC6J.jpg&originHeight=613&originWidth=1200&originalType=binary&ratio=1&rotation=0&showTitle=false&size=34517&status=done&style=none&taskId=uf0f46cbd-f5ab-4b0d-a2a8-f5063ddeea0&title=)

  • 非完全二叉树

    • 非完全二叉树要转成完全二叉树才可以按照上面的方案存储.

    • 但是会造成很大的空间浪费

![ZKX68QZ@O75)YDZ7MKB%UE.jpg](https://img-blog.csdnimg.cn/img_convert/4a67171a6085abc2830e206e8bece1c0.jpeg#clientId=u17d54ebf-7058-4&crop=0&crop=0&crop=1&crop=1&from=drop&height=324&id=uad8df6f9&margin=[object Object]&name=ZKX68QZ@O75)YDZ7MKB%UE.jpg&originHeight=690&originWidth=1200&originalType=binary&ratio=1&rotation=0&showTitle=false&size=63470&status=done&style=none&taskId=ub680df68-48de-4265-9718-6c59ca368c5&title=&width=564)

  • 链表存储

    • 二叉树最常见的方式还是使用链表存储.

    • 每个结点封装成一个Node, Node中包含存储的数据, 左结点的引用, 右结点的引用.

![7CF43_([])9XOUJ]EPG@R]P.jpg](https://img-blog.csdnimg.cn/img_convert/ee0bdff6b942c3b49f2d0c6ba7b5e71e.jpeg#clientId=u17d54ebf-7058-4&crop=0&crop=0&crop=1&crop=1&from=drop&height=136&id=u8edaacdc&margin=[object Object]&name=7CF43_([])9XOUJ]EPG@R]P.jpg&originHeight=288&originWidth=1200&originalType=binary&ratio=1&rotation=0&showTitle=false&size=25604&status=done&style=none&taskId=u1a6fecc1-567f-4e45-bb13-7976d11530b&title=&width=566)

5.3 二叉搜索树

认识二叉搜索树

  • 二叉搜索树(BST,Binary Search Tree),也称二叉排序树或二叉查找树

  • 二叉搜索树是一棵二叉树,可以为空

  • 如果不为空,满足以下性质

    • 非空左子树的所有键值小于其根节点的键值

    • 非空右子树的所有键值大于其根节点的键值

    • 左右子树本身也都是二叉搜索树

  • 下面哪些是二叉搜索树,那些不是?

![XN)SD4FWLU%Y4DGKXHFL4V.jpg](https://img-blog.csdnimg.cn/img_convert/611c4ad6affbd844b6f739936cfc49d2.jpeg#clientId=u17d54ebf-7058-4&crop=0&crop=0&crop=1&crop=1&from=drop&id=u15881a33&margin=[object Object]&name=XN)SD4FWLU%Y4DGKXHFL4V.jpg&originHeight=370&originWidth=1200&originalType=binary&ratio=1&rotation=0&showTitle=false&size=34344&status=done&style=none&taskId=uc2aae36c-30a1-4949-9a1f-054302ebfdc&title=)

总结:

  • 二叉搜索树的特点主要是较小的值总是保存在左节点上,相对较大的值总是保存在右节点上。

  • 这种特点使得二叉搜索树的查询效率非常高,这也就是二叉搜索树中"搜索"的来源

二叉搜索树应用举例

下面是一个二叉搜索树:

![OYZZ%{5}7W1)}]F~T@]6(X9.png](https://img-blog.csdnimg.cn/img_convert/53bc6708dfbb78089c30603c1ef5d1fd.png#clientId=u17d54ebf-7058-4&crop=0&crop=0&crop=1&crop=1&from=drop&height=317&id=uf5dd06e5&margin=[object Object]&name=OYZZ%{5}7W1)}]F~T@]6(X9.png&originHeight=259&originWidth=465&originalType=binary&ratio=1&rotation=0&showTitle=false&size=64847&status=done&style=none&taskId=u720e0402-87b5-4243-9dd6-00803ec7a20&title=&width=570)

若想在其中查找数据10,只需要查找4次,查找效率非常高

![LI2ZWFB%JU_~5L03P2UR{L.png](https://img-blog.csdnimg.cn/img_convert/ee07865106dc51f109b564b7517b1b11.png#clientId=u17d54ebf-7058-4&crop=0&crop=0&crop=1&crop=1&from=drop&height=324&id=uf0e517b4&margin=[object Object]&name=LI2ZWFB%JU_~5L03P2UR{L.png&originHeight=237&originWidth=413&originalType=binary&ratio=1&rotation=0&showTitle=false&size=75810&status=done&style=none&taskId=u8af42436-ea6e-4a2f-b6a7-760b04ecb57&title=&width=564)

  • 第1次:将10与根节点9进行比较,由于10 > 9,所以10下一步与根节点9的右子节点13比较;

  • 第2次:由于10 < 13,所以10下一步与父节点13的左子节点11比较;

  • 第3次:由于10 < 11,所以10下一步与父节点11的左子节点10比较

  • 第4次:由于**10 = 10,最终查找到数据10 **。

5.4 一、二叉搜索树的封装

二叉搜索树的基本属性

![SBMG]YRBZ85Q9[RQ~88%BIT.png](https://img-blog.csdnimg.cn/img_convert/2a56fa32f1b9dc39c02a6f84f92f2cc8.png#clientId=u17d54ebf-7058-4&crop=0&crop=0&crop=1&crop=1&from=drop&id=uce58af6f&margin=[object Object]&name=SBMG]YRBZ85Q9[RQ~88%BIT.png&originHeight=383&originWidth=771&originalType=binary&ratio=1&rotation=0&showTitle=false&size=73487&status=done&style=none&taskId=u23bc2b7d-5f87-412f-b324-c3b5f4aceda&title=)

如图所示:

二叉搜索树有四个最基本的属性:

  • 指向节点的根:root

  • 结点中的键:key

  • 左指针:left

  • 右指针:right

function BinarySearchTree(){
  //创建节点构造函数
  function Node(key) {
    this.key = key
    this.left = null
    this.right = null
  }
  //保存根的属性
  this.root = null
}

二叉搜索树的常见操作

  • insert(key):向树中插入一个新的键;

  • search(key):在树中查找一个键,如果节点存在,则返回true;如果不存在,则返回false;

  • inOrderTraverse:通过中序遍历方式遍历所有节点;

  • preOrderTraverse:通过先序遍历方式遍历所有节点;

  • postOrderTraverse:通过后序遍历方式遍历所有节点;

  • min:返回树中最小的值/键;

  • max:返回树中最大的值/键;

  • remove(key):从树中移除某个键;

插入数据(insert(key))

insert实现思路:
  • 首先根据传入的key来创建节点对象

  • 然后判断根节点是否存在

    • 若不存在,则通过this.root = newNode,直接把新节点作为二叉搜索树的根节点

    • 若存在,则通过insertNode(this.root,newNode),寻找插入点

代码实现:
BinarySearchTree.prototype.insert = key => {
  let newNode = new Node(key)
  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(),直到遇到无左子节点成功插入newNode后,不再符合该情况,也就不再调用insertNode(),递归停止。

代码实现:
BinarySearchTree.prototype.insertNode = (node,newNode) => {
  if(newNode.key < node.key){
    if(node.left == null){
      node.left = newNode
    }
    else {
      this.insertNode(node.left,newNode)
    }
  }
  else if(newNode.key >= node.key){
    if(node.right == null){
      node.right = newNode
    }
    else{
      this.insertNode(node.right,newNode)
    }
  }
}
测试代码
//测试代码
//1.创建BinarySearchTree
let bst = new BinarySearchTree()

//2.插入数据
bst.insert(11);
bst.insert(7);
bst.insert(15);
bst.insert(5);
bst.insert(9);
console.log(bst);

测试代码

![image.png](https://img-blog.csdnimg.cn/img_convert/55e9241553390e64b697d57077ca753e.png#clientId=u17d54ebf-7058-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=488&id=udeab5a16&margin=[object Object]&name=image.png&originHeight=592&originWidth=663&originalType=binary&ratio=1&rotation=0&showTitle=false&size=61133&status=done&style=none&taskId=u17e9fec2-8411-4ac5-af2d-4a80305f7a9&title=&width=546.5)

遍历数据

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

  • 先序遍历;
  • 中序遍历;
  • 后序遍历;
  1. preOrderTraversal(先序遍历)

先序遍历的过程为:

  • 首先,遍历根节点

  • 然后,遍历其左子树

  • 最后,遍历其右子树

![72VZB_}6@@9OV%JEDI9Z8.jpg](https://img-blog.csdnimg.cn/img_convert/44a8df5b595de664cb6a075c69a4c714.jpeg#clientId=u17d54ebf-7058-4&crop=0&crop=0&crop=1&crop=1&from=drop&id=u2b75f67d&margin=[object Object]&name=72VZB_}6@@9OV%JEDI9Z8.jpg&originHeight=438&originWidth=1200&originalType=binary&ratio=1&rotation=0&showTitle=false&size=33670&status=done&style=none&taskId=ue43ab5ef-79e8-4d76-a54b-25afacb2fc4&title=)

代码实现:

BinarySearchTree.prototype.preOrderTraversal = (handler) => {
  this.preOrderTraversalNode(this.root,handler)
}
BinarySearchTree.prototype.preOrderTraversalNode = (node,handler) => {
  if(node != null){
    handler(node.key)
    this.preOrderTraversalNode(node.left,handler)
    this.preOrderTraversalNode(node.right,handler)
  }
}

测试代码:

//测试代码
//1.创建BinarySearchTree
let 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)

测试结果:

![image.png](https://img-blog.csdnimg.cn/img_convert/55f0f588ca90dfb88e44f5f23bc26bac.png#clientId=u17d54ebf-7058-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=150&id=uec8146f6&margin=[object Object]&name=image.png&originHeight=185&originWidth=563&originalType=binary&ratio=1&rotation=0&showTitle=false&size=8118&status=done&style=none&taskId=u8689ccaf-d75f-4a3b-b1aa-00575664520&title=&width=457.5)

T44H6KY}H7BZ1W.jpg

  1. midOrderTraversal(中序遍历)

中序遍历的过程为:

  • 首先,遍历其左子树

  • 然后,遍历根节点

  • 最后,遍历其右子树

![7XUYTP~2@TGXTVW04XC0KRK.jpg](https://img-blog.csdnimg.cn/img_convert/92b91ad7a4bb6baff77f5c31b510aa5a.jpeg#clientId=u17d54ebf-7058-4&crop=0&crop=0&crop=1&crop=1&from=drop&height=237&id=ubd6b0e4a&margin=[object Object]&name=7XUYTP~2@TGXTVW04XC0KRK.jpg&originHeight=501&originWidth=1200&originalType=binary&ratio=1&rotation=0&showTitle=false&size=36168&status=done&style=none&taskId=ud89fea1f-92be-426a-a38d-119dc0e4f54&title=&width=567)

代码实现:

BinarySearchTree.prototype.midOrderTraversal = (handler) => {
  this.midOrderTraversalNode(this.root, handler)
}
BinarySearchTree.prototype.midOrderTraversalNode = (node, handler) => {
  if (node != null) {
    this.midOrderTraversalNode(node.left, handler)
    handler(node.key)
    this.midOrderTraversalNode(node.right, handler)
  }
}

测试代码:

//测试代码
//1.创建BinarySearchTree
let 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.midOrderTraversal(function(key){
  resultString += key + "->"
})
alert(resultString)

测试结果:

![image.png](https://img-blog.csdnimg.cn/img_convert/9011aa96ea8bea30d89dd15a6a80f33a.png#clientId=u17d54ebf-7058-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=170&id=uddd14f8e&margin=[object Object]&name=image.png&originHeight=183&originWidth=563&originalType=binary&ratio=1&rotation=0&showTitle=false&size=7382&status=done&style=none&taskId=u7045acb3-e6c1-45ce-a5a7-7804cc6cfd1&title=&width=521.5)

8FXVMR{`(CU%3GFS3UL.jpg

  1. postOrderTraversal(后序遍历)

后序遍历的过程为:

  • 首先,遍历其左子树

  • 然后,遍历其右子树

  • 最后,遍历其根节点

![M)[RRLGOTZQKYLA(RP@QAR5.jpg](https://img-blog.csdnimg.cn/img_convert/b7fcde0bff29d203406f8d4d5f2dab78.jpeg#clientId=u17d54ebf-7058-4&crop=0&crop=0&crop=1&crop=1&from=drop&id=u9cd50a64&margin=[object Object]&name=M)[RRLGOTZQKYLA(RP@QAR5.jpg&originHeight=478&originWidth=1200&originalType=binary&ratio=1&rotation=0&showTitle=false&size=36261&status=done&style=none&taskId=u3d6c9a9b-7ce6-4ef2-9542-44be6471bf0&title=)

代码实现:

BinarySearchTree.prototype.postOrderTraversal = (handler) => {
  this.postOrderTraversalNode(this.root, handler)
}
BinarySearchTree.prototype.postOrderTraversalNode = (node, handler) => {
  if (node != null) {
    this.postOrderTraversalNode(node.left, handler)
    this.postOrderTraversalNode(node.right, handler)
    handler(node.key)
  }
}

测试代码:

//测试代码
//1.创建BinarySearchTree
let 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.postOrderTraversal(function(key){
  resultString += key + "->"
})
alert(resultString)

测试结果:

![image.png](https://img-blog.csdnimg.cn/img_convert/baca75042932a7d2b24a5d1a84c29d66.png#clientId=u17d54ebf-7058-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=190&id=u6a61a5ac&margin=[object Object]&name=image.png&originHeight=188&originWidth=565&originalType=binary&ratio=1&rotation=0&showTitle=false&size=8545&status=done&style=none&taskId=u4446d581-7f26-428c-9f1a-8d83200cfe9&title=&width=572.5)

![DA]UUVLSSI~}7ZLZ[}%YVU.jpg](https://img-blog.csdnimg.cn/img_convert/44b6ca8d7501aeb1ceded27d2aa9e016.jpeg#clientId=u17d54ebf-7058-4&crop=0&crop=0&crop=1&crop=1&from=drop&id=u27d9e00d&margin=[object Object]&name=DA]UUVLSSI~}7ZLZ[}%YVU.jpg&originHeight=640&originWidth=1024&originalType=binary&ratio=1&rotation=0&showTitle=false&size=55622&status=done&style=none&taskId=ua49ff1ab-edc5-40d6-93c0-9e0fb2b8395&title=)

查找数据

  1. 查找最大值和最小值

思路:

  • 代码依次向左找到最左边的节点就是,最小值

  • 代码依次向左找到最右边的节点就是,最大值

代码实现:

BinarySearchTree.prototype.min = () => {
  let node = this.root
  while(node.left != null){
    node = node.left
  }
  return node.key
}
BinarySearchTree.prototype.max = () => {
  let node = this.root
  while(node.right != null){
    node = node.right
  }
  return node.key
}

测试代码:

console.log(bst.max())
console.log(bst.min())

测试结果:

![image.png](https://img-blog.csdnimg.cn/img_convert/7bbe85b905f2c89c6836e0375849ed19.png#clientId=u62b45d7f-2f05-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=171&id=ubd9f2ee5&margin=[object Object]&name=image.png&originHeight=62&originWidth=98&originalType=binary&ratio=1&rotation=0&showTitle=false&size=616&status=done&style=none&taskId=u0c9e57e1-8555-4d95-ac13-27f00d41e89&title=&width=271)

  1. 查找特定值

代码实现:

BinarySearchTree.prototype.search = key => {
  let node = this.root
  while (node != null) {
    if (key < node.key) {
      node = node.left
    }
    else if (key > node.key) {
      node = node.right
    }
    else { return true }
  }
  return false
}

测试代码:

console.log(bst.search(100));
console.log(bst.search(13));
console.log(bst.search(99));

测试结果:

![image.png](https://img-blog.csdnimg.cn/img_convert/89e9c99e8e89f9f4e388da1b0eb11c1b.png#clientId=u62b45d7f-2f05-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=179&id=u31bb0cbf&margin=[object Object]&name=image.png&originHeight=94&originWidth=155&originalType=binary&ratio=1&rotation=0&showTitle=false&size=1460&status=done&style=none&taskId=uff65b137-fb19-4b19-a98c-10a16192fdb&title=&width=294.5)

删除数据

实现思路:

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

    • 首先定义变量current,用于保存需要删除的节点

    • 变量parent,用于保存它的父节点

    • 变量isLeftChild保存current是否为parent1的左节点

    • 这样方便之后删除节点,改变相关节点的指向

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

    • 删除叶子节点

    • 删除只有一个子节点的节点

    • 删除有两个子节点的节点

初步实现代码:

BinarySearchTree.prototype.remove = key => {
  let current = this.root
  let parent = null
  let isLeftChild = true
  while(current.key != key){
    parent = current
    if(key < current.key){
      isLeftChild = true
      current = current.left
    }
    else{
      isLeftChild = false
      current = current.right
    }
    if(current == null){
      return false
    }
  }
}
  1. 情况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
  }
}
  1. 情况2:有一个子节点

代码实现:

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
  }
}
  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
    }
  }
  1. 情况2:有两个子节点

总结:

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

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

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

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

前驱和后继
在二叉搜索树中,这两个特殊的节点有特殊的名字

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

![VJFW9{I@Z]EJGI6(8EF49QU.jpg](https://img-blog.csdnimg.cn/img_convert/b762e97341bdf6d15193586e9ab6af9c.jpeg#clientId=u62b45d7f-2f05-4&crop=0&crop=0&crop=1&crop=1&from=drop&id=u1f08df68&margin=[object Object]&name=VJFW9{I@Z]EJGI6(8EF49QU.jpg&originHeight=620&originWidth=1183&originalType=binary&ratio=1&rotation=0&showTitle=false&size=45973&status=done&style=none&taskId=u8b20d9fb-6d59-4eb2-b38a-d06f6a7d2b7&title=)

代码实现:

// 5.删除有两个节点的节点
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
  successor.left = current.left

}


// 找后继的方法
BinarySerachTree.prototype.getSuccessor = function (delNode) {
  // 1.使用变量保存临时的节点
  let successorParent = delNode
  let successor = delNode
  let current = delNode.right // 要从右子树开始找

  // 2.寻找节点
  while (current != null) {
    successorParent = successor
    successor = current
    current = current.left
  }

  // 3.如果是删除图中15的情况, 还需要如下代码
  if (successor != delNode.right) {
    successorParent.left = successor.right
    successor.right = delNode.right
  }

  return successor
}
  1. 完整实现
BinarySearchTree.prototype.remove = key => {
  let current = this.root
  let parent = null
  let isLeftChild = true
  while (current.key != key) {
    parent = current
    if (key < current.key) {
      isLeftChild = true
      current = current.left
    }
    else {
      isLeftChild = false
      current = current.right
    }
    if (current == null) {
      return false
    }
  }
  if (current.left == null && current.right == null) {
    if (current == this.root) {
      this.root = null
    }
    else if (isLeftChild) {
      parent.left = null
    }
    else {
      parent.right = null
    }
  }
  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
    }
  }
  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
    }
  }
  // 5.删除有两个节点的节点
  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
    successor.left = current.left

  }
  return true
}
// 找后继的方法
BinarySerachTree.prototype.getSuccessor = function (delNode) {
  // 1.使用变量保存临时的节点
  let successorParent = delNode
  let successor = delNode
  let current = delNode.right // 要从右子树开始找

  // 2.寻找节点
  while (current != null) {
    successorParent = successor
    successor = current
    current = current.left
  }

  // 3.如果是删除图中15的情况, 还需要如下代码
  if (successor != delNode.right) {
    successorParent.left = successor.right
    successor.right = delNode.right
  }

  return successor
}

测试代码:

//测试代码
//1.创建BinarySearchTree
let 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)

测试结果:

![image.png](https://img-blog.csdnimg.cn/img_convert/915e8f0d226873f802165a9ff70d8e71.png#clientId=u62b45d7f-2f05-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=261&id=uac96f95b&margin=[object Object]&name=image.png&originHeight=220&originWidth=134&originalType=binary&ratio=1&rotation=0&showTitle=false&size=2360&status=done&style=none&taskId=ua4bc183d-069a-466d-867f-f66db4a5d47&title=&width=159)

二叉搜索树完整封装

function BinarySearchTree() {
  //创建节点构造函数
  function Node(key) {
    this.key = key
    this.left = null
    this.right = null
  }
  //保存根的属性
  this.root = null
  BinarySearchTree.prototype.insert = key => {
    let newNode = new Node(key)
    if (this.root === null) {
      this.root = newNode
    }
    else {
      this.insertNode(this.root, newNode)
    }
  }
  BinarySearchTree.prototype.insertNode = (node, newNode) => {
    if (newNode.key < node.key) {
      if (node.left == null) {
        node.left = newNode
      }
      else {
        this.insertNode(node.left, newNode)
      }
    }
    else if (newNode.key >= node.key) {
      if (node.right == null) {
        node.right = newNode
      }
      else {
        this.insertNode(node.right, newNode)
      }
    }
  }
  BinarySearchTree.prototype.preOrderTraversal = (handler) => {
    this.preOrderTraversalNode(this.root, handler)
  }
  BinarySearchTree.prototype.preOrderTraversalNode = (node, handler) => {
    if (node != null) {
      handler(node.key)
      this.preOrderTraversalNode(node.left, handler)
      this.preOrderTraversalNode(node.right, handler)
    }
  }
  BinarySearchTree.prototype.midOrderTraversal = (handler) => {
    this.midOrderTraversalNode(this.root, handler)
  }
  BinarySearchTree.prototype.midOrderTraversalNode = (node, handler) => {
    if (node != null) {
      this.midOrderTraversalNode(node.left, handler)
      handler(node.key)
      this.midOrderTraversalNode(node.right, handler)
    }
  }
  BinarySearchTree.prototype.postOrderTraversal = (handler) => {
    this.postOrderTraversalNode(this.root, handler)
  }
  BinarySearchTree.prototype.postOrderTraversalNode = (node, handler) => {
    if (node != null) {
      this.postOrderTraversalNode(node.left, handler)
      this.postOrderTraversalNode(node.right, handler)
      handler(node.key)
    }
  }
  BinarySearchTree.prototype.min = () => {
    let node = this.root
    while (node.left != null) {
      node = node.left
    }
    return node.key
  }
  BinarySearchTree.prototype.max = () => {
    let node = this.root
    while (node.right != null) {
      node = node.right
    }
    return node.key
  }
  BinarySearchTree.prototype.search = key => {
    let node = this.root
    while (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 = key => {
    let current = this.root
    let parent = null
    let isLeftChild = true
    while (current.key != key) {
      parent = current
      if (key < current.key) {
        isLeftChild = true
        current = current.left
      }
      else {
        isLeftChild = false
        current = current.right
      }
      if (current == null) {
        return false
      }
    }
    if (current.left == null && current.right == null) {
      if (current == this.root) {
        this.root = null
      }
      else if (isLeftChild) {
        parent.left = null
      }
      else {
        parent.right = null
      }
    }
    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
      }
    }
    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
      }
    }
    // 5.删除有两个节点的节点
    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
      successor.left = current.left

    }
    return true
  }
  // 找后继的方法
  BinarySearchTree.prototype.getSuccessor = function (delNode) {
    // 1.使用变量保存临时的节点
    let successorParent = delNode
    let successor = delNode
    let current = delNode.right // 要从右子树开始找

    // 2.寻找节点
    while (current != null) {
      successorParent = successor
      successor = current
      current = current.left
    }

    // 3.如果是删除图中15的情况, 还需要如下代码
    if (successor != delNode.right) {
      successorParent.left = successor.right
      successor.right = delNode.right
    }

    return successor
  }


}
//测试代码
//1.创建BinarySearchTree
let 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.postOrderTraversal(function (key) {
  resultString += key + "->"
})
//alert(resultString)
console.log(bst.max())
console.log(bst.min())
console.log(bst.search(100));
console.log(bst.search(13));
console.log(bst.search(99));

//3.测试删除代码
//删除没有子节点的节点
console.log(bst.remove(3))
console.log(bst.remove(8))
console.log(bst.remove(10))

//删除有一个子节点的节点
console.log(bst.remove(5))

//删除有两个子节点的节点
console.log(bst.remove(9))
console.log(bst.remove(7))
console.log(bst.remove(15))


5.5 平衡树

二叉搜索树的缺陷

  • 当插入的数据是有序的数据,就会造成二叉搜索树的深度过大。比如原二叉搜索树又 11 7 15 组成

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

![D8)03KL1{(8@4~@NR(5(S5.png](https://img-blog.csdnimg.cn/img_convert/2d50e806a5450764afcf83fa30813c31.png#clientId=u62b45d7f-2f05-4&crop=0&crop=0&crop=1&crop=1&from=drop&id=uaf76863e&margin=[object Object]&name=D8)03KL1{(8@4~@NR(5(S5.png&originHeight=551&originWidth=412&originalType=binary&ratio=1&rotation=0&showTitle=false&size=72185&status=done&style=none&taskId=uce09dea6-a379-4200-8c72-57f0f850ccf&title=)

非平衡树

  • 比较好的二叉搜索树,它的数据应该是左右均匀分布的;

  • 但是插入连续数据后,二叉搜索树中的数据分布就变得不均匀了,我们称这种树为非平衡树;

  • 对于一棵平衡二叉树来说,插入/查找等操作的效率是O(logN)

  • 而对于一棵非平衡二叉树来说,相当于编写了一个链表,查找效率变成了O(N);

树的平衡性

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

  • 起码大部分是平衡的,此时的时间复杂度也是接近**O(logN)**的;

  • 这就要求树中每个节点左边的子孙节点的个数,应该尽可能地等于右边的子孙节点的个数;

常见的平衡树

  • AVL树:是最早的一种平衡树,它通过在每个节点多存储一个额外的数据来保持树的平衡。由于AVL树是平衡树,所以它的时间复杂度也是O(logN)。但是它的整体效率不如红黑树,开发中比较少用。

  • 红黑树:同样通过一些特性来保持树的平衡,时间复杂度也是O(logN)。进行插入/删除等操作时,性能优于AVL树,所以平衡树的应用基本都是红黑树

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

呐呐呐呐。

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值