JavaScript 二叉树

news2024/11/15 3:13:01

文章目录

  • 前言
  • 一、何为 '树'
    • 1.根节点
    • 2.外&内部节点
    • 3.子树
    • 4.深度
    • 5.高度
  • 二、二叉树 & 二叉搜索树
    • 1.二叉搜索树插入值
    • 2.遍历二叉搜索树
      • I.中序遍历
      • II.先序遍历
      • III.后序遍历
    • 3.查找节点
    • 4.移除节点
  • 总结


前言

同前面说到的散列表结构, 树也是一种非顺序数据结构, 对于存储需要快速查找的数据非常有用.
我会先叙述一下何为树结构, 然后去实现一个基本功能完备的二叉树作为例子.


一、何为 ‘树’

在《学习JavaScript数据结构与算法》第三次修订版本中对树的定义:

树是一种分层的抽象数据模型.

现实生活中比较常见的例子是家谱和公司组织架构图, 这种结构长得很像一棵秃毛的树, 只不过有时候是左右或者上下颠倒:

在这里插入图片描述
这是二叉树, 二叉树只是树结构中的一种, 树结构可以有多个分支.


1.根节点

一个树结构包含 ( 因为里面不只有父子关系所以不能说这是由父子关系构成的) 一系列存在父子关系的节点, 树接地的部分即起始节点为根节点, 除去根节点, 每个节点都有一个父节点以及零个或多个子节点.


2.外&内部节点

至少有一个子节点的节点称为内部节点, 否则为外部节点, 这两个概念和根节点并列, 根节点既非外部节点也非内部节点, 根节点就是根节点.


3.子树

子树指该树结构内部节点构成的树结构, 存在于’树’之中:

在这里插入图片描述

4.深度

一个节点的深度如何取决于其在树中的位置, 越靠近树冠, 深度越大.
具体的深度数值由其具有的祖先节点数量决定, 即’在第几层’的深度, 比如上图树冠节点有3个父节点, 那么其深度为3.


5.高度

等于树冠节点的深度, 当然, 是要取最大值的, 树冠的每个位置不一定同样深.


二、二叉树 & 二叉搜索树

其实上面画的图都是二叉树.

二叉树中的节点最多只能有两个子节点, 一个是左侧子节点, 一个是右侧子节点.

二叉搜索树(BinarySearchTree)是二叉树的一种, 它只允许在左侧节点存储比父节点小的值, 在右侧节点存储比父节点大的值.
创建一个类来代表二叉搜索树中的每个节点:

export class Node {
  constructor (key) {
    this.key = key; // 节点值
    this.left = null; // 右侧子节点引用
    this.right = null; // 左侧子节点引用
  }
}

再创建一个二叉搜索树:

import { Compare, defaultCompare } from '../util';
import { Node } from './models/node';

export default class BinarySearchTree {
  constructor (compareFn = defaultCompare) {
    this.compareFn = compareFn; // 用来比较节点值的函数
    this.root = null;
  }
}
export 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;
}

1.二叉搜索树插入值

首先验证特殊情况, 如果需要插入的节点是根节点(现在树上什么都没有), 那么只需要创建一个Node实例并将这个实例赋值到this.root来将root指向这个新的节点, 而如果并非特殊情况则交付正常增添函数处理:

// 前置判定
insert (key) {
  if (this.root == null) {
    this.root = new Node(key);
  } else {
    this.insertNode(this.root, key);
  }
}

insertNode会在递归中运用, 所以里面会有’当前节点’这样的概念.

insertNode (node, key) { // 当前节点(非根节点) & 要插入的节点
  if (this.compareFn(key , node.key) === Compare.LESS_THAN) { // 如果要插入的节点比当前节点小(那么应当加在左子节点)
    if (node.left == null) { // 并且当前节点的左子节点为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 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 Node {
  constructor(key) {
    this.key = key;
    this.left = null;
    this.right = null;
  }
}

class BinarySearchTree {
  constructor(compareFn = defaultCompare) {
    this.compareFn = compareFn;
    this.root = null;
  }
  insert(key) {
    if (this.root == null) {
      this.root = new Node(key);
    } else {
      this.insertNode(this.root, 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);
      }
    }
  }
}

var tree = new BinarySearchTree();
const arr = [11, 7, 15, 5, 3, 9, 8, 10, 13, 12, 14, 20, 18, 25];

for (let i = 0; i < arr.length; i++) {
    tree.insert(arr[i]);
}

console.log(tree);

在这里插入图片描述


2.遍历二叉搜索树

访问二叉树的每一个节点并对其实施操作, 有中序, 先序和后序三种方式.

I.中序遍历

中序遍历以上行顺序访问所有树节点, 即以最小到最大的方式遍历

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

printNode (value) {
  console.log(value);
}
tree.inOrderTraverse(printNode);

加到树内:

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

function printNode(value) {
  console.log(value);
}

class Node {
  constructor(key) {
    this.key = key; // 节点值
    this.left = null; // 右侧子节点引用
    this.right = null; // 左侧子节点引用
  }
}

class BinarySearchTree {
  constructor(compareFn = defaultCompare) {
    this.compareFn = compareFn; // 用来比较节点值的函数
    this.root = null;
  }
  
  insert(key) {
    if (this.root == null) {
      this.root = new Node(key);
    } else {
      this.insertNode(this.root, 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);
      }
    }
  }
  
  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 tree = new BinarySearchTree();
const arr = [11, 7, 15, 5, 3, 9, 8, 10];

for (let i = 0; i < arr.length; i++) {
  tree.insert(arr[i]);
}

tree.inOrderTraverse(printNode);
console.log(tree);

在这里插入图片描述

如果当前节点存在右分支(右分支不存在会被阻止), 那么执行完左分支再执行右分支.
到了右分支如果发现了左分支也会再优先执行左分支.

在这里插入图片描述
放在此处, 从11开始, 检查到7和15两个子分支, 先检查7的左分支5再检查5的左分支3, 全部检查完堆好了执行栈开始执行, 5结束后7的左支完成开始执行7的右支, 7的右支9上有左支8于是先输出8.
输出了10之后整个7分支执行完毕, 输出11后11的左支完成, 开始执行11的右支输出15.


II.先序遍历

这东西看起来就只是跟中序遍历换了一下输出位置:

inOrderTraverse (callback) {
  this.inOrderTraverseNode (this.root, callback);
}

inOrderTraverseNode (node, callback) {
  if (node !== null) { // 基线条件
    callback(node.key);
    this.inOrderTraverseNode (node.left, callback);
    this.inOrderTraverseNode(node.right, callback);
  }
}

printNode (value) {
  console.log(value);
}
tree.inOrderTraverse(printNode);

但其实执行逻辑是有比较大的变动的, 中序遍历里我们明明从根节点出发, 但是却先输出了左侧最深的子节点, 这是优先访问子节点再访问自身, 但是先序遍历则是先访问自身再访问子节点.
这个逻辑更加通畅, 看起来更合理:

在这里插入图片描述
在这里插入图片描述
优先执行左分支, 11到左分支: 7左分支, 7左分支: 5, 5左分支3, 全部完成后, 7右分支: 9, 9左分支: 8, 9右分支: 10.
然后11右分支15.


III.后序遍历

看起来仍旧只是变化了输出位置, 但逻辑正好与先序遍历相反, 先序遍历会先访问节点本身, 所有不先访问节点本身的都是从树冠往回输出, 而后序遍历在这基础上遍历完左分支后并不先回调回到节点本身, 而是先去右分支, 再回到节点本身:

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

function printNode(value) {
  console.log(value);
}

class Node {
  constructor(key) {
    this.key = key;
    this.left = null;
    this.right = null;
  }
}

class BinarySearchTree {
  constructor(compareFn = defaultCompare) {
    this.compareFn = compareFn;
      this.root = null;
  }
    insert(key) {
        if (this.root == null) {
            this.root = new Node(key);
        } else {
            this.insertNode(this.root, 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);
            }
        }
    }
    inOrderTraverse(callback) {
        this.inOrderTraverseNode(this.root, callback);
    }

    inOrderTraverseNode(node, callback) {
        if (node !== null) { // 基线条件
            this.inOrderTraverseNode(node.left, callback);
            this.inOrderTraverseNode(node.right, callback);
            callback(node.key);
        }
    }
}

var tree = new BinarySearchTree();
const arr = [11, 7, 15, 5, 3, 9, 8, 10, 13, 12, 14, 20, 18, 25];

for (let i = 0; i < arr.length; i++) {
    tree.insert(arr[i]);
}

tree.inOrderTraverse(printNode);

在这里插入图片描述
在这里插入图片描述

3.查找节点

传入一个起始节点和一个值, 查找某属性为该值的节点.
递归, 每次把当前节点和需要查找的节点比较一下大小以确认下一步是向左or向右查找, 此外如果相等就返回.

search (key) {
  return this.searchNode(this.root, key);
}

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 { // 相等, 返回
    console.log(node)
    return node;
  }
}

4.移除节点

难点在于移除一个非树冠节点后剩下的节点该做什么处理以确保树的结构依然正确.
原书使用了一个函数来处理(‘这是我们在本书中要实现的最复杂的方法’), 这里抽离成两部分, 第一部分查找, 第二部分专用于去除.

remove(node, key) { // 查找方向确定, 查找
  if (node == null) {
    return null;
  }
  if (this.compareFn(key, node.key) === Compare.LESS_THAN) { // 向左一路找下去
    node.left = this.remove(node.left, key);
    return node;
  }
  if (this.compareFn(key, node.key) === Compare.BIGGER_THAN) { // 向右一路找下去
    node.right = this.remove(node.right, key);
    return node;
  } else {
    removeNode(node, key); // 相等, 交付removeNode处理
  }
}

removeNode(node, key) {
  if (node.right == null && node.left == null) {
    node = null;
    return node;
  }
  if (node.right == null) {
    node = node.left;
    return node;
  } else {
    node = node.right;
    return node;
  }
  // 有两个子节点的节点
  const aux = this.minNode(node.right); // 获取右侧子树中最小的节点(不一定是它的直接子节点)
  node.key = aux.key; // 右侧子树最小节点赋值到该节点, 该节点值此时合理, 但是存在了两个重复节点
  node.right = this.remove(node.right, aux.key); // 删除右侧子树最小节点以去重
  return node;
}

总结

本来应该叫’JavaScript 树’的, 但是我一直在说二叉树…整个篇幅基本给了二叉树, 然后索性就叫二叉树了.
栈的话前一段时间做了个撤销恢复功能, 用双端队列改造了一下限制了步数.
这种结构目前没想到要怎么去应用, 或许在使用一些库的时候能用到?

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

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

相关文章

【浅学Nginx】Nginx安装和基础使用

Nginx安装和基础使用1. Nginx是什么2. Nginx的安装3. Nginx的目录结构4. Nginx的配置文件结构5. Nginx的具体应用5.1 部署静态资源5.2 反向代理5.3 负载均衡1. Nginx是什么 Nginx是一个轻量级的 web服务器 / 反向代理服务器及电子邮件&#xff08;IMAP/POP3&#xff09;代理服…

kettle开发-Day37-SQ索引优化

前言&#xff1a;在上一个生产项目中&#xff0c;有个单表数据超249G了&#xff0c;里面存储的数据时间跨度就1年左右&#xff0c;那为啥会出现这种情况呢&#xff1f;数据来源为&#xff0c;一个生产基地所有电表的每分钟读数&#xff0c;一个基地大概500个电表左右&#xff0…

【C++】---Stack和Queue的用法及其模拟实现

文章目录Stack最小栈栈的弹出压入序列逆波兰表达式求值用栈实现队列模拟实现queue用队列实现栈模拟实现Stack stack是一种容器适配器&#xff0c;专门用在具有后进先出操作的上下文环境中&#xff0c;其删除只能从容器的一端进行元素的插入与提取操作。它的使用和之前学习的ve…

KDZD880 智能蓄电池放电测试仪

一、产品概述 智能蓄电池放电测试仪主要用于电信、移动、联通、电力直流行业的后备电源铅酸蓄电池的放电测试&#xff0c;具备蓄电池快速容量测试、在线监测及容量核对测试三大功能于一体的产品&#xff0c;集成化程度高、体积小巧、功能完善。 该设备是针对整组 12V-600V 蓄…

JavaScript高级程序设计读书分享之3章——3.4数据类型

JavaScript高级程序设计(第4版)读书分享笔记记录 适用于刚入门前端的同志 ECMAScript 有 6 种简单数据类型&#xff08;也称为原始类型&#xff09;&#xff1a;Undefined、Null、Boolean、Number、String 和 Symbol&#xff08;es6新增&#xff09;。 还有一种复杂数据类型叫…

vim编辑器和gcc/g++编译器和gdb调试器和make/makefile自动化构建工具的使用

vim的三种模式(其实有好多模式 )&#xff08;1&#xff09;.命令模式&#xff08;2&#xff09;.插入模式&#xff08;3&#xff09;.底行模式vim的基本操作vim的命令模式的基本操作vim的插入模式的基本操作vim的底行模式的基本操作vim的配置gcc和g相关操作&#xff08;1&#…

XCP实战系列介绍11-几个常用的XCP命令解析

本文框架 1.概述2. 常用命令解析2.1 CONNECT连接(0xFF)2.2 SHORT_UPLOAD 命令(0xF4)2.2 SET_MTA (0xF6)2.3 MOVE命令(0x19)2.4 GET_CAL_PAGE(0xEA)2.5 SET_CAL_PAGE(0xEB)2.6 DOWNLOAD(0xF0)1.概述 在文章《看了就会的XCP协议介绍》中详细介绍了XCP的协议,在《XCP实战系列介绍…

Python面试——装饰器

知识链接&#xff1a; 装饰器 装饰器可调用的对象&#xff0c;其参数是被装饰的函数。装饰器可能会处理被装饰的函数然后把它返回&#xff0c;或者将其替换成另外一个函数或者可调用对象。 装饰器有两大特性&#xff1a; 能把被装饰的函数替换成其他函数&#xff08;在元编程…

面试腾讯测试岗后感想,真的很后悔这5年一直都干的是基础测试....

前两天&#xff0c;我的一个朋友去大厂面试&#xff0c;跟我聊天时说&#xff1a;输的很彻底… 我问她&#xff1a;什么情况&#xff1f;她说&#xff1a;很后悔这5年来一直都干的是功能测试… 相信许多测试人也跟我朋友一样&#xff0c;从事了软件测试很多年&#xff0c;却依…

树莓派用默认账号和密码登录不上怎么办;修改树莓派的密码

目录 一、重置树莓派的默认账号和密码 二、修改树莓派的密码 三、超级用户和普通用户的切换 一、重置树莓派的默认账号和密码 在SD卡中根目录建立文件userconf 在userconf中输入如下内容&#xff1a; pi:$6$/4.VdYgDm7RJ0qM1$FwXCeQgDKkqrOU3RIRuDSKpauAbBvP11msq9X58c8Q…

STM32开发(10)----CubeMX配置基本定时器

CubeMX配置基本定时器前言一、定时器的介绍二、实验过程1.实验材料2.STM32CubeMX配置基本定时器2.代码实现3.编译烧录4.硬件连接5.实验结果总结前言 本章介绍使用STM32CubeMX对基本定时器进行配置的方法&#xff0c;STM32F103高性能系列设备包括基本定时器、高级控制定时器、通…

JavaEE-HTTP协议(一)

目录什么是HTTP协议&#xff1f;协议格式如何看到HTTP的报文格式&#xff1f;HTTP请求HTTP响应URLURL encode/decode什么是HTTP协议&#xff1f; 计算机网络&#xff0c;核心概念&#xff0c;网络协议 网络协议种类非常多&#xff0c;其中一些耳熟能详的&#xff0c;IP,TCP,UD…

shell命令行并行神器 - parallel

shell命令行并行神奇 - parallel 概述 GNU parallel 是一个 shell 工具&#xff0c;用于使用一台或多台计算机并行执行作业。作业可以是单个命令或必须为输入中的每一行运行的小脚本。典型的输入是文件列表、主机列表、用户列表、URL 列表或表列表。作业也可以是从管道读取的…

98年的确实卷,公司新来的卷王,我们这帮老油条真干不过.....

都说00后躺平了&#xff0c;但是有一说一&#xff0c;该卷的还是卷。这不&#xff0c;前段时间我们公司来了个00后&#xff0c;工作没两年&#xff0c;跳槽到我们公司起薪18K&#xff0c;都快接近我了。后来才知道人家是个卷王&#xff0c;从早干到晚就差搬张床到工位睡觉了。 …

电脑麦克风没声音怎么办?这3招就可以解决!

最近有用户在使用电脑麦克风进行视频录制时&#xff0c;发现麦克风没有声音。这是什么原因&#xff1f;电脑麦克风没有声音怎么办&#xff1f;关于解决方案&#xff0c;我专门整理了三种方法来帮你们&#xff0c;一起来看看吧&#xff01; 操作环境&#xff1a; 演示机型&#…

在TitanIDE中使用ChatGPT辅助科研开发

作者&#xff1a;行云创新CEO 马洪喜 命题&#xff1a;太空望远镜拍摄的照片处理 假设&#xff1a;我是图形科学家&#xff0c;但不是特别懂Python 先上传一张银河系照片&#xff0c;目的是把彩色转成灰度&#xff1a; 然后我不会啊&#xff0c; 问问chatGPT 彩色图片转灰度…

电话号码的字母组合-力扣17-java

一、题目描述给定一个仅包含数字 2-9 的字符串&#xff0c;返回所有它能表示的字母组合。答案可以按 任意顺序 返回。给出数字到字母的映射如下&#xff08;与电话按键相同&#xff09;。注意 1 不对应任何字母。示例 1&#xff1a;输入&#xff1a;digits "23"输出…

Android 一体机研发之修改系统设置————自动锁屏

Android 一体机研发之修改系统设置————屏幕亮度 Android 一体机研发之修改系统设置————声音 Android 一体机研发之修改系统设置————自动锁屏 修改系统设置系列篇章马上开张了&#xff01; 本章将为大家细节讲解自动锁屏。 自动锁屏功能&#xff0c;这个可以根据…

简述springIOC容器的bean加载流程

参考笔记:https://blog.51cto.com/u_14006572/3118363 https://zhuanlan.zhihu.com/p/386335813 https://blog.csdn.net/mrathena/article/details/115654379 目录结构 spring ioc容器的加载&#xff0c;大体上经过以下几个过程&#xff1a; 资源文件定位、解析、注册、实例化…

UWA Pipeline 2.4.1 版本更新说明

UWA Pipeline是一款面向游戏开发团队的本地协作平台&#xff0c;旨在为游戏开发团队搭建专属的DevOps研发交付流水线&#xff0c;提供可视化的CICD操作界面、高可用的自动化测试以及UWA性能保障服务的无缝贴合等实用功能。 在本次UWA Pipeline 2.4.1版本更新中&#xff0c;主要…