数据结构——查找篇

文章详细介绍了查找的基本概念和不同类型,包括顺序查找、折半查找、分块查找的原理、优缺点以及时间复杂度。此外,还讨论了二叉排序树的插入、查找和删除操作,以及如何通过平衡二叉树(AVL树)来维持高效查找。最后,文章提到了散列表的概念,包括散列函数和处理冲突的开放定址法与拉链法。
摘要由CSDN通过智能技术生成

数据结构——查找篇

查找的基本概念

  • 査找:根据给定的关键字的值,检索某个与该值相等的数据元素是否在查找表中找到为查找成功,找不到为查找失败
  • 查找表: 是由类型相同的数据元素(或记录)的集合。
  • 关键字: 是数据元素(或记录)中某个数据项的值,用它可以标识一个数据元素。
    • 主关键字: 此关键字可以唯一地标识一个记录
    • 次关键字: 用以识别若干记录的关键字
    • 查找成功: 表中存在这样一个记录
    • 查找失败:表中不存在关键字等于给定值的记录
  • 动态查找表在查找的同时对表进行修改操作(如插入,删除)。反之称为 静态查找表
  • 平均查找长度(ASL):为确定数据元素在査找表中的位置,需和给定值进行比较的关键字个数的期望值,称为查找算法在查找成功时的平均查找长度.
  • 冲突:两个不同的关键字,其散列函数值相同,因而被映射到同一表位置的现象称为冲突

线性表的查找

适合静态查找,主要采用顺序查找技术,折半查找技术(二分查找)。

顺序查找

顺序查找查找过程为从表的一端开始,依次将记录的关键字和给定值进行比较,若某个记录的关键字和给定值相等,则查找成功。

适用于线性表的顺序存储结构,和 链式存储结构。

时间复杂度: O(n)

// 设置监视哨的顺序查找
int Search_Seq(SSTable T, int key)
{ // 在顺序表T中顺序查找其关键字等于key的数据元素。若找到,则函数值为该元素在表中的位置,否则为0
	T.e[0].key = key; // 在表中的零位置设置哨兵
	int i=0;
	for ( i = T.length; T.e[i].key != key; --i);  // 从后往前找
	return i+1;

}

ASL=n+1/2

顺序查找的优点

  1. 算法简单

  2. 对表结构无任何要求

  3. 无论记录是否按关键字有序均可

缺点

  1. 平均查找长度较大
  2. 查找效率较低

折半查找

时间复杂度: O(log2^n)

折半查找 也叫 二分查找,它是一种效率较高的查找方法。

但是,折半查找要求线性表必须采用顺序存储结构,而且表中元素按关键字有序排列。不适用于数据元素经常变动的线性表

// mid 中间值  key待查找的值  high 尾值  low 头值

mid = (low + high)/2;
key<mid 则 high = mid-1;
key>mid 则 low = mid+1;
key==mid,找到;
hight<low,结束;


int Search(SSTable T, int key)
{
	// 在有序表T中折半查找其关键字等于key的数据元素。若找到,则函数值为该元素在表中的位置,否则为0
	int low = 1;
	int height = T.length-1;     // 置查找区初值
	int mid;
	while (low<=height)
	{
		mid = (low + height) / 2; 
		if (key == T.e[mid].key) return mid;   //找到待查元素
		else if (key < mid)  // 小于中间值 , height提前
			height = mid - 1;
		else if (key > mid) // 大于中间值 low 往后
			low = mid + 1;
	}
	return 0;
}

ASL=log2^(n+1)-1

优点:比较次数少,查找效率高

缺点: 对表结构要求高,只能用于顺序存储的有序表

算法分析 折半查找过程可用二叉树描述。树中每一结点对应表中一个记录,但结点值不是记录的关键字,而是记录在表中的位置序号。把当前查找区间的中间位置作为根,左子表和右子表分别作为根的左子树和右子树,由此得到的二叉树称为折半查找的 判断树

查找成功:
    比较次数= 路径上的结点数
	比较次数= 结点的 层数
    比较次数<= 树的深度([log2^n]+1——完全二叉树的深度)

查找不成功:
    比较次数=路径上的内部结点数
    比较次数<= 树的深度([log2^n]+1——完全二叉树的深度)
折半查找判定树的最后一层的节点数为  n-2^(h-1)+1,n=100,h=7,代入得37

分块查找

又称 索引顺序查找,是一种性能介于顺序查找和折半查找之间的查找方式。

此查找法中,除表本身之外,尚需建立一个“索引表”。把原表分为几个子表,对每个子表(或称块)建立一个索引项,其中内容包括两项:关键字项(其值为子表内最大关键字)和指针项(指示该子表的第一个记录在表中位置)。

索引表按关键字有序,则表或者有序或分块有序 分块有序即,第二个子表中所有记录的关键字均大于第一个子表中的最大关键字,以此类推

查找过程: 先确定待查找记录所在块(顺序或折半查找),再在块内查找(顺序查找)。

ASL= log2^(n/s+1)+s/2

优点

  1. 在表中插入和删除元素时,只要找到该元素对应的块,就可以在该表进行插入和删除运算。 由于块内是无序的,故插入和删除比较容易,无需进行大量移动。 (如果线性表即要快速查找又经常动态变化,则可采用分块查找)

缺点: 要增加一个索引表的存储空间并初始索引表进行排序运算。

线性表查找方法比较

顺序查找折半查找分块查找
ASL最大最小适中
结构有序表,无序表都行有序表分块有序
存储结构顺序存储结构,链式存储结构都可必须采用顺序存储结构顺序存储结构,链式存储结构都可

树表的查找

适用于动态查找,主要采用二叉排序树的查找技术

二叉排序树

二叉排序树,又称二叉查找树 ,它是一种对排序和查找都有用的特殊二叉树。

定义

二叉排序树或是一棵空树,或者是具有以下性质的二叉树

  1. 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值
  2. 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值
  3. 它的左,右子树也分别为二叉排序树。

二叉排序树是递归定义的。由定义可以得出二叉排序树的一个重要性质: 中序遍历一棵二叉排序树可以得到一个结点值 递增的有序序列

二叉排序树的形态和折半查找的判定树相似,其平均查找长度log2^n成正比。

可见,二叉排序树上的查找是和折半查找相差不大,但就维护表的有序性而言,二叉排序树更加有效,只需修改指针即可完成对结点的插入和删除操作。

二叉排序树适合经常进行插入,删除和查找运算的表。


// 二叉排序树的存储结构
typedef struct
{
	int key; // 关键字
}elemt;

typedef struct _BSTNode
{
	elemt data; // 数据域
	struct _BSTNode* lchild, * rchild;
}BSTNode,*BSTree;

//  二叉排序树的插入 ——————————————————————————————————————————————————————————————————————
/*
分析: 若二叉排序树为空树,则新插入的结点为新的根结点,否则,
		新插入的结点必为一个新的叶子节点,其插入位置由查找过程得到。
*/

// 插入一个结点的时间复杂度为O(log2n)
void InsertBST(BSTree* T, int key) 
{   // 当二叉排序树T中不存在关键字等于key的数据元素时,则插入该元素
	if (*T == NULL) 
	{    									// 找到插入位置,递归结束
		*T = (BSTree)malloc(sizeof(BSTNode)); // 生成新结点
		(*T)->data.key = key; // 新结点*s的数据域置为key
		(*T)->lchild = (*T)->rchild = NULL;  // 新结点作为叶子节点
	}
	else if (key < (*T)->data.key) // key比根结点的值小,则插入左子树
		InsertBST(&(*T)->lchild, key);  
	else if (key > (*T)->data.key)    // key比根结点的值大,则插入右子树
		InsertBST(&(*T)->rchild, key);
}

void creatBST(BSTree* T)  //  时间复杂度为O(nlog2n)
{	// 依次读入一个关键字为key的结点,将此结点插入二叉排序树T中
	(*T) = NULL;  // 将二叉排序树T初始化为空树
	int key;
	scanf("%d", &key); 
	while (key!=0)  // key为0时,结束插入
	{
		InsertBST(T, key);  // 将此结点插入二叉排序树T中
		scanf("%d", &key);
	}
}
// ——————————————————————————————————————————————————————————————————————————————————————

// 中序遍历
void Inorder(BSTree T)
{
	if (T)
	{
		Inorder(T->lchild);
		printf("%d ", T->data.key);
		Inorder(T->rchild);
	}
}


// 二叉排序树的查找 -----------------------------------------------------------------------
SearchBST(BSTree T,int key)
{	// 在根指针T所指二叉排序树中递归地查找某关键字等于key的数据元素
	// 若存在成功,则返回指向该数据结点的指针,否则返回0
	if (T == NULL) return 0;
	if (key == T->data.key) return T->data.key; // 找到了,查找结束
	else if (key > T->data.key)  // key比根结点的值大,在右子树查找
		return SearchBST(T->rchild, key);
	else if (key < T->data.key) // key比根结点的值小,在左子树查找
		return SearchBST(T->lchild, key);
		
}
// --------------------------------------------------------------------------------------


//-删除结点--------------------------------------------------------------------------------
void deletNode(BsTree* T)
{
	if ((*T)->lchild == NULL && (*T)->rchild == NULL) // 叶子节点 如果该节点既没有左子树也没有右子树
	{
		BsTree p = (*T);
		(*T) = NULL;
		free(p);
	}
	else if ((*T)->lchild == NULL) // 左子树为空
	{
		BsTree p = (*T);
		(*T) = (*T)->rchild;  // 将删除点的右子树连接到其双亲结点
		free(p);
	}
	else if ((*T)->rchild == NULL) //子树为空
	{
		BsTree p = (*T);
		(*T) = (*T)->lchild;   // 将删除点的左子树连接到其双亲结点
		free(p);
	}
	else {  // 左右子树均不为空
		BsTree parent = (*T);  // 保持要删除节点
		BsTree pre = (*T)->lchild; // 找其左子树的最大结点替代要删除结点
		while (pre->rchild) // 转左,左子树的右子树尽头就是最大结点
		{
			parent = pre;
			pre = pre->rchild;
		}
		(*T)->data.key= pre->data.key; // pre指向要删除结点的前驱,替换(*T)数据
		if (parent != (*T)) // 判定是否执行了上面的while循环 如果存在最大右子树结点则 替换后将该右子树结点的左子树放到其双亲上
			parent->rchild = pre->lchild; // 执行了,重接pre右子树
		else
			parent->lchild = pre->lchild;  // 未执行,重接pre左子树
		free(pre);
	}
}

// 删除结点
void deletBS(BsTree* T, int key)
{
	if (*T == NULL) return 0;
	if ((*T)->data.key == key) deletNode(T);  // 找到关键词,删除一个结点
	else if (key < (*T)->data.key) deletBS(&(*T)->lchild, key);
	else if (key > (*T)->data.key) deletBS(&(*T)->rchild, key);
}
// --------------------------------------------------------------------------------------


int main()
{
	BSTree T;
	creatBST(&T);
	Inorder(T);
	printf("%d",SearchBST(T, 51));
    
	return 0;
}
// eg: 63 90 70 55 58 0
// eg: 38 12 34 56 13 6 98 3 17 40 78
/*
	叶子节点40
	删除34 只有左子树
	删除13 只有右子树
	根节点38 左右子树都有 while
	删除12  左右子树都有 不while
	不存在结点4
*/

二叉排序树插入的小结:

基本过程是查找,是时间复杂度同查找一样 O(log2^n)

  • 一个无序序列可以通过构造一棵二叉排序树而变成一个有序序列
  • 每次插入的新结点都是二叉排序树上新的叶子结点
  • 找到插入位置后,不必移动其它结点,仅需修改每个结点的指针
  • 在左子树/右子树的查找过程与在整棵树上查找过程相同;
  • 新插入的结点没有破坏原有结点之间的关系。

二叉排序树的查找效率在于只需查找二分子树之一O(log2^n)

二叉排序树的查找性能取决于二叉排序树的形状(而二叉排序树的形状取决于其数据集)

在O(log2n)和O(n)之间。

二叉排序树的 删除 O(log2^n)

同二叉排序树插入一样,二叉排序树删除的基本过程也是查找。

在二叉排序树上删除某个结点之后,仍然保持二叉排序树的特性。

分三种情况讨论:

  • 删除的结点是叶子
    • 操作: 将双亲结点中相应指针域的值改为空
  • 被删除的结点只有左子树或者右子树
    • 操作: 将双亲结点的相应指针域的值指向被删除结点的左子树(或右子树)
  • 被删除的结点既有左子树,也有右子树。、
    • 操作:其左子树中最大值结点(或右子树中的最小结点)替代之,然后再删除该结点

平衡二叉排序树 (AVL)

一种特殊类型的二叉排序树

平衡二叉树或是空树,或者是具有如下特征的二叉排序树:

  1. 根节点的左子树和右子树的深度之差的绝对值小于等于1
  2. 根节点的左子树和右子树也是平衡二叉树。

若将二叉树上结点的 平衡因子: 该结点的左子树的深度和右子树的深度之差,

平衡二叉树上所以结点的平衡因子只能是-1,0,1. 只要一个超过1,则该二叉树失衡。

最小不平衡子树: 在平衡二叉树的构造过程中,以距离 插入结点最近的,且平衡因子的绝对值大于1的结点为**根(问题发现者)**的子树

构造平衡二叉树的基本思想: 每插入一个结点:

  1. 从插入结点开始向上计算各结点的平衡因子,如果某结点平衡因子的绝对值超过1,则说明插入操作破坏了二叉树的平衡性,需要进行平衡调整;否则继续执行插入操作。
  2. 如果二叉树不平衡,则找出最小不平衡子树的根结点,根据新插入结点域最小不平衡子树根结点之间的关系判断调整类型。
  3. 根据调整类型进行相应的调整,使之成为新的平衡子树。

对子树进行平衡调整的四种情况

  1. LL型 事件发现者的左子树的左子树是事件发生者
    • 顺时针方向旋转。 (扁担原则)
  2. RR型 事件发现者的右子树的右子树是事件发生者
    • 向逆时针方向旋转
  3. LR型 事件发现者的左子树的右子树是事件发生者
    • 左子树先逆时针旋转(变LL型),再整体顺时针旋转。
  4. **RL型 ** 事件发现者的右子树的左子树是事件发生者
    • 右子树先顺时针旋转(变RR型),再整体逆时针旋转。

调整步骤:

每插入一个结点,

  1. 计算平衡因子(从插入结点开始向上计算)
  2. 判断平衡性
  3. 若不平衡,找到最小不平衡子树的根结点,
  4. 判断调整类型,
  5. 根据调整类型进行调整。 (从问题发现者的下一个结点开始操作

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

散列表的查找

静态查找和动态查找均适用,主要采用散列技术

在各种查找方法中,平均查找长度与结点个数无关的查找方法是 散列查找

如果能在元素的存储位置和其关键字之间建立某种直接关系,那么在进行查找时,就无需做比较或很少次的比较,按照这种关系直接由关键字找到相应的记录 ,就是 散列查找法的思想,它通过对元素的关键字值进行某种运算,直接求出元素的地址,而不需要反复比较散列查找法又叫杂凑法或散列法。

散列即是一种查找技术,也是一种存储技术。

散列技术一般不适用于允许多个记录有同样关键码的情况。散列方法也不适用于范围查找换言之在散列表中,我们不可能找到最大或最小关键码的记录,也不可能找到某一范围内的记录。

术语:

  1. 散列表: 一个有限连续的地址空间,用以存储散列函数计算得到相应散列地址的数据记录。通常散列表的存储空间是一个一维数组,散列地址是数组的下标。
  2. **散列函数: ** 将关键码映射为散列表中适当存储位置的函数。
  3. 散列地址: 由散列函数所得的存储地址
  4. 冲突 : 对不同的关键字可能得到同一地址,这种现象称为冲突。
  5. 同义词: 具有相同函数值的关键字对该散列函数来说称作同义词。

散列函数的构造方法

考虑的因素:

  • 散列表的长度;
  • 关键字的长度;
  • 关键字的分布情况;
  • 计算散列函数所需的时间;
  • 记录的查找频率。

好的散列函数原则:

  1. 函数计算要简单,每一关键字只能有一个散列地址与之对应
  2. 函数的值域需在表长范围内,计算出的散列地址的分布均匀,尽可能减少冲突。

散列函数设计方法

  1. 直接定址法:

在这里插入图片描述

  1. 除留余数法:

在这里插入图片描述

在这里插入图片描述

  1. 数字分析法:

在这里插入图片描述

  1. 平方取中法:
    在这里插入图片描述

  2. 折叠法:

在这里插入图片描述

处理冲突

开放定址法

由关键码得到的散列地址一旦产生了冲突,就去寻找下一个空的散列地址,并将记录存入

由开放地址法处理冲突得到的散列表叫 闭散列表

  1. 线性探测法: 当发生冲突时,从冲突位置的下一个位置起,依次寻找空的散列地址。

    公式: Hi=(H(key)+di)%m (di=1,2,……,m-1)

    堆积: 在处理冲突的过程中出现的 非同义词之间对同一个散列地址争夺的现象。

  2. 二次探测法

    同一次探测类似但+的di不同

    公式 Hi=(H(key)+di)%m (di=1^2,-1^2,2^2,-2^2,……,q^2,-q^2 且 q<=m/2)

  3. 随机探测法

    当发生冲突时,下一个散列地址的位移量是一个随机数列,即查找下一个散列地址的

    公式是 Hi=(H(key)+di)%m (di是一个随机数列,i=1,2,……,m-1)

拉链法(链地址法)

适合表长不确定的情况

基本思想: 将所有散列地址相同的记录,即所有同义词记录存储在一个单链表中(称为同义词子表),在散列表中存储的是所有同义词子表的头指针。

用拉链法处理冲突构造的散列表叫做 开散列表

开散列表不会出现堆积现象。

设n个记录存储在长度为m的散列表中,则同义词子表的平均长度为n/m.

性能分析

由于冲突的存在,产生冲突后的查找仍然是给定值与关键码进行比较的过程。

在查找过程中,关键码的比较次数取决于产生冲突的概率。影响冲突产生的因素有

  1. 散列函数是否均匀

  2. 处理冲突的方法

  3. 散列表的装载因子

    a=表中填入的记录数/散列表长度

产生堆积现象,即产生了冲突,它对存储效率,散列函数和装填因子均不会有影响,而平均查找长度会因为堆积现象而增大

注:本文仅为本人笔记,图片截自哔哩哔哩懒猫老师

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值