线性表的查找——顺序、折半、分块

查找的基本概念

对于栈、队列、树、图等结构,在该算法中查找是十分常见的,而在实际应用中也是一样。所以查找的效率十分重要。像我们平时在图书馆找书、找书上的某一页、互联网上的信息检索等都是查找,一个好的查找方法可以大大地提升效率,节约时间。

  1. 查找表
    查找表是由同一类型的数据元素(或记录)构成的集合。
  2. 关键字
    关键字是数据元素中某个数据项的值,用它可以标识一个数据元素。若能唯一地标识一个数据元素,则称它为主关键字。当数据元素只有一个数据项时,其关键字即为该数据元素的值。例如我们每个人,有姓名、年龄、身份证号等,因为身份证号是唯一的,所以身份证号为我们每个人的主关键字。
  3. 查找
    查找是根据给定的某个值,在查找表中确定一个其关键字等于给定值的记录或数据元素。
    若有这样的记录,则查找成功,返回其在表中的位置;若查找失败,则可以提示表中没有这样的记录。
  4. 动态查找表和静态查找表
    若在查找的同时对表做修改操作,则相应的表称之为动态查找表,否则称为静态查找表。
  5. 平均查找长度
    在查找时,需和给定值进行比较的关键字个数的期望值,称为查找算法的平均查找长度(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=1nPiCi
    其中, 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)也称二分查找。这个方法应该都在小学,或者初中???多多少少都有接触过。所谓折半,就是将查找表分为两半。每次都从查找表的中间元素查找,若查找失败,排除刚查找的数据元素后将该查找表分为左右两个子表,若第一次比较小于那个中间元素,则在左子表中继续又从中间元素开始,若大于那个中间元素,则在右子表中去查找…

那么理解是这样理解,但是貌似少了一条定义的规则,在上面的描述过程中,我们默认了左子表中的数据元素一定小于中间元素,右子表中的数据元素一定大于中间元素。这就对了,折半查找只能采用顺序存储结构,即这些数据元素只能按照从小到大的顺序排列成一行。

那么在算法中我们该如何去实现?

算法分析:

  1. 首先我们肯定是要有一个变量来标识每次查找的中间数据元素,设这个变量为 m i d mid mid。同时左子表和右子表不需要全部都表示出来,在判断给定值和中间元素的大小后,只取左右子表其中一个就可以了,那么该如何去取呢?
  2. 我们不妨将每一个查找表分为三个点,用这三个点来表示一个查找表的范围。第一个点是第一个数据元素,第二个点是中间的数据元素,第三个点最后一个数据元素。若第一次比较小于中间元素,那么我们只需将查找表缩小到左子表就行了,这时只需将第三个点的位置变一下,变到中间元素的前一个元素,再用 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=mid1搞混了,硬生生想来想去想了半个小时。。。。确定没想到这里会出错。以后一定连#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个子表,那么索引表就为每一个子表建立一个索引项,该索引项中的内容有:该子表中最大的关键字,子表的第一个记录在该子表的位置。如下图:在这里插入图片描述
分块查找分为两步:

  1. 先确定待查找记录所在的子表
  2. 在子表中顺序查找

具体代码如下:

#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个块的。

可能代码在某些方面有未完善之处,希望大家多多指正。

总结

查找在生活中十分普遍,上网查资料、搜电影等都是一个查找过程。那么这时我们想要提高我们的查找效率,那么一个好的查找算法就尤为关键了。在这里学习了三种查找方式,分别是顺序查找、折半查找和分块查找,但都是对于线性表的查找,有一定的局限性。而在这三种查找方式间,又各有各自的优缺点。

首先对于顺序查找,它是挨个挨个将所要查找的数据与表中的数据比较,数据量比较大的时候,查找效率极低,但该算法结构简单,易实现,对于顺序和链式结构都使用,所以,只有当数据在一定小的范围内,才适用于顺序查找的方式。

接下来是折半查找,折半查找的效率就高得多了,每一次比较失败后都能排除一般的数据元素。总的下来比较的次数也少。但不足的是,折半查找有一个点,那就是只能在一个递增的顺序表中进行,也就是我们在折半查找之前,还需对一个线性表进行排序,这就有点费时了,而且也不方便对线性表做修改。所以折半查找就适用于递增序列,基本不进行修改的线性表。

最后是分块查找,无外乎是为顺序表添加了一个索引表,类似于折半查找,首先是通过与每个子表的最大值比较,确定其所在子表,然后再按顺序查找的方式在子表中去查找。首先,它比顺序查找要好是因为通过分块,就排除了其它块中的数据元素,它比折半查找要差一点是因为,分块后,它需要顺序查找。但是因为它的线性表中,数据元素是无序的,所以对表的修改比较容易实现。但算法更复杂,存储空间更大。

由此,线性表的查找更适用于静态查找表。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值