基本操作
查询
查询某个“特定的”数据元素是否在查找表中;
检索
检索某个“特定的”数据元素的各种属性;
插入
在查找表中插入一个数据元素;
删除
从查找表中删除某个数据元素。
分类
静态查找表
可以做查询、检索操作的查找表
动态查找表
可以做查询、检索、插入、删除操作的查找表
主关键字
可以唯一地标识一个记录的关键字
次关键字
能识别若干记录
查找成功
表中存在这样的记录,则给出该记录信息或指示该记录在表中的位置
查找不成功
查找表中不存在这一记录,给出“空记录”或“空指针”。
查找成功的平均查找长度
查找成功时为确定记录在查找表中的位置,需和给定值进行比较的关键字个数的期望值
Pi: 查找表中第i个记录的概率,有
Ci:找到表中其关键字与给定值相等的第i个记录时,和给定值比较过的关键字的个数.则:ASL=ΣPiCi
查找不成功平均查找长度
确定查找不成功时和给定值进行比较的关键字个数的期望值称为在查找不成功时平均查找长度。
typedef struct {
KeyType key; //关键字域
//其他域
}ElemType;
typedef struct
{
ElemType* elem; // 0号单元闲置
int length;//顺序表长度
}SSTable;
从表中第一条/最后一条记录开始,逐个进行记录的关键字与给定值的比较,若某个记录的关键字和给定值比较相等,则查找成功,返回其在顺序表中的位序;反之,若直至最后一条/第一条记录其关键字和给定值比较都不等,则查找不成功,返回0。
int Search_Seq(SSTable ST, KeyType key)
{
ST.elem[0].key = key; //0下标为监视哨
/*监视哨:为能自动检验数组下标越界,
在0下标处设置哨兵,若查找不成功,
则循环会在0下标处自动终止,函数返回0。 */
int i;
for (i = ST.length; !EQ(ST.elem[i].key, key); --i);
return i;
}//顺序查找算法
算法分析
平均查找长度
等概率下查找成功的平均查找长度(设Pi=1/n;Ci=n-i+1)
ASL=ΣPiCi=Σ(n+1-i)=(n+1)/2
无论给定什么关键字,顺序查找不成功时和给定值进行比较的关键字个数均为n+1.
优缺点
顺序查找的缺点:平均查找长度大,n越大,效率越低。
顺序查找的优点:算法简单;适用面广;不关心记录是否有序。
折半查找(二分查找):先确定待查记录所在范围,逐步缩小范围,直到找到或找不到该记录止。
int Search_Bin(SSTable ST, KeyType key)
{
int low = 1;//低下标位置,初始化为1
int high = ST.length; //高下标位置,初始化为最大值
int mid;//中间查找位置
while (low <= high)
{
mid = (low + high) / 2;
if (EQ(ST.elem[mid].key, key))//查找成功
return mid;
else if (LT(key, ST.elem[mid].key))//查找元素小于中间关键字
high = mid - 1;//高下标下移至中间位置左一位
else low = mid + 1;//低下标移至中间下标右一位
}
return FALSE;//不成功返回FALSE
}
算法分析
平均查找长度
一般情况下,表长为n的折半查找的判定树的深度和含有n个结点的完全二叉树的深度相同,设 n=2h-1 且查找概率相等,则折半查找成功的平均查找长度
ASL=(n+1)/n*log(n+1)-1
判定树
用二叉树描述折半查找过程,树中每个结点表示一个记录,用结点中的值为该记录在表中的位置。每个非终端结点的左子树表示的是在该结点位置的前半区进行折半查找的过程,其右子树表示的是在该结点位置的后半区进行折半查找的过程。
优缺点
折半查找优缺点:查找效率高,但仅适用于有序的顺序表
二叉排序树定义
二叉排序树(二叉查找树,BST):
空树或具有下列性质的二叉树:
根的左子树若非空,则左子树上所有结点的关键字值均小于根结点的关键字值;
根的右子树若非空,则右子树上所有结点的关键字值均大于根结点的关键字值;
它的左右子树同样是二叉排序树。
链式结构存储
特点
链式结构适于进行插入和删除操作;
树结构本身的排序特性使得查找过程变得高效。
查找
二叉排序树的查找过程
若二叉排序树为空,则查找失败,返回空指针;若二叉排序树不空,首先将给定值和根结点的关键字比较,若相等则查找成功,返回根结点的地址。若给定值小于根结点的关键字值,则在其左子树上继续查找;若给定值大于根结点的关键字值,则在其右子树上继续查找。
二叉排序树的查找算法
BiTree SearchBST(BiTree T, KeyType key)
{
if((!T) || EQ(key,T->data.key)) return T;
if(LT(key,T->data.key))
return SearchBST(T->lchild,key);
else return SearchBST(T->rchild,key);
} //SearchBST
构建
BST建树的过程:就是查找失败时元素不断插入的过程。
二叉排序树的插入
当树中不存在关键字等于给定值的结点时插入,新插入的结点一定是新添加的叶子结点,并且是查找不成功时查找路径上访问的最后一个结点的孩子。
/*---------------------------------------------------*/
- 先查找
- 算法分析
首先进行查找,查找失败时记录查找路径上访问的最后一个结点(待插入结点的双亲结点)
可变参数p查找成功指向查找节点,否则指向双亲节点。f是双亲节点,初始默认根节点的双亲节点为空。
- 算法实现
Status SearchBST0(BiTree T, KeyType key, BiTree f, BiTree& p)//f指向双亲节点,p返回查找到的节点
{
if (T == NULL)
{
p = f; return FALSE;
}
else if EQ(key, T->data.key)
{
p = T; return TRUE;
}
else if LT(key, T->data.key)
return SearchBST0(T->lchild, key, T, p);
else return SearchBST0(T->rchild, key, T, p);
}// 用于创建二叉排序树的查找
- 再插入
- 算法分析
生成待插入结点,判断它是其双亲的哪个孩子,将它作为叶子结点插入
若二叉树为空,则首先单独生成根结点
- 算法实现
Status InsertBST(BiTree& T, ElemType e)
{
BiTree p;
if (!SearchBST0(T, e.key, NULL, p))//没有找到元素
{
BiTree s;
s = (BiTree)malloc(sizeof(BiTNode));
if (!s) exit(OVERFLOW);
s->data = e; s->lchild = s->rchild = NULL;
if (p == NULL) T = s;//p为空时,树为空,建立根节点
else if LT(e.key, p->data.key) p->lchild = s;
else p->rchild = s;
return TRUE;
}
else return FALSE;
}
- 二叉排序树的删除
- 1)被删除的结点*p是叶子结点
- 2)被删除的结点*p只有左子树或者只有右子树
- 3) 被删除的结点*p既有左子树,也有右子树。
二叉排序树的查找分析
查找成功
查找成功的情况:查找走过一条从根结点到该结点的路径,与关键字比较次数等于路径长度+1,总的比较次数不超过树的深度
查找不成功
查找不成功的情况:查找走过一条从根结点到叶子结点的路径,与关键字比较次数等于路径长度+1,总比较次数不超过树的深度
定义
平衡二叉树(AVL树)
平衡二叉树(AVL树):它或者是一棵空树,或者是满足下列性质的二叉树:(1)其左、右子树深度之差的绝对值不大于1(2)其左、右子树都是平衡二叉树
结点的平衡因子BF
结点的平衡因子BF=该结点的左子树深度-右子树深度
平衡二叉树上结点的平衡因子取值={-1,0,+1}
二叉树上有结点的平衡因子的绝对值大于1,则它就不是平衡的。
构造方法
插入
根据初始序列,从空树开始插入新结点
旋转
在插入过程中,一旦有结点的平衡因子的绝对值大于1(失去平衡),则在保持二叉排序树特性的前提下,采用平衡旋转技术,对最小不平衡子树(其左、右子树均平衡,只有子树的根结点不平衡)进行调整,使其平衡。
LL型
由于在A的左孩子的左子树上插入结点,A的平衡因子由+1变为+2,需进行一次向右的顺时针旋转操作
RR型
由于在A的右孩子的右子树上插入结点,A的平衡因子由-1变为-2,需进行一次向左的逆时针旋转操作
LR型
由于在A的左孩子的右子树上插入结点,A的平衡因子由1变为2,需进行两次(先左后右)的旋转操作
RL型
于在A的右孩子的左子树上插入结点,A的平衡因子由-1变为-2,需进行两次(先右后左)的旋转操作
平衡二叉树的性能分析
在平衡的二叉排序树BBST上插入新的数据元素e的递归算法:
(1)若BBST为空树
(2)若e的关键字和BBST的根结点关键字相等
(3)若e的关键字小于BBST的根结点关键字且在BBST的左子树上不存在和e有相同关键字的结点,在BBST的左子树插入e;当由于插入e导致左子树深度增1时:
①插入前BBST根结点的平衡因子为-1
②插入前BBST的根结点的平衡因子为0
③插入前BBST的根结点的平衡因子为1
若BBST左子树根结点的平衡因子为1(LL),进行单向右旋处理
若BBST左子树根结点的平衡因子为-1(LR),进行先左后右的双向旋转平衡处理
(4)若e的关键字大于BBST的根结点的关键字,而且在BBST的右子树中不存在和e有相同关键字的结点,则将e插入在BBST的右子树上,且插入后的右子树深度增1时,分别就不同情况处理,其处理操作和(3)所述对称。
B-树定义
平衡的多路查找树。一棵m阶B-树或为空树,或满足:
1、树中每个结点至多有m棵子树;
2、若根结点不是叶子结点,至少有两棵子树;
3、除根之外的所有非终端结点至少有m/2棵子树;
4、所有非终端结点中包含下列数据信息:
(n,A0,K1, A1,K2, A2, ……… Kn, An)
n:关键字的个数
Ki(i=1,…,n):关键字,且Ki
5、所有的叶子结点都出现在同一层上,不带信息(可看成是外部结点或查找失败的出口,实际并不存在)。
B-树的插入
B-树的生成从空树开始,在查找的基础上逐个插入关键字而得。由于B-树结点中的关键字个数必须≥m/2-1,每次插入一个关键字不是在树中增加一个叶子结点,而是在最底层的某个非终端结点中添加一个关键字。此时有可能打破B-树对结点个数上限的要求,有2种情况:
1、若插入前该结点的关键字个数
1、若插入前该结点的关键字个数 直接将关键字和其它信息按序插入到该结点中
2、若插入前该结点的关键字个数=m-1,则插入后>m-1
先将关键字和其它信息按序插入
原结点*p:(m-1, A0, (K1, A1), (K2, A2), … (Km-1, Am-1))
插入一个关键字后:(m, A0, (K1, A1),…, (Km/2-1,Am/2-1),(Km/2 ,Am/2), …,(Km, Am))
结点分裂为两部分:
p:(m/2-1,A0,(K1,A1),… (Km/2-1,Am/2-1))
新结点p’:(m-m/2,Am/2,(Km/2+1,Am/2+1)… (Km,Am))
将Km/2和指针p’一起插入到该结点的双亲结点中
若双亲由于插入一个关键字也不满足上限要求,则继续分裂,若分裂一直进行到根结点,则树将长高一层
B-树的删除
在B-树删除一个关键字,首先必须找到待删关键字所在结点,从中删除之。若该结点为最下层的非终端结点,且删除前其关键字个数不少于m/2 ,则删除完成;否则要进行结点的合并。若该结点不是最底层的非终端结点,被删关键字是其第i个关键字,则将Ai-1或Ai所指子树中的最大(或最小)关键字Y移上来代替Ki,然后在相应的结点中删去Y。
因此可以只讨论删除最下层非终端结点内关键字的情形。
1)删除前被删关键字所在结点关键字数目≥m/2
直接删除该关键字Ki和Ai即可。将该兄弟结点中最小(最大)关键字上移至双亲结点
双亲结点中小于(或大于)且紧靠该上移关键字的关键字下移至被删关键字所在结点。
3)被删关键字所在结点和其相邻的兄弟结点中的关键字数目均等于m/2-1,设该结点有右兄弟且其右兄弟结点地址由双亲结点中的指针Ai所指
删去关键字后,它所在结点的剩余关键字和指针,加上双亲结点的关键字Ki一起,合并到Ai所指兄弟结点中。
如果因此使得双亲结点的关键字个数小于m/2-1,则依次类推作相应处理,即合并操作有可能向上传递。结点合并的极端情况是使树的高度减少1。
B+树定义
B+树是B-树的变型树
一棵m阶B+树和m阶B-树的差异:
(1)有n棵子树的结点中含有n个关键字;
(2)所有的叶子节点中包含了全部关键字的信息,及指向含这些关键字记录的指针,且叶子节点本身依关键字的大小自小而大顺序链接。
(3)所有的非终端节点可以看成是索引部分,结点中仅含有其子树(根结点)中的最大(或最小)关键字。
对B+树查找方法
散列(Hash)函数
在记录的关键字和其在表中位置之间建立的一种函数关系,即以f(key)作为关键字为key的记录在表中的存储位置。
冲突
不同关键字得到同一散列地址,即:key1!=key2,而f(key1)=f(key2)
同义词
在一个散列函数中具有相同函数值的不同关键字。
散列(Hash)表
根据设定的散列函数H(key)和所选中的处理冲突的方法,将一组关键字映象到一个有限的、地址连续的地址集(区间)上,并以关键字在地址集中的“像”作为相应记录在表中的存储位置,这种表被称为散列表(哈希表)。
散列(Hash)造表或散列
映象过程
散列(Hash)地址
关键字的存储位置
直接定址法
取关键字或关键字的某个线性函数值为散列地址。即H(key)=key或H(key)=a×key+b(a,b为常数)。这种函数也叫自身函数。
特点
直接定址所得地址集合和关键字集合的大小相同。因此不同关键字不会发生冲突,但在实际中使用很少
数字分析法
取关键字分布均匀的若干位或组合作散列地址
特点
适于关键字位数较多而关键字个数较少的情况且关键字已知
平方取中法
取关键字平方后的中间几位为散列地址。
依据:1)通过“平方”扩大差别;2)平方值的中间几位受到关键字中每一位的影响
特点
适于无法预知全部关键字情况,或关键字的每一位都有某些数字重复出现频度很高
折叠法
将关键字分割成位数相同的几部分,取这几部分的叠加和为散列地址。有:移位叠加和间界叠加两种
特点
适于关键字位数很多,且每一位数字分布大致均匀
随机数法
取关键字的随机函数值为散列地址。即:
H(key)=Random(key)
特点
当关键字长度不等时采用此法比较恰当。
除留余数法
取关键字被某个不大于散列表长m的数p除后所得余数作为散列地址。即:
H(key) = key MOD p (p≤m)
p的选择
一般p为≤m且接近m的质数或不含20以内质因数的合数。若p选不好易产生同义词
特点
简单常用,也可与前面各方法结合使用
开放定址法
Hi=(H(key)+di) MOD m i=1,2,…,k (k≤m-1),
H(key)为散列函数;m:散列表长,
di是增量序列, 有三种取法:
di=1,2,…m-1,称为线性探测再散列
di=12,-12,22,-22,…,±k2 (k≤m/2)称为二次探测再散列
di=伪随机数序列,称为随机探测再散列
再哈希法
Hi=RHi(key)i=1,2,…,k,
其中RHi都是不同于Hi的哈希函数。在同义词产生地址冲突时计算另一个哈希函数地址。直到冲突不再发生,
这种方法不易产生聚集,但增加了计算时间。
公共溢出区法
HashTable[0…m-1]:基本表,每个分量存放一个记录。
OverTable[0…v]:溢出表,所有关键字和基本表中关键字为同义词的记录,一旦发生冲突, 均填入溢出表。
链地址法
将所有关键字为同义词的记录存储在同一单链表中。每个地址的链表的头指针组织成一个向量。
int hashsize[] = {
11,19,29,37 }; //递增的质数序列
int m; // 散列表表长,全局变量
typedef struct {
ElemType *elem;
int count; //表中当前数据元素个数
int sizeindex; //hashsize[sizeindex]为当前容量
}HashTable;
散列表的查找
查找过程和造表过程一致。给定K值,根据造表时设定的散列函数求得散列地址,若表中此位置上没有元素,则查找不成功;否则比较关键字,若和给定值相等,则查找成功;否则根据造表时设定的处理冲突的方法找“下一地址”,直至散列表某个位置为“空”或者表中所填元素的关键字等于给定值时为止。
Status SearchHash (HashTable H, KeyType K, int &p, int &c)
{
p= Hash(K); //求得散列地址
while(H.elem[p].key!=NULLKEY &&!EQ(K, H.elem[p].key))
collision(p, ++c);
if (EQ(K,H.elem[p].key))
return SUCCESS; //查找成功
else
return UNSUCCESS; //查找不成功
}//SearchHash
散列表的插入
在查找失败时在失败位置插入元素
Status InsertHash (HashTable &H, ElemType e)
{
c = 0; // c记录冲突次数
if(SearchHash(H,e.key,p,c))
return DUPLICATE;
else if(c<hashsize[H.sizeindex]/2 )
{
H.elem[p] = e;
++H.count;
return OK;
}
else {
RecreateHT(H); return UNSUCCESS; }
} //InsertHash