树(tree)的基本知识
一.定义
树是一种抽象数据类型,或是实作这种抽象数据类型的数据结构,用来模拟具有树状结构性质的数据集合。
二.特点
- 每个节点有零个或多个子节点;
- 没有父节点的节点称为根节点;
- 每一个非根节点有且只有一个父节点;
- 除了根节点外,每个子节点可以分为多个不相交的子树
三.存储结构
- 线性存储
/* 树节点的定义 /
#define MAX_TREE_SIZE 100
typedef struct
{
TElemType data;
int parent; / 父节点位置域 /
} PTNode;
typedef struct
{
PTNode nodes[MAX_TREE_SIZE];
int n; / 节点数 */
} PTree;
-
链式存储
/*树的孩子链表存储表示*/ typedef struct CTNode // 孩子节点 { int child; struct CTNode *next; } *ChildPtr; typedef struct { ElemType data; // 节点的数据元素 ChildPtr firstchild; // 孩子链表头指针 } CTBox; typedef struct { CTBox nodes[MAX_TREE_SIZE]; int n, r; // 节点数和根节点的位置 } CTree;
红黑树(Red-black tree)的基本知识
一.定义
红黑树是一种自平衡二叉查找树,典型的用途是实现关联数组,它是复杂的,但它的操作有着良好的最坏情况运行时间,并且在实践中是高效的O(log n)时间内做查找,插入和删除,这里的n是树中元素的数目。
NIL节点表示数据的结束,在wiki百科中其被当做叶子节点,画图时应该也体现出来。
二.特点
- 任意节点的左子树不空,则左子树上所有节点的值均小于它的根结点的值;
- 任意节点的右子树不空,则右子树上所有节点的值均大于它的根结点的值;
- 任意节点的左、右子树也分别为二叉查找树;
- 没有键值相等的节点;
- 节点是红色或黑色;
- 根是黑色, 所有叶子都是黑色;
- 每个红色节点必须有两个黑色的子节点;(从每个叶子到根的所有路径上不能有两个连续的红色节点。)
- 从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点;
通过节点颜色,限制了二叉树的高度。
操作比如插入、删除和查找某个值的最坏情况时间都要求与树的高度成比例,这个在高度上的理论上限允许红黑树在最坏情况下都是高效的,而不同于普通的二叉查找树。
三.抛出问题
- O(log n)的操作效率是如何得出的?为什么与高度成比例,而不是与节点数成比例?
一个由n个节点随机构成的二叉查找树的高度为(log n ).证明如下:
而时间复杂度是以某个基础数据操作的重复次数作为量度。红黑树的是二叉搜索树,左子树上所有节点的值均小于他的根节点的值,右子树上所有节点均大于根节点的值,左右子节树相对根节点按大小分布。如果把每次节点值的比较看成基础数据操作,那么最差的查找情况是一直查找到高度最大的根节点,那么查找的时间复杂度即与高度成正比,可表示成O(log n)。
如何生成红黑树
简单了解了红黑树的字面定义,下面动手感受下红黑树的相关操作。当你插入或者删除一个节点时,可能会破坏红黑树的性质,所以需要对树节点进行重新着色或者旋转,来保持红黑树的结构。首先看下二叉树的旋转。
一.旋转
- 左旋
假设pivot节点不为空,其右子树不为空,那么左旋即是:使pivot的右孩子Y为子树的根,pivot节点为子树根节点的左孩子,pivot左孩子、Y节点的右孩子不改变,Y节点左孩子变为pivot节点右孩子。
- 右旋
假设pivot节点不为空,其左子树不为空,那么右旋:使pivot的左孩子Y为子树的根,pivot节点为子树根节点的右孩子,pivot的右孩子、Y节点的左孩子不变,Y节点的右孩子变为pivot节点的左孩子。
任意节点的左子树不空,则左子树上所有节点的值均小于它的根结点的值
任意节点的右子树不空,则右子树上所有节点的值均大于它的根结点的值
实战演练之增加、删除节点时,如何保证红黑树的性质不被破坏。
二.增加节点
往一个空的红黑树中,依次插入数据:12 1 9 2 0 11 7 19 4
- 插入12
节点为根节点,所以为黑色,两个null节点为黑色节点。
- 插入 1
1小于12,所以是根节点的左孩子,如果为黑色,那么违反性质:从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点。所以新增节点为红色。
- 插入 9
按照二叉搜索树的逻辑,9小于12、大于1,应该是1节点的右孩子。但,新增的两个NIL节点已经使得12,1,9,NI这条路径的黑色节点至少为两个,而12,NIL这条路径的黑色节点只有两个。所以要对1节点进行左旋,9节点变为12节点的左孩子,发现问题还是存在。继续,对12节点进行右旋,9节点为根节点,1、12分别为9节点的左右孩子。尝试着色,9节点必须为黑色,而1,12节点可以为红色,也可以为黑色。
- 插入 2
2大于1,直接作为1的右孩子即可。2的增加必然会增加两个黑色NIL节点,所以每条路径至少有三个黑色节点。则9,12,NIL这条路径中,12节点应该变为黑色;9,1,NIL这条路径中,1应该变为黑色。9,1,2,NIL这条路径中,2为空色来使得黑色节点个数为三个。验证一下,满足从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点。则颜色调整完毕。
- 插入 0
0节点直接作为1节点的左孩子,保持跟2节点相同的颜色即可。左右子树依旧保持平衡。
-
插入 11
11节点作为12节点的左子树,颜色跟同一层的0,2节点保持一致即可。
- 插入 7
从二叉查找树的性质看,7节点作为2节点的右孩子即可。这时来分析着色问题,我们先看最短路径的黑色分布,9,12,NIL这条路径,有三个黑色节点,以此为参考,尝试改变9节点左子树的着色。目前最长的路径是9,1,2,7,NIL这条路径。保持三个黑色节点的话,9跟NIL已经为黑色节点,而红色节点又不能挨着,所以只能是1为红色节点,2为黑色节点,7为红色节点。那么9,1,0,NIIL这条路径,0就要为黑色节点。调整完毕。
- 插入 19
19节点作为12节点的右孩子,与左孩子保持一样的红色即可。
- 插入 4
4节点应该作为7节点的左子树,无论着什么颜色,以1节点为根节点的子树,都要破坏红黑性质。所以应该进行旋转。先以7为根节点进行一次右旋,再以2为根节点进行一次左旋。尝试着色即可。
思维误区:之前我把左右字数高度相差不能超过1加入到红黑树的性质中,这导致我的推理逻辑发生了错误。因为红黑树是用红黑着色来保证高度,我直接用使用结果来推导红黑着色,这样会忽略红黑树本身的优点,而只关注了平衡二叉树的优点。错误思维路线:我先优先保持二叉搜索树的性质,然后通过旋转使其保持平衡,然后再进行颜色调整。感觉只是在探讨平衡二叉搜索树的性质,红黑性质被弱化了,仅仅是为了构造一个红黑树而调整着色。思维调整:先从二叉搜索树的角度对节点进行插入,然后从着色的角度对树进行旋转。
插入节点的五种情况:
- 新节点N位于树的根上,没有父节点。
- 新节点的父节点P是黑色情形。
- 父节点P、叔叔节点U,都为红色。
- 父节点P是红色,叔叔节点U是黑色或NIL; 插入节点N是其父节点P的右孩子,而父节点P又是其父节点的左孩子。
- 父节点P是红色,而叔父节点U 是黑色或NIL,要插入的节点N 是其父节点的左孩子,而父节点P又是其父G的左孩子。
这里,先不补充每种情况的操作方法,你是不是能自己动手写下呢?欢迎留言~
三.删除节点
类似插入节点的分析、总结,删除节点也可以针对每种场景找到固定的着色方法,就像玩一个游戏,有自己的推理跟玩法。我先做个PPT,这块稍后补充。
使用场景
所有的插入、删除都是有限个情况,基于插入、删除的情况分析,即可编写算法生成红黑树,使其在固定的业务场景中发挥红黑树稳定操作效率的特色了。
-
实现关联数组
关联数组又称映射(Map)、字典(Dictionary)是一个抽象的数据结构,它包含着类似于(键,值)的有序对。C++的STL中。map和set都是用红黑树实现的。
底层采用红黑树保存 key-value对,Key值唯一。添加、取出都需要一些循环操作,但所有键值对都是按照key根据指定排序规则保持有序状态。 -
著名的linux进程调度Completely Fair Scheduler
CFS用红黑树管理进程控制块
每个进程都包含运行时间,通过考虑优先级、系统负载等计算出的加权时间,作为红黑树的key。下一个执行的进程为最小运行时间的进程,对应红黑树的最左叶子。 -
epoll在内核中的实现,用红黑树管理事件块
linux内核的可扩展I/O事件通知机制,应用于高性能网络程序场景,在内核cache中用红黑树储存事件块,保证较快的查询速度。 -
nginx中,用红黑树进行超时管理
Nginx为网页服务器,在进行超时管理时,通过红黑树存储超时时间对象,每次找到key最小的节点,然后进行判断是否超时,超时就处理,直到取出的未超时的事件。 -
Java的TreeMap实现
TreeMap是Java中key-value的集合,根据key值的自然顺序进行排序,或者依据比较函数。
二叉搜索树之间的对比
一.AVL树
在计算机科学中,AVL树是最先发明的自平衡二叉查找树。在AVL树中任何节点的两个子树的高度最大差别为一,所以它也被称为高度平衡树。查找、插入和删除在平均和最坏情况下都是O(log n)。增加和删除可能需要通过一次或多次树旋转来重新平衡这个树。
节点的平衡因子是它的左子树的高度减去它的右子树的高度(有时相反)。带有平衡因子1、0或 -1的节点被认为是平衡的。带有平衡因子 -2或2的节点被认为是不平衡的,并需要重新平衡这个树。平衡因子可以直接存储在每个节点中,或从可能存储在节点中的子树高度计算出来。
红黑树相对于AVL树来说,牺牲了部分平衡性以换取插入/删除操作时少量的旋转操作,整体来说性能要优于AVL树。
未完待续
不提问题的码农不是好程序员。自己写完了红黑树的简单剖析,感觉还是只懂皮毛,没有从触碰到算法的核心内容。所以,不妨留几个小问题,担心自己脑子生锈或者没事想玩手机的时候,再提笔研究下红黑树。
- 举例说明红黑树如何牺牲部分平衡性来保证尽可能少的旋转操作,通过与AVL树做比较。增加节点的时候,如果是在构建AVL树,结果会怎么样。
参考
教你初步了解红黑树
算法的时间复杂度和空间复杂度-总结
红黑树从头至尾插入和删除
AVL树
红黑树C源码实现与剖析
Echo
8 Nov,2016