数据结构 | 第八章 查找

一、查找

1、基本概念

查找表

同一类型数据元素(记录)构成的集合。

关键字

是数据元素中某个数据项的值。

主关键字

若此关键字能唯一标识一个记录

次关键字

识别多个数据元素的关键字。

查找

  • 根据给定的某个值,在表中确定一个其关键字等于给定值的数据元素
  • 静态、动态
1.1 静态查找表
  • 只作查找操作的查找表;
  • 查询某个特定的数据元素是否在查找表中;
  • 检索某个特定的数据元素各种属性
1.2 动态查找表
  • 插入删除操作;
  • 查找所基于的数据结构是集合,而集合的记录之间没有本质关系,可以组织成表、树等结构。
2、顺序表查找
2.1 顺序表查找算法
#include<stdio.h>
#include<stdlib.h>


int SeqSearch(int *a, int n, int key){
	int i;
	a[0] = key;
	i = n;
	while (a[i] != key){
		i--;
	}

	return i;
}

int main(){


	system("pause");
	return 0;
}
  • 时间复杂度:O(n)
  • 平均查找次数:(n+1)/2
  • 效率低,算法简单。
3、有序表查找
3.1 折半(二分)查找
  • 每次取中间记录查找的方法,划分区域,在进行查找;
  • 前提:必须是关键字有序,且为顺序存储
3.2 折半查找算法
/*@ 折半查找 */
int BinarySearch(int *dataList, int len, int key){
	
	int l, h, m;
	l = 1;	// 最低下标为记录的首位
	h = len;

	while (l < h){
		m = (l + h) / 2;	// 折半
		if (key < dataList[m]){	// 判断查找的值在左区域还是右区域
			h = m - 1;
		}
		else if(key>dataList[m]){
			l = m + 1;
		}
		else{
			return m;	// 若相等,则返回该位置
		}
	}
	return 0;
}
  • 时间复杂度:O(logn)
4、插值查找

公式:mid = low + (high-low) * (key-a[low]) / (a[high] - a[low])

  • 只将折半的查找中的公式替换即可;
  • key:要查找的元素;
  • 核心:要查找的key与查找表中最大最小记录的关键字比较后的查找方法。
5、斐波那契查找
  • 利用黄金分割原理
5.1 斐波那契查找算法
int FibinacciSearch(int *dataList, int n, int key){
	int F[] = { 1, 3, 5, 7 };// ....
	int l, m, h, i, k;
	l = 1;	// 记录首位
	h = n;	// 记录末尾
	k = 0;
	while (n>F[k]-1){	// 计算n到F中的位置
		k++;
	}
	for (i = n; i < F[k] - 1; ++i){	// 将不满的数值补全
		dataList[i] = dataList[n];
	}
	while (l < h){	
		m = l + F[k - 1] - 1;	// 当前分隔的下标
		if (key < dataList[m]){	// 若查找记录小于当前分隔记录
			h = m - 1;	// 最高下标调整到分隔下标mid-1处
			k = k - 1;	// 斐波那契数列下标减一位
		}
		else if (key>dataList[m]){	
			l = m + 1;
			k = k - 2;
		}
		else{
			if (m < n){
				return m;
			}
			else{
				return n;
			}
		}
	}
	return 0;
}

  • 时间复杂度:O(logn)
  • 平均性能要优于折半查找;
6、线性索引查找
  • 一个关键字与它对应的记录相关联;
  • 每个索引项至少要包含关键字和其对应的记录在存储器中的位置;
  • 索引技术:组织大型数据库以及磁盘文件的重要技术。
6.1 稠密索引
  • 在线性索引中,将数据集中的每个记录对应一个索引项;
  • 对于稠密索引,索引项一定是按照关键字有序的排列;
  • 空间代价大;
  • 索引项有序,可优先考虑到折半、插值、斐波那契有序查找算法。
6.2 分块索引

分块有序,在对每一个块建立一个索引项,来减少索引项的个数。

满足条件

  • 块内无序,减少空间使用,只能顺序查找;
  • 块间有序,后续记录的关键字均要比前一个大。

结构

  • 最大关键字,保证下一块的最小关键字都要比上一个最大关键字大;
  • 存储块中个数,便于循环时使用;
  • 用于指向块首数据元素的指针,并于开始对这一块数据进行遍历。
  • 块中平均查找长度:Lw = (t+1)/2
  • 分块平均查找长度:ASLw = Lb + Lw = 1/2*(n/t + t) +1
    n为总记录个数,t为块内记录个数。
6.2 倒排索引
  • 最基础的搜索技术;
  • 记录号表存储具有相同次关键字的所有记录的记录号。

结构

  • 次关键字;
  • 记录号表;
7、二叉排序(查找)树
  • 将一颗二叉树,通过中序遍历进行而得到的序列为二叉排序树;
  • 提高查找和插入删除关键字的速度。

性质

  • 若它的子树不为空,则左子树上所有结点的值均小于它的根结构的值
  • 若它的子树不为空,则右子树上所有结点的值均大于它的根结构的值

总结

  • 插入、删除时,仅需修改链接指针即可;
  • 二叉排序树的查找性能取决于二叉排序树的形状;
  • 时间复杂度:O(logn)
8、平衡二叉树(AVL树)

是一种高度平衡的二叉排序树,左右子树的个数差不能超过1

  • 平衡因子BF:将二叉树上结点的左子树深度减去右子树深度的值
  • 最小不平衡子树:距离插入结点最近的,且平衡因子的绝对值大于1的结点为根的子树。
8.1 原理

在构建二叉排序树过程中,每插入一个结点时,先检测是否因插入而破坏了树的平衡性。若是,则找出最小不平衡子树。调整个结点的链接关系,进行相应旋转

8.2 算法

BinarySortTree.h

#include<stdio.h>
#include<stdlib.h>

#define LH +1	// 左高
#define EH 0	// 
#define RH -1	//

#ifdef __cplusplus
extern "C"{
#endif 

	/*@ 二叉树的二叉链表结点结构定义 */ 
	typedef struct BiTNode{
		int data;					// 结点数据
		int bf;						// 平衡因子
		BiTNode *lChild, *rChild;	// 左右孩子
	}BiTNode, *BiTree;

	/*@ 右旋:旋转处理之前的左子树的根结点 */
	void RRotate_BiTree(BiTree *T);

	/*@ 左旋:旋转处理之前的右子树的根结点0 */
	void LRotate_BiTree(BiTree *T);

	/*@ 对以指针T所指结点为根的二叉树作为左平衡旋转处理 */
	void LeftBalance(BiTree *T);

	/*@ 若在平衡的二叉排序树T中不存在和e有相同关键字的结点,则插入一个
		数据元素为e的新结点并返回1,否则返回0.若因插入而使二叉排序树失
		去平衡旋转处理,taller反映T是否长高。
	*/
	int InsertAVL(BiTree *T, int e, int *taller);

	/*@ 对以指针T所指结点为根的二叉树作为右平衡旋转处理 */
	void LeftBalance(BiTree *T);

#ifdef __cplusplus
}
#endif 

BinarySortTree.cpp

#include "BinarySortTree.h"

/*@ 右旋:旋转处理之前的左子树的根结点 */
void RRotate_BiTree(BiTree *T){
	BiTree temp;
		
	temp = (*T)->lChild;	// temp指向T的左子树根结点
	(*T)->lChild = temp->rChild;	// temp的右子树挂接为T的左子树
	temp->rChild = (*T);	

	*T = temp;	// T指向新的根结点
}

/*@ 左旋:旋转处理之前的右子树的根结点0 */
void LRotate_BiTree(BiTree *T){
	BiTree temp;

	temp = (*T)->rChild;	// temp指向T的右子树根结点
	(*T)->rChild = temp->lChild;	// temp的左子树挂接为T的右子树
	temp->lChild = (*T);

	*T = temp;	// T指向新的根结点
}

/*@ 对以指针T所指结点为根的二叉树作为左平衡旋转处理 */
void LeftBalance(BiTree *T){
	BiTree L, Lr;
	L = (*T)->lChild;

	switch (L->bf){
		/* 检查T的左子树的平衡度,并作相应平衡处理 */ 
	case LH:	// 新结点插入在T的左孩子的左子树上,要作单右旋处理
		(*T)->bf = L->bf = EH;
		RRotate_BiTree(T);
		break;
		/* 新结点插入在T的左孩子的右子树上,要作双旋处理 */
	case RH:
		Lr = L->rChild;	// Lr指向T的左孩子的右子树根
		switch (Lr->bf){	// 修改T及其左孩子的平衡因子
		case LH:
			(*T)->bf = RH;
			L->bf = EH;
			break;
		case EH:
			(*T)->bf = L->bf = EH;
			break;
		case RH:
			(*T)->bf = EH;
			L->bf = LH;
			break;
		}
		Lr->bf = EH;
		LRotate_BiTree(&(*T)->lChild);	// 左子树左旋
		RRotate_BiTree(T);	// 右子树右旋
	default:
		break;
	}
}

/*@ 对以指针T所指结点为根的二叉树作为右平衡旋转处理 */
void LeftBalance(BiTree *T){
	BiTree L, Ll;
	L = (*T)->rChild;

	switch (L->bf){
		/* 检查T的左子树的平衡度,并作相应平衡处理 */
	case RH:	// 新结点插入在T的左孩子的左子树上,要作单右旋处理
		(*T)->bf = L->bf = RH;
		LRotate_BiTree(T);
		break;
		/* 新结点插入在T的左孩子的右子树上,要作双旋处理 */
	case LH:
		Ll = L->lChild;	// Lr指向T的左孩子的右子树根
		switch (Ll->bf){	// 修改T及其左孩子的平衡因子
		case RH:
			(*T)->bf = RH;
			L->bf = EH;
			break;
		case EH:
			(*T)->bf = L->bf = EH;
			break;
		case LH:
			(*T)->bf = EH;
			L->bf = LH;
			break;
		}
		Ll->bf = EH;
		RRotate_BiTree(&(*T)->rChild);	// 右子树右旋
		LRotate_BiTree(T);	// 左子树左旋

	default:
		break;
	}
}

/*@ 若在平衡的二叉排序树T中不存在和e有相同关键字的结点,则插入一个
数据元素为e的新结点并返回1,否则返回0.若因插入而使二叉排序树失
去平衡旋转处理,taller反映T是否长高。
*/
int InsertAVL(BiTree *T, int e, int *taller){
	if (!*T){
		// 插入新结点,树长高,则taller为1
		*T = (BiTree)malloc(sizeof(BiTNode));
		(*T)->data = e;
		(*T)->lChild = (*T)->rChild = NULL;
		(*T)->bf = EH;
		*taller = 1;
	}
	else{
		// 树中已存在和e有相同关键字的结点则不再插入
		if (e == (*T)->data){

			*taller = 0;
			return 0;
		}
		// 应继续在T的左子树中进行搜索
		if (e < (*T)->data){

			if (!InsertAVL(&(*T)->lChild, e, taller))	// 未插入
				return 0;
			if (taller){	// 已插入
				switch ((*T)->bf)	// 检查树的平衡度
				{
				case LH:	// 原来的左子树比右子树高,需要左平衡处理
					LeftBalance(T);
					*taller = 0;
					break;
				case EH:	// 左右子树等高,现因左子树增高而树增高
					(*T)->bf = LH;
					*taller = 1;
					break;
				case RH:	// 原本右子树比左子树高,现右子树等高
					(*T)->bf = EH;
					*taller = 0;
					break;
				}
			}
		}
		// 应继续在T的右子树中进行搜索
		else{
			 if (!InsertAVL(&(*T)->rChild, e, taller))	// 未插入
				return 0;
			if (*taller){	// 插入到右子树且长高了
				switch ((*T)->bf)
				{
				case LH:
					(*T)->bf = EH;
					*taller = 0;
					break;
				case EH:
					(*T)->bf = RH;
					*taller = 1;
					break;
				case RH:
					LeftBalance(T);
					*taller = 0;
					break;
				default:
					break;
				}
			}
		}
	}

	return 0;
}

查找、删除、插入的时间复杂度均为:O(logn)

9、多路查找树(B树)
  • 由于内存存取外存次数很多,会在时间效率上存在瓶颈,为此引入多路查找树。
  • 概念:其每个结点的孩子数可以多于俩个,且每个结点处可以存储多个元素
9.1 2-3树
  • 每个结点都具有孩子2个孩子则2结点,3个则3结点。
    • 2结点:一个元素2个孩子或没有孩子;
    • 3结点:一小一大俩个元素和三个孩子或没有孩子;
9.1.1 插入

插入只在叶子结点上发生。

插入三种情况

  • 对于空树,插入一个2结点
  • 若插入到一个2结点中,则需要升级3结点
  • 若插入到一个3结点中,且3结点此时存满了,则需要将此三个元素的其中一个往上移动一层
  • 主要插入会造成层之间的结点调节,并且通过2、3结点互相转换
9.1.2 删除

删除三种情况

  • 删除3结点中的元素,之间删除即可;
  • 删除叶子是2结点的元素,会造成多种情形;
    • 1)此结点的双亲也是2结点,且拥有一个3结点的右孩子;
    • 2)双亲是2结点,它是右孩子也是2结点;
    • 3)双亲是一个3结点;
    • 4)满二叉树,需要考虑到层数的减少。
9.2 2-3-4树

一个3结点包含小中大三个元素和四个孩子或没有孩子

9.3 B树
  • B树是一种平衡的多路查找树;
  • B树的:结点最大的孩子数目;
  • 为内外存的数据交互准备;

一个m阶B树的属性

  • 根结点不是叶结点,则其至少有俩棵子树
  • 每一个非根的分支结点都有k-1个元素和k个孩子,每一个叶子结点n都有k-1个元素
  • 所以叶子结点都位于同一层次;
9.4 B+树
  • 是应文件系统所需而处的一种B树的变形树
  • 在B+树中,出现在分支结点中的元素会被当作他们在该分支结点位置的中序后继者(叶子结点)中再次列出。另外,每个叶子结点都会保存一个指向后一叶子结点的指针
  • 灰色关键字即是根结点中的关键字在叶子结点再次列出

B+树与B树的主要差异

  • n棵子树的结点中包含有n个关键字
  • 所有的叶子结点包含全部关键字的信息,及指向含这些关键字记录的指针,叶子结点本身依关键字的大小自小而大顺序链接;
  • 所有分支结点可以看成是索引,结点中仅含有其子树中的最大(最小)关键字。

适合带有范围的查找。

10、散列表(哈希表)
10.1 散列表查找定义
  • 存储位置 = f(关键字)
  • 散列技术是在记录的存储位置和它的关键字之间建立一个确定的对应关系f
  • f为散列函数,或哈希函数;
  • 散列技术将记录存储在一块连续的存储空间;
  • 散列技术即是一种存储方法,也是一种查找方法。
  • 最适合的求解

冲突

key1 != key2f(key1) = f(key2)

避免冲突

尽量让散列地址均匀分布,保证存储空间的有效利用减少为处理冲突而耗费时间

10.2 散列的构造方法
10.2.1 直接定址法
  • 取关键字的某个线性函数值为散列地址;
    f(key) = a * key + b

优点

  • 简单、均匀、不会产生冲突;

缺点

  • 要事先知道关键字的分布情况,适合查找表较小且连续的情况。
10.2.2 数字分析法
  • 适用关键字的位数较多的数字。

例如:手机号可取后四位。

10.2.3 平方取中法

抽取数字中间几位在,平方。

10.2.4 折叠法
  • 将关键字从左到右分割成位数相等的几部分,在叠加求和,在取后几位。
10.2.5 除留余数法

f(key) = key mod p

  • 若表长为m,而p一般小于或等于m的最小质数或不包含小于20质因子的合数。
10.2.6 随机数法
  • f(key) = random(key)

考虑因素

  • 计算散列地址所需的时间;
  • 关键字的长度;
  • 散列表的大小;
  • 关键字的分布情况;
  • 记录查找的频率。
10.3 处理散列冲突的方法
10.3.1 开放定址法[线性探测法]
  • 若发生冲突,则寻找下一个散列地址
  • fi(key) = (f(key) + di) MOD m

堆积

争夺一个地址的情况。

二次探测法

增加平方运算的目的是为了不让关键字都聚集在某一块区域。

随机探测法

在冲突时,对于位移量di采用随机函数计算得到。

10.3.2 再散列函数法
  • fi(key) = RHi(key)
  • RHi为不同的散列函数
10.3.3 链地址法

将所有关键字为同义词的记录存储再一个单链表中;

10.3.4 公共溢出区法

为冲突建立一个公共溢出区来存放。

10.4 查找性能
  • 查找效率最高:O(1)
  • 散列表的好坏直接影响着出现冲突的频繁程度;
  • 装填因子α = 填入表中的记录个数 / 散列表长度:
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Jxiepc

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

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

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

打赏作者

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

抵扣说明:

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

余额充值