第七章 查找技术
【学习重点】
(1) 折半查找的过程及性能分析;
(2) 二叉排序树的插入、删除和查找操作;
(3) 平衡二叉树的调整方法;
(4) 散列表的构造和查找方法;
(5) 各种查找技术的时间性能及对比。
【学习难点】
(1)二叉排序树的删除操作;
(2)平衡二叉树的调整方法;
(3)闭散列表的删除算法。
7.1 概 述
7.1.1 查找的基本概述
关键码 :可以标识一个记录的某个数据项。关键码的值称为关键值 ,若关键码可以唯一的标识一个记录,则称此关键码为主关键码,反之,称为次关键码。
查找:在具有相同类型的记录构成的集合中找出满足给定条件的记录。
静态查找:不涉及插入和删除操作的查找。
动态查找:涉及插入和删除操作的查找。
查找结构:(1)线性表:适用于静态查找,主要采用顺序查找技术、折半查找技术
(2)树表:适用于动态查找,主要采用二叉排序树的查找技术
(3)散列表:静态查找和动态查找均适用,主要采用散列技术
7.1.2 查找算法的性能
平均查找长度:查找算法进行的关键码比较次数的数学期望值。
在查找不成功的情况, 平均查找长度即为查找失败对应的关键码的比较次数。
7.2 线性表的查找技术
7.2.1 顺序查找
顺序查找又称线性查找,其基本思想:从线性表的一端向另一端逐个将关键码与给定值进行比较,若相等,则查找成功,给出该记录在表中的位置;若整个表检测完仍未找到与给定值相等的关键码,则查找失败,给出失败信息。
1.顺序表顺序查找
int SeqSearch2(intr[ ], intn, intk)
//数组r[1] ~ r[n]存放查找集合
{
r[0] = k; i = n;
while (r[i] != k)
i--;
return i;
}
基本思想:设置“哨兵”。哨兵就是待查值,将它放在查找方向的尽头处,免去了在查找过程中每一次比较后都要判断查找位置是否越界,从而提高查找速度
2. 单链表的顺序查找
7.2.2 折半查找
使用条件:
基本思想:在有序表中,取中间记录作为比较对象,若给定值与中间记录的关键码相等,则查找成功;若给定值小于中间记录的关键码,则在中间记录的左半区继续查找;若给定值大于中间记录的关键码,则在中间记录的右半区继续查找。不断重复上述过程,直到查找成功,或所查找的区域无记录,查找失败。
折半查找——非递归intBinSearch1(intr[ ], intn, intk)
{ //数组r[1] ~ r[n]存放查找集合
low = 1; high = n;
while (low <= high)
{
mid = (low + high) / 2;
if (k < r[mid]) high = mid -1;
else if (k > r[mid]) low = mid+ 1;
else return mid;
}
return 0;
}
intBinSearch2(intr[ ], intlow, inthigh, intk)
{ //数组r[1] ~ r[n]存放查找集合
if (low > high) return 0;
else {
mid = (low + high) / 2;
if (k < r[mid])
return BinSearch2(r, low, mid-1, k);
else if (k > r[mid])
return BinSearch2(r, mid+1,high, k);
else return mid;
}
}
折半查找判定树判定树:折半查找的过程可以用二叉树来描述,树中的每个结点对应有序表中的一个记录,结点的值为该记录在表中的位置。通常称这个描述折半查找过程的二叉树为折半查找判定树,简称判定树。
判定树的构造方法:⑴当n=0时,折半查找判定树为空;
⑵当n>0时,折半查找判定树的根结点是有序表中序号为mid=(n+1)/2的记录,根结点的左子树是与有序表r[1] ~ r[mid-1]相对应的折半查找判定树,根结点的右子树是与r[mid+1] ~ r[n]相对应的折半查找判定树。
查找性能分析:
查找成功:在表中查找任一记录的过程,即是折半查找判定树中从根结点到该记录结点的路径,和给定值的比较次数等于该记录结点在树中的层数。
查找不成功:查找失败的过程就是走了一条从根结点到外部结点的路径,和给定值进行的关键码的比较次数等于该路径上内部结点的个数。
7.3 树表的查找技术7.3.1 二叉排序树
二叉排序树又称二叉查找树,它或者是一棵树的二叉树,或者是具有下列性质的二叉树:
⑴ 若它的左子树不空,则左子树上所有结点的值均小于根结点的值;
⑵ 若它的右子树不空,则右子树上所有结点的值均大于根结点的值;
⑶ 它的左右子树也都是二叉排序树。
1.二叉排序树的插入void BiSortTree::InsertBST(BiNode
{
if (root == NULL)
root = s;
else if (s->data
InsertBST(root->lchild, s);
else InsertBST(root->rchild, s);
}
2. 二叉排序树的构造BiSortTree::BiSortTree(int r[ ], int n)
{
for (i = 0; i < n; i++)
{
s = new BiNode
s->data = r[i];
s->lchild= s->rchild= NULL;
InsertBST(root,s);
}
}
3.二叉排序树的删除在二叉排序树上删除某个结点之后,仍然保持二叉排序树的特性。
分三种情况讨论:
1.若结点p是叶子,则直接删除结点p;
2. 若结点p只有左子树,则只需重接p的左子树;
若结点p只有右子树,则只需重接p的右子树;
3.若结点p的左右子树均不空,则
3.1 查找结点p的右子树上的最左下结点s及其双亲结点par;
3.2 将结点s数据域替换到被删结点p的数据域;
3.3 若结点p的右孩子无左子树,
则将s的右子树接到par的右子树上;
否则,将s的右子树接到结点par的左子树上;
3.4 删除结点s;
4.二叉排序树的查找
⑴ 若root是空树,则查找失败;
⑵若k=root->data,则查找成功;否则
⑶若k<root->data,则在root的左子树上查找;否则
⑷在root的右子树上查找。
上述过程一直持续到k被找到或者待查找的子树为空,如果待查找的子树为空,则查找失败。
二叉排序树的查找效率在于只需查找二个子树之一。
BiNode*BiSortTree::SearchBST(BiNode
{
if (root == NULL)
return NULL;
else if (root->data== k)
return root;
else if (k < root->data)
return SearchBST(root->lchild, k);
else return SearchBST(root->rchild, k);
}
7.3.2 平衡二叉树平衡二叉树:或者是一棵空的二叉排序树,或者是具有下列性质的二叉排序树:
⑴ 根结点的左子树和右子树的深度最多相差1;
⑵ 根结点的左子树和右子树也都是平衡二叉树。
平衡因子:结点的平衡因子是该结点的左子树的深度与右子树的深度之差。
最小不平衡子树:在平衡二叉树的构造过程中,以距离插入结点最近的、且平衡因子的绝对值大于1的结点为根的子树。
构造平衡二叉树的基本思想:每插入一个结点,
(1)从插入结点开始向上计算各结点的平衡因子,如果某结点平衡因子的绝对值超过1,则说明插入操作破坏了二叉排序树的平衡性,需要进行平衡调整;否则继续执行插入操作。
(2)如果二叉排序树不平衡,则找出最小不平衡子树的根结点,根据新插入结点与最小不平衡子树根结点之间的关系判断调整类型。
(3)根据调整类型进行相应的调整,使之成为新的平衡子树。
设结点A为最小不平衡子树的根结点,对该子树进行平衡调整归纳起来有以下四种情况:
1. LL型
2. RR型
3. LR型
4. RL型
7.4 散列表的查找技术
散列的基本思想:在记录的存储地址和它的关键码之间建立一个确定的对应关系。这样,不经过比较,一次读取就能得到所查元素的查找方法。
散列表:采用散列技术将记录存储在一块连续的存储空间中,这块连续的存储空间称为散列表。
散列函数:将关键码映射为散列表中适当存储位置的函数。
散列地址:由散列函数所得的存储地址 。
散列只是通过记录的关键码定位该记录,没有完整地表达记录之间的逻辑关系,所以,散列主要是面向查找的存储结构。
散列技术的关键问题:
⑴散列函数的设计。如何设计一个简单、均匀、存储利用率高的散列函数。
⑵冲突的处理。如何采取合适的处理冲突方法来解决冲突。
设计散列函数一般应遵循以下原则:
⑴ 计算简单。散列函数不应该有很大的计算量,否则会降低查找效率。
⑵函数值即散列地址分布均匀。函数值要尽量均匀散布在地址空间,这样才能保证存储空间的有效利用并减少冲突。
1. 计算散列地址j;
2. 若ht[j]等于k,则查找成功,返回记录在散列表中的下标;
否则执行第3步;
3. 若ht[j]为空或整个散列表探测一遍,则查找失败,转4;
否则,j指向下一单元,转2;
4. 若整个散列表探测一遍,则表满,抛出溢出异常;
否则,将待查值插入;
intHashSearch1(int ht[ ], int m, int k)
{
j = H(k); //计算散列地址
if (ht[j] == k) return j; //没有发生冲突,比较一次查找成功
else if (ht[j] == Empty) {ht[j]= k; return 0; } //查找不成功,插入
i = (j + 1) % m; //设置探测的起始下标
while (ht[i] != Empty && i != j)
{
if (ht[i] == k) return i; //发生冲突,比较若干次查找成功
else i = (i + 1) % m; //向后探测一个位置
}
if (i == j) throw "溢出";
else {ht[i] = k; return 0; } //查找不成功,插入
}
拉链法:Node
{
j = H(k);
p = ht[j];
while (p != NULL && p->data != k)
p = p->next;
if (p->data== k) return p;
else {
q = new Node
q->next = ht[j];
ht[j]= q;
}
}