一、概念
**1、动态查找表/静态查找表:**若在查找的同时对表做修改操作(如插入和删除),则相应的表称之为 动态查找表,否则称为 静态查找表。
**2、平均查找长度(Average Search Length, ASL):**为确定记录在查找表中的位置,需和给定值进行比较的关键字个数的期望值,称为查找算法在查找成功时的 平均查找长度。
二、线性表的查找
1、顺序查找
1)顺序查找(Sequential Search):从表的一端开始,依次将记录的关键字和给定值进行比较,若某个记录的关键字和给定值相等,则查找成功,反之,若扫描整个表后,仍未找到关键字和给定值相等的记录,则查找失败。
2)顺序查找伪代码
//线性表 顺序存储表示
typedef struct{
KeyType key;
InfoType otherinfo;
}ElemType; //数据元素的表示
typedef struct{
ElemType *R;
int length;
}SSTable;
//顺序查找(不带 监视哨)
int Search_Seq(SSTable ST,KeyType key){
for(i=ST.length;i>=1;--i){ //注意ST.R[0]不储存任何东西
if(ST.R[i].key == key){return i;}
}
return 0;
}
//顺序查找(带 监视哨)
int Search_Seq(SSTable ST,KeyTpe key){
ST.R[0].key = key;
for(i=ST.length;ST.R[i].key != key;--i);
return i;
}
总结:
0、顺序查找 即可采用 顺序存储结构,也可采用 链式存储结构;
1、顺序查找 的平均查找长度为 1/n* sum(i) = (n+1)/2;其时间复杂度为O(n);
2、当 ST.length = 1000时,带监视哨的顺序查找 较之一般的顺序查找,其进行一次查找所需的平均时间几乎减少一半;
3、顺序查找 算法简单,但是,其平均查找长度大,查找效率低,所以当ST.length很大时,不宜采用 顺序查找;
2、折半查找
1)折半查找(Binary Search)也称二分查找,是一种效率较高的查找方法。但是,折半查找要求线性表 必须采用 顺序存储结构,而且表中元素需按关键字有序排列。
折半查找过程:从表的中间记录开始,如果给定值和中间记录关键字相等,则查找成功,否则,如果给定值>(<)中间记录关键字,则在表中大于(小于)中间记录的那一半中查找,这样重复操作,直到查找成功,或者在某一步中查找区间为空,则代表查找失败。
2)折半查找伪代码
//折半查找 顺序存储表示
typedef struct{
KeyType key;
InfoType otherinfo;
}ElemType; //数据元素 存储表示
typedef struct{
ElemType *R;
int length;
}SSTabel; //线性表 顺序存储表示
int Search_Bin(SSTable ST,KeyType key){
//ST.R[0]位置不存储 任何东西
low = 1;
hight = ST.length;
while(low<=high){
mid = (low + hight)/2;
if(ST.R[mid].key == key){return mid;}
else if(ST.R[mid].key > key){high = mid-1;}
else{low = mid+1;}
}
return 0;
}
总结:
1、折半查找相当于一棵 二叉树,其查找成功最多经过 log2n + 1 次比较,因此,折半查找的时间复杂度为O(log2n)。
2、折半查找 效率 比 顺序查找高,但折半查找 在查找之前 需要对 表进行排序,且折半查找仅适用于顺序存储结构。因此,对于数据元素经常进行变动的线性表,不适于用 折半查找。
3、分块查找
1)分块查找:分块查找 要建立一个 索引表,索引表中记录着每块中的最大关键字值。使用分块查找,首先 可以在索引表中利用折半查找,查找Key所在的块,然后,在块中进行顺序查找,查找Key。
总结:
1、分块查找 的平均查找长度 不仅与数据元素个数n有关,而且与块中元素个数s有关,如索引表使用折半查找,则分块查找的平均查找长度为:log2(n/s + 1) + s/2;
2、分块查找优点:可以很方便的在表中 插入 或 删除 数据元素,只要找到 该元素
对应的块,就可以在该快内进行插入和删除运算。由于快内是无序的,故插入和删除比较容易,无需进行大量移动。如果线性表要经常变动,则可采用分块查找。
其缺点是:分块查找 要增加一个索引表的存储空间,并对初始索引表要进行排序运算。
二、树表的查找
1、二叉排序树
1)二叉排序树:是一种有序树,根节点的左子树所有结点的值均小于根节点值,右子树所有节点的值均大于根节点值;
2)二叉排序树的 查找、插入、创建、删除
二叉排序树 的存储表示
typedef struct{
KeyType key;
InfoType otherinfo;
}ElemType; //数据元素表示形式
typedef struct BSTNode{
ElemType data;
struct BSTNode *lchild,*rchild;
}BSTNode,*BSTree;
- 查找
BSTree SearchBST(BSTree T,KeyType key){
p=T;
if(!p || p->data.key = key){return p;}
else if(p->data.key > key){SearchBST(p->lchild,key);}
else if(p->data.key < key){SearchBST(p->rchild,key);}
}
- 插入
void InsertBST(BSTree &T,ELemType e){
if(!T){
S = new BSTNode;
S->lchild = S->rchild = NULL;
S->data = e;
T = S;
}
else if(T->data.key > e.key){InsertBST(T->lchild,e);}
else if(T->data.key < e.key){InsertBST(T->rchild,e);}
}
- 创建
void CreatBST(BSTree &T){
T = NULL;
cin>>e;
while(e.key != ENDFLAG){
InsertBST(T,e);
cin>>e;
}
}
- 删除
删除的结点有以下几种情况:
1)删除结点p 只有 lchild or rchild,这种情况下,直接将p=lchild / rchild即可,p的双亲f与p连接即可;
2)删除结点p 既有lchild and rchild,这种情况下将p的左子树中 右下方的 结点s 转到p上,然后将s的左子树转到其s双亲结点的rchild即可。
删除结点伪代码
void DeleteBST(BSTree &T,KeyType key){
p=T;f=NULL;
//寻找key结点
while(p){
if(p->data.key == key){break;}
f=p; //f为所要找的Key的双亲结点
if(p->data.key > key){p = p->lchild;}
else{p = p->rchild;}
}
if(!p){return;} //如果P为NULL,则说明没有找到要删除的结点,返回
q = p; //此时记录的是 所要找的key结点;
//如果所要找的key结点同时有 lchild和rchild,将左子树最右下的结点赋给p,并将最右下结点的lchild赋给其双亲rchild
if(p->lchild && p->rchild){
s = p->lchild;
while(s->rchild){ //找到左子树的最右下 结点
q = s;
s = s->rchild;
}
//p->data = s->data; 可以把这一句提出来
if(q != p){
p->data = s->data; //将左子树最右下 结点值 赋给 所找key结点
q->rchild = s->lchild; //将最右下结点 lchild赋给 其双亲 rchild
}
else{ //若果 key结点 的 lchild和rchild 只是一个叶子节点
p->data = s->data;
p->lchild = s->lchild;
}
delete s;
return;
}
else if(!p->rchild){ //如果 key结点只有 左孩子
p = p->lchild;
}
else if(!p->lchild){ //如果 key结点只有 右孩子
p = p->rchild;
}
if(!f){T=p;} //如果f为空的话,说明key结点为根节点,则将修改后的p赋给T
else if(f->lchild == q){ //如果key结点为其双亲的 lchild
f->lchild = p;
}
else if(f->rchild == q){ //如果key结点为其双亲的 rchild
f->rchild = p;
} //修改f的最后两种情况是 限于 key结点只有 lchild or rchild
delete q; //删除key结点的copy
}
总结:
1、二叉排序树的 查找、插入、删除 操作 均需找到 key元素,因此,三种算法的时间复杂度均为O(log2n);
2、当要创建有n个结点 的二叉排序树时:插入一个结点需要log2n,则插入n个结点的时间复杂度为O(nlog2n);
2、平衡二叉树
1)平衡二叉树(Balanced Binary Tree / Height-Balanced Tree / AVL树):
左右子树的深度之差 绝对值 不超过1;
左右子树也是平衡二叉树;
2)平衡因子(Balance Factor,BF):该结点 左子树和右子树 的深度之差。则平衡因子 只可能是 -1 0 1;
3)平衡二叉树的调整方法:
如何创建一棵平衡二叉树呢?插入结点时,首先按照二叉排序树处理,若插入结点后破坏了平衡二叉树的特性,需对平衡二叉树进行调整。调整方法是:找到离插入节点最近且平衡因子绝对值超过1的祖先结点,以该节点为根的子树称为最小不平衡子树。可将重新平衡的范围局限于这可子树。
- LL型:
- RR型
- LR型
- RL型
三、B-树 and B++树
1、B-树
- B-树的存储表示
#define 3 m
typedef struct BTNode{
int keynum; //关键字的个数
struct BTNode *parent; //结点的双亲结点
KeyType K[m]; //m-1个关键字存储;K[0]不用
struct BTNode *ptr[m+1]; //存储m个子树;K[0]不用
struct BTNode *recptr[m]; //记录m-1个关键字的信息;recptr[0]不用
}BTNode,*BTree;
typedef struct{
BTNode *pt;
int i; //关键字下标
int tag; //是否查找成功
}Result; //查找结果
1)m阶B-树的定义
- 树中每个结点至多有m棵子树;
- 若根节点不是叶子节点,则根节点至少有2个子树;
- 除根节点外的非终端结点至少有m/2(取上)棵子树;
- 非终端结点关键字 最多有 m-1个;
- 所有的叶子节点都出现在同一层次上, 并且不带信息,为失败节点;
- 结点某关键字左子树中的关键字 均小于 该关键字,右子树中的关键字 均大于 该关键字;
2)B-树的查找
B-树的查找分为2步:结点的查找;关键字的查找;
Result SearchBTree(BTree T,KeyType key){
p = T;
while(p){
i = Search(p,key); //返回:p.K[i] <= key < p.K[i+1]
if(i>0 && p->K[i] == key){return (p,i,1);}
else{
q = p;
p = p->ptr[i];
}
}
if(!p){return (q,i,0);} //返回 要插入的结点
}
总结:
1、B-树的查找分为2步:结点查找 ; 关键字查找;
其中结点查找是在磁盘进行,关键字查找 是将 结点中关键字 移入内存进行找;
显然 外存查找 要比 内存查找耗时,因此:
B-树查找的效率主要看 结点查找,即:待查找结点 在B-树中的层次,决定了B-树的查找效率;
2、B-树中,树深h <= logm/2((N+1)/2) + 1; m/2 取上;
3)B-树的插入
当向B-树中插入一个关键字后,如果:
condition1:该结点中关键字个数 <= m-1,则不需要做任何变动;
condition2:该节点中关键字个数 > m-1,则将该节点中 局中的关键字 上移到 双亲结点,并且 将该节点一分为二,作为上移结点的左右子树;
如果双亲结点 关键字个数 也溢出,则同样 做 上移操作;
Status InsertBTree(BTree &T,KeyType key,BTree q,int i){
//将key插入结点q->K[i]和q->K[i+1]之间
x = key;
ap = NULL;
finished = FALSE;
while(q && !finished){
Insert(q,i,x,ap); //将key插入结点q->K[i]与K[i+1]之间,并且添加指针ap
if(q.keynum <= m-1){finished = true;} //如果q结点关键字没有溢出,则成功插入,否则,分裂,上插
else{
s = (m+1)/2;x= q->K[s];
split(q,s,ap); //将结点q切分,并将q->K[i+1],...,q->ptr[i],...,q->recptr[i+1],....都装入ap中
q = q->parent;
if(q){i = Search(q,x);} //搜索x在q中的插入位置
}
}
if(!finished){ //如果T是空树,或者已经上插到根节点
NewRoot(T,q,x,ap);
}
return OK;
}
4)B-树的删除
删除B-树中一个结点的关键字,则将此关键字删除后,可将其左(右)结点中最大(小)的关键字上移,重复执行此操作,直到最后一层非终端结点。
删除最后一层非终端结点的关键字后,分以下几种情况讨论:
condition1:如果该结点关键字个数>= m/2(取上) - 1,则不用做任何操作,删除完成;
condition2:如果该节点关键字个数<m/2(取上) - 1,且其兄弟结点关键字个数>m/2(取上) - 1,则将其兄弟结点关键字上移,双亲结点关键字下移;
如下图删除 50:
condition3:如果该节点关键字个数<m/2(取上) -1,且其兄弟结点关键字个数=m/2(取上) - 1,则将 该节点 与 兄弟结点,以及他们的双亲结点中 关键字 一起合并;
如下图 删除 53:
condition4:需要多次进行 condition3操作:如下图 删除 37:
2、B+树
1)一棵m阶的B+树和B-树的区别:
-有n棵子树的结点中有n个关键字;
- 所有叶子节点中包含了全部的关键字信息,以及指向这些关键字记录的指针,且叶子结点中关键字 按 从小到大 的顺序 排列;
- 所有的非终端结点可以看成是 索引部分,结点中仅含有其子树中的最大(或最小)关键字;
- 下图为一B+树,其上有2个头指针,一个指向根节点,一个指向关键字最小的叶子节点;
2)B+树的 查找、插入、删除 - 查找:和B-树类似,但是 B+树的查找成功与否,都会到达叶子节点;
- 插入:在叶子结点中插入关键字,如果 关键字个数溢出,则一分为二,每个结点有(m+1)/2个关键字,且其双亲结点中多出2个关键字,分别为这2个分裂结点中的最大值/最小值;
- 删除:在叶子节点中进行删除操作,如果删除后的叶子结点关键字个数 < m/2,其处理方法与B-树类似;
四、散列表的查找(Hash Search)
1、基本概念
1)散列查找法:如果能在元素的存储位置和其关键字之间建立某种直接关系,那么在进行查找时,就无需做比较或做很少次的比较,按照这种关系直接由关键字找到相应的记录,这就是 散列查找法(Hash Search)。又称 杂凑法 或 散列法。
2)散列函数 和 散列地址:在记录的存储位置p 和 其关键字key之间建立一个确定的对应关系H,使p=H(key),称这个对应关系H为散列函数,p为散列地址;
3)散列表:一个有限连续的地址空间,用以存储按散列函数计算得到相应散列地址的数据记录。
4)冲突和同义词:如果不同关键字的散列地址 一样,称这种现象为冲突。散列地址相同的 不同关键字 称为 同义词。
在散列查找中,主要需要解决2个问题:
1)如何构造散列函数问题;
2)如何处理冲突问题;
2、散列函数的构造
- 数字分析法:从关键字中取出其中几位 求和 舍进位 后,将最后的 value作为散列地址;
- 平方取中法:取 关键字平方后 的其中几位 作为 散列地址;
- 折叠法:将关键字 分割为 几部分,然后将这几部分 叠加,叠加和 作为最后的散列地址;
- 除留余数法:H(key) = key % p;p可选小于表长的最大质数;
3、处理冲突的方法
- 开放地址法
存储key时,当散列地址冲突时,可用Hi = (H(key)+di)%m 寻找下一个 散列地址:
1)线性探索法
di = 1,2,3,…,m-1
线性探测法 可能会出现 二次聚集(堆积) 现象:即:第一次散列地址不同的两个关键字 争夺 同一个 后继散列地址;
2)二次探测法
di = 12, -12, 22, -22, 32, -32,…
3)伪随机探测法
di为 伪随机数字
二次探测法 和 伪随机法 可以有效避免 二次聚集 现象,但是,缺点也很明显,即:不能确保一定找到 不发生冲突的地址; - 链地址法:把具有相同散列地址的记录放在同一个单链表中,称为同义词链表;
4、散列表的查找
#define 20 m
typedef struct{
KeyType key;
InfoType otherinfo;
}HashTable[m];
//采用 线性探测法 查找key值
int SearchHash(HashTable HT,KeyType key){
H0 = H(key); //利用散列函数求出key的散列地址
if(HT[H0].key == key){return H0;}
else if(HT[H0].key == NULLKEY){return -1;}
else{
for(i=1;i<m;++i){
H0 = (H(key)+i)/m;
if(HT[H0].key == key){return H0;}
else if(HT[H0].key == NULLKEY){return -1;}
}
return -1;
}
}
总结:
1、散列表查找的时间复杂度与 发生冲突的次数有关,因此,实际测评 依然采用 平均查找长度 作为衡量标准。
2、查找过程需和给定值比较的关键字的个数 取决于:散列函数、处理冲突方法、散列表的装填因子:
装填因子 = 表中填入的记录数 / 散列表的长度
装填因子 越大,说明发生冲突的可能性越高,查找时,需和给定值进行比较的关键字个数就越多。
3、散列表的 平均查找长度 是 装填因子 的函数,而不是 记录个数n的函数:
参考资料:《数据结构》 严蔚敏