都说编程算法很重要,可真没见几个.NET程序员研究算法的。这些日子非主流地研究了一些小算法,红黑树和AVL树算是其中复杂的了,但实际也就二三百行代码。悲催的是,网上根本找不到C#的理想版本(包括国外网站),寥寥几个,要么有错,要么非主流的实现方式。
所谓主流方式,就是用表二叉排序树节点TreeNode类的属性,除了左右子节点和值,还包括父节点。
/// <typeparam name="T">Type of the node.</typeparam> /// <typeparam name="K">Type of the node value.</typeparam> class TreeNode<T,K> where T:TreeNode<T,K> where K: IComparable<K> { public T LeftChild { get; set; } public T RightChild { get; set; } public T Parent { get; set; } public K Value { get; set; } }
不同二叉排序树间,为了保证查找性能,在插入和删除时,有不同的调整步骤。表示二叉排序树用抽象类Tree,我们可以把插入和删除节点做成内部虚方法,供公共方法调用(参数是要添加或删除的值)。
比如删除:
public bool Remove(int value) { T root = this.Root; while (root != null) { if (value == root.Value) { T node = root; if (root.LeftChild != null) node = FindMax(root.LeftChild); else if (root.RightChild != null) node = FindMin(root.RightChild); root.Value = node.Value; this.Remove(node); //子类的调整旋转操作. return true; } else if (value < root.Value) root = root.LeftChild; else root = root.RightChild; } return false; } protected abstract void Remove(T node);
下面再说AVL树,AVL树明显比红黑树逻辑简单得多,但应用得少,应该是增删性能差一点,增删时需要旋转的次数可能比较多。
它的逻辑虽然简单,但Wiki上介绍的很粗略,跟红黑树条目没法比,只介绍了旋转的类型,旋转后节点平衡度怎么变化,如何递归都没有讲。偶费了好大好大的劲才琢磨过来,现在想起来其实也不复杂。
添加节点时,节点左右子树高度差,即平衡度绝对值超过1,就要以此为轴,向高度小的一边旋转,这个旋转叫平衡旋转。平衡旋转前,和红黑树类似,要保证高度大的子节点,我叫它驱动节点,平衡度值,符号不能和轴节点相反,如果不满足旋转一次即可,这种旋转叫理顺旋转(名字都是我自己起的)。
在平衡旋转前,如果轴节点平衡度为2,驱动节点平衡度有0、1、2三种情况(如轴节点平衡度-2,则是0、-1、-2)。如图,当E的平衡度大于0时,2是理顺旋转造成的暂时性不平衡,平衡旋转后肯定可以平衡。
就是平衡度这些正啊负啊,加1减1这些细节,有点挠人。其实明白原理,只要简单画图分析一下,细心一点,不难得出正确数据。
另一个要注意的是,局部平衡度调整结束后,可能要影响上级节点。对于插入的新节点,如果父节点平衡由±1变为0,不必调整。否则说明高度发生了变化,需要调整父节点,并依次递归。而平衡调整后,新的轴节点平衡度也必定为0,总高度没有变化,插入过程就结束。
删除的情况则相反,如果删除点节的父节点平衡变为±1,则无须调整。在平衡调整后,会降低轴节点高度,所以还需要向上递归调整。
红黑树的好像应用得比较广,SortDictionary类就用了红黑树算法,MSDN WebCast有讲解,在Wiki百科上介绍得也很详细,就不赘述,只补充一下个人理解的增删操作要点。
四个字简单概括:打红唱黑。
这里也不插图了,语言简洁,印象就会深刻。
红黑树旋转除了分左右外,还有就是调换颜色的,和不换颜色的。调色旋转是交换轴节点,和旋转方向相反的子节点两色交换,插入和删除时,调色旋转后,树就实现了平衡。不换颜色的旋转,是为调色旋转做准备,当左右节点不同色时,变换位置。实际各个节点颜色都没有改变,只是看上去左右颜色互换了。
插入的新节点为红色,如果父节点也是红色,需要设法消除连续的红色节点,是为打红。
删除的子节点若为若为黑色节点,就要弥补增加这条路径上黑节点的损失,是为唱黑。
红黑树是一棵和谐之树,如果因为增删导致不和谐因素出现,如果在局部(追溯到新节点或被删节点的祖父)能解决,就旋转调整,如果不能,基层搞不掂,不稳定影响范围就要扩大,要逐级向上汇报解决,直至中央。
我想了很久,要详细说明步骤,可以打个比喻。
大家看一个树结构,一层一层,顶端只有一个,越向下节点越多,是不是更像一个金字塔。我们的人民教材上,常用金字塔比喻等级森严的社会制度,偶深受其束缚,不过偶因为玩战国游戏和看历史剧原因,对日本历史比较感兴趣,就用日本的武家社会做比喻。
武士中最大的叫将军,管着很多大名,大名是有城有地盘,有兵有旗号,有很多家臣为其服务。家臣又分许多等级,大名的地盘分一部分给家臣,这就是城主。每个家臣都可以有自己的家臣,只要你地盘的收入俸禄养得起。这个等级制度,非常森严,没有出身保证,就算功劳再大也爬不上去。因为除了将军,武士一定得效忠于某个主公的,大名也是将军的家臣。并且这种效忠是继承的,你的子子孙孙都要效忠主公的后代。
日本和中国情况不同,没有人敢喊出王候将相,宁有种乎。所以丰臣秀吉能从,最底层的普通武士做起,一步步做到关白(和将军平级,那时没设将军,相当于将军),成了不可思议的传奇。
当与你的功劳或能力所匹配的待遇,超过了主公所能给予,就可能发生改换门庭,甚至颠覆主公的事情。人们都说日本战国时代就是一个下克上的时代。
这段历史因为真得挺有意思,废话了很多,回到正题来。
我们把红黑树两种颜色节点,代表两个基本的武士类型,黑色代表安分守已,或碌碌之辈,没多大抱负。红色表示既有才能,又有野心。因为俗话说,江山易改本性难移,颜色可以染,但人性格不容易变,所以节点不是表示某一个人,而是这一家族,因为主从关系是继续的。每个武士家族都有家主,家主当然也是继承的,但父子性格南辕北辙是家常便饭。
插入操作时,好像一个新的武士出仕,没有背景,没有功勋。但他有能力,有抱负,就像丰臣秀吉那样。要是跟了个黑色类型的主公,虽然终究会不甘心居人之下,但却没条件“下克上”,因为主公对大主公(主公的主公)忠心耿耿,兢兢业业,你再有才也英雄无用武之地,只能待时而动了。
但要是主公是另一类型的,那新武士就有想法了,看看情形,主公待我如何呢?要是不受待见,那就来阴的,跟大主公报告主公谋反。反正主公忠心有限,把柄很多,只是大主公没那个洞察力。大主公看到一些莫须有的证据,谋反这事宁枉勿纵。于是新武士举报有功,主公反倒成了自己的家臣。昏庸的大主公很快就要付出代价,自己成了新武士的下一个目标。
要是被主公赏识,我就跟着主公混,还要掇主公造反。可是大主公其他家臣不会答应啊,要是其他家臣也一样有能力,那就热闹了,这一家从此两虎相争,不得安宁(此处再略去数万字)。大主公能力平庸,控制不住,最终酿成大乱,经过一番血雨腥风,大主公家出现了一位雄才伟略的家主,平定了内乱,原来两家家臣的强势家主全部被消灭,家主换成了忠心可靠的人。而大主公家的新家主,野心开始膨涨了。
而要是大主公其他家臣是碌碌之辈,那就好办了,主从两个一商量,义旗一举,发个檄文,说奉天命起兵救黎民百姓,然后势不可挡(此处略去数万字),造反成功!
成功后,原来大主公成了主公的家臣。为什么没被消灭掉,因为日本人很重视家名传承,战胜方保留败方家名,这是宽大仁义的表现。但既然败了,就要此家族中不和自己作对的人作家主。这个新家主,被迫接受了敌人安排的婚姻,忍辱负重,卧薪尝胆,等待重新崛起的时机(略去数十万字,不了解的可以看山冈庄八的《德川家康》)。而主公这一家,上位后,家风就堕落了。红色变成黑色:家主不再思进取,或者新家主成了只开法拉利的官二代,毕竟丰臣秀吉不是人人能当的。
关于删除操作,比插入操作还复杂一点,不过想像这个比喻太费脑子了,有时间再补上。
在写算法时,会发现很多操作是左右相反的,但是逻辑一样,可以采用T4模板最大程度地遵照DRY原则,像这样:
<# var direction = new string[]{"Left","Right"}; #> <# foreach(var item in direction){ #> /// <summary> /// Rotate the tree, which may contain two rotatations. And change the color. /// </summary> private void <#=item #>RotateForNewRed(RBTreeNode node, RBTreeNode parent, RBTreeNode grand) { if (node == parent.<#=item #>Child) { <#= item==direction[0]?direction[1]:direction[0] #>Rotate(node, parent); parent = node; } <#=item #>Rotate(parent, grand); ReverseColor(parent, grand); } <# } #>
接下来,做个测试吧,其实偶比较懒于做这种测试,宁可相信别人现成的结果,得到的结果也确实跟人家(参照abatei的文章,虽然实现方式不一样)差不多。AVL树在顺序插入和删除时有20%左右的性能优势,但随机性能反而落后15%左右,现实应用当然一般都是随机情况,所以红黑树得到了更广泛的应用。
来源:https://www.cnblogs.com/XmNotes/archive/2012/06/06/2538392.html