查找表可以分为以下两种:
关键字是数据元素(或记录)中的某个数据项的值,用来标记一个数据元素(或记录)
查找的方法取决于查找表的结构
由于查找表中的数据元素之间不存在明显的组织规律,因此不便于查找
为了提高查找的效率,需要在查找表中的元素之间人为的附加某种确定的关系也就是说使用另外一种结构来表示查找表。
对于静态查找表:
根据表元素的特点可以采用顺序查找,折半查找
对于动态查找表:
通常将序列构造为一棵二叉树,一般是均衡二叉树(AVL树),这样常见的查找操作可以转为对二叉树的操作
特殊的,在存储位置和关键字之间建立一个确定的对应关系——需要用到哈希函数和哈希查找
静态查找表的顺序存储结构:
typedef struct {
ElemType *elem;
//数据元素村粗空间基址
//建表时按实际长度分配 0号单元留空
int length; //表的长度
}SSTable;
数据元素类型的定义:
typedef struct {
keyType key; //关键字域
………… //其他属性域
}ElemType;
int location(SqList L,ElemType e,Status(*compare)(ElemType,ElemType)) {
k = l;
p = L.elem;
while(k<=L.length && !(*compare)(*++p.c))
k++;
if(k<=L.length)
retutn k;
else
return 0;
} //location
int Search_Seq(SSTable ST,KeyType key) {
//顺序表ST中顺序表查找其关键字等于key的数据元素
//若找到则函数值为该元素在表中的位置 否则为0
ST.elem[0].key = key; //哨兵
for(i=ST.length; ST.elem[i].key!=key;--i);
//从后向前找
//找不到时i=0
return i;
}
分析顺序查找的时间性能:
定义:查找算法在查找成功时的平均查找长度(Average Search Length)
为确定记录在查找表中的位置,需要和给定值进行比较的关键字个数的期望值:
ASL = nP1+(n-1)P2+… +2Pn-1+Pn
顺序表查找的平均查找长度:
在不等概率的情况下,ASL在Pn ≥ Pn-1 ≥ ꞏꞏꞏ ≥ P2 ≥ P1 时取最小值。
总结:
若查找概率无法事先测定,则查找 过程采取的改进办法是,在每次查找之后,将刚刚查找到的记录直接移至表尾 的位置上。
虽然顺序查找表的查找算法简单,但是平均查找长度大,特别不适合于表长较大的查找表
如果以有序表表示静态查找表,则查找过程可以基于折半查找。
int Search_Bin(SSTable ST,KeyType key) {
low = 1; high = ST.length; //置区间初值
while(low<=high) {
mid = (low+high)/2;
if(EQ(key,ST.elem[mid].key))
return mid; //找到待查元素
else if(LT((key , ST.elem[mid].key))
high = mid -1; // 继续在前半区间进行查找
else
low = mid + 1; // 继续在后半区间进行查找
}
return0; // 顺序表中不存在待查元素
}
分析折半查找的平均查找长度:
有n个结点的判定树的深度为【log2n】+1
折半查找法在查找过程中进行的比较次数 最多不超过其判定树的深度
一般情况下,表长为n 的折半查找 的判定树的深度和含有n 个结点的完全 二叉树的深度相同
索引顺序表 = 索引 + 顺序表
一般情况下,索引是一个有序表
索引顺序表的查找过程(也叫分块查找特点:块间有序,块内无序)
可见,索引顺序查找的过程也是一个 “缩小区间”的查找过程。
算法实现:
分块查找的过程:
索引顺序查找的平均查找长度= 查找“索引”的平均查找长度 + 查找“顺序表”的平均查找长度
分块查找优缺点:
优点:插入和删除比较容易,无需进行大量移动。
缺点:要增加一个索引表的存储空间并对初始索引 表进行排序运算
使用情况:如果线性表既要快速查找又经常动态变 化,则可采用分块查找
从这几种查找表的特性可以看出:
从查找性能看,最好情况能达(logn),此 时要求表有序
从插入和删除的性能看,最好 情况能达(1),此时要求存储结构是链表。
定义:
二叉排序树或者是一棵空树;或者是具有如 下特性的二叉树:
通常取它的左、右子树也都分别是二叉 排序树
typedef struct BiTNode { //结点结构
TElemType data;
struct BiTNode *lchild,*rchild; //左右孩子指针
}BiTNode,*BiTree;
若二叉排序树为空,则查找不成功;否则
算法:
在根指针T 所指二叉排序树中递归地查找其 关键字等于key 的数据元素,若查找成功, 则返回指针p 指向该数据元素的结点,并返回 函数值为TRUE;否则表明查找不成功,返回 指针p 指向查找路径上访问的最后一个结点, 并返回函数值为FALSE, 指针f 指向当前访问 的结点的双亲,其初始调用值为NULL
Status SearchBST(BiTree T,KeyType key,BiTree f,BiTree &p) {
if(!T) {
p = f;
return FALSE; //查找不成功
}
else if(EQ(key,T->data.key)) {
p = T;
return TRUE;
} //查找成功
else if (LT(key,T->data.key))
SearchBST (T->lchild, key, T, p ); // 在左子树中继续查找
else
SearchBST (T->rchild, key, T, p ); // 在右子树中继续查找
}
二叉排序树的操作-生成
从空树出发,经过一系列的查找、插入操作之后, 可生成一棵二叉排序树
不同插入次序的序列生成不同形态的二叉排序树
Status InsertBST(BiTree &T,ElemType e) {
// 当二叉排序树中不存在关键字等于e.key 的
// 数据元素时,插入元素值为e 的结点,并返
// 回TRUE; 否则,不进行插入并返回FALSE
if(!SearchBST ( T, e.key, NULL, p ))
{
s = (BiTree)malloc(sizeof(BiTNode)); // 为新结点分配空间
s->data = e;
s->lchild = s->rchild = NULL;
if( !p ) T = s; // 插入s 为新的根结点
else if( LT(e.key, p->data.key) )
p->lchild = s;// 插入*s 为*p 的左孩子
else
p->rchild = s; // 插入*s 为*p 的右孩子
return TRUE; // 插入成功
}
else
return FALSE
}
和插入相反,删除在查找成功之后进行,并且要 求在删除二叉排序树上某个结点之后,仍然保持 二叉排序树的特性。
可以分为三种情况:
二叉排序树的删除操作——删除:
删除叶结点,只需将其双亲结点指向它的指 针清零,再释放它即可
被删结点缺右子树,可以拿它的左子女结点 顶替它的位置,再释放它
被删结点缺左子树,可以拿它的右子女结点 顶替它的位置,再释放它
被删结点左、右子树都存在,可以在它的右 子树中寻找中序下的第一个结点(关键码小), 用它的值填补到被删结点中,再来处理这个结 点的删除问题
算法描述:
Status DeleteBST(BiTree &T,KeyType key) {
// 若二叉排序树T 中存在其关键字等于key 的
// 数据元素,则删除该数据元素结点,并返回
// 函数值TRUE,否则返回函数值FALSE
if(!T) reture FALSE;
//不存在关键字等于key的数据元素
else {
if(EQ(key,T->data.key)) {
Delete(T); return TRUE;
else if(LT(key,T->data.key))
DeleteBST(T->lchild,key); //继续在左子树中进行查找
else
DeleteBST(T->rchild,key); // 继续在右子树中进行查找
}
}
}
// 这里并没有调用前面的SearchBST函 数判断结点是否已存在,
// 而是边查找 边确定,这样可以巧妙地利用递归中 的参数T->lchild,T->rchild和父结 点建立了联系,
// 在后面的Delete函数 中,删除的是以p为根的二叉排序树 上的p结点。
//其中的删除操作过程如下所描述:
void Delete(BiTree &p) {
//从二叉排序树中删除结点p,
// 并重接它的左子树或右子树
if(!p->rchild) {
// 右子树为空树则只需重接它的左子树
q = p;
p = p->lchild;
free(q);
}
else if(!p->lchild) {
// 左子树为空树只需重接它的右子树
q= p;
p = p->rchild;
free(q);
}
else {
// 左右子树均不空
q = p;
s= p->lchild;
while(s->rchild) {
q= s;
s = s->rchild; // s 指向被删结点的前驱
}
p->data = s->data;
if(q!=p)
q->rchild = s->lchild;////p->lchild有右子树时, //重接*q的右子树
else q->lchild = s->lchild;// 重接*q的左子树
free(s);
}
}
对于每一棵特定的二叉排序树,均可按照平 均查找长度的定义来求它的ASL 值,显然, 由值相同的n 个关键字,构造所得的不同 形态的各棵二叉排序树的平均查找长度的 值不同,甚至可能差别很大
平均查找长度和二叉树的形态有关:
下面讨论平均情况:
不失一般性,假设长度为n的序列中有k 个关键字小于第一个关键字,则必有n-k-1 个关键字大于第一个关键字,由它构造的二叉 排序树的平均查找长度是n和k 的函数
P(n, k) ( 0 <=k <=n-1 ) 。
假设n 个关键字可能出现的n! 种排列的 可能性相同,则含n 个关键字的二叉排序 树的平均查找长度:
在等概率查找的情况下,
定义:
或者是一棵空树,或者是具有下 列性质的二叉树:它的左子树和右子树 都是平衡二叉树,且左子树和右子树的 深度之差的绝对值不超过1. 若将二叉树上结点的平衡因子(BF)定义 为该结点的左子树的深度减去它的右子 树的深度,则平衡二叉树上所有结点的 平衡因子只可能是-1,0,1
如果在一棵AVL树中插入一个新结点,就有可能造 成失衡,此时必须重新调整树的结构,使之恢复 平衡。我们称调整平衡过程为平衡旋转。
1、LL平衡旋转:
若在A的左子树的左子树上插入 结点,使A的平衡因子从1增加至 2,需要进行一次顺时针旋转。 (以B为旋转轴)
2、RR平衡旋转:
若在A的右子树的右子树上插入 结点,使A的平衡因子从-1增加 至-2,需要进行一次逆时针旋转。 (以B为旋转轴)
3、LR平衡旋转:
若在A的左子树的右子树上插入 结点,使A的平衡因子从1增加至 2,需要先进行逆时针旋转,再 顺时针旋转。 (以插入的结点C为旋转轴)
4、RL平衡旋转:
若在A的右子树的左子树上插入结 点,使A的平衡因子从-1增加至-2, 需要先进行顺时针旋转,再逆时 针旋转。 (以插入的结点C为旋转轴)
对于频繁使用 的查找表:
希望ASL=0,只有一个办法:预先知道所查关键字在表 中的位置,即,要求:记录在表中位置和 其关键字之间存在一种确定的关系。
基本思想:
记录的存储位置与关键字之间存在 对应关系,Loc(i)=H(keyi)
优点:
查找速度极快O(1),查找效率与元素个数n无关
根据设定的哈希函数H(key) 和所选中 的处理冲突的方法,将一组关键字映 象到一个有限的、地址连续的地址集 (区间) 上,并以关键字在地址集中的 “象”作为相应记录在表中的存储位 置,如此构造所得的查找表称之为 “哈希表”。
此法仅适合于: 地址集合的大小= = 关键字集合的大小
假设关键字集合中的每个关键字都是由s 位数字组成(u1, u2, …, us),分析关键字集 中的全体,并从中提取分布均匀的若干位 或它们的组合作为地址。
此方法仅适合于: 能预先估计出全体关键字的每一位上各种数 字出现的频度。
以关键字的平方值的中间几位作为存 储地址。求“关键字的平方值”的目 的是“扩大差别”,同时平方值的中 间各位又能受到整个关键字中各位的 影响
此方法适合于: 关键字中的每一位都有某些数字重复 出现频度很高的现象。
将关键字分割成若干部分,然后取它们的 叠加和为哈希地址。有两种叠加处理的方 法:移位叠加和间界叠加
此方法适合于: 关键字的数字位数特别多。
设定哈希函数为: H(key) = key MOD p
其中,p≤m (表长) 并且p 应为不大于m 的素数 或是 不含20 以下的质因子的合数
设定哈希函数为: H(key) = Random(key)
其中,Random 为伪随机函数
通常,此方法用于对长度不等的关键字构造 哈希函数。
实际造表时,采用何种构造哈希函数的
方法取决于建表的关键字集合的情况 (包括关键字的范围和形态),总的原则 是使产生冲突的可能性降到尽可能地
小。
“处理冲突”的实际含义是: 为产生冲突的地址寻找下一个哈希地址。
1.开放定址法
2.再哈希法
3.链地址法
4.建立一个公共溢出区
1、开放定址法
为产生冲突的地址H(key) 求得一个地址序列:
H0, H1, H2, …, Hs 1≤ s≤m-1
其中:H0 = H(key)
Hi = ( H(key) + di ) MOD m i=1, 2, …, s
di为增量序列,m为哈希表表长
1)线性探测再散列 di= 1,2,…,m-1
2)二次探测(平方探测)再散列 di= 12, -12, 22, -22, …,k2,-k2(k≤m/2)
3) 随机探测再散列 di是一组伪随机数列
2、再哈希法
Hi=RHi(key) i=1,2,…,k RHi均是不同的哈希函数,即在同义词产生 地址冲突时计算另一个哈希函数地址,直 到冲突不再发生。
3、链地址法
将所有哈希地址相同的记录 都链接在同一链表中
•非同义词不会冲突,无“聚集”现象
•链表上结点空间动态申请,更适合于表长不确 定的情况
4、建立一个公共溢出区
假设哈希函数的值域为[0…m-1],则设向 量HashTable[0…m-1]为基本表,每个 分量存储一个记录,另设向量 OverTable[0…v]为溢出表。所有关键字 和基本表中关键字为同义词的记录, 不管它们由哈希函数得到的哈希地址 为多少,一旦发生冲突,都填入溢出 表。
查找过程和造表过程一致。假设采用开放定址处理 冲突,则查找过程为:
对于给定值K,计算哈希地址i = H(K)
若r[i] = NULL 则查找不成功
若r[i].key = K 则查找成功
否则“求下一地址Hi” ,直至 r[Hi] = NULL (查找不成功) 或r[Hi].key = K (查找成功) 为止
//---开放定址哈希表的存储结构--
int hashsize[] = {997,...}; //哈希表容量递增表
typedef struct {
ElemType *elem;
int count; //当前元素个数
int sizeindex; // hashsize[sizeindex]为当前容量
}HashTable;
#define SUCCESS 1
#define UNSUCCESS 0
#define DUPLICATE -1
Status SearchHash(HashTable H,KeyType K,int &p,int &c) {
// 在开放定址哈希表H中查找关键码为K的记录
p = Hash(K); //求得哈希地址
while(H.elem[p].key! = NULLKEY && !EQ(K,H.elem[p].key))\
collision(p,++c); //求得下一探查地址p
if(EQ(K,H.elem[p].key)) return SUCCESS;
//查找成功 返回待查数据元素位置p
else return UNSUCCESS; //查找不成功
}
Status InsertHash(HashTable &H.ElemType e) {
c = 0;
if(SearchHash(H,e.key,p,c) == SUCCESS)
return DUPLICATE;
//表中已有与e 有相同关键字的元素
else if(c<hashsize[H.sizeindex]/2) {
// 冲突次数c 未达到上限,(阀值c 可调)
H./elem[p] = e;
++H.count;
return OK;
}// 查找不成功时,返回p为插入位置
else RecreateHashTable(H); // 重建哈希表
}
从查找过程得知,哈希表查找的平均查找长度实际上并不等 于零。
决定哈希表查找的ASL的因素:
一般情况下,可以认为选用的哈希函数是 “均匀”的,则在讨论ASL时,可以不考虑 它的因素。 因此,哈希表的ASL是处理冲突方法和装载 因子的函数。
哈希表查找效率分析:
使用平均查找长度ASL来衡量查找算法,ASL取决于 :
装载因子越大,表中记录数越多,说明表装得越 满,发生冲突的可能性就越大,查找时比 较次数就越多
ASL与装填因子有关!既不是严格的O(1),也不是O(n)
可以证明,查找成功时有以下结果:
从以上结果可见: