这篇文章讨论一些常用的树结构和它们的一点相关性质和算法。
1. 哈弗曼树 哈弗曼树的主要目的是压缩。比如对一系列频率不同的字符,分别给它们不同长度而没有歧义的编码,使得期望的总编码长度降到最低。哈弗曼树比较好理解,不多说,具体过程大概是先选择频率最低的两个字符,分别作为树的最底层两个节点,然后给它们一个父节点,父节点的频率记为它们的合,考察当前剩余的节点和当前没有父节点的树中节点,找出两个最低的,再重复上面的过程合并。直到每个字符都被放进树中,此时节点构成一颗二叉树,除根节点外每层两个节点。每个节点的层数就是该节点应当给的编码长度。
2. 二叉查找树 二叉查找树(Binary Search Tree)也叫二叉排序树(Sorted Binary Tree),主要性质是对于二叉树的每一个节点,如果它有左子树,那么左子树的每个节点的值均小于该节点。如果它有右子树,那么右子树的每个节点的值大于该节点。不允许存在两个节点的值相等。这样的树的结构非常常用,主要目的是二分查找方便。一颗好的二叉查找树的一次查找复杂度为O(lgn)
3. 平衡二叉树 平衡二叉树是对二叉查找树的优化。二叉查找树的问题在于,假如树的左边和右边高度差很多,比较倾斜,那么导致树的高度被加大了,那么查找的复杂度事实上是会大于lgn的,最坏可以达到n。因此平衡二叉树就要尽量保持平衡,他要求每个节点的左子树和右子树的高度差不得超过1,一旦发现某个节点出现了失衡的情况,就需要进行旋转恢复平衡。 旋转分为左左、右右、左右、右左四种情况。左左顺时针转,右右逆时针转,左右先把下边逆时针,再把上边顺时针,右左先把下边顺时针,再把上边逆时针。关键要定位到哪个节点出现了失衡的情况,旋转时需要包括它的父节点。旋转之后这个失衡点取代了原先父节点的位置,而它的父节点变成了它的子节点。
4. B树 首先纠正一个常见的误认问题:很多人以为B树、B-树、B+树是三种树,其实这个完全是翻译造成的误解。B-Tree就是B树,但很多人中文著作翻译为B-树,再加上还正好有一个B树的改进版本B+树,更让人误解了,其实B树和B-树是一个东西B-Tree。 B树(Balanced-Tree)主要解决的是减少IO的问题。由于磁盘的速度很慢,而内存往往不够用,当树很大的时候,我们需要把树的很大一部分留在磁盘上需要时读取。平衡二叉树的查询复杂度已经是lgn,如果想要更低的话,一种想法就是把树变成多路的,这样就减少了IO的次数。B树的思想和它的改良版本被广泛在数据库系统中运用。 B树相对平衡二叉树的改良在于,给每个中间节点多于1个的key,从而实现多路。 B树是多路的,一颗B树有一个度m。度为m的B树的每一个节点的子树最多有m颗。中间节点至少要有m/2个子树(如果m是奇数那么向上取整)。每个节点存在多个值,这些值用于查询时的比较,如果这个节点有x个值,那么它有x+1个子树(x个值线性排列,那么包括左右两端一共有x+1个空挡)。B树要求所有叶子节点在同一层,是严格平衡的。经过这样的改良,B树得到了比平衡二叉树更好的效率。
typedef struct node
{
int keyNum;
struct node* parent;
struct node* child; // 指向子节点线性表,keyNum+1个
type* key; // 指向key线性表,keyNum个
} node;
5. B+树 B+树在数据库和文件系统中被广泛使用。B+树是对B树的改良,基本相同,所以讲的都是不同点。B树中的中间节点存放的值既是值,又是比较用的key。而B+树的中间节点的key就只是key而已。也就是说,B树的查询可能到某个中间节点就完成了,而B+树必须查到叶子才行,因为只有叶子的值才有实际意义,中间节点的key只是用于加快查询的而已。 B+树的每个节点如果有x个key的话,那么它有x个子树。第i个子树的全部key落在(x[i-1], x[i]]前开后闭这个区间内
(update:这一点在不同的资料上存在着不一样的描述,维基百科英文版上描述的是x-1个key对于x个子树,一些国内的博客则是两种说法都有,维基百科中文版并没有说明这个问题。如果读者有确定答案请在下面评论告诉我,谢谢)。B+树的叶子上保存了完整的数据信息,同时还有一个指针顺序指向下一个叶子节点。 B+树由于中间节点仅存储了key而没有实际的数据,因此整个树较小,可以把更多的树的部分放到内存中,进一步地减少了IO的量。