查找的基本概念
对于栈、队列、树、图等结构,在该算法中查找是十分常见的,而在实际应用中也是一样。所以查找的效率十分重要。像我们平时在图书馆找书、找书上的某一页、互联网上的信息检索等都是查找,一个好的查找方法可以大大地提升效率,节约时间。
- 查找表
查找表是由同一类型的数据元素(或记录)构成的集合。 - 关键字
关键字是数据元素中某个数据项的值,用它可以标识一个数据元素。若能唯一地标识一个数据元素,则称它为主关键字。当数据元素只有一个数据项时,其关键字即为该数据元素的值。例如我们每个人,有姓名、年龄、身份证号等,因为身份证号是唯一的,所以身份证号为我们每个人的主关键字。 - 查找
查找是根据给定的某个值,在查找表中确定一个其关键字等于给定值的记录或数据元素。
若有这样的记录,则查找成功,返回其在表中的位置;若查找失败,则可以提示表中没有这样的记录。 - 动态查找表和静态查找表
若在查找的同时对表做修改操作,则相应的表称之为动态查找表,否则称为静态查找表。 - 平均查找长度
在查找时,需和给定值进行比较的关键字个数的期望值,称为查找算法的平均查找长度(Average Search Length,ASL)。对于含有n个记录的表,查找成功的平均查找长度为 A S L = ∑ i = 1 n P i C i ASL=\sum_{i=1}^nP_{i}C_{i} ASL=i=1∑nPiCi
其中, P i P_{i} Pi为查找表中第i个记录的概率,且 ∑ i = 1 n P i = 1 \sum_{i=1}^nP_{i}=1 ∑i=1nPi=1。
C i C_{i} Ci为找到表中其关键字与给定值相等的第i个记录时,和给定值已进行果比较的关键字个数。显然, C i C_{i} Ci随查找过程不同而不同。
线性表的查找
线性表的查找分为:顺序查找、折半查找和分块查找。
顺序查找
顺序查找(Sequential Search)的过程为:从表的一端(可以是开头也可以是结尾)开始,依次将记录的关键字和给定值进行比较,若某个记录的关键字和给定值相等,则查找成功;若扫描整个表后,仍未找到关键字和给定值相等的记录,则查找失败。
顺序查找方法既适用于线性表的顺序存储结构,又适用于线性表的链式存储结构。
数据元素类型定义如下:
typedef struct
{
KeyType key; //关键字域
InfoType otherinfo; //其他域
}ElemType;
顺序表的定义如下:
typedef struct
{
ElemType *R;
int length;
}SSTable;
然后就是顺序查找的算法实现,十分简单。就是通过挨个比较就完了。不过为了方便,假设元素是从下标为1的位置开始顺序存放的。下标为0的位置闲置不用。
int Search_Seq(SSTable ST,KeyType key)
{
for(i=ST.length;i>=1;i--)
if(ST.R[i].key==key)
return i;
return 0;
}
折半查找
折半查找(Binary Search)也称二分查找。这个方法应该都在小学,或者初中???多多少少都有接触过。所谓折半,就是将查找表分为两半。每次都从查找表的中间元素查找,若查找失败,排除刚查找的数据元素后将该查找表分为左右两个子表,若第一次比较小于那个中间元素,则在左子表中继续又从中间元素开始,若大于那个中间元素,则在右子表中去查找…
那么理解是这样理解,但是貌似少了一条定义的规则,在上面的描述过程中,我们默认了左子表中的数据元素一定小于中间元素,右子表中的数据元素一定大于中间元素。这就对了,折半查找只能采用顺序存储结构,即这些数据元素只能按照从小到大的顺序排列成一行。
那么在算法中我们该如何去实现?
算法分析:
- 首先我们肯定是要有一个变量来标识每次查找的中间数据元素,设这个变量为 m i d mid mid。同时左子表和右子表不需要全部都表示出来,在判断给定值和中间元素的大小后,只取左右子表其中一个就可以了,那么该如何去取呢?
- 我们不妨将每一个查找表分为三个点,用这三个点来表示一个查找表的范围。第一个点是第一个数据元素,第二个点是中间的数据元素,第三个点最后一个数据元素。若第一次比较小于中间元素,那么我们只需将查找表缩小到左子表就行了,这时只需将第三个点的位置变一下,变到中间元素的前一个元素,再用 m i d mid mid重新标识左子表的中间元素即可。那么我们用变量 l o w low low来表示第一个数据元素,用变量 h i g h high high来表示第二个数据元素。
具体代码:
int Search_Bin(SSTable ST,KeyType key)
{
int low=1,high=ST.length,mid; //仍是从下标为1开始存储
while(low<=high)
{
mid=(low+high)/2; //取整,省略小数
if(ST.R[mid].key==key)
return mid;
else if(ST.R[mid].key<key)
low=mid+1;
else
high=mid-1;
}
return 0;
}
其实上述算法也可以用递归实现,请看下面例子:
#include <stdio.h>
#include <malloc.h>
#include <stdlib.h>
typedef int KeyType;
typedef char InfoType;
typedef struct
{
KeyType key;
InfoType otherinfo;
}ElemType;
typedef struct
{
ElemType *R;
int length;
}SSTable;
extern int mid=0;
void Search_Bin(SSTable ST,KeyType key,int low,int high)
{
mid=(low+high)/2;
if(ST.R[mid].key==key)
{
printf("The number is set at:%d",mid); //输出该数据元素的下标
exit(0);
}
else if(ST.R[mid].key<key)
low=mid+1;
else
high=mid-1;
if(low<=high)
Search_Bin(ST,key,low,high);
else
printf("Searching fails");
}
int main()
{
SSTable st;
int i,low,high;
KeyType key;
printf("Please input the length of data:"); //输入数据元素的个数
scanf("%d",&st.length);
printf("Please input a sequense of increasing numbers:"); //挨个输入数据元素
st.R=(ElemType*)malloc((st.length+1)*sizeof(ElemType));
for(i=1;i<=st.length;i++)
scanf("%d",&st.R[i].key);
low=1;
high=st.length;
printf("Please input the number you are searching for:"); //输入要查找的数据元素
scanf("%d",&key);
Search_Bin(st,key,low,high);
free(st.R);
return 0;
}
写这段代码的时候可谓是历经千辛万苦,把 l o w = m i d + 1 low=mid+1 low=mid+1和 h i g h = m i d − 1 high=mid-1 high=mid−1搞混了,硬生生想来想去想了半个小时。。。。确定没想到这里会出错。以后一定连#include都不放过检查了…有些地方还是不能太过自信。
上面的算法因为输入的时候就是递增输入,所以跳过了排序的阶段。
现在在回过头来想一想,一个查找表按折半查找可以分为三个部分,分别是左子表、中间数据元素和右子表。是不是跟二叉树的结构很像?左子树、根结点、右子树。所以,折半查找的过程就可以用二叉树来描述。树中每一结点对应表中的一个记录,但结点值不是记录的关键字,而是记录在表中的位置序号。
把当前查找区间的中间位置作为根,左子表和右子表分别作为根的左子树和右子树,由此得到的二叉树称为折半查找的判定树。
成功的这般查找恰好是走了一条从判定树的根到被查结点的路径,经历比较的关键字个数恰为该结点在树中的层次。
例如,对数字1~11这般查找的判定树为:
这里要注意,假如我们要找1,最后来到
l
o
w
=
1
low=1
low=1、
h
i
g
h
=
2
high=2
high=2,此时
m
i
d
=
(
l
o
w
+
h
i
g
h
)
/
2
=
1
mid=(low+high)/2=1
mid=(low+high)/2=1,所以此时1为中间数据元素。因为
“
/
”
“/”
“/”是整除运算。
因为上面的判定树是建立在查找成功的情况下,那么如果要看查找失败的情况的话,我们可以在每个结点的左右子树(不存在的情况下)建立一个失败域,如下图:
当查找到空白的方形结点时表示查找失败。
折半查找的时间复杂度为
O
(
log
2
n
)
O(\log_2n)
O(log2n)。
分块查找
分块查找(Blocking Search)又称索引顺序查找,这是一种性能介于顺序查找和折半查找之间的一种查找方法。在此方法中,需建立一个索引表。若将查找表分为3个子表,那么索引表就为每一个子表建立一个索引项,该索引项中的内容有:该子表中最大的关键字,子表的第一个记录在该子表的位置。如下图:
分块查找分为两步:
- 先确定待查找记录所在的子表
- 在子表中顺序查找
具体代码如下:
#include <stdio.h>
#include <malloc.h>
#include <stdlib.h>
typedef int KeyType;
typedef char InfoType;
typedef struct
{
KeyType key;
InfoType otherinfo;
}ElemType;
typedef struct
{
ElemType *R;
int length;
}SSTable;
typedef struct
{
KeyType MaxKey;
int StartPlace;
}Index;
int main()
{
//对查找表进行初始化
SSTable st;
//输入顺序表中数据元素的个数(3的倍数)
printf("Please input the length of the table(multiples of 3):");
scanf("%d",&st.length);
st.R=(ElemType*)malloc((st.length+1)*sizeof(ElemType));
//依次输入数据元素
printf("Please input the data of the table:");
for(int i=1;i<=st.length;i++)
scanf("%d",&st.R[i].key);
Index index[3];
int j=2;
//对索引表初始化
int n=st.length/3;
for(int i=0;i<3;i++)
{
index[i].StartPlace=1+n*i; //索引表每个数据项指向每个子表的起始位置
index[i].MaxKey=st.R[1+n*i].key; //从每个子表中第一个数据元素开始
for(;j<=n*(i+1);j++) //因为给索引表中最大关键赋值的是子表中第一个数据元素,所以从第二个开始比较
{
if(index[i].MaxKey<st.R[j].key)
index[i].MaxKey=st.R[j].key;
}
j++; //for循环执行完后,j是下一个子表中第一个数据元素的位置,因为前面说了从第二个开始比较,所以再自加一次
}
int SearDa,num;
//开始查找
//输入你要查找的数据元素
printf("Please input the data you are searching:");
scanf("%d",&SearDa);
for(int i=0;i<=2;i++)
{
if(SearDa<=index[i].MaxKey) //找出该数据元素在哪个子表
{
num=index[i].StartPlace;
while(num<=n*(i+1))
{
if(SearDa==st.R[num].key)
{
printf("The data is set at:%d",num);
exit(0);
}
num++;
}
}
}
return 0;
}
这里分块我是默认分为3个块的。
可能代码在某些方面有未完善之处,希望大家多多指正。
总结
查找在生活中十分普遍,上网查资料、搜电影等都是一个查找过程。那么这时我们想要提高我们的查找效率,那么一个好的查找算法就尤为关键了。在这里学习了三种查找方式,分别是顺序查找、折半查找和分块查找,但都是对于线性表的查找,有一定的局限性。而在这三种查找方式间,又各有各自的优缺点。
首先对于顺序查找,它是挨个挨个将所要查找的数据与表中的数据比较,数据量比较大的时候,查找效率极低,但该算法结构简单,易实现,对于顺序和链式结构都使用,所以,只有当数据在一定小的范围内,才适用于顺序查找的方式。
接下来是折半查找,折半查找的效率就高得多了,每一次比较失败后都能排除一般的数据元素。总的下来比较的次数也少。但不足的是,折半查找有一个点,那就是只能在一个递增的顺序表中进行,也就是我们在折半查找之前,还需对一个线性表进行排序,这就有点费时了,而且也不方便对线性表做修改。所以折半查找就适用于递增序列,基本不进行修改的线性表。
最后是分块查找,无外乎是为顺序表添加了一个索引表,类似于折半查找,首先是通过与每个子表的最大值比较,确定其所在子表,然后再按顺序查找的方式在子表中去查找。首先,它比顺序查找要好是因为通过分块,就排除了其它块中的数据元素,它比折半查找要差一点是因为,分块后,它需要顺序查找。但是因为它的线性表中,数据元素是无序的,所以对表的修改比较容易实现。但算法更复杂,存储空间更大。
由此,线性表的查找更适用于静态查找表。