数据映射–平衡二叉有序树
上次我们提到了使用有序的数组来进行二分查找,从而提高映射查询的效率,使时间复杂度从O(n)降低到O(log2N).
本周让我来介绍一下二叉树。
一谈到二叉树,相信很多人一定会有一个疑问: 这玩意儿有什么用? (当然这么多人里面肯定包括大学时候的我- -)
其实,我个人觉得这并不怪我们,是教科书写的有点问题,开始的时候没有给到大家明确的学习意义,开始就去讲如何遍历,如何从树变森林,如何做树的前序中序后序遍历。但这样的学习会让整个过程很无聊,太容易让人放弃了。所以在今天,请允许我用另外的方式来重新讲解一下吧~
首先,明确一下意义,二叉树主要用来表示树形结构的数据,主要的应用场景是,实现数据映射,或者实现压缩算法(哈夫曼树)。
下面,让我们从二叉树的特性,来看看二叉树能做些什么。
1. 有一个ROOT节点
意味着每一次查询都需要从一个节点开始进行,与之相对的,如果是图类的数据结构,则起点是不固定的。
2. 一个节点有两个子节点。
与拥有多个子节点的树相比,两个子节点会让树变得更深,但好处我们在后面的二叉排序树时会看到。
3. 父节点都有一个到子节点的引用(指针)。有些时候,为了遍历方便,还需要一个从子节点到父节点的引用(指针)
有些时候,我们需要顺序的遍历一棵树,这时候如果有了双向指针,那么遍历就会变得更为方便。
一个纯粹的二叉树,除了能支持遍历以外,没有什么油水,下面让我们来看看二叉树的最主要用途—-映射 ,是如何利用平衡二叉排序树来实现的吧。
先从二叉排序树开始说起,所谓的二叉排序树,其实只是在二叉树上额外增加了一个条件,左边的子节点上的数据一定比父节点的小,而右边子节点一定比父节点的数据大。
我们还是用上周我们使用过的例子来做一下讲解:
给定有序结果集S={1对应a,2对应b,3对应c,4对应e,6对应f} ,让我们以这个映射关系来做例子,以便大家能够更快速的理解。
这样做的最大好处么~就是可以进行顺序遍历了。其他好处似乎是没有。比如以下这样的二叉树:
这就是一个非常极端的情况了。也就是没有左面的孩子,只有右面的孩子,这样的一棵树其实就退化成了链表,因为访问只能从root开始,所以除了能够提供顺序遍历之外,无法提供更多的功能了。
然而,如果我们再加上一个条件,那么二叉排序树就立刻能够麻雀变凤凰,成为我们的一种重要的实现映射的利器了。
这个条件,就是平衡,于是全称就变成了:平衡二叉排序树。我们来看看平衡的定义:
一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
也就是说,除了最底层的节点,树的左子节点和右子节点应该保证都有值。 则树就会平衡,并且树的高度是log2N。
嘿嘿,看到log2N,是不是有似曾相识的感觉?没错,就是二分查找的时间复杂度,或者用更容易理解的一个词的话,为了查找到指定的数据所需要遍历节点的次数。
为什么二分查找和树的高度在数值上如此的一致呢?这就是我们下面要重点分析的东西了。
首先来回忆一下我们在有序数组章节
(http://blog.sina.com.cn/s/blog_693f08470101mi2o.html)里面提到的二分查找算法的必要条件吧:
1. 数据能够按照某种条件进行排序 , 比如S={0,1,2,3,4,5,6,7,100,101,102} 就是排好序的数据,左面的数据一定小于右面的。
2. 可以通过某种方式,取出该数据集中任意子集的中间值。 比如对于S={0,1,2,3,4,5,6,7,100,101,102},取中值意味着应该取出下标为整数除法 (0 11)/2 = 5 的数字。 如果我们能够快速而直接的取出这个中间值,也就是能够快速的取出下标为5的位置所对应的数据,那么我们就能够进行二分查找。
在平衡二叉排序树中,上面的两个要求是能够被满足的。
首先,数据本身是有序的,左边的子节点内的数据,一定小于父节点内的数据,而右边节点内的数据,一定大于父节点内的数据。
其次,在平衡二叉树中,你会发现父节点永远是两个子节点的“中值”,因此可以利用这个中值非常快速的排除掉一半的数据。
因此,几乎可以认为,一颗平衡二叉树,也一样可以利用二分查找的方式快速的从整个数据集合上面快速的取到符合要求的结果。
那么,既然排序后数组和平衡二叉排序树都可以以O(log2N)的代价来快速的根据一个key定位到value.那么我为什么不用数组来做这件事,而要选择使用平衡二叉排序树呢?
这里就涉及到一个问题,排序数组有什么短板没有呢? 当然是有的,就是不支持更新。而平衡二叉树更新的代价则要小很多,原因也很简单,因为父节点和子节点之间使用了引用(指针)来进行的数据组织的,所以,需要插入新数据的时候,只需要调整指针就可以让树从新平衡并有序了。
当然,这种调整的方式有很多种,他们各有优势和劣势。不过目前因为不需要我们花功夫去实现这些数据结构了,所以只需要简单了解一下我觉得就可以了。
首先被提出来的平衡树的方式是AVL树,然后提出来的是Tree Heap树,最后目前在实践中最为高效的红黑树。
在Java中,目前的TreeMap就是使用了红黑树来实现的,各位感兴趣也可以去看一下他的代码,对这类二叉树的平衡算法,教科书上面教的已经很完美了,带伪码带原理,这里我就不展开了。
在文章的末尾,我们还是以我们定义的几个集合的评价标准来看看这平衡二叉排序树的技术特性
1. 是否支持范围查找
因为数据是有序的,所以理论上来说是能够支持范围查找的,但从细节来说,支持的方法却不是完全相同。
这里会用到大家在教科书上面学的,二叉树的遍历方法中的中序遍历方法,也即如果要顺序的访问数据,需要不断地重复 左子树->根节点->右子树的遍历方式,直到查询结束。
如果我们只存了从父节点到子节点的指针,那么在遍历过程中,我们就必须使用一个额外的栈来存放某个子节点的父节点的引用,否则我们是无法从子节点回到父节点的。
而如果我们在每个节点都存放父节点到子节点的指针后,额外的再存一个从子节点到父节点的指针,那么我们就不需要用额外的栈来帮助我们进行遍历了,可以直接按照中序遍历的方式即可。
2. 集合是否能够随着数据的增长而自动扩展
费了那么半天劲,也就是为了能让数据增长变得更简单。 所以我们可以很高兴的告知大家,使用平衡排序二叉树,是可以支持数据自动扩展的,鼓掌~
让树能够在保持有序的前提下尽可能平衡的主要方式就是我们上面提到过的AVL,Tree heap,以及红黑树。
3. 读写性能如何
在内存中指针的跳转速度虽然不如使用数组快,不过也是很快的,基本上我们可以认为查询效率就是O(log2N)。
对于写来说,效率也是O(log2N)
4. 是否面向磁盘结构
回忆我们提到过的磁盘的特性,一次取出一块数据,能比较好的处理顺序读写,而对随机读写则不擅长。
对于内存来说,根据指针的要求在内存中进行跳跃,代价并不高,但如果这个操作在磁盘中进行,那么根据指针的要求的每一次跳跃,都是一次磁盘的随机读写,因为在取出节点来实际看看之前,我们无法预测这个父节点的子节点被放在磁盘的哪个位置上的。那么查询一次数据需要跳跃多少次呢?O(log2N-1)次。。跳跃的次数还是非常夸张的。
因此,平衡二叉查找树,不是个面向磁盘的结构。
5. 并行指标
不大适合并行操作,在进行结构调整的时候读取肯定是错误的,能够使用的并行读写的思路,主要是两类:
一个是锁分离
一个是copy on write
不过因为在目前的平衡二叉树实现中基本都需要做旋转操作,无法保证这个旋转的原子性,所以在主流的平衡二叉排序树中没见过能够很好地处理并发的。
6. 内存占用
相比较数组而言,链表的内存占用是固定的,每个节点上固定的两个到下层子节点的指针,以及到父节点的一个指针。 其他空间全部可以存放数据,空间消耗上,如果数组上的数据是全满的,那么链表没优势。不过如果要是自增的数组,也即每次都以2倍空间大小做自动扩展,那么链表的内存占用一般是优于自动扩展数组的各类实现的,因为自动扩展数组最坏情况下有一半的空间都是空着的。
建议继续学习:
- 数据结构定义中的中(大陆地区)美差异 (阅读:5619)
- 二叉树迭代器算法 (阅读:3177)
- STL笔记之二叉查找树 (阅读:3029)
- 数据结构之treap (阅读:2665)
- 数据映射–有序数组 (阅读:1793)
- redhat el5如何映射裸设备到逻辑卷 (阅读:1565)
- JavaScript中真正的哈希映射(译) (阅读:1458)
- 数据映射–映射概述 (阅读:1231)
- 对象到数字 ID 的映射 (阅读:939)
扫一扫订阅我的微信号:IT技术博客大学习
- 作者:沈询/Whisper 来源: 淘宝中间件团队博客
- 标签: 二叉树 平衡二叉有序树 映射
- 发布时间:2013-09-02 13:28:14
- [67] Oracle MTS模式下 进程地址与会话信
- [65] 如何拿下简短的域名
- [65] Go Reflect 性能
- [59] 图书馆的世界纪录
- [59] 【社会化设计】自我(self)部分――欢迎区
- [59] android 开发入门
- [58] IOS安全–浅谈关于IOS加固的几种方法
- [53] 视觉调整-设计师 vs. 逻辑
- [47] 读书笔记-壹百度:百度十年千倍的29条法则
- [47] 界面设计速成