一、顺序查找 O(n)
一般线性表的顺序查找 有哨兵
typedef struct{
ElemType *elem; //存储空间基址,建表时按实际长度分配,0号单元留空
int TableLen;
}SSTable;
int Search_Seq(SSTable ST,ElemType key)
{
ST.elem[0] = key; //哨兵
for(int i = ST.TableLen;ST.elem[i] != key;i--);//从后往前找
return i;//若不存在关键字为key的元素,将查找到i为0时退出for循环
}
二、折半查找 O(logn)
int Binary_Search(SSTable L,ElemType key)
{
int low = 0,high = L.TableLen-1,mid;
while(low <= high)
{
mid = (low + high) / 2;
if(L.elem[mid] == key) return mid; //查找成功
else if(L.elem[mid] > key) high = mid - 1;//前半部分继续查找
else low = mid + 1;//后半部分继续查找
}
return -1;//查找失败
}
三、分块查找
四、B树和B+树 不考算法 考察二者概念及区别
B树
m阶B树是所有结点的平衡因子均等于0的m路平衡查找树
阶数主要看结点中关键字个数的最大值,不要看子树个数
特性
- [m/2] ≤ 非叶结点的子树个数 ≤ m
- 2 ≤ 根结点的子树个数 ≤ m
- 关键字个数 = 子树个数 - 1
- 所有叶结点都出现在同一层次上,并且不带信息(可以视为外部结点或查找失败结点),实际上这些结点不存在,指向这些结点的指针为空
- 总的叶结点数 = 总的关键字数 + 1
非叶结点结构
其中,Ki(i = 1,2,…,n)为结点的关键字,且满足K1<K2<…<Kn;Pi为指向子树根结点的指针,且指针Pi-1所指子树中所有结点的关键字均小于Ki,Pi所指所指子树中所有结点的关键字均大于Ki,n([m/2]-1≤n≤m-1)为结点中关键字的个数。
B树的高度
问题:含n个关键字的m阶B树,最小高度、最大高度是多少?
最小高度——让每个结点尽可能的满,有m-1个关键字,m个分叉,则有:
n ≤ (m-1)*(1+m+m ^ 2+……m ^ h-1)
最大高度——让各层分叉尽可能的少,即根结点只有2个分叉,其他结点只有[m/2]个分叉
各层结点至少有:第一层 1、第二层 2、第三层 2[m/2]……第h层 2([m/2])^(h-2)
第h+1层共有叶子结点(失败节点)2([m/2])^(h-1)
n个关键字的B树必有n+1个叶子结点,则n+1 ≥ 2([m/2])^(h-1),即h≤log[m/2] ((n+1)/2+1)
B树的插入
[m/2] - 1 ≤ 非叶结点的关键字个数 ≤ m - 1,插入结点可能导致整棵树不满足B树定义中的要求。插入后的关键字个数小于m,可以直接插入;插入后关键字个数大于m-1时,必须对结点进行分裂。
分裂方法
- 取一个新结点,在插入key后的原结点,从中间位置([m/2])将其中的关键字分为两部分
- 左部分包含的关键字放在原结点中,右部分包含的关键字放到新结点中,中间位置[m/2]的结点插入原结点的父结点
- 若导致其父结点的关键字个数也超过了上限,则继续进行这种分裂操作,直至这个过程传到根结点为止,进而导致B树高度加1(不是一定发生)
B树的删除
删除结点可能导致删除后的结点中的关键字个数<[m/2] - 1,因此设计结点的“合并”问题
三种情况
直接删除:被删关键字所在结点的关键字个数≥[m/2],直接删去
兄弟够借:被删关键字所在结点关键字个数=[m/2] - 1,且该结点的相邻兄弟结点关键字个数≥[m/2],则需要调整该结点、右(或左)兄弟结点及其双亲结点(父子换位法),以达到新的平衡
兄弟不够借:被删关键字所在结点关键字个数=[m/2] - 1,且左右兄弟结点的关键字个数均=[m/2] - 1,则将关键字删除后与左(右)兄弟结点及双亲结点中的关键字进行合并
B+树
B+树是数据库、操作系统文件(索引)中使用的一种B树的变形树,支持从最小关键字开始的顺序查找和从根结点开始的多路查找
特点/条件
- [m/2] ≤ 非叶结点的子树个数 ≤ m,2 ≤ 根结点的子树个数 ≤ m
- 结点的子树个数与关键字个数相等
- 所有叶结点包括全部关键字及指向相应记录的指针,所有叶结点均在同一层上。叶结点中将关键字按大小顺序排序,且相邻叶结点按大小顺序相互链接起来
- 所有分支结点(索引的索引)中只包含它的各个子节点中关键字的最大值及指向其子结点的指针
与B树的主要差别
- B+树中,n个关键字的结点有n个子树;B树中,n个关键字的结点有n+1个子树;
- B+树中,[m/2] ≤ 非叶结点的关键字个数 ≤ m,1 ≤ 根结点的关键字个数 ≤ m;B树中,[m/2] - 1 ≤ 非叶结点的关键字个数 ≤ m - 1,1 ≤ 根结点的关键字个数 ≤ m - 1
- B+树中,叶结点包含了全部关键字,非叶结点中出现的关键字也会重复出现在叶结点中;B树中,叶结点和非叶结点关键字不重复
- B+树中,叶结点包含信息,所有非叶结点仅起索引作用
- B+树支持顺序查找、随机查找;而B树只支持随机查找
五、散列表(哈希表)
散列函数构造方法
直接定址法(一般不会遇到)
直接取关键字的某个线性函数值为散列地址,散列函数为H(key) = key or H(key) = a*key + b
优点:计算最简单,且不会产生冲突,适合关键字分布基本连续的情况
缺点:若关键字分布不连续,空位较多,则会造成存储空间的浪费
除留取余法(最简单、最常用)
假设散列表表长为m,取一个不大于m但最接近或等于m的质数p,散列函数为H(key) = key % p
除留取余法的关键是选好p,从而尽可能减少冲突的可能性
数字分析法
选取分布较为均匀的若干位作为散列地址,此方法适合于已知的关键字集合,若更换了关键字,则需要重新构造散列函数
平方取中法
取关键字的平方值的中间几位作为散列地址,因此使得散列地址分布比较均匀
冲突处理的方法
开放定址法(数组) 不能随便物理删除,否则截断其它具有相同散列地址的元素的查找地址
线性探测法(线性探测再散列)
冲突发生时,顺序查看表中下一个单元(探测到表尾时,下个探测地址是表首地址0),直到找出一个空闲单元或查遍全表
评价:线性探测法可能使第i个散列地址的同义词存入第i+1个散列地址,从而导致大量元素(同义词或非同义词)在相邻的散列地址上聚集(堆积)起来,大大降低查找效率
Hi = (H(key) + di) % m
平方探测法(二次探测再散列)
di = 02,12,-12,22,-22,...,k2,-k2,其中k≤m/2,散列表长度必须是一个可以表示成4k+3的素数
评价:平方探测法可以避免出现“堆积”问题,其缺点是不能探测到散列表上的所有单元,但至少能探测到一半单元
双散列法(两个散列函数)
di = Hash2(key),需要使用两个散列函数,当发生冲突时,利用第二个散列函数Hash2(key)计算地址增量
Hi = (H(key) + i*Hash2(key)) % m
,i是冲突的次数,初始为0,双散列中,最多经过m-1次探测就会遍历表中所有位置,回到H(key) % m位置
伪随机序列法
di = 伪随机数序列
拉链法(链接法) 允许物理删除
把所有的同义词存储在一个线性链表中,这些线性链表由其散列地址唯一标识。
拉链法适用于经常进行插入和删除的情况
散列查找及性能分析
散列表的查找效率取决于三个因素:散列函数、处理冲突的方法和装填因子
装填因子α = 表中记录数n / 散列表长度m
查找成功
开放寻址法(数组)
对于散列函数为H(key) = key % p
,表中记录数为n的散列表而言,ASL = ∑查找次数/n,查找次数与冲突处理方法有关。
拉链法(链表)
与元素位置有关,ASL = ∑查找次数/n
查找失败
开放寻址法(数组)
对于散列函数为H(key) = key % p
,表中记录数为n的散列表而言,查找失败对应p种情况,即表中每个位置对应一类失败元素,故ASL = ∑查找次数/p
查找次数与冲突处理方法及表中元素分布有关,一般查找到空位置时认定查找失败。不同处理方法的查找次数不同。
拉链法(链表)
当对应哈希地址链表为空时,认为比较0次(比较次数只看比较的元素个数,而不算链表头结点,也不算链表尾部的空结点)
公式:ASL = n/p
六、易错总结
- 折半查找和二叉排序树的时间性能有时不相同,因为二叉排序树的结构可能为一边倒的线性结构(O(n))
- 折半查找算法中,查找失败的结点长度等于根结点到达其父结点的路径长度
- 对于给出一棵二叉树结构,判断是否为折半查找判定树的方法:数结点总数,为[n/2],对树分别按照下取整和上取整的编号方式遍历,若均遍历成功,则可为折半查找树
- B树、B+树所有叶结点均在同一层上
- B树插入过程中,尽量将元素插入到最下层结点
- 开放寻址法中散列到同一地址而引起的堆积问题是由同义词之间或非同义词之间的冲突引起的
- 散列表查找成功时的平均查找长度与装填因子α有关
- 开放寻址法发生聚集的原因主要是解决冲突的方法选择不当
- 拉链法可以物理删除,开放寻址法只能逻辑删除
- 拉链法计算平均查找长度时只计算元素的个数,而不管头结点或空结点