第九章 查找

引言

  1. 思维导图中,标红的是重点内容,标黄的是次重点。
  2. 本章实验点此查看。
  3. 码字不易,如果这篇文章对您有帮助的话,希望您能点赞、收藏、加关注!您的鼓励就是我前进的动力!

知识点思维导图

在这里插入图片描述

(点此查看原图)

补充:
(NULL)

注意事项与易错点

(NULL)

题型总结

一、散列函数的构造方法

  1. 直接定址法:
    1)内容:取关键字或关键字的某个线性函数作哈希地址,即 H ( k e y ) = a ⋅ k e y + b H(key)=a·key+b H(key)=akey+b
    2)该方法不会产生冲突,但实际用该方法的情况很少。
  2. 数字分析法:
    1)内容:如果事先知道关键字的集合,且关键字的位数比哈希表的地址位数多,则可选取数字分布比较均匀的若干位作为哈希函数
    2)该方法适于关键字位数比哈希地址位数大,且可能出现的关键字事先知道的情况。
  3. 平方取中法:
    1)内容:取关键字平方后中间几位作哈希地址。
    2)适于不知道全部关键字情况。
  4. 折叠法:
    1)内容:将关键字分割成位数相同的几部分(最后一部分的位数可以不同),然后取这几部分的叠加和(舍去进位)做哈希地址。
    2)种类:
    ①移位叠加:将分割后的几部分低位对齐相加。
    ②间界叠加:从一端沿分割界来回折送,然后对齐相加。
    3)适于关键字位数很多,且每一位上数字分布大致均匀情况。
  5. 除留余数法:
    1)内容:取关键字被某个不大于哈希表表长 m 的数 p 除后所得余数作哈希地址,即 H ( k e y ) = k e y M O D p H(key)=key MOD p H(key)=keyMODp p ≤ m p≤m pm
    2)特点:简单、常用,可与上述几种方法结合使用。
    3)p一般选取质数,若哈希表表长为m,则要求p≤m,且接近m或等于m。

二、处理散列冲突的方法

  1. 开放定址法:
    1)内容:一旦发生冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到。
    2)公式: H i = ( H ( k e y ) + d i ) M O D m , i = 1 , 2 , … k ( k ≤ m − 1 ) Hi=(H(key)+di) MOD m,i=1,2,…k(k≤m-1) Hi=(H(key)+di)MODmi=1,2,k(km1)。其中:H(key):哈希函数, m:哈希表表长, di:增量序列。
    3)分类:
    ①线性探测再散列: d i = 1 , 2 , 3 , … … m − 1 di=1,2,3,……m-1 di=1,2,3,……m1
    ②二次探测再散列: d i = 1 2 , − 1 2 , 2 2 , − 2 2 , 3 2 , … k 2 ( k ≤ m / 2 ) di=1²,-1²,2²,-2²,3²,…k²(k≤m/2) di=12,12,22,22,32,k2(km/2)
  2. 链地址法:
    1)思想:将所有关键字为同义词的记录存储在一个单链表中,并用一维数组存放头指针
    2)方法:设哈希地址在区间 [ 0 , m − 1 ] [0,m-1] [0,m1]上,以每个哈希地址作为一个指针,指向一个链,即分配指针数组 H T P [ m ] HTP[m] HTP[m],建立 m m m 个空链表,由哈希函数对关键字运算后,映射到同一哈希地址 i i i 的同义词均加入到 H T P [ i ] HTP[i] HTP[i] 指向的链表中。
    3)优点:
    ①链地址法不会产生堆积现象,因而平均查找长度比较短;
    ②由于链地址法中各单链表上的结点空间是动态申请的,故它更适合于造表前无法确定表长的情况;
    ③在链地址法构造的哈希表中,在单链表中对结点进行删除操作易于实现。

三、重要算法

(一)顺序表查找

  • 顺序表查找算法
typedef int KeyType; 
typedef struct{
	KeyType key;
}ElemType;

//顺序存储结构
typedef struct{
	ElemType *elem;	//指向数据元素基址,0号不存元素
	int length ;
}SSTable;//静态顺序表

//顺序表查找(优化版)
int Search_Seq(SSTable ST, KeyType key){
	ST.elem[0].key = key;//设置哨兵
	int i=ST.length;
	while( ST.elem[i].key!=key )
		i--;
	return i;
}

1)注意顺序表的存储结构。
2)设置哨兵,避免了每次都要判断查找位置是否越界。
3)该算法的时间复杂度是 O ( n ) O(n) O(n)。查找效率较低,适用于小数据的查找。

  • 折半查找
int Search_Bin(SSTable ST, KeyType key){
	//先确定low, high, mid的值
	int low=1; 
	int high=ST.length;
	while (low<=high){
		mid=(low+high)/2;
		
		//查找的主体
		if (key==ST.elem[mid].key)  
			return mid;
		else if(key<ST.elem[mid].key) 
			high=mid-1;
		else if(key>ST.elem[mid].key) 
			low=mid+1;
		
	}
	return 0;
}  

1)适用条件:采用顺序存储结构的有序表。
2)该算法的时间复杂度为 O ( l o g n ) O(logn) O(logn),远好于顺序查找的时间复杂度,但其不适用于要频繁执行插入和删除操作的数据集。

(二)二叉排序树查找

  • 二叉排序树查找操作
typedef int KeyType; 
typedef struct{
	KeyType key;
	……
}ElemType;

//二叉排序树的存储结构
typedef struct Node{
ElemType data; 		//结点的关键字
struct Node *lchild,*rchild; 	
}BiTNode, *BiTree;

//二叉排序树的查找(递归查找)
/*递归查找二叉排序树T中是否存在key,指针f指向t的双亲,其初始调用值为NULL*/
/*若查找成功,则指针p指向该数据的元素结点*/
/*否则,指针p指向查找路径上访问的最后一个结点并返回FALSE*/
BiTree SearchBST(BiTree& T, KeyType key, BiTree f, BiTree& p)
{
	//未找到对应元素
	if(T == NULL){
		p = f;//p指向查找路径上访问的最后一个结点
		return NULL;
	}
	
	//查找成功
	else if(key == T->data.key){ 
		p=T;
		return T;
	}
	
	//递归查找
	else if(key < T->data.key) 
		return SearchBST(T->lchild, key, T, p);
	else if(key > T->data.key) 
		return SearchBST(T->rchild, key, T, p);  
}

1)二叉排序树的查找思路:若二叉排序树为空,则查找不成功;否则:若给定值等于根结点的关键字,则查找成功;若给定值小于根结点的关键字,则在左子树上进行查找;若给定值大于根结点的关键字,则在右子树上进行查找。
2)该函数返回的T实际上是个结点。
3)二叉排序树的效率:含有n个结点的BST不唯一。因此,含有n个结点的BST的ASL和树的形态有关。最好情况与折半查找相同,即 O ( l o g n ) O(logn) O(logn);最差情况是退化为单支树,ASL=(n+1)/2,即 O ( N ) O(N) O(N)

  • 二叉排序树插入操作
//利用查找函数实现插入操作
Status InsertBST_1(BiTree &T, ElemType e){ 
	BiTree p, s;
	
	//用查找函数找到结点应插入的位置
	if(SearchBST(T, e.key, NULL, p) == NULL){
		
		//给要插入的元素分配存储空间及初始化
		s=(BiTree)malloc(sizeof(BiTNode));
		s->data=e; 
		s->lchild=s->rchild=NULL;
		
		//函数主体
		if(p == NULL)//p为搜索时的最后一个树结点 
			T=s;    
		else if(e.key < p->data.key)  
			p->lchild=s;
		else if(e.key > p->data.key)
			p->rchild=s;
		
		return True;
	}
	else //原二叉排序树中已有该元素
		return False;
}

//递归法实现插入操作(先序遍历)
int InsertBST_2(BiTree &p,KeyType k){
	
	//若原树为空,直接插入第一个结点
	if (p==NULL){	 	
		p=(BiTree)malloc(sizeof(BiTNode));
		p->key=k;
		p->lchild=p->rchild=NULL;
		return 1;
	}
	//若存在相同关键字的结点,返回0
	else if (k==p->key) 
		return 0;
	//若若关键字小于当前结点关键字,则插入到当前结点的左子树中
	else if (k<p->key) 
		return InsertBST_2(p->lchild,k);
	//否则,插入到右子树中
	else  
		return InsertBST_2(p->rchild,k);	
 }

1)插入有两种方法,一是利用查找函数实现插入操作;二是用递归法实现插入操作。查找函数法较常考。
2)二叉排序树插入新结点都是新的叶子,不必移动其它结点,仅需修改某个结点的指针。这相当于在一个有序序列上插入一个记录而又不需要移动其他记录。

  • 二叉排序树的创建(了解)
//创建二叉排序树
BiTree CreateBST(){
	KeyType key;
	scanf(&key);
	while(key){ //k为0时输入结束
		InsertBST(T, key);
		scanf(&key);
	}
	return T;
} 

1)二叉排序树既拥有折半查找的特性,又采用链式存储。
2)利用二叉排序树的查找插入操作,从空树出发,经过一系列的查找插入之后,生成一棵二叉排序树,每输入一个结点数据,就插入到当前已生成的二叉排序树中。

  • 二叉排序树删除操作(了解)
//若二叉排序树T中存在关键字等于key的数据元素时,则删除该数据元素结点, 并返回TRUE;否则返回FALSE。
Status DeleteBST(BiTree &T,KeyType key)
{ 
	if(!*T) //不存在关键字等于key的数据元素
		return FALSE;
	else{
		if (key==T->data.key) //找到关键字等于key的数据元素  
			return Delete(T);
		else if (key<T->data.key)
			return DeleteBST(T->lchild,key);
		else
			return DeleteBST(T->rchild,key); 
	}
}

//从二叉排序树中删除结点p,并重接它的左或右子树
Status Delete(BiTree &p){
	BiTree q,s;
	if(p->rchild==NULL){ //右子树空则只需重接它的左子树
		q=p;
		p=p->lchild; 
		free(q);
	}
	else if(p->lchild==NULL){ //只需重接它的右子树
		q=p; 
		p=p->rchild;
		free(q);
	}
	else{ //左右子树均不空
		q=p; 
		s=p->lchild;
		while(s->rchild){ //转左,然后向右到尽头
			q=s;
			s=s->rchild;
		}
		p->data=s->data; // s指向被删结点的直接前驱
		if(q!=p)
			q->rchild=s->lchild; //重接*q的右子树 
		else
			q->lchild=s->lchild; //重接*q的左子树
		free(s);
	}
	return TRUE;
}

1)删除二叉排序树上某个结点之后,仍然保持二叉排序树的特性。删除分三种情况:被删除的结点是叶子;被删除的结点只有左子树或者只有右子树;被删除的结点既有左子树,也有右子树。
2)删除叶子结点,只需将其双亲结点指向它的指针置为空,再释放它即可。
3)删除的结点只有左子树或者只有右子树,则使其双亲结点的相应指针域的值改为指向被删除结点的左子树或右子树。
4)删除的结点既有左子树又有右子树。方法一:将结点的右子树链接到其中序前驱结点的右指针域。把要删除结点的双亲结点原本指向要删除结点的指针链接到其的左子树上即可。方法二:把要删除结点的中序前驱结点的值赋给该结点。将中序前驱结点的双亲结点的右指针链接到中序前驱结点的子树即可。

(三)散列表查找(了解)

  • 相关操作
typedef struct{
   int *elem; /* 数据元素存储基址,动态分配数组 */
   int count; /*  当前数据元素个数 */
}HashTable;

int m=0; /* 散列表表长,全局变量 */

/* 初始化散列表 */
Status InitHashTable(HashTable *H){
	int i;
	m=HASHSIZE;
	H->count=m;
	H->elem=(int *)malloc(m*sizeof(int));
	for(i=0;i<m;i++)
		H->elem[i]=NULLKEY; 
	return OK;
}

/* 散列函数 */
int Hash(int key){
	return key % m; /* 除留余数法 */
}

/* 插入关键字进散列表 */
void InsertHash(HashTable *H,int key){
	int addr = Hash(key); /* 求散列地址 */
	while (H->elem[addr] != NULLKEY){ /* 如果不为空,则冲突 */
		addr = (addr+1) % m; /* 开放定址法的线性探测 */
	}
	H->elem[addr] = key; /* 直到有空位后插入关键字 */
}

/* 散列表查找关键字 */
Status SearchHash(HashTable H,int key,int *addr){
	*addr = Hash(key);  /* 求散列地址 */
	while(H.elem[*addr] != key){ /* 如果不为空,则冲突 */
		*addr = (*addr+1) % m; /* 开放定址法的线性探测 */
		if (H.elem[*addr] == NULLKEY || *addr == Hash(key)) /* 如果循环回到原点 */
			return UNSUCCESS;	/* 则说明关键字不存在 */
	}
	return SUCCESS;
}

方法心得

(NULL)

参考资料:
[1] 程杰. 大话数据结构. 北京:清华大学出版社, 2020.
[2]严蔚敏,吴伟民. 数据结构 (C语言版). 北京:清华大学出版社, 1997.

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

沉远

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值