主要分两部分吧,前面是铺垫,讲了二叉树、二叉查找树、AVL树、2-3树、2-3-4树,红黑树,后面讲B树索引。
—— —— ——声明:这只能算是我的学习笔记,和大家分享一下,如果文中有错,欢迎提出,但请勿喷—— —— ——
《数据结构》是大学里计算机专业都设置的有的一门课程,最简单的数据结构比如栈、队列、链表等,复杂的比如树、图等,我在大学里比较仔细的学过一段《数据结构》的课程,但都是理论,没有实际的操作过。而树这种数据结构是一种应用的十分广泛的一种:
下图:
树是一个或多个节点组成的有限集合。(递归的定义)
结点:
根节点:
结点的度:该结点的子树个数。(如A结点的度为3)
树的度:树中所有结点的度的最大值。
叶子结点:度为0 的结点。
兄弟结点:
结点的层:B结点在第2层。
树的高度:。最大层数,图中一共有4层,树的高度为4.
Ø 不同于上面图形的第二种表示方式:A(B(D(#,#),#)C(E(#,#),#))
l 问题:上面的图形是很形象的对树这种数据结构的表示,一看就明白,但所有的数据结构都要面临一个问题,就是:在计算机中怎么存储和实现这种数据结构?不同的存储和实现方式,效率的差别是很大的。
针对上面的树结构,可以有两种存储方式:
(一):左儿子-右兄弟
typedef struct node *tree-pointer;
typedef struct node{
int data;//存储的值
tree-pointerleft_child,right_borther;
}
(二):二叉树存储形式。
将上面的图形顺时针旋转45度就可以得到一课二叉树。
结论:任一棵树,都可以转换成为一棵二叉树。【但是旋转之后得到的二叉树要保持旋转等信息!!!】
————————第二部分————————
u 二叉树是一种应用很广泛的数据结构。
二叉树:一个根节点,和两棵互不相交的左右子树构成,每个结点最多只能有两个孩子。
完全二叉树:所有的叶子结点都出现在最后两层上。
(一):数组表示
其实可以用数组表示任何二叉树(虽然数组是一种很简单的数据结构),但是每一棵二叉树都要按照完全二叉树的形式存储,所以仅在用于表示完全二叉树的时候才不浪费任何空间,大多数情况下,数组表示二叉树会浪费大多数空间。(用数组表示二叉树就是将每个结点依次存储(!))
(二):链表表示
链表是最经常使用的表示二叉树的方式,每个二叉树结点的形式如下:
typedef struct node *tree-pointer;
typedef struct node{
int data;//存储的值
tree-pointerleft_child,right_child;//左儿子、右儿子
}
但它有一个问题,就是无法从子结点找到父结点,但是对于大多数应用都是OK的。
二叉树的遍历:
(一):中序遍历
遍历的代码很简单:
void inorder(tree_pointer ptr){
if(prt){
inorder(ptr->left_child);
printf(“%d”,ptr->data);//其实前序、后序、中序遍历之间的不同,就在于这一行输出代码的位置
inorder(ptr->left_child);
}
}
(二):前序遍历
void inorder(tree_pointer ptr){
if(prt){
printf(“%d”,ptr->data);//其实前序、后序、中序遍历之间的不同,就在于这一行输出代码的位置
inorder(ptr->left_child);
inorder(ptr->left_child);
}
}
(三):后序遍历
void inorder(tree_pointer ptr){
if(prt){
inorder(ptr->left_child);
inorder(ptr->left_child);
printf(“%d”,ptr->data);//其实前序、后序、中序遍历之间的不同,就在于这一行输出代码的位置
}
}
(四):层次遍历
层次遍历就是从上到下、从左往右,依次遍历。
Ø 线索二叉树:仔细观察会发现任一二叉树的链接存储表示,都有大量空链,在2n个链中,有n+1个是空的。
【如果该结点左儿子为空,则指向其前驱结点,如果右儿子为空,则指向其后继结点。(此处前驱、后继结点,对于不同的遍历方式,是不一样的)】
l 堆:
最大堆:是一棵完全二叉树,父节点的值都大于等于儿子结点。
最小堆:是一棵完全二叉树,父节点小于等于儿子结点。
堆这种数据结构经常用来实现优先级队列,优先级队列一般只删除/获取最高优先级/最低优先级的元素。
当要删除最高优先级的元素,可以使用最大堆;当要删除最低优先级的元素,可以使用最小堆。
【堆只是实现优先级队列的一种形式;其实用数组也可以实现优先级队列】
Ø 重点:(第一次涉及到数据结构的插入和删除)
所有的数据结构都要处理的三个问题是:
1,查找/遍历
2,插入(插入后要继续维持这种数据结构)
3,删除(删除后要继续维持这种数据结构)
堆的插入和删除:
如上图的最大堆,如果要插入一个11,该插入在哪呢?
有两个地方都可以插入,别的地方就不可以了。
—————————————第三部分—————————————
重点:
二叉查找树
二叉查找树是一种极其重要的数据结构,后面要说的AVL树、2-3树、2-3-4树、红黑树、B树,都是在二叉查找树的基础的发展而来的。
先说一下堆这种数据结构的不好,最大堆和最小堆在用作优先级队列时速度非常快,但是在用于查找具有n个元素中的任一元素时,其复杂度依然是0(n)
书中定义的二叉查找树比较复杂,其实简单来说:二叉查找树就是:左子树父结点都大于子结点、右子树的父节点都小于小于其子结点,此定义是递归的。【也可以说:左子树孩子都小于父亲,右子树孩子都大于父亲】
Ø 二叉树的查找、插入、删除
查找:
根据二叉树的定义,在查找元素时,先和根结点比较,如果等于要查找的值,直接返回,否则:如果比根节点小,直接去左子树上查找,如果比根结点大,直接去右子树上查找,依次递归。所以我们可以知道:对于一棵高度为h的二叉查找树,复杂度为0(h)。
插入:
AVL树:
考虑一个问题,一个二叉查找树我们是怎么构造出来的呢?一般都是在我们依次插入元素的时候构造的(!),但是在最坏情况,我们得到一棵二叉查找树可能是这样子的:
它依然满足二叉查找树的性质,左子树孩子都比父结点小,但是这样的二叉查找树复杂度依然是0(n),而且极大的浪费了空间。所以出现了另一种更效率更高的二叉查找树,即AVL树,关于AVL树的定义:
(1) 首先这是一棵二叉查找树。
(2) 叶子结点所处的层,最大相差1或-1
Ø 要构建一棵AVL树,我们要在插入结点的过程中不断的平衡这棵二叉查找树,而平衡的方法就是旋转,旋转只有4种方式:
1,LL方式旋转。
2,RR方式旋转。
3,LR方式旋转。
4,RL方式旋转。
LL旋转和RR旋转:
LR旋转:
RL旋转:
2-3树:
我们上面讲的树结构都有一个共同点,就是每一个结点都只有一个值!很明显我们可以想到,如果一个结点有2个、3个。。。个值呢?如果一个结点有2个、3个。。。孩子呢?【其实可以想到如果是这样的话,整棵树的高度会低很多】而下面要讲的2-3树就是这样一种结构:
2-3树的定义:
一棵2-3树是一棵查找树(查找树即:左子树所有子结点小于父节点,右子树所有子结点大于父节点),并且:
(1) 每个内部结点或者是一个2结点,或者是一个3结点。(2结点的意思是有2个孩子)
(2) 一个2结点只能保存一个值,一个3结点只能保存两个值。
(3) 关于这一点书上的定义比较复杂,看下图,意思就是说:下图中的10、20,得小于40;80得大于40;9得小于10,15得在10和20之间,21得大于20,79得小于80,81得大于80.
(4) 所有外部结点在同一层(!)
Ø 关于2-3树的插入:
如上图,现插入一个值70:
从根节点A开始遍历,因为70大于40,所以往A结点的右子树上面找,找到C结点,因为C结点只有一个值80,所以把80插入到C结点里面,插入后的效果如下:
如上图,再在上面插入一个30,插入的逻辑是这样的:
从根结开始遍历树,根节点A是40,因为30比40小,所以往A结点的左孩子上面查找,即结点B,因为结点B是一个3结点,且已经包含2个值,所以不能再往B结点上插入值了,所以得新建一个结点,设此新建的结点是D,则D中存放的值得是结点B中的值(即10、20)和30三个值中的最大值,最小值(即10)在结点B中,而中间的结点(即20)放到父节点A中,所以在上图的2-3树中插入一个30后的树是:
注意:2-3树中只会以当前节点已满(如3结点已经有两个值了,无法再插入值)新建结点的方式插入新结点。
2-3-4树:
2-3-4树是对2-3树的扩展,即允许4结点的存在(4结点即一个结点最多可以有3个值)。
看下图:
2-3-4树的插入和删除更为复杂,此处略。但是必须要确保的是,每次插入和删除后的树,依然是一棵查找树,而且还是2-3-4树。
红黑树:
红黑树也是一种十分出名的数据结构,简而言之,可以这样理解:红黑树就是2-3-4树的二叉树形式。
红黑树定义:在红黑树中,每个结点的儿子指针分为两种颜色:红色和黑色。如果是原2-3-4树中的儿子指针,则在红黑树中就是黑色指针,否则就是红色指针。
由此可知,在红黑树结点的定义中,每个结点除了要有一个指向左儿子的指针、一个指向右儿子的指针外,还得有两个代表黑色、红色的指针。
既然红黑树是2-3-4树转换成的二叉树形式,那么对于2-3-4树中的每个结点都得转换成红黑树结点的形式,具体转换规则如下:
(1)2结点转换:
(略)
(2)3结点转换
(略)
(3)4结点转换:
(略)
红黑树的查找:因为从2-3-4树转换后的红黑树就是一棵二叉查找树,所以其查找逻辑和二叉查找树一样。
红黑树的插入和删除比较复杂,先不讲。
B树:
上面我们所讲的二叉树、二叉查找树、AVL树、2-3树、2-3-4树、红黑树都是以数据在内存中存储的数据结构,下面要讲的B树考虑数据在磁盘上存储的一种数据结构。
关于B树这种数据结构,十分复杂,比上面讲的各种数据结构要复杂的多,很多索引和文件系统都是以B树这种数据结构存储数据的。大家可能会注意到了,上面讲的数据结构中,每个节点所能存储的值的个数中,2-3-4树是最多的,2-3-4树中的4结点可以存3个值,但是3个值还是太少,而且4结点可以有4个孩子,4个孩子还是太少,当数据量很大时,整棵树的高度会变的很高,对于磁盘来说,树的高度过高,会增加磁盘的访问次数,而磁盘的访问速度相对于内存是相差极大的,
首先要说的是m路查找树:
m路查找树,简单点说就是所有的结点中,每个结点最多只能由m个孩子(即最多有m个子树)。这个定义并没有要求根节点必须有m棵子树,关于m路查找树的准确定义是:
——————————————————————
(1) 根节点至多含有m棵子树,且具有以下结构:
n,A0,(K1,A1),(K2,A2),….(Kn,An)
其中Ai是指向子树的指针,0<=i<=n<m,Ki是关键字,0<=i<=n<m
(2) Ki<K(i+1)
(3) 子树Ai中的所有关键字值都小于K(i+1),而大于Ki。0<i<n
(4) 子树An中的关键字都大于Kn。
(5) 每棵子树Ai都是m路查找树,0<=i<=n
——————————————————————
上面的定义有些难以理解,我们参照下图,其实它的意思是这样的:
(1) 根节点至多含有m棵子树,且具有以下结构:
n,A0,(K1,A1),(K2,A2),….(Kn,An)
【上面的n是指这个结点里的值的个数,如下图的根节点,n=2;下图根节点一共3个子树,所以上面的A0/A1/A2就是指向这三个孩子的指针】
其中Ai是指向子树的指针,0<=i<=n<m,Ki是关键字,0<=i<=n<m
(2) Ki<K(i+1)
【这条定义意思就是:每个结点内的元素从左至右,依次递增,如根结点20<40】
(3) 子树Ai中的所有关键字值都小于K(i+1),而大于Ki。0<i<n
(4) 子树An中的关键字都大于Kn。
【上面第3、4条的意思是:下图中10、15得小于20;25、30在20、40之间;45、50大于40】
(5) 每棵子树Ai都是m路查找树,0<=i<=n
关于m路查找树,有一些规律:
对于一棵高为h的m路查找树,其结点数量的最大值以及最多可以容纳的关键字(即上图中的20、40等)是固定的。(具体的算法大家可以上网查,此处略过)
B树:
B树的定义:首先B树是一棵m路查找树。
一棵m阶B树:
(1) 根节点至少包含2个儿子。
(2) 除根节点和叶子结点外,所有结点的儿子数量至少是:m/2取上。
(3) 所有叶子结点在同一层。
简单一点理解B树:其实B树就是一个结点可以有个孩子、一个结点内可以存放多个值(因为这样可以降低树的高度),它只是对2-3树、2-3-4树的加强,只不过B树的每个结点指向的磁盘块,而不再是内存中的地址。
因为B树索引指向的数据是在磁盘上存储的,所以网上很多讲解B树索引的文章都会讲一些磁盘的知识,如磁盘的构造、读取数据的方式等等,但是我觉得其实关于磁盘的知识,我们只需要知道两点就足够了:
(1) 磁盘和内存交换数据是以磁盘块为单位的,为了提高从磁盘读取数据的效率,一个磁盘块中的数据会一次全部读出来。
(2) 磁盘寻找数据要经过3个步骤:1:首先移动臂根据柱面号使磁头移动到所需要的柱面上,这一过程被称为定位或查找(即查找时间)。2:根据盘面号来确定指定盘面上的磁道 3:盘面确定以后,盘片开始旋转,将指定块号的磁道段移动至磁头下,将磁盘块的数据读到内存中。
其中上面3个步骤中的第一个步骤花费的时间最大。
再讲另外一个知识:
上面已经说过,B树的每个结点都是指向一个磁盘块。
假设一个数据页(即磁盘块)的大小是8192字节,但用户最多只能存储 8060字节的数据。假如我们在一个char(60)的列上创建索引,在数据表中每一行则需要60字节的存储,同时这60字节也是索引每一行所占有的存储。
如果数据表中只有100行数据,那么总共的存储需要6000字节,这个时候仅需要一个数据页就可以了,那么这个数据页既是根节点也是叶级页。实际上,你可以在这个表中存储8060/60=134行数据,同时只需分配一个数据页来存储索引的值。
当你添加第135行数据的时候,这个时候一个数据页已经不能存储了,数据库就会增加两个数据页;这个操作就会创建包含一个根节点和两个叶级页的索引,两个叶级页分别各自包含一半数据,根节点页仅包含两行数据。这种情况下不需要中间层级页,因为根节点能够包含所有的叶级页第一个输入。这样一来,一次查询仅需要查询两个数据页就可以定位这个数据表中的任何一行。
当我们继续添加数据到这个表中,直到134*134+1=17957行数据。当有17956行数据的时候,这个时候根节点有134个叶级页,每个叶级页包含134个值。可是当添加第17957行数据的时候,这个根节点已经不能存储多余的数据了,否则就超过8060字节的限制了,这个时候数据库会自动添加中间层级,这个层级包含两个数据页,每个数据页存储一半叶级页的第一行输入索引值,同时根节点页包含两行数据,分别为两个中间层级页的第一行数据索引值。
当第17956*134+1=2406105行数据添加到数据表中的时候,会再分配一个中间层级页。
通过上面的分析我们可以发现即便是超过25亿数据的时候每次查找也最多查找3个数据页就可以定位每一行数据了,这就是索引能够提高效率的原理。不过有一点就是创建索引需要分配额外的数据页用于存储索引值,所以会增加数据库的磁盘占有量。
关于B树索引可以参下面的图有一个大概的认识:
注意:根节点中的A也出现在了中间层最左边结点中,也出现在了最下面一层最左边结点中;根节点中的H出现在了中间层第二个结点中、最下层第三个结点中……。如果你细心点,还会发现,其实上图只是把A到Z这26个字母划分成很多区间了,最大的区间是根节点的A,H,0,然后又对A到H、H到O进行了划分。
再看下面这张图:
这张图在网上很多讲解B树的文章用引用的很多,但它还不是很准确,所以我给他改造后,如下图:
我们讲解一下上图:
图中每个结点中的值是按顺序存放的(递增),前面讲B树的定义时已经讲过了。
对于非叶子结点:
每个索引条目(即:如根节点中的就是一个索引条目)都包含3个字段【原图中只有两个字段,第一个字段表示当前索引条目存储的值;第二个字段表示所链接的索引块的地址,该地址指向下面一个索引块,而实际上,每个索引条目还有一个字段,就是该索引条目存储的值的地址,如上图中增加的两条蓝线】
对于叶子结点:
和上面非叶子结点不同的是:叶子结点存储的地址,是该索引条目的值的所在行的物理地址。
比如数据表中有一列,这一列的值一共就是下面这些值:
那么在该列上建立的B树索引就像上上所画的那样。
其实还有一个问题就是B树的高度,B树的高度是怎么确定的呢?
对于B树索引来说,肯定是高度越低,磁盘IO的次数就越少,但是如果高度是1的话,如果索引很大,索引的数据就会一次全部读到内存中,内存不一定能装的下这么多数据,所以肯定B树的高度肯定是有一个最优值的。