-
Notifications
You must be signed in to change notification settings - Fork 721
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
JavaScript 数据结构与算法之美 - 非线性表(树、堆) #37
Comments
平衡二叉查找树
|
怎么感觉 searchNode当中return searchNode(node.left, key); 参数传反了呢 |
@zyt-cloud 你看得很仔细,谢谢指出错误,已经修改正确。 |
如果把测试数据arr改为 var arr = [11, 7, 5, 3, 6, 9, 8, 10, 15]; 原因在于这一句:node = null; 尝试对一个复制后的对象重新赋值,这样是不会改变原对象的。 |
@biaochenxuying 貌似“先序遍历”和“中序遍历”的图颠倒了 |
1. 前言
非线性表(树、堆),可以说是前端程序员的内功,要知其然,知其所以然。
笔者写的 JavaScript 数据结构与算法之美 系列用的语言是 JavaScript ,旨在入门数据结构与算法和方便以后复习。
非线性表中的树、堆是干嘛用的 ?其数据结构是怎样的 ?
希望大家带着这两个问题阅读下文。
2. 树
树
的数据结构就像我们生活中的真实的树,只不过是倒过来的形状。术语定义
最长路径
所包含的边数。高度是从
下往上
度量,比如一个人的身高 180cm ,起点就是从 0 开始的。深度是从
上往下
度量,比如泳池的深度 180cm ,起点也是从 0 开始的。高度和深度是带有
度
字的,都是从 0 开始计数的。而层数的计算,是和我们平时的楼层的计算是一样的,最底下那层是第 1 层,是从 1 开始计数的,所以根节点位于第 1 层,其他子节点依次加 1。
二叉树分类
二叉树
最多只有
2 个子节点的树,这两个节点分别是左子节点和右子节点。如上图中的 1、 2、3。不过,二叉树并不要求每个节点都有两个子节点,有的节点只有左子节点,有的节点只有右子节点。以此类推,自己想四叉树、八叉树的结构图。
满二叉树
都有
左右两个子节点,这种二叉树叫做满二叉树。如上图中的 2。完全二叉树
左
排列,并且除了最后
一层,其他层的节点个数都要达到最大
,这种二叉树叫做完全二叉树。如上图的 3。完全二叉树与不是完全二叉树的区分比较难,所以对比下图看看。
堆
之前的文章 栈内存与堆内存 、浅拷贝与深拷贝 中有说到:JavaScript 中的引用类型(如对象、数组、函数等)是保存在堆内存中的对象,值大小不固定,栈内存中存放的该对象的访问地址指向堆内存中的对象,JavaScript 不允许直接访问堆内存中的位置,因此操作对象时,实际操作对象的引用。
那么
堆
到底是什么呢 ?其数据结构又是怎样的呢 ?堆其实是一种特殊的树。只要满足这两点,它就是一个堆。
完全二叉树:除了最后一层,其他层的节点个数都是满的,最后一层的节点都靠左排列。
也可以说:堆中每个节点的值都大于等于(或者小于等于)其左右子节点的值。这两种表述是等价的。
对于每个节点的值都大于等于子树中每个节点值的堆,我们叫作
大顶堆
。对于每个节点的值都小于等于子树中每个节点值的堆,我们叫作小顶堆
。其中图 1 和 图 2 是大顶堆,图 3 是小顶堆,图 4 不是堆。除此之外,从图中还可以看出来,对于同一组数据,我们可以构建多种不同形态的堆。
二叉查找树(Binary Search Tree)
较小
的值保存在左节点
中,较大
的值保存在右节点
中,叫二叉查找树,也叫二叉搜索树。二叉查找树是一种有序的树,所以支持快速查找、快速插入、删除一个数据。
下图中, 3 个都是二叉查找树,
平衡二叉查找树
从这个定义来看,完全二叉树、满二叉树其实都是平衡二叉树,但是非完全二叉树也有可能是平衡二叉树。
平衡二叉查找树中
平衡
的意思,其实就是让整棵树左右看起来比较对称
、比较平衡
,不要出现左子树很高、右子树很矮的情况。这样就能让整棵树的高度相对来说低一些,相应的插入、删除、查找等操作的效率高一些。平衡二叉查找树其实有很多,比如,Splay Tree(伸展树)、Treap(树堆)等,但是我们提到平衡二叉查找树,听到的基本都是红黑树。
红黑树(Red-Black Tree)
红黑树中的节点,一类被标记为黑色,一类被标记为红色。除此之外,一棵红黑树还需要满足这样几个要求:
下面两个都是红黑树。
存储
完全二叉树的存储
每个节点由 3 个字段,其中一个存储数据,另外两个是指向左右子节点的指针。
我们只要拎住根节点,就可以通过左右子节点的指针,把整棵树都串起来。
这种存储方式比较常用,大部分二叉树代码都是通过这种方式实现的。
用数组来存储,对于完全二叉树,如果节点 X 存储在数组中的下标为 i ,那么它的左子节点的存储下标为 2 * i ,右子节点的下标为 2 * i + 1,反过来,下标 i / 2 位置存储的就是该节点的父节点。
注意,根节点存储在下标为 1 的位置。完全二叉树用数组来存储是最省内存的方式。
二叉树的遍历
经典的方法有三种:前序遍历、中序遍历、后序遍历。其中,前、中、后序,表示的是节点与它的左右子树节点遍历访问的先后顺序。
前序遍历(根 => 左 => 右)
中序遍历(左 => 根 => 右)
后序遍历(左 => 右 => 根)
实际上,二叉树的前、中、后序遍历就是一个递归的过程。
时间复杂度:3 种遍历方式中,每个节点最多会被访问 2 次,跟节点的个数 n 成正比,所以时间复杂度是 O(n)。
实现二叉查找树
二叉查找树的特点是:相对较小的值保存在左节点中,较大的值保存在右节点中。
代码实现二叉查找树,方法有以下这些。
方法
遍历
先序遍历
方式遍历所有节点。中序遍历
方式遍历所有节点。后序遍历
方式遍历所有节点。具体代码
遍历树,将插入节点的键值与遍历到的节点键值比较,如果前者大于后者,继续递归遍历右子节点,反之,继续遍历左子节点,直到找到一个空的节点,在该位置插入。
在下图的树中插入健值为 6 的节点,过程如下:
在二叉搜索树里,不管是整个树还是其子树,最小值一定在树最左侧的最底层。
因此给定一颗树或其子树,只需要一直向左节点遍历到底就行了。
搜索最大值与搜索最小值类似,只是沿着树的右侧遍历。
搜索特定值的处理与插入值的处理类似。遍历树,将要搜索的值与遍历到的节点比较,如果前者大于后者,则递归遍历右侧子节点,反之,则递归遍历左侧子节点。
移除节点,首先要在树中查找到要移除的节点,再判断该节点是否有子节点、有一个子节点或者有两个子节点,最后分别处理。
第三种情况的处理过程,如下图所示。
当要删除的节点有两个子节点时,为了不破坏树的结构,删除后要替补上来的节点的键值大小必须在已删除节点的左、右子节点的键值之间,且替补上来的节点不应该有子节点,否则会产生一个节点有多个字节点的情况,因此,找右侧子树的最小值替换上来。
同理,找左侧子树的最大值替换上来也可以。
用先序遍历遍历下图所示的树,并打印节点键值。
输出结果:11 7 5 3 6 9 8 10 15 13 12 14 20 18 25。
遍历过程如图:
对下图的树做中序遍历,并输出各个节点的键值。
依次输出:3 5 6 7 8 9 10 11 12 13 14 15 18 20 25。
遍历过程如图:
可以看到,中序、先序、后序遍历的实现方式几乎一模一样,只是 {1}、{2}、{3} 行代码的执行顺序不同。
对下图的树进行后序遍历,并打印键值:3 6 5 8 10 9 7 12 14 13 18 25 20 15 11。
遍历过程如图:
完整代码请看文件 binary-search-tree.html
测试过程:
结果如下:
2. 最后
看到这里,你能解答文章的题目 非线性表中的树、堆是干嘛用的 ?其数据结构是怎样的 ?
如果不能,建议再回头仔细看看哦。
参考文章:
数据结构与算法之美
学习JavaScript数据结构与算法 — 树
喜欢就点个赞吧,听说点
在看
的都会很有钱。The text was updated successfully, but these errors were encountered: