在使用javascript实现基本的数据结构中,练习了好几周,对基本的数据结构如 栈、队列、链表、集合、哈希表、树、图等内容进行了总结并且写了笔记和代码。

在 github中可以看到  点击查看,可以关注一下我哈。

树的基本术语

二叉树节点的存储结构

创建一个二叉搜索树

二叉树的先序、中序、后续遍历算法

二叉树的非递归先序、中序、后续遍历算法

文章对树了解的不多的人有点不友好,这里简单介绍(从书上抄下来)那些基本的一点概念吧。

看下面这个示意图

树的基本术语:

结点:A、B、C等都是结点,结点不仅包含数据元素,而且包含指向子树的分支。例如,A结点不仅包含数据元素A、还包含3个指向子树的指针。

结点的度:结点拥有的子树个数或者分支的个数,例如A结点有3棵子树,所以A结点的度为3.

树的度:树中各结点度的最大值。如例子中结点度最大为3(A、D结点)。最小为0(F、G、I、J、K、L、M),所以树的度为3。

叶子节点:又叫做终端节点,指度为0的节点,F、G、I、J、K、L、M节点都是叶子节点。

孩子:结点的子树的根,如A节点的孩子为B、C、D。

双亲:与孩子的定义对应,如B C D结点的双亲都是A。

兄弟:同一个双亲的孩子之间互为兄弟。如B、C、D互为兄弟,因为它们都是A节点的孩子。

祖先:从根到某节点的路径上的所有结点,都是这个节点的祖先。如K的祖先是A B E,因为从A到K的路径为 A---B---E--K。

子孙: 以某节点为根的子树中的所有结点,都是该结点的子孙。如D的子孙为H I J M。

层次:从根开始,根为第一层,根的孩子为第二层,根的孩子的孩子为第三层,以此类推。

树的高度(或者深度):树中结点的最大层次,如例子中的树共有4层,所以高度为4.

理解了上面的树一些基本一些的概念后,我们来看一下什么是二叉树。

1)每个结点最多只有两棵子树,即二叉树中的节点的度只能为0、1、2

2) 子树有左右之分,不能颠倒。

以下二叉树的5中基本状态

构造一个二叉树的节点存储结构

我们会发现,二叉树中的存储结构一个是值,一个是左边有一个,右边有一个。他们分别叫左孩子/左子树  右孩子/右子树。

所以我们会很容易的写出来一个节点的构造函数。

 // 树的结构
class BTNode {
constructor() {
this.key = key;
this.lchild = null;
this.rchild = null;
}
}

实现一个二叉搜索树 / 二叉排序树

看一下定义

二叉排序树或者是空树,或者是满足以下性质的二叉树。

1) 若它的左子树不空,则左子树上的所有关键字的值均小于根关键字的值。

2)若它的右子树不空,则右子树上所有关键字的值均大于根关键字的值。

3)左右子树又是一棵二叉排序树。

举个例子

假如要插入一堆数字,数字为 20 10 5 15 13 18 17 30

那么用代码如何实现呢?

 let BST = (function () {

   let ROOT = Symbol();

   // 节点结构
let BTNode = class {
constructor(key) {
this.key = key;
this.lchild = null;
this.rchild = null;
}
} // 递归插入节点
let recursionInsert = function (root, node) {
if (node.key < root.key) {
if (root.lchild) {
recursionInsert(root.lchild, node);
} else {
root.lchild = node;
}
} else {
if (root.rchild) {
recursionInsert(root.rchild, node);
} else {
root.rchild = node;
}
}
} // 二叉搜索树类
return class {
constructor() {
this[ROOT] = null;
} // 插入
insert(key) {
let node = new BTNode(key);
let root = this[ROOT];
if (!root) {
this[ROOT] = node;
return;
}
// 递归插入
recursionInsert(root, node);
}
} })(); let bst = new BST(); bst.insert(20);
bst.insert(10);
bst.insert(5);
bst.insert(15);
bst.insert(13);
bst.insert(18);
bst.insert(17);
bst.insert(30); console.log(bst);

在浏览器中一层一层的展开看看是否和我们的一样。

二叉树的遍历算法

二叉树的遍历算法,二叉树的遍历就是按照某种规则将二叉树中的所有数据都访问一遍。

二叉树的遍历方式有先序遍历、中序遍历、后续遍历和层次遍历,很多算法都是基于这几种遍历方式衍生出来的。

递归方式的具体的代码实现

 let BST = (function () {

   let ROOT = Symbol();

   // 节点结构
let BTNode = class {
constructor(key) {
this.key = key;
this.lchild = null;
this.rchild = null;
}
} // 递归插入节点
let recursionInsert = function (root, node) {
if (node.key < root.key) {
if (root.lchild) {
recursionInsert(root.lchild, node);
} else {
root.lchild = node;
}
} else {
if (root.rchild) {
recursionInsert(root.rchild, node);
} else {
root.rchild = node;
}
}
}; // 用于中序遍历二叉树的方法
let inorderTraversal = function (root, arr) {
if (!root) return;
inorderTraversal(root.lchild, arr);
arr.push(root.key);
inorderTraversal(root.rchild, arr);
}; // 用于先序遍历的递归函数
let preOrderTraversal = function (root, arr) {
if (!root) return;
arr.push(root.key);
preOrderTraversal(root.lchild, arr);
preOrderTraversal(root.rchild, arr);
}; // 用于后续遍历的递归函数
let lastOrderTraversal = function (root, arr) {
if (!root) return;
lastOrderTraversal(root.lchild, arr);
lastOrderTraversal(root.rchild, arr);
arr.push(root.key);
}; // 二叉搜索树类
return class {
constructor() {
this[ROOT] = null;
} // 插入
insert(key) {
let node = new BTNode(key);
let root = this[ROOT];
if (!root) {
this[ROOT] = node;
return;
}
// 递归插入
recursionInsert(root, node);
} // 中序遍历二叉树
inorderTraversal() {
let arr = [];
inorderTraversal(this[ROOT], arr);
return arr;
} // 先序遍历二叉树
preOrderTraversal() {
let arr = [];
preOrderTraversal(this[ROOT], arr);
return arr;
} // 后续遍历
lastOrderTraversal() {
let arr = [];
lastOrderTraversal(this[ROOT], arr);
return arr;
}
} })(); let bst = new BST(); bst.insert(20);
bst.insert(15);
bst.insert(7);
bst.insert(40);
bst.insert(30);
bst.insert(45);
bst.insert(50); console.log(bst); let a = bst.inorderTraversal();
let b = bst.preOrderTraversal();
let c = bst.lastOrderTraversal(); console.log(a);
console.log(b);
console.log(c);

广度优先遍历

// 广度优先遍历
breadthRirstSearch() {
// 初始化用于广度优先遍历的队列
let queue = new Queue();
console.log('根节点', this[ROOT]); let arr = [];
let root = this[ROOT];
if (!root) return; queue.enqueue(root); while (queue.size()) {
let queueFirst = queue.dequeue();
arr.push(queueFirst.key);
queueFirst.lchild && queue.enqueue(queueFirst.lchild);
queueFirst.rchild && queue.enqueue(queueFirst.rchild);
} return arr;
}

二叉树遍历算法的改进

上面介绍的二傻叔的深度优先遍历算法都是用递归函数实现的,这是很低效的,原因就在于系统帮你调用了一个栈并做了诸如保护现场和恢复现场等复杂的操作,才使得遍历可以用非常简洁的代码实现。

有的人可能会想到,关于二叉树深度优先遍历算法的非递归实现和递归实现,一个是用户自己定义栈,一个是系统栈,为什么用户自己定义的栈要比系统栈执行高效?
一个较为通俗的解释是:递归函数所申请的系统栈,是一个所有递归函数都通用的栈,对于二叉树深度优先遍历算法,系统栈除了记录访问过的节点信息之外,还有其他信息需要记录,以实现函数的递归调用,用户自己定义的栈仅保存了遍历所需的节点信息,是对遍历算法的一个针对性的设计,对于遍历算法来说,显然要比递归函数通用的系统栈更高,也就是一般情况下,专业的要比通用的要好一些。

然而在实际应用中不是这样的,如尾递归在很多机器上都会被优化为循环,因此递归函数不一定就比非递归函数执行效率低。

栈数据结构,满足后进先出的规则用来辅佐我们遍历

// 栈结构 用来辅助非递归遍历
class Stack {
constructor() {
this.items = [];
}
push(data) {
this.items.push(data);
}
pop() {
return this.items.pop();
}
peek() {
return this.items[this.items.length - 1];
}
size() {
return this.items.length;
}
}

非递归的先序遍历

preOrderTraversal() {
console.log('先序遍历');
let root = this[ROOT];
// 初始化辅助遍历存储的栈
let stack = new Stack(); let arr = []; // 用于存储先序遍历的顺序 stack.push(root); // 如果栈不为空 则一直走
while (stack.size()) {
let stackTop = stack.pop(); // 访问栈顶元素
arr.push(stackTop.key);
stackTop.rchild && stack.push(stackTop.rchild);
stackTop.lchild && stack.push(stackTop.lchild);
} return arr;
}

非递归的中序排序

// 中序遍历二叉树
inorderTraversal() {
// 初始化用于辅助排序的栈
let stack = new Stack; let p = null; // 用于指向当前遍历到的元素
let arr = []; // 用户记录排序的顺序
p = this[ROOT]; while (stack.size() || p !== null) {
while (p !== null) {
stack.push(p);
p = p.lchild;
} // 如果栈不为空 出栈
if (stack.size()) {
p = stack.pop();
arr.push(p.key);
p = p.rchild;
}
}
return arr;
}

非递归的后序排序

// 中序遍历二叉树
inorderTraversal() {
// 初始化用于辅助排序的栈
let stack = new Stack; let p = null; // 用于指向当前遍历到的元素
let arr = []; // 用户记录排序的顺序
p = this[ROOT]; while (stack.size() || p !== null) {
while (p !== null) {
stack.push(p);
p = p.lchild;
} // 如果栈不为空 出栈
if (stack.size()) {
p = stack.pop();
arr.push(p.key);
p = p.rchild;
}
}
return arr;
}

想一下,如果我们的插入顺序第一个数非常大,然后后面的数字都是越来越小的会有什么问题产生呢?

下一篇文章讲述这种问题的一个解决方案,平衡二叉树。

javascript实现二叉搜索树的更多相关文章

  1. 《剑指offer》— JavaScript(26)二叉搜索树与双向链表

    二叉搜索树与双向链表 题目描述 输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表.要求不能创建任何新的结点,只能调整树中结点指针的指向. 思路 递归思想:把大问题转换为若干小问题: 由于Ja ...

  2. 【JavaScript】Leetcode每日一题-二叉搜索树的范围和

    [JavaScript]Leetcode每日一题-二叉搜索树的范围和 [题目描述] 给定二叉搜索树的根结点 root,返回值位于范围 [low, high] 之间的所有结点的值的和. 示例1: 输入: ...

  3. javascript数据结构——写一个二叉搜索树

    二叉搜索树就是左侧子节点值比根节点值小,右侧子节点值比根节点值大的二叉树. 照着书敲了一遍. function BinarySearchTree(){ var Node = function(key) ...

  4. 《剑指offer》— JavaScript(23)二叉搜索树的后序遍历序列

    二叉搜索树的后序遍历序列 题目描述 输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历的结果.如果是则输出Yes,否则输出No.假设输入的数组的任意两个数字都互不相同. 相关知识 二叉查找树(B ...

  5. 数据结构-二叉搜索树的js实现

    一.树的相关概念 1.基本概念 子树 一个子树由一个节点和它的后代构成. 节点的度 节点所拥有的子树的个数. 树的度 树中各节点度的最大值 节点的深度 节点的深度等于祖先节点的数量 树的高度 树的高度 ...

  6. js 二叉搜索树

    二叉搜索树:顾名思义,树上每个节点最多只有二根分叉:而且左分叉节点的值 < 右分叉节点的值 . 特点:插入节点.找最大/最小节点.节点值排序 非常方便 1 2 3 4 5 6 7 8 9 10 ...

  7. 数据结构☞二叉搜索树BST

    二叉查找树(Binary Search Tree),(又:二叉搜索树,二叉排序树)它可以是一棵空树,也可以是具有下列性质的二叉树: 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值: 若它 ...

  8. [数据结构]——二叉树(Binary Tree)、二叉搜索树(Binary Search Tree)及其衍生算法

    二叉树(Binary Tree)是最简单的树形数据结构,然而却十分精妙.其衍生出各种算法,以致于占据了数据结构的半壁江山.STL中大名顶顶的关联容器--集合(set).映射(map)便是使用二叉树实现 ...

  9. [LeetCode] Serialize and Deserialize BST 二叉搜索树的序列化和去序列化

    Serialization is the process of converting a data structure or object into a sequence of bits so tha ...

随机推荐

  1. Flink 源码解析 —— 如何获取 JobGraph?

    JobGraph https://t.zsxq.com/naaMf6y 博客 1.Flink 从0到1学习 -- Apache Flink 介绍 2.Flink 从0到1学习 -- Mac 上搭建 F ...

  2. 使用idea在linux上启动springboot项目

    springboot项目启动方式 1.改成war包放到tomcat上,网上方法很多不再介绍. 2.直接用jar包启动,比较方便,不需要修改项目文件,推荐使用jar包起 将项目和package打成jar ...

  3. ES6中比较实用的几个特性

    1.Default Parameters(默认参数) in ES6 es6之前,定义默认参数的方法是在一个方法内部定义 var link = function (height, color, url) ...

  4. DNS解析综合学习案例

    DNS解析综合学习案例 #图右侧为做题前环境配置 #命令为红色 #命令加载内容为绿色 #vi编辑内容为蓝色 1.用户需把/dev/myvg/mylv逻辑卷以支持磁盘配额的方式挂载到网页目录下 [roo ...

  5. 深入剖析PHP7内核源码(一)- PHP架构与生命周期

    PHP7 为什么这么快? 全新的zval 更节约的空间,栈上分配内存 zend_string 存储字符串的Hash值,数组查询的时候不需要进行Hash计算 在HashTable桶内直接存数据,减少了内 ...

  6. 11.源码分析---SOFARPC数据透传是实现的?

    先把栗子放上,让大家方便测试用: Service端 public static void main(String[] args) { ServerConfig serverConfig = new S ...

  7. ts 学习笔记 - 泛型

    目录 泛型 举个栗子 泛型约束 多个参数时也可以在泛型约束中使用类型参数 泛型接口 泛型类 泛型参数的默认类型 泛型 泛型(Generics)是指在定义函数.接口或者类的时候, 不预先指定其类型,而是 ...

  8. 90001PS相关操作

    第一章   PS基础操作 1.1 PS界面介绍 (1)界面包含:菜单栏.状态样式栏.工具栏.绘图区域.工作区. (2)布局可以在左上角进行切换,区分主要为工作区的侧重点不同. (3)布局想恢复可在窗口 ...

  9. 三维动画形变算法(Linear rotation-invariant coordinates和As-Rigid-As-Possible)

    在三维网格形变算法中,个人比较喜欢下面两个算法,算法的效果都比较不错, 不同的是文章[Lipman et al. 2005]算法对控制点平移不太敏感.下面分别介绍这两个算法: 文章[Lipman et ...

  10. go-web程序的热更新

    前言: 一直编译累死人啊,该偷懒就得偷懒 当使用go开发web程序时,修改点代码就得编译,虽然编译速度很快,但是也累啊,想起java的spring-boot有热更新插件, php根本都不需要重启,go ...