数据结构与算法笔记7 查找

201 阅读11分钟

1. 查找的基本概念

查找:在数据集合中寻找满足某种条件的数据元素的过程

查找表(查找结合):用于查找的数据集合,它是由同一类型的数据元素(或记录)组成,就是执行查找操作的数据结构的统称 如表 图

关键字:数据元素中唯一标识该元素的某个数据项的值,使用基于关键字的查找,查找结果应该是唯一的

1.1 对查找表的常见操作

  • 1.查找符合条件的数据元素
    1. 插入、删除某个数据元素

image.png

1.2 查找算法的评价指标

查找长度:在查找运算中,需要对比关键字的次数成为查找长度 平均查找长度 ASL:素有查找过程中进行关键字的比较次数的平均值

image.png

2. 查找算法1:顺序查找

顺序查找(线性查找):通常用于线性表。

2.1 算法思想:从头到脚挨个找,或者反过来也行

2.2 顺序查找的实现

代码示例

//查找表的数据结构(顺序表)
typedef struct 
{
	Elemtype* elem; //动态数组基地址
	int TableLen;      //表的长度
} SSTable;

//顺序查找
int Search_Seq(SSTable St, ElemType key) 
{
	int i;
	for (i = 0; i < St.TableLen && St.elem[i] != key; i++);
	//查找成功,则返回元素下标;查找失败,则返回-1
	return i == St.TableLen ? -1 : i;
}

顺序查找的实现(哨兵)

image.png

2.3 查找效率分析:O(n)O(n)

2.3.1 查找成功

image.png

2.3.2 查找失败

image.png

2.4 顺序查找的优化(对有序表)

image.png

2.5 用查找判定树分析ASL

image.png

2.6 顺序查找的优化(对被查概率不相等)

image.png

3. 查找算法2:折半查找(二分查找)

适用于有序的顺序表。

3.1 算法思想

    1. 用两个指针low和high分别指向顺序表最小和最大的元素
    1. 检查low和high中间的元素,mid游标=(low游标+high游标)/2(向下取证)
    1. 通过当前元素和mid元素的大小比较来确定元素在mid左边还是右边
    1. 相应的更改low和high的游标,不断比较
    1. 如果mid == 确定的元素则查找成功;如果low > high则查找失败

3.2 折半查找的实现

//查找表的数据结构(顺序表)
typedef struct
{
	Elemtype* elem; //动态数组基地址
	int TableLen;      //表的长度
} SSTable;

//折半查找
int Binary_Search(SSTable st, ElemType key) 
{
	int low = 0, high = st.TableLen - 1, mid;
	while (low < high) 
	{
		mid = (low + high)
		{
			mid(low + high) / 2; //取中间位置
		if (st.elem[mid] == key)
			return mid;				//查找成功则返回所在位置
		else if (st.elem[mid] > key)
			high = mid - 1;		//从前半部分继续查找
		else
			low = mid + 1;		//从后半部分继续查找
		}
		return -1;					//查找失败,返回-1
	}
}

折半查找不可能基于链表实现,因为顺序表拥有随机访问的特性,链表没有

image.png

3.3 查找效率分析(基于查找判定树)

image.png

3.3.1 折半查找查找判定树的构造

    1. 如果当前low和high之间有奇数个元素,则mid分割后,左右两部分元素个数相等
    1. 如果当前low和high之间有偶数个元素,则mid分割后,左半部分比右半部分少一个元素

image.png

3.3.2 根据元素个数所构造的折半查找判定树的样子

image.png

折半查找判定树一定是平衡二叉树

折半查找的判定树中,只有最下面一层是不满的。树高计算方法和完全二叉树计算方法相同,树高直接反应了时间复杂度。

image.png

判定树的结点关键字,左<中<右,满足二叉排序树的定义,并且是平衡的二叉排序树,失败节点:n+1个,等于成功结点的空链域的数量。

3.3.3 折半查找的时间复杂度 O(log2n)O(log_{2}n)

image.png

注意如果mid改为向上取整,则分隔的判定树不同

4. 分块查找(索引顺序查找)

特点:块内无序,快件有序。

重点是建立索引表 image.png

4.1 分块查找算法过程

    1. 在索引表中确定待查记录所属的分块(可顺序、可折半)
    1. 在块内顺序查找

4.2 用折半查找查索引

    1. 若索引表中含有元素,则mid == key;
    1. 若索引表中不包含目标关键字,则折半查找索引表最终停在low>high,要在low所指分块中查找

    原因:最终low左边一定小于目标关键字,high右边一定大于目标关键字。而分块存储的索引表中保存的是各个分块的最大关键字。

image.png

    1. 若low超出了索引表的长度则查找失败

4.3 折半查找找效率分析

image.png 27要对比很多次直到能够确定low>high

折半查找查找失败的情况比较复杂

4.3.1 当索引表的分布有一定规律时的查找效率

image.png

image.png

5. B树

二叉查找树(本质上是通过一个关键字将树分隔成了大小的两部分)

5.1 5叉查找树

5叉排序树的结点定义

struct Node
{
    ElemType keys[4];       //最多4个关键字
    struct Node * child[5]; //最多5个孩子
    int num;                //结点有几个关键字
}

image.png

5.2 保证m叉查找树的查找效率

5.2.1 减少层数

若每个结点内关键字太少,导致树变高,要查找更多层结点,效率低。

**策略:m叉查找树中,规定除了根结点外,任何结点至少有m/2\left \lceil m/2 \right \rceil个分叉,即至少含有m/21\left \lceil m/2 \right \rceil -1 **个关键字

例如:对5叉排序树,规定除了根结点外,任何结点都至少有3个分叉,2个关键字。

5.2.2 保证m叉查找树平衡

不够平衡,树会很高,要查很多层结点

策略:m叉查找树中,规定对于任何一个结点,其所有子树的高度都要相同

image.png

5.3 B树的概念

满足上述5.2.1 和5.2.2两个保证查找效率的策略的m叉树,就是B树。

B树,又称多路平衡查找树,B树中所有结点的孩子个数的最大值称为B树的阶,通常用m表示。一棵m阶的B树或为空树,或为满足图中特性的m叉树。

image.png

image.png

5.4 m阶B树的和新特性

    1. 根结点的子树数[2,m]\in[2,m],关键字数[1,m1]\in[1,m-1],其他节点的子树数目[m/2,m]\in[\left \lceil m/2 \right \rceil,m];关键字数[m/21,m1]\in[\left\lceil m/2 \right \rceil-1,m-1]
    1. 对任一结点,其所有子树高度都相同
    1. 关键字的值:子树0<关键字1<子树1<关键字2<子树2<... (类比二叉查找树 左<中<右)

5.5 B树的高度(不包括叶子结点)

问题:包含n个关键字的m阶B树,最小高度、最大高度是多少

最小高度:让每个结点尽可能的满,有m-1个关键字,m个分叉,则有n(m1)(1+m+m2+m3+...+mh1)=mh1n\le(m-1)(1+m+m^{2}+m^{3}+...+m^{h-1})=m^{h}-1,因此 hlogm(n+1)h\le log_{m}(n+1)

image.png 最大高度:让各层的分叉尽可能的少,即根结点只有2个分叉,其他节点只有m/2\left\lceil m/2 \right\rceil个分叉,各层节点至少有:第1层1、第二层2、第三层 2m/22\left\lceil m/2 \right\rceil... 第h层2(m/2)h22(\left\lceil m/2 \right\rceil)^{h-2}

第h+1层共有叶子结点(失败结点)2(m/2)h12(\left\lceil m/2 \right\rceil)^{h-1}

n个关键字的B树必有n+1个叶子结点,则n+12(m/2)h1n+1\ge 2(\left\lceil m/2 \right\rceil)^{h-1}, 即hlogm/2n+12+1h\le log_{m/2} \frac{n+1}{2}+1

image.png 通过关键字的计算方法

image.png

5.6 B树的插入(查找6)

5.6.1 核心要求

    1. 对m阶B树——除根结点外,结点关键字个数 m/21nm1\left\lceil m/2 \right\rceil-1 \le n \le m-1
    1. 子树0 < 关键字1 < 子树1 < 关键字2 < 子树2 < ......

新元素一定是插入到最底层“终端节点”,用“查找”来确定插入位置

在插入key后,若导致原结点关键字数超过上线,则从中间位置(m/2\left\lceil m/2 \right\rceil)将其中的关键字分为两部分,左部分包含的关键字放在原结点中,右部分包含的关键字放到新结点中,中间位置(m/2\left\lceil m/2 \right\rceil)的结点插入原结点的父结点。若此时导致其父结点的关键字个数也超过了上限,则继续进行这种分裂操作,直至这个过程传到根结点为止,进而导致B树高度增加1.

5.7 B树的删除

  • 若被删除关键字在终端节点,则直接删除该关键字(要注意节点关键字个数是否低于下限m/2\left\lceil m/2\right \rceil-1)
  • 若被删除关键字在非终端节点,则用直接前驱或直接后继来替代被删除的关键字
    • 直接前驱:当前关键字左侧指针所指子树中“最右下”的元素
    • 直接后继:当前关键字右侧指针所指子树中“最左下”的元素
    对非终端结点关键字的删除操作,必然可以转化为对终端结点的删除操作
  • 当对终端结点删除后,关键字个数低于下限时:
      1. 兄弟够借:若被删除关键字所在结点删除前的关键字个数低于下限,且与此结点右(或左)兄弟结点的关键字个数还很宽裕,则需要调整该结点、右(或左)兄弟结点及其双亲结点(父子换位法)
      • 当右兄弟很宽裕时,用当前结点的后继、后继的后继来填补空缺
      • 当左兄弟很宽裕时,用当前结点的前驱、前驱的前驱来填补空缺
      • 本质:要永远保证B树的特性:子树0 < 关键字1 < 子树1 < 关键字2 < 子树2 < ......
      1. 兄弟不够借:若被删除关键字所在结点删除前的关键字个数低于下限,且此时与该结点相邻的左、右兄弟的关键字个数均为m/21\left\lceil m/2\right \rceil -1。则将关键字删除后与左(或右)兄弟结点及双亲结点中的关键字进行合并。
      • 在合并过程中,双亲结点中的关键字个数会减1,若其双亲结点是根结点且关键字个数减少至0(根结点关键字个数为1时,有两棵子树),则直接将根结点删除,合并后的新节点称为根;若双亲结点不是根结点,且关键字个数减少到m/22\left\lceil m/2 \right\rceil -2,则又要与它自己的兄弟结点进行调整或合并操作,并重复上述步骤,直至符合B树的要求为止。

6. B+树

一棵m阶的B+树需满足下列条件:

    1. 每个分支节点最多有m棵子树(孩子结点)
    1. 非叶根结点至少有两棵子树,其他每个分支节点至少有m/2\left\lceil m/2 \right\rceil棵子树
    • 要追求【平衡】和【效率】
    1. 结点的子树个数与关键字个数相等
    1. 所有叶结点包含全部关键字及指向相应记录的指针,叶结点中将关键字按大小顺序排列,并且相邻叶结点按大小顺序相互链接起来(支持顺序查找,可以通过p指针从头遍历)
    1. 所有分支结点中仅包含它的各个子节点中关键字的最大值及指向其子结点的指针

image.png

6.1 B+树的查找

B+树查找不论成功失败,都要查找到最后一层;B树查找成功可能停在任何一层 image.png

6.2 B+树和B树对比

image.png

image.png

image.png

image.png

image.png

image.png

image.png

image.png

image.png

image.png

7. 散列查找(Hash Table)

散列表,又称哈希表,是一种数据结构,特点是:数据元素的关键字与其存储地址直接相关。

如何建立“关键字”与“存储地址”的练习

通过“散列函数(哈希函数)”:Addr=H(key)

image.png

7.1 处理冲突的方法1——拉链法(链接法、链地址法)处理冲突

把所有“同义词”存储在一个链表中

image.png

在插入新元素时,保持关键字有序,可微微提高查找效率

7.2 散列查找

通过散列函数计算目标元素存储地址,在此地址进行查找

image.png

7.2.1 查找成功情况下的时间复杂度

image.png

散列查找冲突越多,效率越低;如果哈希函数设计的足够好,最理想情况散列查找的时间复杂度可达O(1)O(1)

7.2.2 查找失败情况下的时间复杂度

image.png

装填因子会直接影响散列表的查找效率

7.3 如何设计冲突更少的散列函数

7.3.1 除留余数法——H(key) = key % p

散列表表长为m,取一个不大于m但最接近或等于m的质数p

具体应用中使用什么数还是要根据数据特性来考虑 image.png

7.3.2 直接定址法——H(key)=key或H(key)=a*key+b

其中a和b是常数,这种方法计算最简单,且不会产生冲突。它适合关键字的分布基本连续的情况,若关键字分布不连续,空位较多,则会造成存储空间的浪费。

image.png

7.3.3 数字分析法——选取数码分布较为均匀的若干位作为散列地址

image.png

7.3.4 平方取中法

image.png

image.png

散列查找是典型的用空间换时间的算法。

7.4 处理冲突的方法2——开放定址法

数组中存放的仍是数据元素,可存放新表项的空闲地址既向它的同义词表项开放,又向它的非同义词表项开放。

根据增量did_{i}的不同设置方法可以分为

  • 线性探测法
  • 平方探测法
  • 伪随机

image.png

7.4.1 线性探测法

7.4.1.1 线性探测法:查找操作

image.png 空位置的判断也要算做一次比较,因为空位置存放的也是元素。

越早遇到空位置,越早可以确定查找失败。

7.4.1.2 线性探测法:删除操作

采用“开放定址法”时,删除结点不能简单地将被删结点的空间置为空,否则将截断在它之后散列表的同义词结点的查找路径,可以做一个“删除标记”,进行逻辑删除。

7.4.1.3 线性探测法查找效率分析

查找成功的情况 image.png 查找失败的情况 image.png

线性探测法很容易造成同义词、非同义词的“聚集(堆积)”现象,严重影响查找效率

产生原因:冲突后再探测一定是放在某个连续的位置

7.4.2 平方探测法(二次探测法)

image.png

开放地址法的查找方式是根据增量序列确定的

小坑:散列表长度m必须是一个可以表示成4j+34j+3的素数(质数)才能探测到所有位置

image.png

7.4.3 伪随机序列法

did_{i}是一个伪随机序列。

7.5 处理冲突的方法3——再散列法

image.png