你好,树

清晨就收到这个,既然是行文中出现的,不妨发上来,这样会有一种感觉,自己还是个搞技术的:]

前言

本来是想起个类似“xxxx树详解”之类的名字,觉得可能详解不了,所以还是小清新一点,篇幅一定不要很长。今天用屌丝的语言来行文,再次回复自由,屌丝的生活让我热情满血:p

当初学数据结构的时候(嗯,如果你是抱着看生态文章的态度进来,那只能说你被我坑了),树就让我恐惧,当我知道树也是一种图的时候,我几乎已经不想吃饭。

现在,这篇文章,不可能出现关于树性质的一些证明、推导和详细释义,只从应用分析的角度来看看树,今天看看B+树和红黑树。其中红黑树的伪代码本人手敲,是为了方便写注释,详细请参见普林斯顿大学的算法或算法导论等经典。

B+树

这里的B+树将和B*树不加区分,下图只画出逻辑结构,如果索引层的兄弟节点之间有互相指向的指针,你可以认为是B*树,比如innoDB的索引组织。

这幅图传达几个信息:

1.这是一棵4阶未满的B+树;

2.蓝色是非叶节点的索引记录,不保存实际数据记录。索引节点一般从磁盘拉到内存后,会缓存在内存中,根节点一般比缓存,第二层看情况而定;

3.红色代表指针或者说是地址.

叶子层,叶子之间的相互指针,你想到了什么,对,就是:

select * from table where key > 5 and key < 100.

范围查询,逻辑上是不是很爽!这也是B+树和B-树的一个不同之一(B是Balanced,“-”也不是减号)。

而且你看到了数据在页节点内部是有序的,你还要什么?

order by key asc/desc

4.绿色节点是叶子节点。

在这幅图的情况下,一次select * from table where key = 5的操作,有缓存地情况下大致会经历三次内存查找(找到根、第二层索引、叶子),用于在索引层找到正确的指向5的叶子节点。

无缓存的情况下会经历三次磁盘查找根->第二层->叶子块,用于将5所在的叶子块的数据读出。

内存查找中,没有磁盘I/O的顾虑,索引数据又是有序的,你想怎么查那还不够high?

磁盘查找,会经历寻道(柱面)、盘片旋转(到对应扇区)、电子化读的操作。

磁盘和内存速度差多少?极端情况下,一次磁盘操作,可以进行几万到几十万次内存访问。

这一点也就是为什么分布式、大并发系统的瓶颈大多最终会集中在数据持久化层。

5.block-oriented的文件系统,一般都有块儿这个概念,hadoop也不例外,block是hadoop的fs的原语,也是很多文件系统的原语。

一般叶子最小是一个块,块内记录逻辑有序且物理连续(磁盘上连续),但块间不连续,HDFS也是,从文件系统来讲,块位置的随机化是一种全局磁盘空间高效利用的表现。

但数据库的块总想着特殊点儿,总想着连续,这是有原因的,谁不想穿过索引,直接从第一块开始连续扫到最后(磁盘转啊转,这是磁盘的最爱),可惜很难,只能在块粒度上做文章,大了浪费空间,小了不连续几率更高,而且容易外部碎片(360大师帮你整理的那个磁盘优化)。

facebook经典的逻辑预读取,来自OLAP和逻辑全表备份的需求,但是表太大,几十亿甚至百亿条数据,反复的插、改、标记删除,longtime aged的一棵B+,会导致叶子块的物理连续性完全崩坏,和逻辑有序越来越远。全表一次,最最极端的情况下,有多少个叶子块,会有多少次磁盘寻道+旋转。有兴趣的同学请自行百度。

你说线性预读取?开玩笑,一个块才多大。命中率上不去的缓存比直接读磁盘还可怕!

B+树实际意义在哪里?为啥几乎所有的关系型数据库都有这种索引B-TREE,原因就是:

磁盘爱连续不爱随机,机械磁盘比SSD便宜,SSD还容易坏!你有钱另当别论,而且很多公司做数据挖掘和商业智能的,不在乎那点钱.

put them in SSD as more as you can!

平衡树-红黑树和AVL树

2-3树演化而来,如果对红黑树的代码比较晕,建议从AVL树和递归实现入手。

AVL树和红黑树都是平衡树,而且都是二叉搜索树(Binary Search Tree,俗称BST),平衡就是要压低树高,让平均查询路径更短,看看下图就知道为什么需要平衡:

上图展示了二叉搜索树在非随机情况下退化为一个顺序链表,二叉搜索树的查找退化为O(N).

红黑树比AVL树的插入(put、write)性能好,虽然不如AVL树那般严格平衡。

红黑树的高效秘密只有一个:

任何简单查询路径的长度不会大过最短路径的两倍。

为什么?

因为最短路径全是黑,红又不能连续出现,叶子又是黑,所以最长路径红黑交替着来,你算算。

平衡操作-旋转

AVL树和红黑树最让开始接触的人崩溃的就是rotation,好多人,好多博客、甚至好多书(非经典),左旋、右旋、左右双旋、右左双旋不分。

要分清,得有下面的认识:

什么是case(型)?

left-left(LL)、left-right(LR)、right-left(RL)、right-right(RR),这些是case.

什么是op(操作)?

left-rotation(左旋)、right-rotation(右旋),这些是操作。

  1. LL型明显右边缺了,得平衡,怎么办,右边缺补右边,右单旋;
  2. RR是个镜像,所以左单旋。
  3. LR型你怎么单旋都平衡不了,所以考虑从孩子入手,孩子右旋,然后自己再左旋。
  4. RL是个镜像,孩子左旋,自己右旋。

AVL树,处理RR型,左单旋(left-rotation):

AVL树,处理LR型,左-右双旋(left-right-rotation)

红黑树的性质

1.节点有颜色(一个布尔量),红或者黑;

2.根节点必为黑;

3.叶节点(空NIL)必为黑;

4.红节点的孩子必为黑;

5.对每个节点,从其到叶子的简单路径(一直往下),包含的黑节点相同.

第4、5条性质说明了任何简单查询路径的长度不会大过最短路径的两倍。

红黑树在旋转上和AVL没有区别(但有的实现中将颜色变换包含进去,比如普林斯顿 algorithm 4),先看红黑的左单旋代码,这里上nignx的:

//左单旋
static ngx_inline void
ngx_rbtree_left_rotate(ngx_rbtree_node_t **root, ngx_rbtree_node_t *sentinel,
    ngx_rbtree_node_t *node)
{
    ngx_rbtree_node_t  *temp;

    //暂存右孩子
    temp = node->right;

    //自己要占据右孩子的左孩子的位置,所以先把人家的左孩子拎过来,拎到右边,为什么?红黑树也是搜索树,有保序要求(左小右大)
    node->right = temp->left;

    //让原来右孩子的左孩子(你孙子)认个亲,毕竟你要把你孙子拎走
    if (temp->left != sentinel) {
        temp->left->parent = node;
    }

    //好了,拎了孙子,右孩子升级了成咱爹的儿了,和咱同辈?还有后续呢...
    temp->parent = node->parent;

    //一切手续都齐备了,现在自己的爹来认新儿子了
    if (node == *root) {
        *root = temp;
    } else if (node == node->parent->left) {
        node->parent->left = temp;

    } else {
        node->parent->right = temp;
    }

    //果然,爹不仅不要咱了,咱还成了爹的孙子
    temp->left = node;

    //成孙子了,那爹也换得了,换成咱平衡前的右孩子了...
    node->parent = temp;
}

看这样的代码,需要以图为相,参考上面的AVL树的旋转示意图。

多画画,看图写码,直接看代码能想象出一个树形的,是大师。

红黑树的插入

先看看伪代码:

/**
* 红黑树插入
*
* @param t 红黑树
* @param z 待插入节点
*/
RB-INSERT(t,z) {
    y = T.nil
    x = T.root 

    //和二叉搜索树一样,找到插入位置
    while(x != T.nil) {
        y = x
        if(z.key < x.key) {
            x = x.left
        }else{
            x = x.right
        }
    }

    z.p = y
    if(y == T.nil) {
        T.root = z
    }else if(y.right == z) {
        z.left = T.nil
    }else{
        z.right = T.nil
    }

    //新插入的节点着色为红色,这一点会贯穿始终,也叫invariant.
    //如果你经常看算法、jdk、java语言规范,对这个词应该不陌生,暂且叫不变式吧
    //比如java中的volatile关键字一条不变式就是保证其修饰的变量跨线程的内存可见性和一致性
    z.color = RED

    //调整平衡及着色,保证红黑树的性质
    RB-INSERT-FIXUP(T,z)
}

插入没什么好说的,和二叉搜索树类似,最后多了着色、哨兵处理,最重要的是这个函数RB-INSERT-FIXUP(T,z),下面给出图,不然根本不会看懂伪代码:

注:图来自算法导论D版,以后会换掉此图

情况1:z的叔叔节点y为红色,z上升,其父亲和叔叔变色;

情况2:z是棵右子树,上升,左旋,同时保证红黑树性质,进行对应变色

情况3:z在左旋后成了左孩子,因为一直要保证z是红色,所以其父变黑,爷爷变红,出现不平衡,所以右旋平衡,最终形成图(d),满足红黑树的性质。

RB-INSERT-FIXUP(T,z)函数伪代码,结合着图看效果更佳:

/**
* 红黑树调整,保证其性质
*
* @param t 红黑树
* @param z 待插入节点
*/
RB-INSERT-FIXUP(T,z) {

    //红黑树性质,红节点的孩子不能是红节点
    //所以调整到节点z的父亲是黑就ok
    while(z.p.color == RED) {
        //z的父节点是棵左子树
        if(z.p == z.p.p.left) {
            //考察z的叔叔节点,这里是y
            y = z.p.p.right
            //叔叔是红色
            if(y.color == RED) {
                //z变上升到其爷爷节点,并保证其为红色
                //同时z原来的父亲和叔叔变为黑色
                z.p.color = BLACK
                y.color = BLACK
                z.p.p.color = RED
                z = z.p.p
            }else if(z == z.p.right) {
                //z是右子树,上升一层
                z = z.p

                /* 这里虽然不是先对孩子右旋再自己左旋,
                而是自己先左旋,孩子右旋,同样可满足平衡 */

                //对z所代表的子树进行左线
                LEFT-ROTATE(T,z)

                //左旋后的z成了左孩子,所以要将其新父亲染黑
                z.p.color = BLACK
                //保证红黑树的性质,其爷爷必为红色
                z.p.p.color = RED

                RIGHT-ROTATE(T,z.p.p)
            }
        }else{

            //镜像操作,也就是z的父亲是棵右子树
        }

        //红黑树性质,根节点必为黑色
        T.root.color = BLACK
    }
}

红黑树的删除

本篇已经很长了,留个坑下次填上。

// todo: this is a 坑.

平衡树的应用价值

1.不像B、B+、B*树,平衡树基本都是针对内存的数据结构,目的也很简单,压低树高,保持平衡,减少平均查找长度,同时继承二叉搜索树对数级的操作效率(有序可二分)。

2.nginx采用红黑树(rbtree)管理定时器(timer)和缓存,具体可参考其源码。

3.linux内核中也实现了一个rbtree,用于virtual memory area(VMA)的映射管理,就是包含在vm_area_struct,比如将一个vm_area_struct插入红黑树:

static inline void vma_rb_insert(struct vm_area_struct *vma,
                 struct rb_root *root)
{
    /* All rb_subtree_gap values must be consistent prior to insertion */
    validate_mm_rb(root, NULL);

    rb_insert_augmented(&vma->vm_rb, root, &vma_gap_callbacks);
}

而具体怎么和rbtree联系的?就是vm_area_struct

/*
 * This struct defines a memory VMM memory area. There is one of these
 * per VM-area/task.  A VM area is any part of the process virtual memory
 * space that has a special rule for the page-fault handlers (ie a shared
 * library, the executable area etc).
 */
struct vm_area_struct {
    /* The first cache line has the info for VMA tree walking. */

    unsigned long vm_start;     /* Our start address within vm_mm. */
    unsigned long vm_end;       /* The first byte after our end address
                       within vm_mm. */

    /* linked list of VM areas per task, sorted by address */
    struct vm_area_struct *vm_next, *vm_prev;

    struct rb_node vm_rb;

    /*
     * Largest free memory gap in bytes to the left of this VMA.
     * Either between this VMA and vma->vm_prev, or between one of the
     * VMAs below us in the VMA rbtree and its ->vm_prev. This helps
     * get_unmapped_area find a free area of the right size.
     */
    unsigned long rb_subtree_gap;

    /* Second cache line starts here. */

    struct mm_struct *vm_mm;    /* The address space we belong to. */
    pgprot_t vm_page_prot;      /* Access permissions of this VMA. */
    unsigned long vm_flags;     /* Flags, see mm.h. */

    /*
     * For areas with an address space and backing store,
     * linkage into the address_space->i_mmap interval tree, or
     * linkage of vma in the address_space->i_mmap_nonlinear list.
     */
    union {
        struct {
            struct rb_node rb;
            unsigned long rb_subtree_last;
        } linear;
        struct list_head nonlinear;
    } shared;

    /*
     * A file‘s MAP_PRIVATE vma can be in both i_mmap tree and anon_vma
     * list, after a COW of one of the file pages.  A MAP_SHARED vma
     * can only be in the i_mmap tree.  An anonymous MAP_PRIVATE, stack
     * or brk vma (with NULL file) can only be in an anon_vma list.
     */
    struct list_head anon_vma_chain; /* Serialized by mmap_sem &
                      * page_table_lock */
    struct anon_vma *anon_vma;  /* Serialized by page_table_lock */

    /* Function pointers to deal with this struct. */
    const struct vm_operations_struct *vm_ops;

    /* Information about our backing store: */
    unsigned long vm_pgoff;     /* Offset (within vm_file) in PAGE_SIZE
                       units, *not* PAGE_CACHE_SIZE */
    struct file * vm_file;      /* File we map to (can be NULL). */
    void * vm_private_data;     /* was vm_pte (shared mem) */

#ifndef CONFIG_MMU
    struct vm_region *vm_region;    /* NOMMU mapping region */
#endif
#ifdef CONFIG_NUMA
    struct mempolicy *vm_policy;    /* NUMA policy for the VMA */
#endif

    /* reserved for Red Hat */
    unsigned long rh_reserved1;
    unsigned long rh_reserved2;
    unsigned long rh_reserved3;
    unsigned long rh_reserved4;
};

总结

树是数据结构中的明珠(自发赞美),在数据处理中,是几乎除了基本的数组、链表、队列、栈之外应用最广泛的数据结构了,本篇只窥其一斑,其实什么大并发、高性能、高可用、一致性等等被人挂在嘴上不放的名词,几乎都是数据结构、线程模型、算法、通讯和软件工程等基础却犀利的东西支撑着的。

如果能在多种应用中借鉴并回归原理,仔细揣摩,会对分布式、并行/并发、同步、性能,有新的认知,这句话是写给自己的。

当然一个成功的系统或framework,离不开软件工程和一流的项目管理,以及程序员自身的编码素质,这个不懂,也就不妄言了。

希望对您有益。

时间: 2024-08-19 17:32:07

你好,树的相关文章

树链剖分-Hello!链剖-[NOIP2015]运输计划-[填坑]

This article is made by Jason-Cow.Welcome to reprint.But please post the writer's address. http://www.cnblogs.com/JasonCow/ [NOIP2015]运输计划    Hello!链剖.你好吗? 题意: 给出一棵n个节点的带权树,m对树上点对 现在允许删除一条边,(权值修改为0) 输出: 最小化的点对间最大距离 1.链剖 2.树上差分 3.二分 链剖我就不多说了,就是两dfs 注意

将委托持久化及利用表达式树从持久化库还原委托

在领域事件中,有时为了数据的一致性,需要先将事件持久化,然后在读取数据时还原并执行事件保证数据一致. 持久化委托时,我们需要持久化委托的类型.方法名称和方法参数类型. 如申明一个委托: public delegate void DomainEventHandler(object sender, BaseDomainEventArgs eventArgs); 定义一个事件: public static event DomainEventHandler HelloDomainEvent; 获取事件订

沉舟侧畔,病树前头;日月恒升,山高水长

在每个醒来的清晨相信天还是蓝的,在每个崩裂的时刻相信正义的聚集,在每个反转的时刻相信价值的坚定.你为着理想的坚定与努力,值得更美好的世界与之匹配. 北国风光仍然千里冰封:南方冬阳依旧若有若无. 我们已经辞别了2015.的确没有什么能够阻挡时间,这时间里所蕴含着万物的力量. 你好,2016.不得不说,越是郑重的告别,越在用力描绘理想的形状,以及那属于初心的殿堂. 一切并不都是那么难以言喻.一个大国的行程,其来有自. 又是一年冬往春来,花开花落.当十八届五中全会方略初定,全面小康开始冲刺,军队改革大

转自豆瓣,读了无数遍的文,写的真的很感人哎|||江直树视角之:那些河川留给地形的改变。

最近被日版刷屏,每集都追,顺带复习了台版恶吻.在我看来,台版是永恒的经典,瞿导几乎环环紧扣,每个角色的内心都抓的很到位.新日版也有不错的地方,最欣赏的是湘琴煮咖啡的两个镜头,交代了直树的心动.当然,不管哪个版本的成功,其实最终都是多田薰老师的成功.要谢谢她,跟我们分享的这样一个故事. 新日版的大概是因为集数限制,很多细节没有抓到.情节衔接有点怪.其实当初回来追台版只是不爽那句直树终于不是蜡像,湘琴终于不是白痴.在多田薰老师的设置里,直树不曾是蜡像,湘琴也从不是白痴.这点在我回来重看台版的时候更为

(转载)你好,C++(3)2.1 一个C++程序的自白

你好,C++(3)2.1 一个C++程序的自白 第2部分 与C++第一次亲密接触 在浏览了C++“三分天下”的世界版图之后,便对C++有了基本的了解,算是一只脚跨入了C++世界的大门.那么,怎样将我们的另外一只脚也跨入C++世界的大门呢?是该即刻开始编写C++程序?还是…… 正在我们犹豫的时候,便看到前面有一个人被一群满头问号的C++初学者围在当中.我们赶紧挤进去一看,噢,原来是一个C++程序正做自我介绍呢. 2.1  一个C++程序的自白 “大家好,欢迎来到奇妙的C++世界.我是C++世界的迎

oi再见,你好明天。

oi再见,你好明天.录Menci大佬的翻唱<模你抄>如下:屏幕在深夜微微发亮思想在那虚树路径上彷徨平面的向量交错生长织成 忧伤的网 剪枝剪去我们的疯狂SPFA告诉我前途在何方01背包装下了忧伤笑颜 洋溢脸庞 键盘微凉 鼠标微凉指尖流淌 代码千行凸包周长 直径多长一进考场 全都忘光你在OJ上提交了千百遍却依然不能卡进那时限双手敲尽代码也敲尽岁月只有我一人 写的题解凋零在OJ里面 tarjan陪伴强联通分量生成树完成后思路才闪光欧拉跑过的七桥古塘让你 心驰神往 队列进出图上的方向线段树区间修改求出

致我的2018 你好2019

就不置顶了--随便写写的东西就凑活一下吧-- 本人文笔较渣,不适者轻喷. 本文1800字左右,请务必保证入睡前食用. 其实我元旦就想发的,只是在准备PKUWC,没时间,现在补掉好了. 现在我一个人在机房.时间过得真快啊,一切都像做梦一样,一转眼就过去了.2018年活得跟我想的差不多吧,如果用一个词来概括2018年的话,那就是机遇. 记得去年这个时候我期末考刚考完.我无法忘记那个社会考完的早上,我和同学一起打球,一起玩狼人杀--那种感觉真好,没有升学的压力,也没有竞赛的氛围,大家就一起做题一起玩.

搞懂Mysql InnoDB B+树索引

一.InnoDB索引 InnoDB支持以下几种索引: B+树索引 全文索引 哈希索引 本文将着重介绍B+树索引.其他两个全文索引和哈希索引只是做简单介绍一笔带过. 哈希索引是自适应的,也就是说这个不能人为干预在一张表生成哈希索引,InnoDB会根据这张表的使用情况来自动生成. 全文索引是将存在数据库的整本书的任意内容信息查找出来的技术,InnoDB从1.2.x版本支持.每张表只能有一个全文检索的索引. B+树索引是传统意义上的索引,B+树索引并不能根据键值找到具体的行数据,B+树索引只能找到行数

JavaScript---Bom树的操作,内置方法和内置对象(window对象,location对象,navigator对象,history对象,screen对象)

JavaScript---Bom树的操作,内置方法和内置对象(window对象,location对象,navigator对象,history对象,screen对象) 一丶什么是BOM ???????Bom:Browser Object Model,浏览器对象模型.操作浏览器部分的功能的API(事件/函数). 结构图: ???分析:1.window对象是Bom的顶层对象.所有的对象都是从windom延伸出来的,称其为window子对象. ?????? 2.dom是bom的一部分 ?????? 3.