数据结构——(9)查找

一、查找概念

1、列表

由同一类型的数据元素(或记录)构成的集合,可利用任意数据结构实现。

2、关键字

数据元素的某个数据项的值,用它可以标识列表中的一个或一组数据元素。

如果一个关键字可以唯一标识列表中的一个数据元素,则称其为主关键字,否则为次关键字。 当数据元素仅有一个数据项时,数据元素的值就是关键字。

3、查找

根据给定的关键字值,在特定的列表中确定一个其关键字与给定值相同的数据元素,并返回该数据元素在列表中的位置。

若表中存在要查找的记录,则称查找是成功的,此时查找的结果为给出整个记录的信息,或指示该记录在查找表中的位置;若表中不存在关键字等于给定值的记录,则称查找不成功,此时查找的结果可给出一个“空”记录或“空”指针。

表的查找分为静态查找和动态查找:

(1)、静态查找

在查找过程中只是对数据元素进行查找

  1. 查询某个“特定的”数据元素是否在查找表中

  2. 检索某个“特定的”数据元素的各种属性

(2)、动态查找

在实施查找的同时,插入找不到的元素,或从查找表中删除已查到的某个元素,即允许表中元素变化

  1. 在查找表中插入一个数据元素

  2. 从查找表中删去某个特定元素

4、平均查找长度(ASL)

为确定数据元素在列表中的位置,需和给定值进行比较的关键字个数的期望值,称为查找算法在查找成功时的平均查找长度(ASL)。对于长度为 n 的列表,查找成功时的 平均查找长度为:

ASL=\sum\limits_{n = 1}^\infty  PiCi

其中,n是查找表的长度; Pi是查找第 i i i个数据元素的概率,一般认为每个数据元素的查找概率相等,即 P i = 1 / n,Ci是找到第 i 个数据元素所需进行的比较次数。平均查找长度是衡量查找算法效率的最主要的指标。

二、基于线性表的查找

平均查找长度

最大

最小

二者之间

表结构

有序表,无序表

有序表

分块有序表

存储结构

顺序存储结构,线性链表

顺序存储结构

顺序存储结构,线性链表

1、顺序表查找法

从表中的第一个(或最后一个)记录开始,逐个进行记录的关键字和给定值比较,若某个记录的关键字和给定值相等,则查找成功,找到所查的记录;如果直到最后一个(或第一个)记录,其关键字和给定值比较都不等时,则表中没有多查的记录,查找不成功。

顺序查找适合于存储结构为顺序存储或链式存储的线性表

  1. 优点:算法简单且适应面广。(表中元素有序无序都可用。)

  2. 缺点:比较次数较多,特别是 n 很大时,检索效率较低

复杂度分析:O(n)

/*有哨兵顺序查找*/
int Sequential_Search(int *a, int n, int key){
	int i;
	a[0] = key;	//设置a[0]为关键字,称之为“哨兵”
	i = n;	//循环从数组尾部开始
	while(a[i] != key){
		i--;
	}
	return i;	//返回0则说明查找失败
}

2、折半查找

折半查找的主要步骤为:

  1. 置初始查找范围:low=1,high=n;

  2. 求查找范围中间项:mid=(low+high)/2;

  3. 将指定的关键字值 k 与中间项 a[mid].key 比较

    若相等,查找成功,找到的数据元素为此时 mid 指向的位置;

若小于,查找范围的低端数据元素指针 low 不变,高端数据元素指针 high 更新为 mid-1;

若大于,查找范围的高端数据元素指针 high 不变,低端数据元素指针 low 更新为 mid+1;

  1. 重复步骤(2)、(3)直到查找成功或查找范围空(low>high),即查找失败为止。

  2. 如果查找成功,返回找到元素的存放位置,即当前的中间项位置指针 mid;否则返回查找失败标志。

int Search_Bin_Recursive(SSTable ST, int key, int low, int high) { 
  	if (low > high) 
  		return 0;	//查找不到时返回0 
  	
  	mid = (low + high) / 2; 
  	
  	if(ST.elem[mid].key == key) 
  		return mid; 
  	else if(ST.elem[mid].key>key) 
    	return Search_Bin_Recursive(ST, key, low, mid-1); 
  	else 
  		return Search_Bin_Recursive(ST, key, mid+1, high); 
}

3、分块查找

数据分成若干块,块内数据不必有序,但块间必须有序。

(1)对索引表使用折半查找法(因为索引表是有序表);

(2)确定了待查关键字所在的子表后,在子表内采用顺序查找法(因为各子表内部是无序表);

三、基于树表的查找

1、二叉排序树

(1)、定义

二叉排序树又称为二叉查找树,它是一种特殊的二叉树。

其定义为:二叉树排序树或者是一棵空树,或者是具有如下性质的二叉树: 

  • 左子树的所有结点均小于根的值; 

  • 右子树的所有结点均大于根的值; 

  • 它的左右子树也分别为二叉排序树。

(2)、性质

  1. 任一非终端结点若有左孩子,则该结点的关键字值大于其左孩子结点的关键字值。

  2. 任一非终端结点若有右孩子,则该结点的关键字值小于其右孩子结点的关键字值。

  3. 它的左、右子树也分别为二叉排序树

(3)、二叉排序树的创建

bstree  CreatBstree (void) {
	bstree t = NULL;
    KeyType key;  
    printf(“\n请输入一个以-1为结束标记的结点序列:\n”)
    scanf(“%d”,&key);          //输入一个关键字
    while (key!=-1) {
    	InsertBstree (&t, key); //将key插入到树t中
        scanf(“%d”,&key);
    }   
    return (t);  //返回树的根地址
} 

(4)、二叉排序树的插入

  1. 若二叉排序树是空树,则 key 成为二叉排序树的根

  2. 若二叉排序树非空,则将 key 与二叉排序树的根进行比较

  3. 如果 key 的值等于根结点的值,则停止插入

  4. 如果 key 的值小于根结点的值,则将 key 插入左子树

  5. 如果 key 的值大于根结点的值,则将 key 插入右子树

代码:

void InsertBstree (bstree *t, KeyType k) {
	bstree f = NULL, p;
    p = *t;
    while (p) {
    	if (k == p->key)
    		return;
        f = p;
        p = (k<p->key) ? p->lchild : p->rchild;
    }
    
    p=(bstree)malloc(sizeof(bsnode));	//生成新结点
    p->key=k;
    p->lchild = p->rchild=NULL;	//为叶子结点
    
    if (*t==NULL)
    	*t=p;	//原树为空
    else if (x<f->key)
    	f->lchild=p;
    else   
     	f->rchild=p;
}

(5)、二叉排序树的删除

    1. 若要删除的结点为叶子结点,可以直接进行删除

    2. 若要删除结点有右子树,但无左子树,可用其右子树的根结点取代要删除结点的位置

    3. 若要删除结点有左子树,但无右子树,可用其左子树的根结点取代要删除结点的位置

    4. 若要删除结点的左右子树均非空,则首先找到要删除结点的右子树中关键字值最小的结点(即子树中最左结点),利用上面的方法将该结点从右子树中删除,并用它取代要删除结点的位置,这样处理的结果一定能够保证删除结点后二叉树的性质不变

(6)、二叉排序树查找的过程

  1. 若二叉树为空树,则查找失败

  2. 将给定值 k 与根结点的关键字值比较,若相等,则查找成功

  3. 若根结点的关键字值小于给定值 k,则在左子树中继续搜索

  4. 否则,在右子树中继续查找

(7)、二叉排序树中查找一个关键字值为 k 的结点的基本思想

用给定值 k 与根结点关键字值比较,如果 k 小于根结点的值,则要找的结点只可能在左子树中,所以继续在左子树中查找,否则将继续在右子树中查找,依此方法,查找下去,至直查找成功或查找失败为止。

2、平衡二叉树

(1)、定义

平衡二叉排序树又称为AVL树,一棵平衡二叉排序树或者是空树,或者是具有下列性质的二叉排序树:

  • 左子树与右子树的高度之差的绝对值小于等于1

  • 左子树和右子树也是平衡二叉排序树。

引入平衡二叉排序树的目的,是为了提高查找效率,其平均查找长度为O(log2n)。

(2)、平衡因子 BF

定义为该结点的左子树的深度减去它的右子树的深度。则平衡二叉树上所有结点的平衡因子只可能是-1,0 或 1。只要二叉树上有一个结点的平衡因子的绝对值大于 1,则该二叉树就是不平衡的。

特点:表结构本身是查找过程中动态生成的,即对于给定值key,若表中存在其关键字等于key的记录,则查找成功返回,否则插入关键字等于key的记录;树的结构通常不是一次生成的,而是在查找过程中,当树中不存在关键字等于给定值的结点时再进行插入

(3)、平衡旋转

如果在一棵AVL树中插入一个新结点,就有可能造成失衡,此时必须重新调整树的结构,使之恢复平衡。我们称调整平衡过程为平衡旋转。

  • LL型平衡旋转

  • RR型平衡旋转

  • LR型平衡旋转

  • RL型平衡旋转

(4)、B树

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

  • 树中每个结点至多有 m m m棵子树,即至多含有 m − 1 m-1 m−1个关键字。

  • 若根结点不是终端结点,则至少有两棵子树。

  • 除根结点外的所有非叶结点至少有 ⌈ m / 2 ⌉ ⌈m/2⌉ ⌈m/2⌉棵子树,即至少含有 ⌈ m / 2 ⌉ − 1 ⌈m/2⌉- 1 ⌈m/2⌉−1个关键字。

所有非叶结点的结构如图:

其中, K i ( i = 1 , 2 , . . . , n ) 为结点的关键字,且满足 K 1 < K 2 < . . . < K n ; P i ( i = 0 , 1 , . . . , n )为指向子树根结点的指针,且指针 P i − 1 所指子树中所有结点的关键字均小于 K i , P i 所指子树中所有结点的关键字均大于 K i (即符合二叉排序树的左小右大),n (⌈m/2⌉- 1≤n≤m-1)为结点中关键字的个数。

(5)、B树的查找

在上图中查找关键字 42,首先从根结点开始,根结点只有一个关键字,且 42 > 22 ,若存在,必在关键字 22 的右边子树上,右孩子结点有两个关键字,而 36 < 42 < 45,则若存在,必在 36 和 45 中间的子树上,在该子结点中查到关键字 42,查找成功。若查找到叶结点时(对应指针为空指针),则说明树中没有对应的关键字,查找失败。

(6)、B树的插入

插入过程:结点不溢出时直接插入;如果结点溢出那就分裂,并把中间结点合并到父节点。

  • 定位。利用前述的B树査找算法,找出插入该关键字的最低层中的某个非叶结点(在B树中查找key时,会找到表示查找失败的叶结点,这样就确定了最底层非叶结点的插入位置。注意:插入位置一定是最低层中的某个非叶结点)。

  • 插入。在B树中,每个非失败结点的关键字个数都在区间 [ ⌈ m / 2 ⌉ − 1 , m − 1 ] 内。插入后的结点关键字个数小于 m ,可以直接插入;插入后检查被插入结点内关键字的个数,当插入后的结点关键字个数大于 m − 1 时,必须对结点进行分裂。

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

对于 m = 3 的B树,所有结点中最多有 m − 1 = 2 个关键字,若某结点中已有两个关键字,则结点已满,如下图a所示。插入一个关键字 60 后,结点内的关键字个数超过了 m − 1 ,如图2所示,此时必须进行结点分裂,分裂的结果如图3所示。

(7)、B树的删除

删除过程:多留就少补复杂点:当兄弟够借时,就向左旋转一次(即往左挪一个位置,重构根节点关键字的前驱和后继);当兄弟不够借时就拆根节点,合并到兄弟结点,合并拆分要始终保证B树平衡。

a、当被删关键字 k 不在终端结点(最低层非叶结点)中时,可以用 k 的前驱(或后继) k ′来替替代 k ,然后在相应的结点中删除 k ,关键字 k 必定落在某个终端结点中,则转换成了被删关键字在终端结点中的情形。在下图的B树中,删除关键字80,用其前驱78替代,然后在终端结点中删除78。因此只需讨论删除终端结点中关键字的情形。

b、当被删关键字在终端结点(最低层非叶结点)中时,有下列三种情况:

① 直接删除关键字。若被删除关键字所在结点的关键字个数 ≥ ⌈ m / 2 ⌉ ,表明删除该关键字后仍满足B树的定义,则直接删去该关键字。

② 兄弟够借。若被删除关键字所在结点删除前的关键字个数 = ⌈ m / 2 ⌉ − 1 ,且与此结点相邻的右(或左)兄弟结点的关键字个数 ≥ ⌈ m / 2 ⌉,则需要调整该结点、右(或左)兄弟结点及其双亲结点(父子换位法),以达到新的平衡。在图(a)中删除B树的关键字 65 65 65,右兄弟关键字个数 ≥ ⌈ m / 2 ⌉,将 71 71 71取代原 65 的位置,将 74 调整到 71 的位置。

③ 兄弟不够借。若被删除关键字所在结点删除前的关键字个数 = ⌈ m / 2 ⌉ − 1,且此时与该结点相邻的左、右兄弟结点的关键字个数均 = ⌈ m / 2 ⌉ − 1 ,则将关键字删除后与左(或右)兄弟结点及双亲结点中的关键字进行合并。在图(b)中删除B树的关键字 5 ,它及其右兄弟结点的关键字个数 = ⌈ m / 2 ⌉ − 1 = 1 ,故在 5 删除后将 60 合并到 65 结点中。

在合并过程中,双亲结点中的关键字个数会减 1 。若其双亲结点是根结点且关键字个数减少至 0 (根结点关键字个数为 1 时,有 2 棵子树),则直接将根结点删除,合并后的新结点成为根;若双亲结点不是根结点,且关键字个数减少到 ⌈ m / 2 ⌉ − 2 ,则又要与它自己的兄弟结点进行调整或合并操作,并重复上述步骤,直至符合B树的要求为止。

(8)、B+树

B+树是应文件系统(比如数据库)所需而出现的一种B树的变形树。

m阶的B+树与 m 阶的B树的主要差异如下:

  • 有 n 棵子树的结点中包含有 n 个关键字;

  • 所有的叶子结点包含全部关键字的信息,及指向含这些关键字记录的指针,叶子结点本身依关键字的大小自小而大顺序链接;

  • 所有分支结点可以看成是索引,结点中仅含有其子树中的最大(或最小)关键字。

  • 在B+树中,每个结点(非根内部结点)的关键字个数n的范围是 ⌈ m / 2 ⌉ ≤ n ≤ m (根结点: 1 ≤ n ≤ m ;在B树中,每个结点(非根内部结点)的关键字个数 n 范围是 ⌈ m / 2 ⌉ − 1 ≤ n ≤ m − 1 (根结点: 1 ≤ n ≤ m − 1 。

(9)、B树和B+树区别

类比

二叉查找树的进化——>m叉查找树

分块查找的进化——>多级分块查找

关键字与分叉

n个关键字对应n+1个分叉(子树)

n个关键字对应n个分叉

结点包含的信息

所有结点中包含记录的信息

只有最下层叶子才包含记录的信息(可使树更矮)

查找方式

不支持顺序查找。查找成功时,可能停在任何一层结点,查找速度不稳定

支持顺序查找。查找成功或失败都会到达最下一层结点,查找速度”稳定“

相同点

除根结点外,最少 m/2 个分叉(确保结点不要太空),任何一个结点的子树都要一样高(确保绝对平衡)

除根结点外,最少 m/2 个分叉(确保结点不要太空),任何一个结点的子树都要一样高(确保绝对平衡)

四、哈希表(散列表)

1、散列表查找的基本概念

哈希表是根据关键码值(Key Value)而直接进行访问的数据结构。它通过把关键码值映射到哈希表中的一个位置来访问记录,以加快查找的速度。这个映射函数就做散列函数,存放记录的数组叫做散列表。

存储位置=f(关键字)

2、散列函数的构造方法

注意:

  1. 散列函数的定义域必须包含全部需要存储的关键字,而值域的范围则依赖于散列表的大小或地址范围。

  2. 散列函数计算出来的地址应该能等概率、均匀地分布在整个地址空间中,从而减少冲突的发生。

  3. 散列函数应尽量简单,能够在较短的时间内计算出任一关键字对应的散列地址。

(1)、直接定址法

直接取关键字的某个线性函数值为散列地址

H(key)=key或H(key)=a∗key+b

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

(2)、数字分析法

例如当手机号码为关键字时,其11位数字是有规则的,此时是无需把11位数值全部当做散列地址,这时我们给关键词抽取, 抽取方法是使用关键字的一部分来计算散列存储位置的方法,这在散列函数中是常常用到的手段。

数字分析法通常适合处理关键字位数比较大的情况,如果事先知道关键字的分布且关键字的若干位分布较均匀,就可以考虑用这个方法。这种方法适合于已知的关键字集合,若更换了关键字,则需要重新构造新的散列函数。

(3)、平方取中法

假设关键字是1234,那么它的平方就是1522756,再抽取中间的3位就是227,用做散列地址。再比如关键字是4321,那么它的平方就是18671041,抽取中间的3位就可以是671,也可以是710,用做散列地址。平方取中法比较适合于不知道关键字的分布,而位数又不是很大的情况。

(4)、除留余数法

假定散列表表长为 m m m,取一个不大于 m m m但最接近或等于 m m m的质数 p p p,利用以下公式把关键字转换成散列地址。散列函数为

H(key)=key%p  (p<=m)

(5)、随机数法

选择一个随机数,取关键字的随机函数值为它的散列地址。也就是

H(key)=random(key)

这里random是随机函数。当关键字的长度不等时,采用这个方法构造散列函数是比较合适的。

3、处理散列冲突

(1)、开放定址法

所谓的开放定址法就是一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入。

(2)、链地址法

不同处理冲突的平均查找长度

查找成功时的平均查找长度:

ASL = (1*6+2*4+3*1+4*1)/12 = 7/4

查找不成功时的平均查找长度:

ASL = (4+2+2+1+2+1)/13

注意:查找成功时,分母为哈希表元素个数,查找不成功时,分母为哈希表长度。

(3)、线性探测法

查找成功时的查找次数等于插入元素时的比较次数,查找成功的平均查找长度为:

ASL = (1+2+1+4+3+1+1+3+9+1+1+3)/12 = 2.5

查找成功时的查找次数:第n个位置不成功时的比较次数为,第n个位置到第1个没有数据位置的距离:如第0个位置取值为1,第1个位置取值为2.

查找不成功的平均查找次数为:

ASL = (1+2+3+4+5+6+7+8+9+10+11+12)/ 13 = 91/1

おすすめ

転載: blog.csdn.net/qq_41819893/article/details/121324341