文章目录
查找的基本概念
查找表
:是由一组记录组成的表或文件,而每个记录由若干个数据项组成,并假设每个记录都有一个能唯一标识该记录的关键字。
关键字
:记录中某个数据项的值,可用来识别一个记录;
主关键字:唯一标识数据元素;
次关键字:可以标识若干个数据元素;
若整个查找过程都在内存进行,则称之为内查找
;
若查找过程中需要访问外存,则称之为外查找
;
若在查找的同时对表做修改操作(如插入和删除),则相应的表称之为动态查找表;否则称之为静态查找表。
静态表查找
顺序查找
int SeqSearch(SqList L,KeyType k)
{ int i=L.length;
while (i>0 && L.R[i].key!=k) //从表尾往前找
i--;
if (i<=0)
return 0; //未找到返回0
else
return i; //找到返回逻辑序号i
}
改进:把待查关键字key存入表头(“监视哨”),从后向前逐个比较,可免去查找过程中每一步都要检测是否查找完毕,加快速度
int SeqSearch(SqList L,KeyType k)
{ int i=L.length;
L.R[0].key=k;
while (L.R[i].key!=k) //从表尾往前找
i--;
return i;
}
成功情况下的平均查找长度:
A
S
L
=
∑
i
=
1
n
p
i
c
i
=
1
n
∑
i
=
1
n
i
=
1
n
×
n
(
n
+
1
)
2
=
n
+
1
2
ASL=\sum_{i=1}^{n} p_{i} c_{i}=\frac{1}{n} \sum_{i=1}^{n} i=\frac{1}{n} \times \frac{n(n+1)}{2}=\frac{n+1}{2}
ASL=i=1∑npici=n1i=1∑ni=n1×2n(n+1)=2n+1
不成功情况下的平均查找长度
A
S
L
=
n
+
1
ASL=n+1
ASL=n+1
二分查找(折半查找)——有序表的查找
若k==R[mid].key,查找成功
若k<R[mid].key,则high=mid-1
若k>R[mid].key,则low=mid+1
若low>high,查找失败
非递归算法
int Search_Bin( SqList L , KeyType key )
{ //若找到,则函数值为该元素在表中的逻辑序号,否则为0
int low=1 , high=L.length , mid;
while(low<=high)
{
mid=( low + high )/2;
if( key == L.R[mid].key)
return mid;
else if( key < L.R[mid].key)
high=mid-1; //修改上界
else low=mid+1; //修改下界
}
return 0; //表中不存在待查元素
}
递归算法
int Search_Bin (SqList L, keyType key, int low, int high)
{ int mid=(low+high)/2;
if(low<=high)
{
if(key == L.R[mid].key) return mid;
else if( key < L.R[mid].key)
return Search_Bin (L, key, low, mid-1);
else
return Search_Bin (L, key, mid+1,high);
}
else
return 0; //查找不到时返回0
}
动态表查找(树表查找)
树表
:以二叉树或树作为表的组织形式,称为树表,它是一类动态查找表,不仅适合于数据查找,也适合于表插入和删除操作。
二叉排序树
二叉排序树或是空树,或是满足如下性质的二叉树:
- 若其左子树非空,则左子树上所有结点的值均小于根结点的值;
- 若其右子树非空,则右子树上所有结点的值均大于等于根结点的值;
- 其左右子树本身又各是一棵二叉排序树
在二叉排序树bt上查找关键字为k的记录,成功时返回该结点指针,否则返回NULL
BSTNode *SearchBST(BSTNode *bt,KeyType k)
{
if (bt==NULL || bt->key==k) //递归出口
return bt;
if (k<bt->key)
return SearchBST(bt->lchild,k); //在左子树中递归查找
else
return SearchBST(bt->rchild,k); //在右子树中递归查找
}
在二叉排序树中插入一个关键字为k的新结点,要保证插入后仍满足BST性质。
int InsertBST(BSTNode *&p,KeyType k)
{ if (p==NULL) //原树为空, 新插入的记录为根结点
{ p=(BSTNode *)malloc(sizeof(BSTNode));
p->key=k;p->lchild=p->rchild=NULL;
return 1;
}
else if (k==p->key) //存在相同关键字的结点,返回0
return 0;
else if (k<p->key)
return InsertBST(p->lchild,k); //插入到左子树中
else
return InsertBST(p->rchild,k); //插入到右子树中
}
平衡二叉树(AVL)
若一棵二叉树中每个结点的左、右子树的高度至多相差1,则称此二叉树为平衡二叉树(AVL)。
如果在一棵AVL树中插入一个新结点,就有可能造成失衡,此时必须重新调整树的结构,使之恢复平衡。我们称调整平衡过程为平衡旋转。调整操作可归纳为4种情况。
- LL平衡旋转
- RR平衡旋转
- LR平衡旋转
- RL平衡旋转
LL型调整
调整过程:
- A结点成为B的右孩子
- 原来B结点的右子树β作为A的左子树
LR型调整
调整过程:
- B结点成为C的左孩子,A结点成为C的右孩子
- 原来C结点的左子树β 作为B的右子树;原来C结点的右子树γ 作为A的左子树
哈希表查找
哈希存储基本思想:选取某个函数,将关键字作为自变量进行计算,将运算后的函数值作为元素的存储位置,并按此存放。
哈希地址
:将分散的关键字利用hash函数映射到一个存放关键码信息的地址(通常为数组下标) 。
Hash函数
(哈希):将关键字k转换为地址的函数。
哈希冲突(同义词冲突): 不同的关键码映射到同一个哈希地址:
k
e
y
1
≠
k
e
y
2
,
但
H
(
k
e
y
1
)
=
H
(
k
e
y
2
)
key1 \neq key2,但H(key1)=H(key2)
key1=key2,但H(key1)=H(key2)
同义词
:具有相同函数值的两个关键字
影响冲突的因素:
- 装填因子
α
=
元
素
个
数
/
哈
希
表
长
=
n
/
m
α=元素个数/哈希表长=n/m
α=元素个数/哈希表长=n/m
α越小,冲突的可能性就越小;
α越大(最大可取1),冲突的可能性就越大。 - 与所采用的哈希函数有关
- 与解决冲突方法有关
构造哈希函数
- 直接定址法
H ( k e y ) = a ⋅ k e y + b H(key) = a·key + b H(key)=a⋅key+b
优点:不会产生冲突。
事先知道关键码,关键码集合不是很大且连续性较好。 - 除留余数法
H ( k e y ) = k e y % p , ( % 为 求 余 运 算 , p ≤ m ) H(key)=key \% p, (\%为求余运算,p≤m) H(key)=key%p,(%为求余运算,p≤m) - 数字分析法
根据关键码在各个位上的分布情况,选取分布比较均匀的若干位组成哈希地址。 - 平方取中法
对关键字平方后,按哈希表大小,取中间的若干位作为哈希地址(平方后截取)。
解决哈希冲突
开放定址法(开地址法,线性探测法)
基本思想
:有冲突时就去寻找下一个空的哈希地址,只要哈希表足够大,空的哈希地址总能找到,并将数据元素存入。
优点
:
只要哈希表未被填满,就能找到一个空地址单元存放有冲突的元素;
缺点
:
可能出现很多元素在相邻的哈希地址上“堆积”,降低了查找效率。
解决方案
:
可采用平方探测法或伪随机探测法,以改善“堆积”问题。
平方探测法的数学描述公式为:
h
i
=
(
h
(
k
)
±
i
2
)
%
m
(
1
≤
i
≤
m
−
1
)
hi=(h(k) ± i2) \% m (1≤i≤m-1)
hi=(h(k)±i2)%m(1≤i≤m−1),即查找的位置依次为:
h
(
k
)
、
h
(
k
)
+
1
、
h
(
k
)
−
1
、
h
(
k
)
+
4
、
h
(
k
)
−
4
、
…
h(k) 、h(k) +1、h(k) -1 、h(k) +4、h(k) -4、…
h(k)、h(k)+1、h(k)−1、h(k)+4、h(k)−4、…
链地址法
基本思想
:相同哈希地址的记录链成一单链表,m个哈希地址就设m个单链表,然后用用一个数组将m个单链表的表头指针存储起来,形成一个动态的结构
typedef struct node
{
KeyType key; //关键字域
struct node *next; //下一个结点指针
} Node; //单链表结点类型
typedef struct
{
node *firstp[m]; //首结点指针
int length;
} HashChain; //哈希表类型
链地址法建立哈希表步骤
- 取数据元素的关键字key,计算其哈希函数值(地址)。若该地址对应的链表为空,则将该元素插入此链表;否则执行step2解决冲突。
- 根据选择的冲突处理方法,计算关键字key的下一个存储地址。若该地址对应的链表为不为空,则利用链表的前插法或后插法将该元素插入此链表。
void SearchHT(HashChain ha,int p,KeyType k)
//在哈希表中查找关键字k
{ int i=0,adr;
adr=k % p; //计算哈希函数值
node *q;
q=ha. firstp[adr]; //q指向对应单链表的首结点
while (q!=NULL && q->key!=k)//扫描单链表adr的所有结点
{
i++;
q=q->next;
}
if (q!=NULL) //查找成功
printf("成功:关键字%d,比较%d次\n",k,i);
else //查找失败
printf("失败:关键字%d,比较%d次\n",k,i);
}
优点
:非同义词不会冲突,无“聚集”现象;链表上结点空间动态申请,更适合于表长不确定的情况
小结
- 链地址法优于开地址法
- 除留余数法作哈希函数优于其它类型函数