如果要求一个线性表既能较快的查找,又能适应动态变化的要求,则可采用 (分块)查找法。
分块查找,也叫索引查找,是将表分成若干块,分块的原则是数据元素的关键字在块与块之间是有序的,而块内元素的关键字是无序的。其可以适应动态变化的要求。
关于题目所说的适应动态变化的要求,这里的动态变化应该是说频繁的插入删除元素;因为分块查找的特点是“块间有序,块内无序”,因此适合做频繁的插入删除
具有12个关键字的有序表,折半查找的平均查找长度(3.1)
折半查找的平均时间复杂度log n,带入12得到了大概3点多。
具体的推导通过把数组画成完全二叉树来确定。将12个数画成完全二叉树,第一层有1个、第二次2个、第三层4个,第四层只有5个。
一个有序表为{1,3,9,12,32,41,45,62,75,77,82,95,100},当折半查找值为82的结点时,查找成功时的比较次数为(4)
首先注意,折半查找当数字个数为偶数时,是向下取整而非向上取整。
{1,3,9,12,32,41,45,62,75,77,82,95,100} 一共13个数,折半取中间第7个数,第一次比较即82与第7个数45比较。45<82,取45右边的第一个数至最后一个数的数列进行折半。此时折半取第10个数。第二次比较即82与第10个数77比较。77<82,取77右边第一个数至最后一个数的数列进行折半。此时折半取第12个数,注:(11+13)/2=12。第三次比较即82与第12个数95比较。95>82,取95左边第一个数至77右边第一个数即82进行折半,此时82=82。折半完毕。
下面关于二分查找的叙述正确的是 ( D )
- 表必须有序,表可以顺序方式存储,也可以链表方式存储
- 表必须有序且表中数据必须是整型,实型或字符型
- 表必须有序,而且只能从小到大排列
- 表必须有序,且表只能以顺序方式存储
二分查找的算法要求 1.必须采用顺序存储结构。 2.必须按关键字大小有序排列。
顺序查找法适合于存储结构为( 顺序存储或链式存储 )的线性表,折半查找法则只适于存储结构为顺序存储的线性表
因为顺序存储支持随机存取,可以直接从数组里边找到对应位置元素;而链式存储要一个个的访问
二叉查找树的查找效率与二叉树的树型有关, 在 ( 呈单支树 )时其查找效率最低。
二叉查找树,即二叉排序树,如果左子树不为空,左子树上的所有节点均小于根节点,如果右子树不为空,右子树上所有节点均大于根节点。
当先后插入的关键字有序时,构成的二叉排序树就会变成单支树,则退化成链表,链表增删快/查找慢,其平均查找长度为(n+1)/2,和顺序查找是相同的,这是最差的情况。
最好的情况就是和折半查找的判定树相同,平均查找长度和log2n成正比
在具有n个结点的二叉排序树上插入一个新结点的平均时间复杂性为( O(log2n) )
问最坏情况下的时间复杂度,就是O(n);问平均时间复杂度,是O(log2n)
当二叉排序树深度不平衡时,会发展成单链的形状,就是一条线 n个点那么深;如果是深度平衡的二叉树 o(logn)
平衡二叉树插入失去平衡时,可采用( 4 )种旋转方式来作调整。
平衡二叉树又称AVL树
性质:
它或者是颗空树,或者是具有下列性质的二叉树:
- 它的左子树和右子树都是平衡二叉树,且左子树和右子树的深度之差的绝对值不超过1。
- 若将二叉树节点的平衡因子BF定义为该节点的左子树的深度减去它的右子树的深度,则平衡二叉树上所有节点的平衡因子只可能为-1,0,1.
- 只要二叉树上有一个节点的平衡因子的绝对值大于1,那么这颗平衡二叉树就失去了平衡。
根据上述性质我们可以发现图(a)是一棵平衡二叉树,而图(b)是一棵不平衡二叉树。图中结点的数值代表的就是当前结点的平衡因子。也验证了上述性质,一棵平衡二叉树的所有结点的平衡因子只可能是-1、0、1三种。
为什么需要平衡二叉树?
当然,我们都希望所有的二叉排序树的初始序列都是平衡的,因为平衡二叉树上的任何一个结点左右字数的深度之差都不会超过1,则可以证明它的深度和logn是同数量级的,所以其平均查找长度也和logn同数量级。但是事于愿违有些二叉排序树的插入,或者初始序列由于其插入的先后顺序等缘故,将导致我们的二叉排序树的效率大大降低。如下图
为了避免这种情况的发生,我们希望可以有一种算法,将我们的不平衡的二叉排序树转化为平衡二叉排序树。这样就可以让我们的二叉排序树结构最优化。
平衡二叉树的算法
看到上述例子,我们就慢慢有点感觉了,至少知道了为什么会需要平衡二叉树,接下来我们看一下平衡二叉树是怎样将我们不平衡的树转换为平衡二叉树。
如何时构成的二叉排序树编程平衡二叉树呢?先看一个具体的例子
- 图a 一颗空树也算是平衡二叉树
- 图b 只有一个结点13的树也算是平衡二叉树
- 图c 在图b的基础上插入新的结点24之后,仍然是平衡二叉树,只是根结点的平衡因子从0变到了-1(左子树的深度为0减去右子树的深度1等于-1)
- 图d 在图c的基础上再插入一个结点37,这个时候整棵树出现了不平衡现象,根结点13的平衡因子从-1变成了-2。我们想要让这课树平衡,而且要保证该树二叉排序树的性质,那么我们只要将根结点13换为24,结点13作为结点24的左子树,这棵树就又会回到平衡状态,如图e。我们把这种对树做向左逆时针“旋转”的操作称为单向左旋平衡处理。左旋之后,我们发现13、24、37结点的平衡因子都变为0。而且仍然保持着二叉排序树的特性
- 图f当我们继续插入结点90之后,二叉树仍然平衡,只是24、37两个结点的平衡因子变为了-1,再次插入53结点之后,结点37的平衡因子BF由-1变为-2,这意味着该排序树中出现了新的不平衡现象,需要进行调整。但此时由于结点53插在结点90的左子树上,因此不能如上面一样作简单的调整。对于以上结点37为根的子树来说,既要保持二叉排序树的特性,又要平衡,则必须以53结点作为根结点,而使3**7结点成为它左子树的根,**90结点称为它右子树的根。这就好比做了两次旋转,首先我们让37、53、90这棵树单先向右顺时针转变成图g,再像左逆时针变成图h,这样我们的二叉树就能够再次回到平衡状态。对于以上旋转操作我们称为双向旋转(先右后左)平衡处理
- *
平衡算法总结
看完了上面的例子,我们总结一下二叉排序树的不平衡情况以及如何将其转化为平衡情况。
一般情况下,假设由于在二叉排序树上插入结点而失去平衡的最小子树根结点的指针为a(即a是离插入结点最近,且平衡因子绝对值不超过1的祖先结点),则失去平衡后进行调整的规律可以归纳为一下4种情况:
-
单向右旋平衡处理:由于在a的左子树根结点的左子树上插入结点,a的平衡因子由1增加到2,致使以a为根结点的子树失去平衡,则需要进行一次右向顺时针旋转操作。简称LL型旋转
-
单想左旋平衡处理:由于在a的右子树根结点的右子树上插入结点,a的平衡因子由-1增加到-2,致使以a为根结点的子树失去平衡,则需要进行一次左向逆时针旋转操作。简称RR型旋转
-
双向旋转(先左后右)平衡处理:由于在a的左子树的根结点的右子树上插入结点,a的平衡因子由1增加到2,致使a为根结点的子树失去平衡,则需要进行两次旋转(先左旋后右旋)操作。简称LR型旋转
-
双向旋转(先右后左)平衡处理:由于在a的右子树的根结点的左子树上插入结点,a的平衡因子由1增加到2,致使a为根结点的子树失去平衡,则需要进行两次旋转(先右旋后左旋)操作。简称RL型旋转
如何证明我们插入的正确性:中序遍历所得关键字的值序列从小到大即可(二叉排序树的性质)
下列关于m阶B-树的说法错误的是( C) 。
- 根结点至多有m棵子树
- 所有叶子都在同一层次上
- 非叶子结点至少有m/2 (m为偶数)或m/2+1(m为奇数)棵子树
- 根结点中的数据是有序的
B-树(B-tree)是一种常用的查找树,但是不是二叉树。
阶为M的B-树是一颗具有下列结构特性的树:
1.树的根或者是一片树叶,或者其儿子数在2和M之间。
2.除根外,所有非树叶结点的儿子树在【M/2】(向上取整,那个符号不会打)和M之间。(m叉)
3.所有的树叶都在相同的深度上。 (平衡树)
所有的数据都存储在树叶上,树叶包含所有的实际数据,这些数据或者是关键字本身,或者是指向含有这些关键字的记录的指针。
树叶中的关键字是有序的
就平均查找长度而言,折半查找最小,分块查找次之,顺序查找最大。
分块查找,是将顺序表分为若干块,块内元素顺序任意,块间有序,即前一块中的最大值小于后一块中的最小值。并且有一张索引表,每一项存放每一块的最大值和指向该块第一个元素的指针。索引表有序,块内无序。所以,块间查找用二分查找,块内用顺序查找,效率介于顺序和二分之间。
B+树插入操作仅能在叶子结点进行,而不能在非叶子结点进行。
B+树特征
B+ 树是一种树数据结构,是一个n叉树,每个节点通常有多个孩子,一颗B+树包含根节点、内部节点和叶子节点。B+ 树通常用于数据库和操作系统的文件系统中。 B+ 树的特点是能够保持数据稳定有序,其插入与修改拥有较稳定的对数时间复杂度。 B+ 树元素自底向上插入。
一个m阶的B树具有如下几个特征:
1.根结点至少有两个子女。
2.每个中间节点都至少包含ceil(m / 2)
个孩子,最多有m个孩子。
3.每一个叶子节点都包含k-1个元素,其中 m/2 <= k <= m。
4.所有的叶子结点都位于同一层。
5.每个节点中的元素从小到大排列,节点当中k-1个元素正好是k个孩子包含的元素的值域分划。
image.png
在本例中每一个父节点都出现在子节点中,是子节点最大或者最小的元素。而下面的例子中存在如果父结点存储的为子节点最小值,那么便不需要存储第一个子节点的内容。【例如子节点5、8--->10、15--->16、17、18意味着我父节点存10与16即可。而同样的例子如果父节点存最大值,那么便需要存8、15、18 】
在这里,根节点中最大的元素是15,也就是整个树中最大的元素。以后无论插入多少元素要始终保持最大元素在根节点当中。
每个叶子节点都有一个指针,指向下一个数据,形成一个有序链表。
image.png
而只有叶子节点才会有data,其他都是索引。
B+树与B树的区别
- 有k个子结点的结点必然有k个关键码;
- 非叶结点仅具有索引作用,跟记录有关的信息均存放在叶结点中。
- 树的所有叶结点构成一个有序链表,可以按照关键码排序的次序遍历全部记录。
B+树的查询操作
在单元查询的时候,B+树会自定向下逐层查找,最终找到匹配的叶子节点。例如我们查找3 。
image.png
image.png
image.png
而B+树中间节点没有Data数据,所以同样大小的磁盘页可以容纳更多的节点元素。所以数据量相同的情况下,B+树比B树更加“矮胖“,因此使用的IO查询次数更少。
由于B树的查找并不稳定(最好的情况是查询根节点,最坏查询叶子节点)。而B树每一次查找都是稳定的。
比起B树,B+树 ①IO次数更少 ②查询性能很稳定 ③范围查询更简便
下面我放入一个讲解的很好的博客:https://blog.csdn.net/Fmuma/article/details/80287924
B+树的插入操作
①若为空树,那么创建一个节点并将记录插入其中,此时这个叶子结点也是根结点,插入操作结束。
此处的图片中例子的介数为5 。
a)空树中插入5。
image.png
②针对叶子类型结点:根据key值找到叶子结点,向这个叶子结点插入记录。插入后,若当前结点key的个数小于等于m-1(5-1 = 4),则插入结束。否则将这个叶子结点分裂成左右两个叶子结点,左叶子结点包含前m/2个(2个)记录,右结点包含剩下的记录,将第m/2+1个(3个)记录的key进位到父结点中(父结点一定是索引类型结点),进位到父结点的key左孩子指针向左结点,右孩子指针向右结点。将当前结点的指针指向父结点,然后执行第3步。
b)依次插入8,10,15。
image.png
c)插入16
image.png
插入16后超过了关键字的个数限制,所以要进行分裂。在叶子结点分裂时,分裂出来的左结点2个记录,右边3个记录,中间第三个数成为索引结点中的key(10),分裂后当前结点指向了父结点(根结点)。结果如下图所示。
image.png
③针对索引类型结点:若当前结点key的个数小于等于m-1(4),则插入结束。否则,将这个索引类型结点分裂成两个索引结点,左索引结点包含前(m-1)/2个key(2个),右结点包含m-(m-1)/2个key(3个),将第m/2个key进位到父结点中,进位到父结点的key左孩子指向左结点,,进位到父结点的key右孩子指向右结点。将当前结点的指针指向父结点,然后重复第3步。
d)插入17
image.png
e)插入18,插入后如下图所示
image.png
当前结点的关键字个数大于5,进行分裂。分裂成两个结点,左结点2个记录,右结点3个记录,关键字16进位到父结点(索引类型)中,将当前结点的指针指向父结点。
image.png
f)插入若干数据后
image.png
g)在上图中插入7,结果如下图所示
image.png
当前结点的关键字个数超过4,需要分裂。左结点2个记录,右结点3个记录。分裂后关键字7进入到父结点中,将当前结点的指针指向父结点,结果如下图所示。
image.png
当前结点的关键字个数超过4,需要继续分裂。左结点2个关键字,右结点2个关键字,关键字16进入到父结点中,将当前结点指向父结点,结果如下图所示。
image.png
当前结点的关键字个数满足条件,插入结束。
此处参考了:https://blog.csdn.net/Fmuma/article/details/80287924
B+树的删除操作
下面是一颗5阶B树的删除过程,5阶B数的结点最少2个key,最多4个key。
如果叶子结点中没有相应的key,则删除失败。否则执行下面的步骤。
①删除叶子结点中对应的key。删除后若结点的key的个数大于等于Math.ceil(m/2) – 1(>=2),删除操作结束,否则执行第2步。
a)初始状态
image.png
b)删除22,删除后结果如下图
image.png
删除后叶子结点中key的个数大于等于2,删除结束。
②若结点的key的个数小于Math.ceil(m/2) – 1(<2),且兄弟结点key有富余(大于Math.ceil(m/2)– 1)(>2),向兄弟结点借一个记录,同时用借到的key替换父结(指当前结点和兄弟结点共同的父结点)点中的key,删除结束。否则执行第3步。
c)删除15,删除后的结果如下图所示。
image.png
删除后当前结点只有一个key,不满足条件,而兄弟结点有三个key,可以从兄弟结点借一个关键字为9的记录,同时更新将父结点中的关键字由10也变为9,删除结束。
③若结点的key的个数小于Math.ceil(m/2) – 1(<2),且兄弟结点中没有富余的key(小于Math.ceil(m/2)– 1),则当前结点和兄弟结点合并成一个新的叶子结点,并删除父结点中的key,将当前结点指向父结点(必为索引结点),执行第4步(第4步以后的操作和B树就完全一样了,主要是为了更新索引结点)。
d)删除7,删除后的结果如下图所示
image.png
当前结点关键字个数小于2,(左)兄弟结点中的也没有富余的关键字(当前结点还有个右兄弟,不过选择任意一个进行分析就可以了,这里我们选择了左边的),所以当前结点和兄弟结点合并,并删除父结点中的key,当前结点指向父结点。
image.png
④若索引结点的key的个数大于等于Math.ceil(m/2) – 1(>=2),则删除操作结束。否则执行第5步。
⑤若兄弟结点有富余,父结点key下移,兄弟结点key上移,删除结束。否则执行第6步
⑥当前结点和兄弟结点及父结点下移key合并成一个新的结点。将当前结点指向父结点,重复第4步。
此时当前结点的关键字个数小于2,兄弟结点的关键字也没有富余,所以父结点中的关键字下移,和两个孩子结点合并,结果如下图所示。
image.png
注意,通过B+树的删除操作后,索引结点中存在的key,不一定在叶子结点中存在对应的记录。
哈希函数的选取平方取中法最好(错)
哈希函数的构造方法无好坏之分
散列函数的构造方法
1、散列函数的选择有两条标准:简单和均匀。
简单指散列函数的计算简单快速;
均匀指对于关键字集合中的任一关键字,散列函数能以等概率将其映射到表空间的任何一个位置上。也就是说,散列函数能将子集K随机均匀地分布在表的地址集{0,1,…,m-1}上,以使冲突最小化。
2、常用散列函数
为简单起见,假定关键字是定义在自然数集合上。
(1)平方取中法
具体方法:先通过求关键字的平方值扩大相近数的差别,然后根据表长度取中间的几位数作为散列函数值。又因为一个乘积的中间几位数和乘数的每一位都相关,所以由此产生的散列地址较为均匀。
【例】将一组关键字(0100,0110,1010,1001,0111)平方后得
(0010000,0012100,1020100,1002001,0012321)
若取表长为1000,则可取中间的三位数作为散列地址集:
(100,121,201,020,123)。
相应的散列函数用C实现很简单:
int Hash(int key){ //假设key是4位整数
key*=key; key/=100; //先求平方值,后去掉末尾的两位数
return key%1000; //取中间三位数作为散列地址返回
}
(2)除余法
该方法是最为简单常用的一种方法。它是以表长m来除关键字,取其余数作为散列地址,即 h(key)=key%m
该方法的关键是选取m。选取的m应使得散列函数值尽可能与关键字的各位相关。m最好为素数。
【例】若选m是关键字的基数的幂次,则就等于是选择关键字的最后若干位数字作为地址,而与高位无关。于是高位不同而低位相同的关键字均互为同义词。
【例】若关键字是十进制整数,其基为10,则当m=100时,159,259,359,…,等均互为同义词。
(3)相乘取整法
该方法包括两个步骤:首先用关键字key乘上某个常数A(0<A<1),并抽取出key.A的小数部分;然后用m乘以该小数后取整。即:
该方法最大的优点是选取m不再像除余法那样关键。比如,完全可选择它是2的整数次幂。虽然该方法对任何A的值都适用,但对某些值效果会更好。Knuth建议选取
该函数的C代码为:
int Hash(int key){
double d=key *A; //不妨设A和m已有定义
return (int)(m*(d-(int)d));//(int)表示强制转换后面的表达式为整数
}
(4)随机数法
选择一个随机函数,取关键字的随机函数值为它的散列地址,即
h(key)=random(key)
其中random为伪随机函数,但要保证函数值是在0到m-1之间。
采用线性探测法处理散列时的冲突,当从哈希表删除一个记录时,不应将这个记录的所在位置置空,因为这会影响以后的查找
该方法的基本思想是:
将散列表T[0..m-1]看成是一个循环向量,若初始探查的地址为d(即h(key)=d),则最长的探查序列为:
d,d+l,d+2,…,m-1,0,1,…,d-1
即:探查时从地址d开始,首先探查T[d],然后依次探查T[d+1],…,直到T[m-1],此后又循环到T[0],T[1],…,直到探查到T[d-1]为止。
探查过程终止于三种情况:
(1)若当前探查的单元为空,则表示查找失败(若是插入则将key写入其中);
(2)若当前探查的单元中含有key,则查找成功,但对于插入意味着失败;
(3)若探查到T[d-1]时仍未发现空单元也未找到key,则无论是查找还是插入均意味着失败(此时表满)。
用线性探测法处理冲突,思路清晰,算法简单,但存在下列缺点:
① 处理溢出需另编程序。一般可另外设立一个溢出表,专门用来存放上述哈希表中放不下的记录。此溢出表最简单的结构是顺序表,查找方法可用顺序查找。
② 按上述算法建立起来的哈希表,删除工作非常困难。假如要从哈希表 HT 中删除一个记录,按理应将这个记录所在位置置为空,但我们不能这样做,而只能标上已被删除的标记,否则,将会影响以后的查找。
③ 线性探测法很容易产生堆聚现象。所谓堆聚现象,就是存入哈希表的记录在表中连成一片。按照线性探测法处理冲突,如果生成哈希地址的连续序列愈长 ( 即不同关键字值的哈希地址相邻在一起愈长 ) ,则当新的记录加入该表时,与这个序列发生冲突的可能性愈大。因此,哈希地址的较长连续序列比较短连续序列生长得快,这就意味着,一旦出现堆聚 ( 伴随着冲突 ) ,就将引起进一步的堆聚。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。