标签:
前面介绍了二叉查找树(Binary Search Tree),他对于大多数情况下的查找和插入在效率上来说是没有问题的,但是他在最差的情况下效率比较低。本文及后面文章介绍的平衡查找树的数据结构能够保证在最差的情况下也能达到lgN的效率,要实现这一目标我们需要保证树在插入完成之后始终保持平衡状态,这就是平衡查找树(Balanced Search Tree)。在一棵具有N 个节点的树中,我们希望该树的高度能够维持在lgN左右,这样我们就能保证只需要lgN次比较操作就可以查找到想要的值。不幸的是,每次插入元素之后维持树的平衡状态太昂贵。所以这里会介绍一些新的数据结构来保证在最坏的情况下插入和查找效率都能保证在对数的时间复杂度内完成。本文首先介绍2-3查找树(2-3 Search Tree),后面会在此基础上介绍红黑树和B树。
和二叉树不一样,2-3树运行每个节点保存1个或者两个的值。对于普通的2节点(2-node),他保存1个key和左右两个自己点。对应3节点(3-node),保存两个Key,2-3查找树的定义如下:
1. 要么为空,要么:
2. 对于2节点,该节点保存一个key及对应value,以及两个指向左右节点的节点,左节点也是一个2-3节点,所有的值都比key有效,有节点也是一个2-3节点,所有的值比key要大。
3. 对于3节点,该节点保存两个key及对应value,以及三个指向左中右的节点。左节点也是一个2-3节点,所有的值均比两个key中的最小的key还要小;中间节点也是一个2-3节点,中间节点的key值在两个跟节点key值之间;右节点也是一个2-3节点,节点的所有key值比两个key中的最大的key还要大。
如果中序遍历2-3查找树,就可以得到排好序的序列。在一个完全平衡的2-3查找树中,根节点到每一个为空节点的距离都相同。
在进行2-3树的平衡之前,我们先假设已经处于平衡状态,我们先看基本的查找操作。
2-3树的查找和二叉查找树类似,要确定一个树是否属于2-3树,我们首先和其跟节点进行比较,如果相等,则查找成功;否则根据比较的条件,在其左中右子树中递归查找,如果找到的节点为空,则未找到,否则返回。查找过程如下图:
往2-3树中插入元素和往二叉查找树中插入元素一样,首先要进行查找,然后将节点挂到未找到的节点上。2-3树之所以能够保证在最差的情况下的效率的原因在于其插入之后仍然能够保持平衡状态。如果查找后未找到的节点是一个2-node节点,那么很容易,我们只需要将新的元素放到这个2-node节点里面使其变成一个3-node节点即可。但是如果查找的节点结束于一个3-node节点,那么可能有点麻烦。
往一个3-node节点插入一个新的节点可能会遇到很多种不同的情况,下面首先从一个最简单的只包含一个3-node节点的树开始讨论。
只包含一个3-node节点
如上图,假设2-3树只包含一个3-node节点,这个节点有两个key,没有空间来插入第三个key了,最自然的方式是我们假设这个节点能存放三个元素,暂时使其变成一个4-node节点,同时他包含四个子节点。然后,我们将这个4-node节点的中间元素提升,左边的节点作为其左节点,右边的元素作为其右节点。插入完成,变为平衡2-3查找树,树的高度从0变为1。
节点是3-node,父节点是2-node
和第一种情况一样,我们也可以将新的元素插入到3-node节点中,使其成为一个临时的4-node节点,然后,将该节点中的中间元素提升到父节点即2-node节点中,使其父节点成为一个3-node节点,然后将左右节点分别挂在这个3-node节点的恰当位置。操作如下图:
节点是3-node,父节点也是3-node
当我们插入的节点是3-node的时候,我们将该节点拆分,中间元素提升至父节点,但是此时父节点是一个3-node节点,插入之后,父节点变成了4-node节点,然后继续将中间元素提升至其父节点,直至遇到一个父节点是2-node节点,然后将其变为3-node,不需要继续进行拆分。
根节点分裂
当根节点到字节点都是3-node节点的时候,这是如果我们要在字节点插入新的元素的时候,会一直查分到跟节点,在最后一步的时候,跟节点变成了一个4-node节点,这个时候,就需要将跟节点查分为两个2-node节点,树的高度加1,这个操作过程如下:
本地转换
将一个4-node拆分为2-3node涉及到6种可能的操作。这4-node可能在跟节点,也可能是2-node的左子节点或者右子节点。或者是一个3-node的左,中,右子节点。所有的这些改变都是本地的,不需要检查或者修改其他部分的节点。所以只需要常数次操作即可完成2-3树的平衡。
性质
这些本地操作保持了2-3树的平衡。对于4-node节点变形为2-3节点,变形前后树的高度没有发生变化。只有当跟节点是4-node节点,变形后树的高度才加一。如下图所示:
完全平衡的2-3查找树如下图,每个根节点到叶子节点的距离是相同的:
2-3树的查找效率与树的高度是息息相关的。
距离来说,对于1百万个节点的2-3树,树的高度为12-20之间,对于10亿个节点的2-3树,树的高度为18-30之间。
对于插入来说,只需要常数次操作即可完成,因为他只需要修改与该节点关联的节点即可,不需要检查其他节点,所以效率和查找类似。下面是2-3查找树的效率:
直接实现2-3树比较复杂,因为:
2-3查找树实现起来比较复杂,在某些情况插入后的平衡操作可能会使得效率降低。在2-3查找树基础上改进的红黑树不仅具有较高的效率,并且实现起来较2-3查找树简单。
但是2-3查找树作为一种比较重要的概念和思路对于后文要讲到的红黑树和B树非常重要。希望本文对您了解2-3查找树有所帮助
http://www.cnblogs.com/yangecnu/p/Introduce-Red-Black-Tree.html
前面一篇文章介绍了2-3查找树,可以看到,2-3查找树能保证在插入元素之后能保持树的平衡状态,最坏情况下即所有的子节点都是2-node,树的高度为lgN,从而保证了最坏情况下的时间复杂度。但是2-3树实现起来比较复杂,本文介绍一种简单实现2-3树的数据结构,即红黑树(Red-Black Tree)
红黑树的主要是像是对2-3查找树进行编码,尤其是对2-3查找树中的3-nodes节点添加额外的信息。红黑树中将节点之间的链接分为两种不同类型,红色链接,他用来链接两个2-nodes节点来表示一个3-nodes节点。黑色链接用来链接普通的2-3节点。特别的,使用红色链接的两个2-nodes来表示一个3-nodes节点,并且向左倾斜,即一个2-node是另一个2-node的左子节点。这种做法的好处是查找的时候不用做任何修改,和普通的二叉查找树相同。
根据以上描述,红黑树定义如下:
红黑树是一种具有红色和黑色链接的平衡查找树,同时满足:
下图可以看到红黑树其实是2-3树的另外一种表现形式:如果我们将红色的连线水平绘制,那么他链接的两个2-node节点就是2-3树中的一个3-node节点了。
我们可以在二叉查找树的每一个节点上增加一个新的表示颜色的标记。该标记指示该节点指向其父节点的颜色。
private const bool RED = true; private const bool BLACK = false; private Node root; class Node { public Node Left { get; set; } public Node Right { get; set; } public TKey Key { get; set; } public TValue Value { get; set; } public int Number { get; set; } public bool Color { get; set; } public Node(TKey key, TValue value,int number, bool color) { this.Key = key; this.Value = value; this.Number = number; this.Color = color; } } private bool IsRed(Node node) { if (node == null) return false; return node.Color == RED; }
红黑树是一种特殊的二叉查找树,他的查找方法也和二叉查找树一样,不需要做太多更改。
但是由于红黑树比一般的二叉查找树具有更好的平衡,所以查找起来更快。
//查找获取指定的值 public override TValue Get(TKey key) { return GetValue(root, key); } private TValue GetValue(Node node, TKey key) { if (node == null) return default(TValue); int cmp = key.CompareTo(node.Key); if (cmp == 0) return node.Value; else if (cmp > 0) return GetValue(node.Right, key); else return GetValue(node.Left, key); }
在介绍插入之前,我们先介绍如何让红黑树保持平衡,因为一般的,我们插入完成之后,需要对树进行平衡化操作以使其满足平衡化。
旋转又分为左旋和右旋。通常左旋操作用于将一个向右倾斜的红色链接旋转为向左链接。对比操作前后,可以看出,该操作实际上是将红线链接的两个节点中的一个较大的节点移动到根节点上。
左旋操作如下图:
//左旋转 private Node RotateLeft(Node h) { Node x = h.Right; //将x的左节点复制给h右节点 h.Right = x.Left; //将h复制给x右节点 x.Left = h; x.Color = h.Color; h.Color = RED; return x; }
左旋的动画效果如下:
右旋是左旋的逆操作,过程如下:
代码如下:
//右旋转 private Node RotateRight(Node h) { Node x = h.Left; h.Left = x.Right; x.Right = h; x.Color = h.Color; h.Color = RED; return x; }
右旋的动画效果如下:
颜色反转
当出现一个临时的4-node的时候,即一个节点的两个子节点均为红色,如下图:
这其实是个A,E,S 4-node连接,我们需要将E提升至父节点,操作方法很简单,就是把E对子节点的连线设置为黑色,自己的颜色设置为红色。
有了以上基本操作方法之后,我们现在对应之前对2-3树的平衡操作来对红黑树进行平衡操作,这两者是可以一一对应的,如下图:
现在来讨论各种情况:
Case 1 往一个2-node节点底部插入新的节点
先热身一下,首先我们看对于只有一个节点的红黑树,插入一个新的节点的操作:
这种情况很简单,只需要:
Case 2往一个3-node节点底部插入新的节点
先热身一下,假设我们往一个只有两个节点的树中插入元素,如下图,根据待插入元素与已有元素的大小,又可以分为如下三种情况:
有了以上基础,我们现在来总结一下往一个3-node节点底部插入新的节点的操作步骤,下面是一个典型的操作过程图:
可以看出,操作步骤如下:
经过上面的平衡化讨论,现在就来实现插入操作,一般地插入操作就是先执行标准的二叉查找树插入,然后再进行平衡化。对照2-3树,我们可以通过前面讨论的,左旋,右旋,FlipColor这三种操作来完成平衡化。
具体操作方式如下:
根据这一逻辑,我们就可以实现插入的Put方法了。
public override void Put(TKey key, TValue value) { root = Put(root, key, value); root.Color = BLACK; } private Node Put(Node h, TKey key, TValue value) { if (h == null) return new Node(key, value, 1, RED); int cmp = key.CompareTo(h.Key); if (cmp < 0) h.Left = Put(h.Left, key, value); else if (cmp > 0) h.Right = Put(h.Right, key, value); else h.Value = value; //平衡化操作 if (IsRed(h.Right) && !IsRed(h.Left)) h = RotateLeft(h); if (IsRed(h.Right) && IsRed(h.Left.Left)) h = RotateRight(h); if (IsRed(h.Left) && IsRed(h.Right)) h = FlipColor(h); h.Number = Size(h.Left) + Size(h.Right) + 1; return h; } private int Size(Node node) { if (node == null) return 0; return node.Number; }
对红黑树的分析其实就是对2-3查找树的分析,红黑树能够保证符号表的所有操作即使在最坏的情况下都能保证对数的时间复杂度,也就是树的高度。
在分析之前,为了更加直观,下面是以升序,降序和随机构建一颗红黑树的动画:
从上面三张动画效果中,可以很直观的看出,红黑树在各种情况下都能维护良好的平衡性,从而能够保证最差情况下的查找,插入效率。
下面来详细分析下红黑树的效率:
最坏的情况就是,红黑树中除了最左侧路径全部是由3-node节点组成,即红黑相间的路径长度是全黑路径长度的2倍。
下图是一个典型的红黑树,从中可以看到最长的路径(红黑相间的路径)是最短路径的2倍:
下图是红黑树在各种情况下的时间复杂度,可以看出红黑树是2-3查找树的一种实现,他能保证最坏情况下仍然具有对数的时间复杂度。
下图是红黑树各种操作的时间复杂度。
红黑树这种数据结构应用十分广泛,在多种编程语言中被用作符号表的实现,如:
下面以.NET中为例,通过Reflector工具,我们可以看到SortedDictionary的Add方法如下:
public void Add(T item) { if (this.root == null) { this.root = new Node<T>(item, false); this.count = 1; } else { Node<T> root = this.root; Node<T> node = null; Node<T> grandParent = null; Node<T> greatGrandParent = null; int num = 0; while (root != null) { num = this.comparer.Compare(item, root.Item); if (num == 0) { this.root.IsRed = false; ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_AddingDuplicate); } if (TreeSet<T>.Is4Node(root)) { TreeSet<T>.Split4Node(root); if (TreeSet<T>.IsRed(node)) { this.InsertionBalance(root, ref node, grandParent, greatGrandParent); } } greatGrandParent = grandParent; grandParent = node; node = root; root = (num < 0) ? root.Left : root.Right; } Node<T> current = new Node<T>(item); if (num > 0) { node.Right = current; } else { node.Left = current; } if (node.IsRed) { this.InsertionBalance(current, ref node, grandParent, greatGrandParent); } this.root.IsRed = false; this.count++; this.version++; } }
可以看到,内部实现也是一个红黑树,其操作方法和本文将的大同小异,感兴趣的话,您可以使用Reflector工具跟进去查看源代码。
前文讲解了自平衡查找树中的2-3查找树,这种数据结构在插入之后能够进行自平衡操作,从而保证了树的高度在一定的范围内进而能够保证最坏情况下的时间复杂度。但是2-3查找树实现起来比较困难,红黑树是2-3树的一种简单高效的实现,他巧妙地使用颜色标记来替代2-3树中比较难处理的3-node节点问题。红黑树是一种比较高效的平衡查找树,应用非常广泛,很多编程语言的内部实现都或多或少的采用了红黑树。
希望本文对您了解红黑树有所帮助,下文将介绍在文件系统以及数据库系统中应用非常广泛的另外一种平衡树结构:B树。
http://www.cnblogs.com/yangecnu/p/Introduce-B-Tree-and-B-Plus-Tree.html
http://www.cnblogs.com/yangecnu/p/Introduce-B-Tree-and-B-Plus-Tree.html
前面讲解了平衡查找树中的2-3树以及其实现红黑树。2-3树种,一个节点最多有2个key,而红黑树则使用染色的方式来标识这两个key。
维基百科对B树的定义为“在计算机科学中,B树(B-tree)是一种树状数据结构,它能够存储数据、对其进行排序并允许以O(log n)的时间复杂度运行进行查找、顺序读取、插入和删除的数据结构。B树,概括来说是一个节点可以拥有多于2个子节点的二叉查找树。与自平衡二叉查找树不同,B-树为系统最优化大块数据的读和写操作。B-tree算法减少定位记录时所经历的中间过程,从而加快存取速度。普遍运用在数据库和文件系统。”
B 树可以看作是对2-3查找树的一种扩展,即他允许每个节点有M-1个子节点。
下图是一个M=4 阶的B树:
可以看到B树是2-3树的一种扩展,他允许一个节点有多于2个的元素。
B树的插入及平衡化操作和2-3树很相似,这里就不介绍了。下面是往B树中依次插入
6 10 4 14 5 11 15 3 2 12 1 7 8 8 6 3 6 21 5 15 15 6 32 23 45 65 7 8 6 5 4
的演示动画:
B+树是对B树的一种变形树,它与B树的差异在于:
如下图,是一个B+树:
下图是B+树的插入动画:
B和B+树的区别在于,B+树的非叶子结点只包含导航信息,不包含实际的值,所有的叶子结点和相连的节点使用链表相连,便于区间查找和遍历。
B+ 树的优点在于:
但是B树也有优点,其优点在于,由于B树的每一个节点都包含key和value,因此经常访问的元素可能离根节点更近,因此访问也更迅速。下面是B 树和B+树的区别图:
对B树和B+树的分析和对前面讲解的2-3树的分析类似,
对于一颗节点为N度为M的子树,查找和插入需要logM-1N ~ logM/2N次比较。这个很好证明,对于度为M的B树,每一个节点的子节点个数为M/2 到 M-1之间,所以树的高度在logM-1N至logM/2N之间。
这种效率是很高的,对于N=62*1000000000个节点,如果度为1024,则logM/2N <=4,即在620亿个元素中,如果这棵树的度为1024,则只需要小于4次即可定位到该节点,然后再采用二分查找即可找到要找的值。
B树和B+广泛应用于文件存储系统以及数据库系统中,在讲解应用之前,我们看一下常见的存储结构:
我们计算机的主存基本都是随机访问存储器(Random-Access Memory,RAM),他分为两类:静态随机访问存储器(SRAM)和动态随机访问存储器(DRAM)。SRAM比DRAM快,但是也贵的多,一般作为CPU的高速缓存,DRAM通常作为内存。这类存储器他们的结构和存储原理比较复杂,基本是使用电信号来保存信息的,不存在机器操作,所以访问速度非常快,具体的访问原理可以查看CSAPP,另外,他们是易失的,即如果断电,保存DRAM和SRAM保存的信息就会丢失。
我们使用的更多的是使用磁盘,磁盘能够保存大量的数据,从GB一直到TB级,但是 他的读取速度比较慢,因为涉及到机器操作,读取速度为毫秒级,从DRAM读速度比从磁盘度快10万倍,从SRAM读速度比从磁盘读快100万倍。下面来看下磁盘的结构:
如上图,磁盘由盘片构成,每个盘片有两面,又称为盘面(Surface),这些盘面覆盖有磁性材料。盘片中央有一个可以旋转的主轴(spindle),他使得盘片以固定的旋转速率旋转,通常是5400转每分钟(Revolution Per Minute,RPM)或者是7200RPM。磁盘包含一个多多个这样的盘片并封装在一个密封的容器内。上图左,展示了一个典型的磁盘表面结构。每个表面是由一组成为磁道(track)的同心圆组成的,每个磁道被划分为了一组扇区(sector).每个扇区包含相等数量的数据位,通常是(512)子节。扇区之间由一些间隔(gap)隔开,不存储数据。
以上是磁盘的物理结构,现在来看下磁盘的读写操作:
如上图,磁盘用读/写头来读写存储在磁性表面的位,而读写头连接到一个传动臂的一端。通过沿着半径轴前后移动传动臂,驱动器可以将读写头定位到任何磁道上,这称之为寻道操作。一旦定位到磁道后,盘片转动,磁道上的每个位经过磁头时,读写磁头就可以感知到位的值,也可以修改值。对磁盘的访问时间分为 寻道时间,旋转时间,以及传送时间。
由于存储介质的特性,磁盘本身存取就比主存慢很多,再加上机械运动耗费,因此为了提高效率,要尽量减少磁盘I/O,减少读写操作。为了达到这个目的,磁盘往往不是严格按需读取,而是每次都会预读,即使只需要一个字节,磁盘也会从这个位置开始,顺序向后读取一定长度的数据放入内存。这样做的理论依据是计算机科学中著名的局部性原理:
当一个数据被用到时,其附近的数据也通常会马上被使用。
程序运行期间所需要的数据通常比较集中。
由于磁盘顺序读取的效率很高(不需要寻道时间,只需很少的旋转时间),因此对于具有局部性的程序来说,预读可以提高I/O效率。
预读的长度一般为页(page)的整倍数。页是计算机管理存储器的逻辑块,硬件及操作系统往往将主存和磁盘存储区分割为连续的大小相等的块,每个存储块称为一页(在许多操作系统中,页得大小通常为4k),主存和磁盘以页为单位交换数据。当程序要读取的数据不在主存中时,会触发一个缺页异常,此时系统会向磁盘发出读盘信号,磁盘会找到数据的起始位置并向后连续读取一页或几页载入内存中,然后异常返回,程序继续运行。
文件系统及数据库系统的设计者利用了磁盘预读原理,将一个节点的大小设为等于一个页,这样每个节点只需要一次I/O就可以完全载入。为了达到这个目的,在实际实现B-Tree还需要使用如下技巧:
每次新建一个节点的同时,直接申请一个页的空间( 512或者1024),这样就保证一个节点物理上也存储在一个页里,加之计算机存储分配都是按页对齐的,就实现了一个node只需一次I/O。如,将B树的度M设置为1024,这样在前面的例子中,600亿个元素中只需要小于4次查找即可定位到某一存储位置。
同时在B+树中,内节点只存储导航用到的key,并不存储具体值,这样内节点个数较少,能够全部读取到主存中,外接点存储key及值,并且顺序排列,具有良好的空间局部性。所以B及B+树比较适合与文件系统的数据结构。下面是一颗B树,用来进行内容存储。
另外B/B+树也经常用做数据库的索引,这方面推荐您直接看张洋的MySQL索引背后的数据结构及算法原理 这篇文章,这篇文章对MySQL中的如何使用B+树进行索引有比较详细的介绍,推荐阅读。
在前面两篇文章介绍了平衡查找树中的2-3树,红黑树之后,本文介绍了文件系统和数据库系统中常用的B/B+ 树,他通过对每个节点存储个数的扩展,使得对连续的数据能够进行较快的定位和访问,能够有效减少查找时间,提高存储的空间局部性从而减少IO操作。他广泛用于文件系统及数据库中,如:
希望本文对您了解B/B+ 树有所帮助。
浅谈算法和数据结构: 七 二叉查找树 八 平衡查找树之2-3树 九 平衡查找树之红黑树 十 平衡查找树之B树
标签:
原文地址:http://www.cnblogs.com/virusolf/p/5434259.html