1,如何确定一颗二叉树(唯一)???
2,二叉树度的问题(叶子节点与度为2的节点加1)???
3,二叉排序树的插入,删除,简单理解
4,平衡二叉树的构建过程,,,,
扩充二叉树:
扩充二叉树是二叉树的一种,也叫扩展二叉树,是指在二叉树中出现空子树的位置增加空树叶所形成的二叉树、
、也就是空指针引出一个虚节点。。。。。其值为一特定值。。。
1,二叉树的一些基本特性
1,每个结点最多有两颗子树,所以二叉树中不存在度大于2的结点。注意不是只有两颗子树,而是最多有。
没有子树或者有一颗子树都是可以的。
2,在一个确定,唯一的二叉树中,左子树,右子树都是确定的,也就是说都是有顺序的,次序是不能颠倒的
3,即使树中只有一颗子树,也要区分它是左子树还是右子树,如下,它们就是不同的二叉树
简单的来说具有5中形态
1,空二叉树
2,只有一个跟结点
3,根结点只有左子树
4,根结点只有右子树
5,根结点有左右子树
一些奇怪的二叉树:
1,斜树,也就是说:二叉树从整体来看是斜的,所有的结点都只有左子树的叫左斜树,所有的结点都只有右子树
的叫做右斜树,有人说:这不就跟线性表差不多了吗???对,其实,线性表就是一个特殊的二叉树
可以很直观的看到:斜树的个数就是二叉树深度
2,满二叉树
对于二叉树而言,所有的叶子结点都在同一层并且都在最下层,其他的非叶子节点(每一个都有自己的左右孩
子),也就是说二叉树中,n层的话,那么节点数就是2^n – 1.
如下:
3,最优二叉树
若设二叉树的深度为h,除第h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第h层出现的节点,也都是从
左往右,不会说是出现右边的没有左边的
好啦,上面我们了解了二叉树的一些基本特性,下面我们来看一些问题
2,唯一确定的二叉树
问题1:
如何确定一个唯一的二叉树,结构,形态等都是唯一的???
1,可以通过先序遍历吗??
比如:存储的是A-B-C
可以确定的是:如果A-B-C存储的话,那么我们并不知道它具体是那一种的,如上,我们通过先序的遍历
并不能确定具体的形态,所以说,先序并不能确定唯一的二叉树
2,可以通过中序遍历吗??
可以知道:中序遍历上面两颗二叉树得到的结果也是一样的
3,可以通过后序吗??
如上图:也是不可以的,后序遍历的结果都是A-B
4,那么到这里,我们是如何确定一个稳定的唯一确定的二叉树呢???
能不能将上面的任意两种序列结合起来呢???比如:(先序加中序)
1,试试(先序加中序)
为了不影响结果的正确性,我们先设计一个确定的二叉树(如下),写出它的先序和中序,然后
在将其还原成一个二叉树
二叉树如上:
先序遍历:ABCDEF
中序遍历:BCAEDF
分析过程:从先序我们可以知道第一个A一定是当前二叉树的根节点,同时结合中序(左中右),可
以知道BC一定在A的左边,也就是说是A的左孩子那边的,那么EDF就一定是A右孩子那边的;
继续从先序得知下一个是B,可以知道在BC之间,B一定是A的直接左孩子,同时结合中序的序列
BC可知,C一定是B的右孩子;
所以A的左孩子这边就是:
同样的分析过程:EDF位于A根节点的右子树,结合先序可知:A的第一个直接右孩子是D,同时
因为在中序里面,先访问E,返回才访问D,最后是F,所以可以知道A这边右子树就是:
所以整合到一起就是:上面给出的那个完整的二叉树
5,那么可以通过中序加后序吗???先序加后序呢???
如同上面的分析过程,中序加后序也是一定可以确定一颗唯一的二叉树的
但是,对于先序加后序而言,,,就不能确定唯一一颗二叉树了,
如下结构:
不管是先序还是后序,但是它们的结构就是不一样
那么有的同学可能就会说了,这不对啊,我们平时创建不是这样子的啊,我们平时的创建并不会是类似于这样
确定一个先序和中序然后再去按照一定的算法去创建一个合理的二叉树,我们平时都是按照二叉树的先序建立
,好像是可以建立的吧,,,,
是吗????
好像的吧,,,哈哈,看,回答的也不是很确定了
其实是这样的,我们平时的创建方式是:采用扩充二叉树的先序方式来建立一颗唯一的二叉树的,关于扩充二叉
树,我们前面提到了,例如:我们来建立这样一颗二叉树的话:
那么我们的输入方式就是这样的:AB#C##DE##F##,所以说,我们当前所创建的二叉树是唯一确定的
所以在这里总结一下:要建立一颗唯一确定的二叉树,那么有一下方式:
(先序+中序),(中序+后序),(扩充先序二叉树)
树、森林和二叉树的转换
树转换为二叉树
(1)加线。在所有兄弟结点之间加一条连线。
(2)去线。树中的每个结点,只保留它与第一个孩子结点的连线,删除它与其它孩子结点之间的连线。
(3)层次调整。以树的根节点为轴心,将整棵树顺时针旋转一定角度,使之结构层次分明。(注意第一个孩子是结点的左孩子,兄弟转换过来的孩子是结点的右孩子)
森林转换为二叉树
(1)把每棵树转换为二叉树。
(2)第一棵二叉树不动,从第二棵二叉树开始,依次把后一棵二叉树的根结点作为前一棵二叉树的根结点的右孩子,用线连接起来。
二叉树转换为树
是树转换为二叉树的逆过程。
(1)加线。若某结点X的左孩子结点存在,则将这个左孩子的右孩子结点、右孩子的右孩子结点、右孩子的右孩子的右孩子结点…,都作为结点X的孩子。将结点X与这些右孩子结点用线连接起来。
(2)去线。删除原二叉树中所有结点与其右孩子结点的连线。
(3)层次调整。
二叉树转换为森林
假如一棵二叉树的根节点有右孩子,则这棵二叉树能够转换为森林,否则将转换为一棵树。
(1)从根节点开始,若右孩子存在,则把与右孩子结点的连线删除。再查看分离后的二叉树,若其根节点的右孩子存在,则连线删除…。直到所有这些根节点与右孩子的连线都删除为止。
(2)将每棵分离后的二叉树转换为树。
2,关于二叉树度的问题(面试经常会遇到)
设树T的度为4(例外,非二叉树),其中度为1,2,3,4的节点个数分别是4,2,1,1,那么
T中的叶子树为???
分析:一条边有两个结点,两条边有三个节点,所以
条边 = 结点 – 1
度为4:边为4; 度为3:边为3; 度为2:边为2; 度为1:变为1
所以,总的边数是:n = 4 × 1 + 3 × 1 + 2 × 2 + 1×4 = 15
所以,总的结点就是:16
叶子节点 = 16 – 度不为0的结点(4+2+1+1) = 8
关于二叉树的度
二叉树的度,只有0,1,2三种情况,度为0(叶子节点)度为1(只有一个孩子)度为2(一定有两个孩子)
首先先说一个结论:度为0的节点(叶子结点)是度为2的数量加1
证明:一个二叉树,共有结点N,二度结点为X,一度结点为Y,求叶子节点的个数(设为Z)??
N个结点总共N – 1条边,一度节点一条边,二度节点二条边,叶子节点0跳边
2X + Y = N – 1
X + Y + Z = N
好啦,两者联合求得: Z = X + 1;(哈哈,小时候的解方程这里用到了)
为了证明其合理行:我们先给出一颗二叉树(为了便于理解,二叉树很奇怪)
问题:一颗二叉树,度为1的结点数为4,度为2的节点树为4,那么该二叉树的叶子结点是多少呢,
总结点呢???
根据上面的这个结论,那么叶子节点就是5(度为2的个数加1)
总节点是:度为1 + 度为2 + 度为0 = 13
我们用上面的二叉树验证,完全正确
#include<stdio.h>
#include <malloc.h>
#include <conio.h>
typedef int DataType;
typedef struct Node
{
DataType data;
struct Node *LChild;
struct Node *RChild;
}BitNode,*BitTree;
void CreatBiTree(BitTree *bt)//用扩展先序遍历序列创建二叉树,如果是#当前树根置为空,否则申请一个新节点//
{
char ch;
ch=getchar();
if(ch=='.')*bt=NULL;
else
{
*bt=(BitTree)malloc(sizeof(BitNode));
(*bt)->data=ch;
CreatBiTree(&((*bt)->LChild));
CreatBiTree(&((*bt)->RChild));
}
}
void Visit(char ch)//访问根节点
{
printf("%c ",ch);
}
void PreOrder(BitTree root) /*先序遍历二叉树, root为指向二叉树(或某一子树)根结点的指针*/
{
if (root!=NULL)
{
Visit(root ->data); /*访问根结点*/
PreOrder(root ->LChild); /*先序遍历左子树*/
PreOrder(root ->RChild); /*先序遍历右子树*/
}
}
void InOrder(BitTree root)
/*中序遍历二叉树, root为指向二叉树(或某一子树)根结点的指针*/
{
if (root!=NULL)
{
InOrder(root ->LChild); /*中序遍历左子树*/
Visit(root ->data); /*访问根结点*/
InOrder(root ->RChild); /*中序遍历右子树*/
}
}
void PostOrder(BitTree root)
/* 后序遍历二叉树,root为指向二叉树(或某一子树)根结点的指针*/
{
if(root!=NULL)
{
PostOrder(root ->LChild); /*后序遍历左子树*/
PostOrder(root ->RChild); /*后序遍历右子树*/
Visit(root ->data); /*访问根结点*/
}
}
int PostTreeDepth(BitTree bt) //后序遍历求二叉树的高度递归算法//
{
int hl,hr,max;
if(bt!=NULL)
{
hl=PostTreeDepth(bt->LChild); //求左子树的深度
hr=PostTreeDepth(bt->RChild); //求右子树的深度
max=hl>hr?hl:hr; //得到左、右子树深度较大者
return(max+1); //返回树的深度
}
else return(0); //如果是空树,则返回0
}
void PrintTree(BitTree Boot,int nLayer) //按竖向树状打印的二叉树 //
{
int i;
if(Boot==NULL) return;
PrintTree(Boot->RChild,nLayer+1);
for(i=0;i<nLayer;i++)
printf(" ");
printf("%c\n",Boot->data);
PrintTree(Boot->LChild,nLayer+1);
}
void main()
{
BitTree T;
int h;
int layer;
int treeleaf;
layer=0;
printf("请输入二叉树中的元素(以扩展先序遍历序列输入,其中.代表空子树):\n");
CreatBiTree(&T);
printf("先序遍历序列为:");
PreOrder(T);
printf("\n中序遍历序列为:");
InOrder(T);
printf("\n后序遍历序列为:");
PostOrder(T);
h=PostTreeDepth(T);
printf("\nThe depth of this tree is:%d\n",h);
PrintTree(T,layer);
}
二叉树的建立、遍历、打印 - rocky - 学高为师,身正为范
3, 线索二叉树
为什么会出现线索二叉树呢????
1, 基于之前我们建立的二叉树,里面存在太多的空指针域,
对于一个有n个节点的二叉链表,每个节点有指向左右孩子的两个指针域,所以一共是2n个指针域,而n个节点
的二叉树一共有n -1 条分支线数,也就是说,其实存在2n – (n -1) = n + 1个空指针域,
可以看到上图是7个节点,而空指针域(“^”)为8个
这些空间不存从任何事情,白白的浪费着内存的资源。。。。。
2,我们在遍历的时候,按照某种顺序,得到了一串字符序列,遍历过后,我们可以知到:任意一个节点,它
的前趋和后继是哪一个。。。
也就是说,如果我们要知道某个节点的前趋和后继,必须遍历一次,,,以后想要知道,必须再次遍历
所以考虑,可以用刚次那些空节点存放一些关于前趋和后继的信息。。。
线索:指向前趋和后继的指针
线索二叉树:加上线索的二叉树(Threaded Binary Tree)
但是值得注意的是:线索,必须建立在某一种确定的排序方式下,先序,中序等的线索是不一样的。。。。
最后,我们会发现,线索二叉树,其实就是相当于把一颗二叉树转变了一个双向链表。。。。
对二叉树以某种次序遍历使其变为线索二叉树的过程被称之为线索化,,,,,
二叉排序树:
思考1:假设查找的数据集是普通的顺序存储(也就是说:整个数据集是没有什么顺序):
那么插入操作就是将记录放在表的末端(简单来说),然后给表记录数加1即可,,,
删除操作可以是删除后,后面的记录向前移,也可是要删除的元素与最后一个元素互换,表记录
减1,
对此而言:效率还是可以接收的,但是对于这样的无序表造成的查找效率很低
思考2:加入我们查找的是有序线性表的话,我们的查找,可以用折半,比较等查找算法来实现,
但是,如果真的这样的话,那么在插入和删除操作上,就需要花费很大的时间,
效率极低,,,,,,
那么,有没有一种即可以使得插入和删除,查找的效率都不错,又可以较高效的实现查找的算法呢??
有,,,,自此,我们引入了一个新的概念:二叉排序树
首先声明一点:需要在查找时插入或删除的查找表称为动态查找表。。。。
举个简单的例子:
我们首先将问题简单化:现在我们的目标是插入和查找同样高效
假设,我们数据集开始只有一个数{62},然后现在我们需要将88插入数据集,于是数据集就成了
{62,88},还保持着从小到大,再查找有没有58,没有则插入,但是,但是,此时要想在线性表的
顺序存储中有效,就必须移动62和88的位置,那么可以不移动吗???
我们的目的,不正是为了提高插入的效率吗???,恩的,可以的,那就是二叉树的结构了。。。
当,我们用二叉树的方式时,首先我们将第一个数62定为根节点,88因此比62大,因此
让它作为62的右子树,58比62小,所以称为它的左子树。此时58的插入并没有影响到62和88的位置关系。。。。
然后,我们就得到了一个二叉树,那么当我们对他进行“中序遍历时”就可以得到一个有序的序列了
那么需要对集合{62,88,58,47,35,73,51,99,37,93}构建成二叉排序树:
接下来:我们只需要进行中序遍历,就可以得到一个有序的序列。。。。。。
二叉排序树:(Binary Sort Tree),又称为二叉查找树。或者是一颗空树,或者具有以下的性质:
1,若它的左子树不空,则左子树上所有结点的值均小于它的根结点
2,若它的右子树不空,则右子树上所有结点的值均大于它的根结点
3,它的子树也都是二叉排序树
二叉排序树的插入:
1,首先我们应该找寻我们已经建立好的二叉排序树,如果里面有我们要插入的元素,那么就直接返回
不然,我们返回一个合理的位置
2,然后我们要插入的数值跟这个合理的位置进行比较,看是右孩子还是左孩子
举个例子:对于刚才的序列{62,88,58,47,35,73,51,99,37,93},我们考虑插入40
二叉排序树的删除:
首先根据我们的可能删除型:
1,叶子节点(这样处理比较简单,因为并不影响当前排序二叉树的基本结构)
2,删除的节点只有左子树或只有右子树,这样也比较好理解,那就是删除后,将它的左子树或右子树整个移动
到删除节点的位置即可。。。
对于上图而言:(我们删除了58,只有左子树)
3,删除的节点如果即有左子树,又有右子树,那么我们该咋样呢????(比如,我们要删除47)
1,我们可以假设47只有一个左子树(或者右子树),然后我们对另一边进行插入操作,
嗯嗯,可以是可以,但是,这样做的效率不高啊,而且我们的结构可能还会发生大的变化,
极有可能会增加树的高度。。。
2,其实,我们可以在左子树(或右子树)中找出一个值来代替47,那么我们应该找出那个值呢???
经过观察和发现,我们应该找的就是40,或者51,嗯嗯,其实,他们正好是47的前趋和后继
经过考虑,我们选择第二种:找到需要删除的节点p的直接前趋(或直接后继)s,用s来替换节点P,
然后再删除s
删除47(对上图而言)!!!!!
二叉排序树概述:
1,为什么对于二叉树的插入和删除效率高,究其根本,只是因为:是以链接的方式存储
不用移动。。。只要找到合适的插入和删除位置后,仅需要修改连接指针的指向而已,,
对于二叉树的查找,其比较的次数就是根节点到要查找节点的层数。。。
2,极端情况下,最少为1次,即根节点就是要找的节点,但是最多不会超过树的深度。
简单的说:二叉排序树的查找性能取决于二叉排序树的形状。。。但是二叉树的形状也是
不确定的。。。。。
3,对于一个有序数列而言,如果我们要建二叉排序树,那么他就是一个极端二叉树,
只有左孩子,或者只有右孩子。。。
综上所述:我们希望二叉树是比较平衡的,即其深度与完全二叉树相同,均
为:log2N+ 1,
那么查找的时间复杂度也就是O(logN),近似于折半查找
而不平衡的最坏情况也就是O(N),等同于顺序查找。。。
所以说:为了提高效率,我们应该尽可能的构建一颗平衡二叉树(AVL树)
平衡二叉树(AVL树)
1,平衡二叉树(Self-BalancingBinary Search Tree或Height-BalancedBinary Search Tree)是
一种二叉排序树,其中每一个节点的左子树和右子树的高度差至多等于1.
2,平衡二叉树是一种高度平衡的二叉排序树。那么什么是高度平衡呢???
就是说:是一颗空树,或者它的左子树和右子树都是平衡二叉树,且
左子树和右子树的深度之差的绝对值不超过1,我们将二叉树上节点的左子树深度减去右子树深度的值称为平衡因
BF(BalanceFactor)
那么,可以知道,平衡因子只可能是:-1,0,1
但是,二叉平衡树的前提就是二叉排序树!!!!!!!!!!!!!!
距离插入节点最近的,且平衡因子的绝对值大于1的节点为根的子树,我们称之为最小不平衡树。。。。。。
首先,我们来看看几种二叉树:判断一下,是不是平衡二叉树
根据平衡二叉树的判断:平衡因子大于1,那么不是平衡二叉树。。。。。。。
眨眼一看:好像是个平衡二叉树,但是,但是,首先它不满足排序二叉树,所以不是
平衡二叉树实现原理:
1,就是在构建二叉排序树的过程中,每当插入一个节点时,先检查是不是因为插入而破坏了树的平衡性,
若是,那么找出最小的不平衡子树。在保持二叉排序树特性的前提下,调整最小不平衡(最下层,假设
根在最上层)子树中各节点之间的连接关系,进行相应的旋转,使之称为新的平衡树
为了能合理的说明构建的过程:我们构建{3,2,1,4,5,6,7,10,9,8}
如果我们构建二叉排序树,那应该是这个样子的。。。。。。。。。。。。。。
而我们的平衡二叉树构建完后是这个样子的,,,,,,,,,
可以看到,明显的层数变少了,,,极大的提高了效率。。。。。
嗯嗯,我们一步步来看待这个问题,,,,,,,,,,,
1,我们先插入前两位3,2,正常的构建,当我们插入第三个数的时候,发现平衡被打破(也就是成了最小的不平衡
树)
(出现了为2的平衡因子,因此平衡被打破,此时整棵树都变成了最小不平衡树,需
要调整,此时,因为平衡因子BF为2,BF为正,那么整棵树需要右旋,这个可以理解
的,,,)
(好啦,这样看起来就平衡多了,,,,,)
2,接着,我们插入第四个数,树(如下,左图)还是平衡的(结构没有变化),,,继续增加节点5(如下:右
图),,,,,(平衡再次破坏)
出现了平衡因子-2(图中出现了两个-2可以对任意一个旋转),但是为了标准统一,我们选择下层的,又因为
此刻BF为负值,所以我们需要左旋,这样才能达到新的平衡 ,如下图,完美的平衡。。。。
3,接着,增加节点6时,发现根节点2的BF变成了-2,如下左图,所以我们继续左旋,但是值得注意的是:由于本来
节点3是节点4的左孩子,由于旋转后需要满足二叉排序树的特性,因此成了节点2的右孩子。。。。。。如下右图
4,继续,我们增加节点7,同样需要左旋,如下图:::::
(此图,为插入节点7,调整后的图)
5,继续,我们增加节点10,由于并没有改变结构,平衡行依然,,,,再增加节点9,此时平衡行破坏,继续调整
(插入节点9后)
6,这个时候,我们不要着急,,,加入我们直接以7为中心左旋,那么10变为根节点,7是左孩子,9是右孩子,但是
我们都知道,平衡树是二叉排序树的基础,9是右孩子,不合理啊。。。。。。
所以,这个时候,我们不能直接简单的左旋,那么为什么呢????
经过仔细的观察发现,,,,,节点7的BF为-2,节点10的BF为1,是一正一负,,符号并不统一,然而我们前面
几次的左旋右旋,符号是统一的,那么着就不能直接旋转,至此,我们可以先将符号转化到统一再
说,那么也就是说:我们要尽量将10的BF转化为-1。。。。。。
我们先对节点9,节点10进行右旋,使得10成为9的右孩子, 节点 9的BF为-1,此时符号就统一了。。。。。,
然后在做简单的旋转,就可以了,。。。。。。。。。。
(如此,已经调好了)
7,接下来,我们继续增加节点8,,平衡行被破坏,继续右旋(对7,8,9,10)即可。。。。。。
8,我们调整之后,还是不平衡,此刻,我们应该继续翻转,,,,
至此,我们的平衡二叉树建立完毕。。。。。。。。。。。。。。。
为什么会产生B树,B+树,B*树?????
我们前面讨论的那些数据结构,处理数据都是到内存中,因此考虑的都是内存中的运算时间复杂度,
如果,我们要操作的数据集非常大,,,大到内存已经没有办法处理了,,如数据库中的上千万条记录的数据表,
硬盘中的上万个文件等,,,在这种情况下,对数据的处理需要不断从硬盘等存储设备中调入或调出内存页面。。
一旦涉及到这样的外部存储设备,关于时间复杂度的计算就会发生变化,,,,访问该集合元素的时间已经不仅仅
是寻找该元素所需要比较次数的函数,,,我们必须考虑对硬盘等外部存储设备的访问时间以及将会对该设备作出
多少次单独访问。。。。
试想一下,为了要在一个拥有几十万个文件的磁盘中查找一个文本文件,,,我们肯定是想让它读取几十次而不是
上万次,,,此时,为了极大的降低访问的次数,我们。。。。。。
如果,还是利用上面二叉树的结构,在元素很多的情况下,不是树的度非常多,那么就是树的高度非常大,,,,
而且,极有可能是两者兼有之,,,那么内外存的存取次数极多,,,,
为此,我们引入了B树,B+树,B*树,,,(多路查找树)
那么B树的结构如何减少次数呢????
1,我们的外存,比如硬盘,是将所有的信息分割成相等大小的页面,每次硬盘读写的都是一个或多个完整的页面
对于一个硬盘而言,一页的长度有可能是211到214个字节。。。。
在一个典型的B树应用中,要处理的硬盘数据量很大,因此无法一次全部装入内存。因此我们会对B树进行调整
,使得B树的阶数(或节点的元素个数)与硬盘存储的页面大小匹配。
比如:说一颗B树的阶为1001(即1个节点最大可以包含1000个数据,(1000个数据时)但是必须有1001个孩子或
者没有一个孩子),高度为2,它可以存储超过10亿个数据,我们知道让根节点持久的保留在内存中,那么
在这颗树上,寻找某一个数据至多需要两次硬盘的读写。。。。。。
通过这种方式,在有闲内存情况下,每一次磁盘的访问我们都可以获得最大数量的数据。。。。
所以说:B树的数据结构就是为内存外存的数据交互准备的。。。。。。。。。。。。。。
那么对于n个数据的m阶B树,最坏的情况要查找几次??????
2,还有就是磁盘
磁盘的构造
磁盘是一个扁平的圆盘(与电唱机的唱片类似)。盘面上有许多称为磁道的圆圈,数据就记录在这些磁道上。磁盘可
以是单片的,也可以是由若干盘片组成的盘组,每一盘片上有两个面。如下图11.3中所示的6片盘组为例,除去最顶
端和最底端的外侧面不存储数据之外,一共有10个面可以用来保存信息。
当磁盘驱动器执行读/写功能时。盘片装在一个主轴上,并绕主轴高速旋转,当磁道在读/写头(又叫磁头)下通过
时,就可以进行数据的读 /写了。一般磁盘分为固定头盘(磁头固定)和活动头盘。固定头盘的每一个磁道上都有独
立的磁头,它是固定不动的,专门负责这一磁道上数据的读/写。
活动头盘 (如上图)的磁头是可移动的。每一个盘面上只有一个磁头(磁头是双向的,因此正反盘面都能读写)。它
可以从该面的一个磁道移动到另一个磁道。所有磁头都装在同一个动臂上,因此不同盘面上的所有磁头都是同时
移动的(行动整齐划一)。当盘片绕主轴旋转的时候,磁头与旋转的盘片形成一个圆柱体。各个盘面上半径相同的
磁道组成了一个圆柱面,我们称为柱面。因此,柱面的个数也就是盘面上的磁道数。
2.2磁盘的读/写原理和效率
磁盘上数据必须用一个三维地址唯一标示:柱面号、盘面号、块号(磁道上的盘块)。
读/写磁盘上某一指定数据需要下面3个步骤:
(1) 首先移动臂根据柱面号使磁头移动到所需要的柱面上,这一过程被称为定位或查找。
(2) 如上图11.3中所示的6盘组示意图中,所有磁头都定位到了10个盘面的10条磁道上(磁头都是双向的)。这时根
据盘面号来确定指定盘面上的磁道。
(3) 盘面确定以后,盘片开始旋转,将指定块号的磁道段移动至磁头下。
经过上面三个步骤,指定数据的存储位置就被找到。这时就可以开始读/写操作了。
访问某一具体信息,由3部分时间组成:
● 查找时间(seek time) Ts:完成上述步骤(1)所需要的时间。这部分时间代价最高,最大可达到0.1s左右。
● 等待时间(latency time) Tl:完成上述步骤(3)所需要的时间。由于盘片绕主轴旋转速度很快,一般为7200转/分(电
脑硬盘的性能指标之一,家用的普通硬盘的转速一般有5400rpm(笔记本)、7200rpm几种)。因此一般旋转一圈大
约0.0083s。
● 传输时间(transmission time) Tt:数据通过系统总线传送到内存的时间,一般传输一个字节(byte)大概
0.02us=2*10^(-8)s
磁盘读取数据是以盘块(block)为基本单位的。位于同一盘块中的所有数据都能被一次性全部读取出来。而磁盘IO
代价主要花费在查找时间Ts上。因此我们应该尽量将相关信息存放在同一盘块,同一磁道中。或者至少放在同
一柱面或相邻柱面上,以求在读/写信息时尽量减少磁头来回移动的次数,避免过多的查找时间Ts。
所以,在大规模数据存储方面,大量数据存储在外存磁盘中,而在外存磁盘中读取/写入块(block)中某数据时,
首先需要定位到磁盘中的某块,如何有效地查找磁盘中的数据,需要一种合理高效的外存数据结构,就是下面
所要重点阐述的B-tree结构,以及相关的变种结构:B+-tree结构和B*-tree结构
为什么又说B树与红黑树很相似呢?因为与红黑树一样,一棵含n个结点的B树的高度也为O(lgn),但可能比一棵红黑树的高度小许多,应为它的分支因子比较大。所以,B树可以在O(logn)时间内,实现各种如插入(insert),删除(delete)等动态集合操作。
B
树又叫平衡多路查找树。一棵
m
阶的B 树:
- 树中每个结点最多含有m个孩子(m>=2);
- 除根结点和叶子结点外,其它每个结点至少有[ceil(m / 2)]个孩子(其中ceil(x)是一个取上限的函数);
- 若根结点不是叶子结点,则至少有2个孩子(特殊情况:没有孩子的根结点,即根结点为叶子结点,整棵树只有一个根节点);
- 所有叶子结点都出现在同一层,叶子结点不包含任何关键字信息(可以看做是外部接点或查询失败的接点,实际上这些结点不存在,指向这些结点的指针都为null);(读者反馈@冷岳:这里有错,叶子节点只是没有孩子和指向孩子的指针,这些节点也存在,也有元素。@研究者July:其实,关键是把什么当做叶子结点,因为如红黑树中,每一个NULL指针即当做叶子结点,只是没画出来而已)。
- 每个非终端结点中包含有n个关键字信息: (n,P0,K1,P1,K2,P2,……,Kn,Pn)。其中:
a) Ki (i=1…n)为关键字,且关键字按顺序升序排序K(i-1)< Ki。
b) Pi为指向子树根的接点,且指针P(i-1)指向子树种所有结点的关键字均小于Ki,但都大于K(i-1)。
c) 关键字的个数n必须满足: [ceil(m / 2)-1]<= n <= m-1。 如下图所示:
B树中的每个结点根据实际情况可以包含大量的关键字信息和分支(当然是不能超过磁盘块的大小,根据磁盘驱动(disk drives)的不同,一般块的大小在1k~4k左右);这样树的深度降低了,这就意味着查找一个元素只要很少结点从外存磁盘中读入内存,很快访问到要查找的数据。如果你看完上面关于B树定义的介绍,思维感觉不够清晰,请继续参阅下文第6小节、B树的插入、删除操作 部分
B树的类型和节点定义::::::
3.3文件查找的具体过程(涉及磁盘IO操作)
为了简单,这里用少量数据构造一棵3叉树的形式,实际应用中的B树结点中关键字很多的。上面的图中比如根结点,其中17表示一个磁盘文件的文件名;小红方块表示这个17文件内容在硬盘中的存储位置;p1表示指向17左子树的指针。
其结构可以简单定义为:
typedefstruct {
/*文件数*/
int file_num;
/*文件名(key)*/
char * file_name[max_file_num];
/*指向子节点的指针*/
BTNode * BTptr[max_file_num+1];
/*文件在硬盘中的存储位置*/
FILE_HARD_ADDR offset[max_file_num];
}BTNode;
假如每个盘块可以正好存放一个B树的结点(正好存放2个文件名)。那么一个BTNODE结点就代表一个盘块,而子树指针就是存放另外一个盘块的地址。
下面,咱们来模拟下查找文件29的过程:
- 根据根结点指针找到文件目录的根磁盘块1,将其中的信息导入内存。【磁盘 IO 操作 1次】
- 此时内存中有两个文件名17、 35 和三个存储其他磁盘页面地址的数据。根据算法我们发现:17<29<35,因此我们找到指针 p2 。
- 根据p2指针,我们定位到磁盘块3,并将其中的信息导入内存。【磁盘IO操作 2次】
- 此时内存中有两个文件名26, 30 和三个存储其他磁盘页面地址的数据。根据算法我们发现:26<29<30,因此我们找到指针 p2 。
- 根据p2指针,我们定位到磁盘块8,并将其中的信息导入内存。【磁盘IO操作 3次】
- 此时内存中有两个文件名28, 29 。根据算法我们查找到文件名29,并定位了该文件内存的磁盘地址。
分析上面的过程,发现需要3次磁盘IO操作和3次内存查找操作。关于内存中的文件名查找,由于是一个有序表结构,可以利用折半查找提高效率。至于IO操作是影响整个B树查找效率的决定因素。
当然,如果我们使用平衡二叉树的磁盘存储结构来进行查找,磁盘4次,最多5次,而且文件越多,B树比平衡二叉树所用的磁盘IO操作次数将越少,效率也越高。
可是在B树结构中,我们往返于每个节点之间也就意味着,我们必须得在硬盘的页面(磁盘的块)之间进行多次访问
我们又引入了B+树:
一棵m阶的B+树和m阶的B树的异同点在于:
1.有n棵子树的结点中含有n-1 个关键字; (此处颇有争议,B+树到底是与B 树n棵子树有n-1个关键字 保持一致,还是不一致:B树n棵子树的结点中含有n个关键字,待后续查证。暂先提供两个参考链接:①wikipedia http://en.wikipedia.org/wiki/B%2B_tree#Overview;②http://hedengcheng.com/?p=525。而下面B+树的图尚未最终确定是否有问题,请读者注意)
2.所有的叶子结点中包含了全部关键字的信息,及指向含有这些关键字记录的指针,且叶子结点本身依关键字的大小自小而大的顺序链接。 (而B 树的叶子节点并没有包括全部需要查找的信息)
3.所有的非终端结点可以看成是索引部分,结点中仅含有其子树根结点中最大(或最小)关键字。 (而B 树的非终节点也包含需要查找的有效信息)
a) 为什么B+树比B 树更适合实际应用中操作系统的文件索引和数据库索引?
1) B+-tree的磁盘读写代价更低
B+树的内部结点并没有指向关键字具体信息的指针。因此其内部结点相对B 树更小。如果把所有同一内部结点的关键字存放在同一盘块中,那么盘块所能容纳的关键字数量也越多。一次性读入内存中的需要查找的关键字也就越多。相对来说IO读写次数也就降低了。
举个例子,假设磁盘中的一个盘块容纳16bytes,而一个关键字2bytes,一个关键字具体信息指针2bytes。一棵9阶B-tree(一个结点最多8个关键字)的内部结点需要2个盘快。而B+树内部结点只需要1个盘快。当需要把内部结点读入内存中的时候,B 树就比B+树多一次盘块查找时间(在磁盘中就是盘片旋转的时间)。
2) B+-tree的查询效率更加稳定
由于非终结点并不是最终指向文件内容的结点,而只是叶子结点中关键字的索引。所以任何关键字的查找必须走一条从根结点到叶子结点的路。所有关键字查询的路径长度相同,导致每一个数据的查询效率相当。
读者点评
本文评论下第149楼,fanyy1991针对上文所说的两点,道:个人觉得这两个原因都不是主要原因。数据库索引采用B+树的主要原因是 B树在提高了磁盘IO性能的同时并没有解决元素遍历的效率低下的问题。正是为了解决这个问题,B+树应运而生。B+树只要遍历叶子节点就可以实现整棵树的遍历。而且在数据库中基于范围的查询是非常频繁的,而B树不支持这样的操作(或者说效率太低)。
b) B+-tree的应用: VSAM(虚拟存储存取法)文件(来源论文the ubiquitous Btree 作者:D COMER – 1979 )
5.B*-tree
B*-tree是B+-tree的变体,在B+树的基础上(所有的叶子结点中包含了全部关键字的信息,及指向含有这些关键字记录的指针),B*树中非根和非叶子结点再增加指向兄弟的指针;B*树定义了非叶子结点关键字个数至少为(2/3)*M,即块的最低使用率为2/3(代替B+树的1/2)。给出了一个简单实例,如下图所示:
本文严重感谢:
http://blog.csdn.net/v_JULY_v/article/details/6530142/
百度百科
大话数据结构:程杰