JS算法之树(一)

news2024/11/17 3:26:02

前言

        之前我们已经介绍过一种非顺序数据结构,是散列表。

JavaScript散列表及其扩展http://t.csdn.cn/RliQf        还有另外一种非顺序数据结构---树。

树数据结构

        树是一种分层数据的抽象模型。公司组织架构图就是常见的树的例子。

        

相关术语

        一个树结构,包含若干父子关系的节点。每个节点(除了根节点)都有一个父子点以及0个或多个子节点。

        树中的每个元素都叫做节点。

        位于树的顶部的节点叫做根节点。

        节点分为外部节点和内部节点。外部节点没有子节点。内部节点有子节点。

        一个节点(除了根节点)可以有祖先和后代。

        祖先节点包括 父节点、祖父节点、曾祖父节点等。

        后代节点包括子节点、孙子节点、曾孙节点等。

         

        子树:由节点和它的后代组成 

         节点的一个属性是深度。节点的深度取决于它的祖先节点的数量。

        树的高度属性取决于所有节点深度的最大值。

二叉树

         二叉树的节点最多只能有两个子节点。 

        

         二叉树的设计是为了让我们写出更高效地在树中插入、查找和删除节点的算法。

        二叉搜索树

        二叉树中的一种,只允许你在左侧节点存储(比父节点)小的值。在右侧节点存储(比父节点)大的值。

创建BinarySearchTree类(二叉搜索树)

        我们需要先设计节点类。

        通过示意图我们可以发现二叉树的节点跟链表的子节点很像。链表的节点包含值和前后引用。而树的节点包含了值和左右两侧节点的引用。

        在树相关的术语中,我们也把树的节点称之为键

        键类:

export class Node {
  constructor(key) {
    this.key = key;
    this.left = undefined;
    this.right = undefined;
  }
  toString() {
    return `${this.key}`;
  }
}

        二叉查询树类:

export default class BinarySearchTree {
  constructor() {
    // 根节点
    this.root = undefined;
  }
}

        向二叉查询树中插入一个键:

import { defaultCompare } from '../util';
export default class BinarySearchTree {
  constructor(compareFn = defaultCompare) {
    this.compareFn = compareFn;
    this.root = undefined;
  }
}

 这里需要导入自定义的对比方法(为了对比插入节点值和想要比较的节点的节点值),这里展示一个常用的比较方法。当然你完全也可以自定义自己的比较方法。

const Compare = {
  LESS_THAN: -1,
  BIGGER_THAN: 1,
  EQUALS: 0
};
export function defaultCompare(a, b) {
  if (a === b) {
    return Compare.EQUALS;
  }
  return a < b ? Compare.LESS_THAN : Compare.BIGGER_THAN;
}
insert(value) {
    if (this.root == null) {
        this.root = new Node(value)
    }else {
        this.insertNode(this.root, value)   
    }
}
insertNode(node, key) {
    if (this.compareFn(key, node.key) === Compare.EQUALS)  {
        // 重复节点不生成
         return false ;
    }
    else if (this.compareFn(key, node.key) === Compare.LESS_THAN) {
      if (node.left == null) {
        node.left = new Node(key);
      } else {
        this.insertNode(node.left, key);
      }
    } else if (node.right == null) {
      node.right = new Node(key);
    } else {
      this.insertNode(node.right, key);
    }
}

 测试:

const bbb = new BinarySearchTree();
bbb.insert(11)
bbb.insert(22)
bbb.insert(9)
bbb.insert(15)

得到:

完整代码:

class Node {
  constructor(key) {
    this.key = key;
    this.left = undefined;
    this.right = undefined;
  }
  toString() {
    return `${this.key}`;
  }
}
const Compare = {
  LESS_THAN: -1,
  BIGGER_THAN: 1,
  EQUALS: 0
};
function defaultCompare(a, b) {
  if (a === b) {
    return Compare.EQUALS;
  }
  return a < b ? Compare.LESS_THAN : Compare.BIGGER_THAN;
}
class BinarySearchTree {
  constructor(compareFn = defaultCompare) {
    this.compareFn = compareFn;
    this.root = undefined;
  }
  insert(value) {
    if (this.root == null) {
        this.root = new Node(value)
    }else {
         this.insertNode(this.root, value)  
    }
  }
  insertNode(node, key) {
    if (this.compareFn(key, node.key) === Compare.EQUALS)  {
        // 重复节点不生成
         return false ;
    }
    else if (this.compareFn(key, node.key) === Compare.LESS_THAN) {
      if (node.left == null) {
        node.left = new Node(key);
      } else {
        this.insertNode(node.left, key);
      }
    } else if (node.right == null) {
      node.right = new Node(key);
    } else {
      this.insertNode(node.right, key);
    }
   }
}

树的遍历

三种方法:中序、先序、后序

中序遍历

中序遍历是一种以上行顺序访问树节点的遍历方式。

中序遍历不是从中间开始遍历,至于为什么叫中序遍历,请看后文。

应用于:对树进行排序操作。

  inOrderTraverseNode(node, callback) {
    if (node != null) {
      this.inOrderTraverseNode(node.left, callback);
      callback(node.key);
      this.inOrderTraverseNode(node.right, callback);
    }
  }

这里的逻辑用了递归的思想。从上至下遍历到最左边最下面的节点然后再自下往上开始回调。

写个实例试试:

 添加遍历方法:

class BinarySearchTree {
 ...
 inOrderTraverse(callback) {
    this.inOrderTraverseNode(this.root, callback);
 }
 inOrderTraverseNode(node, callback) {
    if (node != null) {
      this.inOrderTraverseNode(node.left, callback);
      callback(node.key);
      this.inOrderTraverseNode(node.right, callback);
    }
 }
 ...
}
var aa = new BinarySearchTree()
aa.insert(11)
aa.insert(7)
aa.insert(15)
aa.insert(5)
aa.insert(9)
aa.insert(13)
aa.insert(20)
aa.insert(3)
aa.insert(6)
aa.insert(8)
aa.insert(10)
aa.insert(12)
aa.insert(14)
aa.insert(18)
aa.insert(25)

开始遍历:

const printCb = (value) => console.log(value)
aa.inOrderTraverse(printCb);

输出:

 

插图(方便下文排序的理解)

先序遍历

以优先于后代节点的顺序访问每个节点。

常用的应用场景是打印一个结构化文档。

preOrderTraverseNode(node, callback) {
    if (node != null) {
      callback(node.key);
      this.preOrderTraverseNode(node.left, callback);
      this.preOrderTraverseNode(node.right, callback);
    }
}

 和中序遍历不同的是:先序遍历会先访问节点本身,然后再访问它左侧的子节点,最后是右侧子节点。

  preOrderTraverseNode(node, callback) {
    if (node != null) {
      callback(node.key);
      this.preOrderTraverseNode(node.left, callback);
      this.preOrderTraverseNode(node.right, callback);
    }
  }
  preOrderTraverse(callback) {
    this.preOrderTraverseNode(this.root, callback);
  }

输出:11  7  5  3 6  9  8 10 15 13 12 14  20 18 25

后序遍历

后序遍历先访问节点的后代节点。再访问节点本身。

应用场景:计算一个目录及其子目录中所有文件所占空间的大小。

由上文可知,后序遍历的逻辑是:

 postOrderTraverse(callback) {
    this.postOrderTraverseNode(this.root, callback);
  }
  postOrderTraverseNode(node, callback) {
    if (node != null) {
      this.postOrderTraverseNode(node.left, callback);
      this.postOrderTraverseNode(node.right, callback);
      callback(node.key);
    }
  }

输出:

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

树的搜索

在树中,常用搜索有三种:

  • 搜索最小值
  • 搜索最大值
  • 搜索特定值

我们来看看上文提到的insert方法:

insertNode(node, key) {
    if (this.compareFn(key, node.key) === Compare.LESS_THAN) {
      if (node.left == null) {
        node.left = new Node(key);
      } else {
        this.insertNode(node.left, key);
      }
    } else if (node.right == null) {
      node.right = new Node(key);
    } else {
      this.insertNode(node.right, key);
    }
  }

这里不考虑插入相等的节点值(因为这样违背了二叉搜索树的应用前提)

在学习树的搜索之前,我们必须再深刻认识一下二叉搜索树的模型。

加深认识

我们特别关注这个方法:

insertNode(node, key) 
    if (this.compareFn(key, node.key) === Compare.LESS_THAN) {
      if (node.left == null) {
        node.left = new Node(key);
      } else {
        this.insertNode(node.left, key);
      }
    } else if (node.right == null) {
      node.right = new Node(key);
    } else {
      this.insertNode(node.right, key);
    }
}

在书写上面的实例的时候,你一定有疑惑,树的插入顺序到底会不会影响树的结果?

比如现在有这么个树:

 除了顶部节点11必须第一个插入,其他的节点 7 5 9 15 13 20是否有插入顺序限制呢?

我们再仔细咀嚼代码。可知:

规律一,顶点左侧树永远小于顶点节点值。右侧永远小于顶点节点值。

 也就是左侧的树节点群(7 5  9  3  6  8 10)的顺序不会影响右侧树节点群(15 13 20)的顺序

当我们进入到下一个节点,比如插入了7之后,5  3 6的插入顺序又不会影响9  8 10...

以此,形成多个独立嵌套块

 块里面的顺序会影响树结构。比如7-5-9可以被7-6-9替代

最大值最小值搜索

 显而易见,右边大的越大,左边小得越小

所以最大值最小值我们只需要遍历找到最底部的左右侧节点值。

//  找最小键
getMin() {
    return  this.minNode(this.root)
}
minNode(node)   {
    let current  = node;
    while (current !=  null &&  current.left !== null ) {
        current = current.left
    }
    return current
}
//  找最大键
getMax() {
    return  this.maxNode(this.root)
}
maxNode(node)   {
    let current  = node;
    while (current !=  null &&  current.right!== null ) {
        current = current.right
    }
    return current
}

特定值节点搜索

给出一个特定的节点值,我们应该如何快速地去找到他的位置呢?

还是利用二叉树的左小右大原理:

searchNode(node,key)  {
    if  (node == null) {
        //  没有找到节点
        return false
    }
    if (this.compareFn(key,node.key) === Compare.LESS_THAN)   {
        // 比它小往左边找
        return this.searchNode(node.left,key)
    }else if (this.compareFn(key,node.key) === Compare.BIGGER_THAN){
      // 比它大往右边找
       return this.searchNode(node.right,key) 
    }else {
      // 找到
        return node
    }
}

移除节点

很复杂,需要认真理解。

remove(key) {
  this.root = this.removeNode(this.root, key);
}

这里选择将root赋值为removeNode的返回值。是理解的难点。

removeNode(node, key) {
    if (node == null) { // {1}
      return undefined;
    }
    if (this.compareFn(key, node.key) === Compare.LESS_THAN) {
      node.left = this.removeNode(node.left, key);
      return node;
    } else if (this.compareFn(key, node.key) === Compare.BIGGER_THAN) {
      node.right = this.removeNode(node.right, key);
      return node;
    
    if (node.left == null && node.right == null) {
      node = null;
      return node;
    }
    if (node.left == null) {
      node = node.right;
      return node;
    } else if (node.right == null) {
      node = node.left;
      return node;
    }
    const aux = this.minNode(node.right);
    node.key = aux.key;
    node.right = this.removeNode(node.right, aux.key);
    return node;
}

实现思路:

{1}如果正在检测的节点为null,则说明该键不存在于树中,返回null。

通过比大小往左下或右下找节点。当找到我们要删除的节点后。需要处理三种情况:

①移除一个叶节点(无左右子节点)

②移除有一个左侧或右侧子节点的节点

③移除有左侧和右侧子节点的节点

第①种情况是最简单的情况。

         比如我们当前要删除节点3.除了把节点3赋NULL之外,还会影响的节点只有一个。即3号节点的父节点五号节点。所以需要通过返回null来将对应的父节点指针赋予null值。

        现在节点的值是null了,父节点指向它的指针也会收到这个值。这也就是为什么我们要在函数中返回节点的值。父节点总是会接收到函数的返回值。

if (node.left == null && node.right == null) {
      node = null;
      return node;
}

第②种情况,需要跳过这个节点。将父节点指向它的指针指向子节点。

 if (node.left == null) {
      node = node.right;
      return node;
} else if (node.right == null) {
      node = node.left;
      return node;
}

第①第②种情况摘除节点都不会影响到树的结构。第①种没子节点的不说。第②种带子节点的摘除中间节点并不会影响树节点的大小排列关系。

第③种情况,也是最复杂的情况。

 前文已经提到了。节点的右边子节点排列并不会影响左边子节点排列。而摘掉5号节点。3<5,5<6,变成3<6也完全衔接得上。所以①②两种情况需要执行的步骤很少。麻烦的是去除的节点包含了左右子节点。

比如我们现在要删掉15节点。那么删掉15节点之后,那个节点肯定不能为null,因为它下面还挂着子节点。所以我们必须找一个子节点来替换他。

 画圈的都可以。但是13  12不行。

选13填15位置。会变成这样:

 选12,13就得放右边了,更不合理。

那么选谁来替换15呢?为了保证树的结构的统一性。我们选叶节点来替换是最好的。就剩下14 18 25 。然后我们排除25,因为25比20大。20不能作为右叶存在了。所以剩下两个:

14和18。

也就是被删除节点左子树里最大的一个。和右子树里最小的一个。那么两者都可以吗?

 在样例树上,确实可以将左子树中最大叶节点替换被删除节点。但是如果是这样:

 左侧子树没有右子树,所以最大的节点在13节点。此时与上面不同,因为13没有右侧节点,所以他可以顶替15。所以删除存在左右节点的节点。可以找他左树最大的节点和右数最小的节点。

const aux = this.minNode(node.right);
node.key = aux.key;
node.right = this.removeNode(node.right, aux.key);
return node;

        

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

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

相关文章

x86的内存寻址方式

文章目录 一、实模式寻址二、保护模式寻址三、段页式内存管理四、Linux的内存寻址五、进程与内存1、内核空间和用户空间2、内存映射3、进程内存分配与回收 一、实模式寻址 在16位的8086时代&#xff0c;CPU为了能寻址超过16位地址能表示的最大空间&#xff08;因为 8086 的地址…

小研究 - J2EE 应用服务器的软件老化测试研究

软件老化现象是影响软件可靠性的重要因素&#xff0c;长期运行的软件系统存在软件老化现象&#xff0c;这将影响整个业务系统的正常运行&#xff0c;给企事业单位带来无可估量的经济损失。软件老化出现的主要原因是操作系统资源消耗殆尽&#xff0c;导致应用系统的性能下降甚至…

virtuoso61x中集成calibre

以virtuoso618为例&#xff0c;在搭建完电路、完成前仿工作之后绘制版图&#xff0c;版图绘制完成之后需要进行drc和lvs【仅对于学校内部通常的模拟后端流程而言】&#xff0c;一般采用mentor的calibre来完成drc和lvs。 服务器上安装有virtuoso和calibre&#xff0c;但是打开la…

servlet,Filter,责任的设计模式,静态代理

servlet servlet是前端和数据库交互的一个桥梁 静态网页资源的技术&#xff1a;在前端整个运行的过程中 我们的网页代码不发生改变的这种情况就称为静态的网页资源技术动态网页资源的技术&#xff1a;在前端运行的过程中 我们的前端页面代码会发生改变的这种情况就称为 动态的网…

电商版面设计之优惠券设计

1、画一个矩形---最快的方法&#xff0c;提前写好 2、ALT复制矩形图层 3、提前把优惠券的文案准备好 4、改一下字体---72 5、字体改成12号字体 6、上面对齐选择第二个去做&#xff0c;最上方 7、后面那个就是门槛 8、用Alt复制4个 9、改字就行 10、看见不错的优惠劵设计可以参…

word如何调整页码

文章目录 如何调整页码 如何调整页码 用 word 写报告的时候&#xff0c;经常遇到要求说是要从正文开始才显示页码&#xff0c;那如何实现呢 把鼠标放在我们正文的那一页的顶部&#xff0c;点击 布局 ,再点击分隔符&#xff0c;再点击连续 再点击编译页脚 选择你想要的页脚格式…

十四、pikachu之XSS

文章目录 1、XSS概述2、实战2.1 反射型XSS&#xff08;get&#xff09;2.2 反射型XSS&#xff08;POST型&#xff09;2.3 存储型XSS2.4 DOM型XSS2.5 DOM型XSS-X2.6 XSS之盲打2.7 XSS之过滤2.8 XSS之htmlspecialchars2.9 XSS之href输出2.10 XSS之JS输出 1、XSS概述 Cross-Site S…

探讨uniapp的组件使用的问题

1 view Flex是Flexible Box的缩写&#xff0c;意为“弹性布局”&#xff0c;用来为盒状模型提供最大的灵活性。 当设置display: flex后&#xff0c;继续给view等容器组件设置flex-direction:row或column&#xff0c;就可以在该容器内按行或列排布子组件。uni-app推荐使用flex布…

[Linux]进程

文章目录 1. 进程控制1.1 进程概述1.1.1 并行和并发1.1.2 PCB1.1.4 进程状态1.1.5 进程命令 1.2 进程创建1.2.1 函数1.2.2 fork() 剖析 1.3 父子进程1.3.1 进程执行位置1.3.2 循环创建子进程1.3.3 终端显示问题1.3.4 进程数数 1.4 execl和execlp函数1.4.1 execl()1.4.2 execlp(…

Android 13.0 首次开机默认授予app运行时权限(去掉运行时授权弹窗)

1.概述 在13.0的系统产品开发中,在android6.0以后对于权限的申请,都需要动态申请,所以会在系统首次启动后,在app的首次运行时,会弹出授权窗口,会让用户手动授予app运行时权限,在由于系统产品开发需要要求默认授予app运行时权限,不需要用户默认授予运行时弹窗,所以需要…

基于OpenCV的迷宫路径查找

附上代码&#xff1a; import cv2 import numpy as np# 读取图像 img cv2.imread("img_3.png") thres_min 150 # 二值化最小阈值if not img is None:# 二值化处理ret, img cv2.threshold(img, thres_min, 255, cv2.THRESH_BINARY)cv2.imshow("img_thres&qu…

【C++】list类的模拟实现

&#x1f3d6;️作者&#xff1a;malloc不出对象 ⛺专栏&#xff1a;C的学习之路 &#x1f466;个人简介&#xff1a;一名双非本科院校大二在读的科班编程菜鸟&#xff0c;努力编程只为赶上各位大佬的步伐&#x1f648;&#x1f648; 目录 前言一、list类的模拟实现1.1 list的…

使用Python写入数据到Excel:实战指南

在数据科学领域&#xff0c;Excel是一种广泛使用的电子表格工具&#xff0c;可以方便地进行数据管理和分析。然而&#xff0c;当数据规模较大或需要自动化处理时&#xff0c;手动操作Excel可能会变得繁琐。此时&#xff0c;使用Python编写程序将数据写入Excel文件是一个高效且便…

如何推广你的开源项目?

&#x1f337;&#x1f341; 博主猫头虎 带您 Go to New World.✨&#x1f341; &#x1f984; 博客首页——猫头虎的博客&#x1f390; &#x1f433;《面试题大全专栏》 文章图文并茂&#x1f995;生动形象&#x1f996;简单易学&#xff01;欢迎大家来踩踩~&#x1f33a; &a…

mongodb聚合排序的一个巨坑

现象&#xff1a; mongodb cpu动不动要100%&#xff0c;如下图 分析原因&#xff1a; 查看慢日志发现&#xff0c;很多条这样的查询&#xff0c;一直未执行行完成&#xff0c;占用大量的CPU [{$match: {"tags.taskId": "64dae0a9deb52d2f9a1bd71e",grnty: …

电商版面设计之首页设计

首页设计资料 1、首页----多看大美工 2、手表首页 3、水密码官方旗舰店 4、AK男装 5、百雀羚首页设计 6、活动专区 7、店铺有一些活动&#xff0c;会在里面进行体现 8、提前构思&#xff0c;多看别人的店铺设计&#xff0c;是提升自己店铺设计最好的方法 9、产品专区 10、买一送…

date_range()函数--Pandas

1. 函数功能 生成连续的日期时间序列 2. 函数语法 pandas.date_range(startNone, endNone, periodsNone, freqNone, tzNone, normalizeFalse, nameNone, inclusiveboth, *, unitNone, **kwargs)3. 函数参数 参数含义start可选参数&#xff0c;起始日期end可选参数&#xff…

物理机ping不通windows server 2012

刚才尝试各种方法&#xff0c;在物理机上就是ping不能wmware中的windows server 2012 . 折腾了几个小时&#xff0c;原来是icmp 被windows server 2012 禁用了 现在使用使用以下协议就能启用Icmp协议。 netsh firewall set icmpsetting 8然后&#xff0c;就能正常ping 通虚…

银河麒麟服务器系统服务安装流程,会根据服务是否正常判断是否重装服务

流程图 【金山文档】 linux系统服务安装相关https://kdocs.cn/l/csiyUvMWjmwc 总结 要站在面上考虑问题&#xff0c;解压和拷贝这个过程仅仅是系统服务安装的其中一个步骤&#xff0c;不能只是判断文件是否解压去判断&#xff0c;去执行相关逻辑

Linux c++ - 01-开发环境配置

一、环境配置 1.安装gcc,gdb sudo apt update sudo apt install build-essential gdb 安装成功确认 gcc --version g --version gdb --version 2.安装cmake sudo apt install cmake 安装成功确认 cmake --version 3.总结 gcc 用于编译C代码 g 用于编译C代码 VSCode是通…