对AVL树和红黑树的个人理解

萝らか妹 提交于 2020-04-08 10:48:10

都说编程算法很重要,可真没见几个.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是理顺旋转造成的暂时性不平衡,平衡旋转后肯定可以平衡。

image

就是平衡度这些正啊负啊,加1减1这些细节,有点挠人。其实明白原理,只要简单画图分析一下,细心一点,不难得出正确数据。

另一个要注意的是,局部平衡度调整结束后,可能要影响上级节点。对于插入的新节点,如果父节点平衡由±1变为0,不必调整。否则说明高度发生了变化,需要调整父节点,并依次递归。而平衡调整后,新的轴节点平衡度也必定为0,总高度没有变化,插入过程就结束。

删除的情况则相反,如果删除点节的父节点平衡变为±1,则无须调整。在平衡调整后,会降低轴节点高度,所以还需要向上递归调整。

 

红黑树的好像应用得比较广,SortDictionary类就用了红黑树算法,MSDN WebCast有讲解,在Wiki百科上介绍得也很详细,就不赘述,只补充一下个人理解的增删操作要点。

四个字简单概括:打红唱黑

这里也不插图了,语言简洁,印象就会深刻。

红黑树旋转除了分左右外,还有就是调换颜色的,和不换颜色的。调色旋转是交换轴节点,和旋转方向相反的子节点两色交换,插入和删除时,调色旋转后,树就实现了平衡。不换颜色的旋转,是为调色旋转做准备,当左右节点不同色时,变换位置。实际各个节点颜色都没有改变,只是看上去左右颜色互换了。

插入的新节点为红色,如果父节点也是红色,需要设法消除连续的红色节点,是为打红。

删除的子节点若为若为黑色节点,就要弥补增加这条路径上黑节点的损失,是为唱黑。

红黑树是一棵和谐之树,如果因为增删导致不和谐因素出现,如果在局部(追溯到新节点或被删节点的祖父)能解决,就旋转调整,如果不能,基层搞不掂,不稳定影响范围就要扩大,要逐级向上汇报解决,直至中央。

我想了很久,要详细说明步骤,可以打个比喻。

大家看一个树结构,一层一层,顶端只有一个,越向下节点越多,是不是更像一个金字塔。我们的人民教材上,常用金字塔比喻等级森严的社会制度,偶深受其束缚,不过偶因为玩战国游戏和看历史剧原因,对日本历史比较感兴趣,就用日本的武家社会做比喻。

image

武士中最大的叫将军,管着很多大名,大名是有城有地盘,有兵有旗号,有很多家臣为其服务。家臣又分许多等级,大名的地盘分一部分给家臣,这就是城主。每个家臣都可以有自己的家臣,只要你地盘的收入俸禄养得起。这个等级制度,非常森严,没有出身保证,就算功劳再大也爬不上去。因为除了将军,武士一定得效忠于某个主公的,大名也是将军的家臣。并且这种效忠是继承的,你的子子孙孙都要效忠主公的后代。

日本和中国情况不同,没有人敢喊出王候将相,宁有种乎。所以丰臣秀吉能从,最底层的普通武士做起,一步步做到关白(和将军平级,那时没设将军,相当于将军),成了不可思议的传奇。

当与你的功劳或能力所匹配的待遇,超过了主公所能给予,就可能发生改换门庭,甚至颠覆主公的事情。人们都说日本战国时代就是一个下克上的时代。

这段历史因为真得挺有意思,废话了很多,回到正题来。

我们把红黑树两种颜色节点,代表两个基本的武士类型,黑色代表安分守已,或碌碌之辈,没多大抱负。红色表示既有才能,又有野心。因为俗话说,江山易改本性难移,颜色可以染,但人性格不容易变,所以节点不是表示某一个人,而是这一家族,因为主从关系是继续的。每个武士家族都有家主,家主当然也是继承的,但父子性格南辕北辙是家常便饭。

插入操作时,好像一个新的武士出仕,没有背景,没有功勋。但他有能力,有抱负,就像丰臣秀吉那样。要是跟了个黑色类型的主公,虽然终究会不甘心居人之下,但却没条件“下克上”,因为主公对大主公(主公的主公)忠心耿耿,兢兢业业,你再有才也英雄无用武之地,只能待时而动了。

但要是主公是另一类型的,那新武士就有想法了,看看情形,主公待我如何呢?要是不受待见,那就来阴的,跟大主公报告主公谋反。反正主公忠心有限,把柄很多,只是大主公没那个洞察力。大主公看到一些莫须有的证据,谋反这事宁枉勿纵。于是新武士举报有功,主公反倒成了自己的家臣。昏庸的大主公很快就要付出代价,自己成了新武士的下一个目标。

要是被主公赏识,我就跟着主公混,还要掇主公造反。可是大主公其他家臣不会答应啊,要是其他家臣也一样有能力,那就热闹了,这一家从此两虎相争,不得安宁(此处再略去数万字)。大主公能力平庸,控制不住,最终酿成大乱,经过一番血雨腥风,大主公家出现了一位雄才伟略的家主,平定了内乱,原来两家家臣的强势家主全部被消灭,家主换成了忠心可靠的人。而大主公家的新家主,野心开始膨涨了。

而要是大主公其他家臣是碌碌之辈,那就好办了,主从两个一商量,义旗一举,发个檄文,说奉天命起兵救黎民百姓,然后势不可挡(此处略去数万字),造反成功!

成功后,原来大主公成了主公的家臣。为什么没被消灭掉,因为日本人很重视家名传承,战胜方保留败方家名,这是宽大仁义的表现。但既然败了,就要此家族中不和自己作对的人作家主。这个新家主,被迫接受了敌人安排的婚姻,忍辱负重,卧薪尝胆,等待重新崛起的时机(略去数十万字,不了解的可以看山冈庄八的《德川家康》)。而主公这一家,上位后,家风就堕落了。红色变成黑色:家主不再思进取,或者新家主成了只开法拉利的官二代,毕竟丰臣秀吉不是人人能当的。

关于删除操作,比插入操作还复杂一点,不过想像这个比喻太费脑子了,有时间再补上。

在写算法时,会发现很多操作是左右相反的,但是逻辑一样,可以采用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%左右,现实应用当然一般都是随机情况,所以红黑树得到了更广泛的应用。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!