数据结构知识点复习(五)

好久不见. 提交于 2019-12-26 17:03:16

第七章 查找

相关概念:
(1)关键字:数据元素中某个数据项的值,可以标识一个数据元素(或记录)。如果可以唯一标识:主关键字。不唯一不标识,次关键字
(2)动态查找表和静态查找表:如果在查找的过程中有修改表的信息,那就是动态查找表,反之为静态查找表
(3)**平均查找长度:**为确定关键字在查找表中的位置,需和给定值进行比较的关键字个数的期望值,称为查找算法在查找成功时的平均查找长度。
对于含有n个记录的表,查找成功的平均查找长度为:ASL=i=1nPiCiASL=\sum_{i=1}^{n}P_iC_i
其中,pip_i为查找表中第i个记录的概率,i=1nPi=1\sum_{i=1}^{n}P_i=1.
CiC_i为找到表中与之相等的关键字的第i个记录的时候,和给定值比较的次数。
可以用平均查找长度来衡量算法的性能

7.2.1顺序查找
顺序查找就是从头开始比较查找,如果查找到了,就成功,否则,失败。
假设以顺序表作为存储结构

typedef struct
{
	KeyType key;	//关键字域
	InfoType OtherInfo;	//其他域
}ElemType;

顺序表的定义

typedef struct 
{
	ElemType *R;
	int length;
}SSTable;

注意:在此假设从ST.R[1]开始存放元素,ST.R[0]闲置不用,查找时从表的最后开始查找。

int Search_Seq(SSTable ST,KeyType key)
{
	for(i=ST.length;i>=1;--i)
	{
		if(ST.R[i].key==key)
			return i;
	}
	return 0;
}

因为该程序每次都要检测整个表是否查找完毕,所以下面给了改进:
设置了监视哨的顺序查找:

int Search_Seq(SSTable ST,KeyType key)
{
	ST.R[0].key=key;
	for(i=ST.length;ST.R[i].key!=key;--i)
	return i;
}

算法的时间复杂度为O(n)。
顺序查找的优点和缺点:
优点:算法简单,对表无任何结构要求,既可以顺序存储的,也可以链式存储的,且不要求关键字有序。
缺点:平均查找长度大,查找效率较低,所以当n很大时,不适合用顺序查找,
补充:顺序查找的平均查找长度:
ASL=1ni=1ni=n+12ASL=\frac{1}{n}\sum_{i=1}^{n}i=\frac{n+1}{2}

7.2.2折半查找
折半查找又称二分查找。他必须要求顺序存储结构,并且要求有序。
为了标记查找过程中的查找区间,下面用low和high来表示当前查找区间的上界和下界,mid为区间中间的位置

int Search_Bin(SSTable ST,KeyType key)
{
	low=1;
	high=ST.length;
	while(low<=high)
	{
		mid=(low+high)/2;
		if(ST.R[mid].key==key) return mid;
		else if(ST.R[mid]>key) high=mid-1;
		else low=mid+1; 
	}
	return 0;	
}

注意:循环执行的条件是low<=high

例子:已知如下11个元素的有序表:
(5,16,20,27,30,36,44,55,60,67,71)(5,16,20,27,30,36,44,55,60,67,71)
在这里插入图片描述在这里插入图片描述折半查找的过程可以用二叉树来描述,形成的二叉树就叫做折半查找的判定树
从判定树上可见,成功的折半查找恰恰好是走了一条从判定树的根节点到被查节点的路径,经历的比较次数就是该节点在判定树中的层次
如果要求折半查找的平均查找长度,需要将判定树列出来:
上面的例子的平均查找长度为:需要比较1次的节点数有1个,需要比较两次的节点数有2个,需要比较3次的节点数有4个,需要比较4次的节点数有4个,所以,平均查找长度为:
ASL=111(1+22+34+44)=3ASL=\frac{1}{11}(1+2*2+3*4+4*4)=3
由此可见,折半查找查找成功的次数不超过判定数的深度。
具有n个节点的判定树的深度为log2n+1|log_2^n|+1.

如图所示,在判定树中给一些空子树添加一些方格表示,那么失败的查找就是走到了这些方格中的查找。很明显的:折半查找不成功和关键字的比较次数也是不超过log2n+1|log_2^n|+1
在这里插入图片描述
为了方便讨论,假设有序表的长度n=2h1n=2^h-1,则判定树的深度为h=log2(n+1)h=|log_2^(n+1)|的满二叉树,树中层次为h的节点有2h12^h-1个。假设表中所以记录查找的概率都相等,那么:
ASL=i=1nPiCi=1nj=1hj2(j1)ASL=\sum_{i=1}^{n}P_iC_i=\frac{1}{n}\sum_{j=1}^{h}j2^(j-1)
当n较大时,有这样的结果:ASL=log2(n+1)1ASL=log_2(n+1)-1
因此可见,折半查找的时间复杂度为0(log2nlog_2^n)
折半查找不适用于数据需要大规模变动的查找

7.2.3分块查找
分块查找又称索引顺序查找,除了表本身之外,还需要建立一个索引表,索引表包括两项内容,关键字和起始地址,所谓关键字就是每一个块中的最大的值,起始地址就是该块的第一个记录所在的位置
在这里插入图片描述分块查找分两步进行:
(1)先确定关键字所在的子块
(2)在块中利用顺序查找
分块查找的平均查找长度为:ASL=Lb+LwASL=L_b+L_w;其中LbL_b是确定所在块的平均查找长度,LwL_w是在块中查找元素的平均查找长度
平均查找长度不仅与表长有关,还和每块中的元素个数有关。

分块查找的优点和缺点:
优点:在表中插入或删除元素的时候,只要找到对应的块即可,就可以直接在块内进行插入或删除。如果线性表要求快速查找又需要经常动态变化,适合用分块查找

7.3.1 二叉排序树
注意:二叉排序树可以是一棵空树
二叉排序树的特点:所有的左子树都小于根节点,所有的右子树都大于根节点,并且,子树也是一棵二叉排序树。
中序遍历一棵二叉排序树可以得到一个递增的序列
在这里插入图片描述
在下面讨论二叉排序树的操作时,使用二叉链表作为存储结构

typedef struct
{
	KeyType key;		//关键字域
	InfoType otherInfo;	//其他数据项
}ElemType;
typedef strcut BSTnode;
{
	ElemType data;
	struct BSTnode *lchild,*rchild;
}BSTndoe,*BSTree;

使用递归进行二叉排序树的查找:

BSTnode SearchBST(BSTree T,KeyType key)
{
	if(!(T)||key==T.data.key) return T;
	else if(key>T.data.key) return SearchBST(T->rchild,key);
	else return SearchBST(T->lchilde,key);
}

在二叉排序树上寻找和给定节点值的过程,恰是走一条从根节点到该节点的路径,和给定值的比较个数等于路径长度+1(或等于所在节点层数),因此查找比较的次数不超过树的深度,与折半查找类似
但是,折半查找长度为n的顺序表的判定树是惟一的 ,但是二叉排序树却是不唯一的。
因此,含n个节点的二叉排序树的平均查找长度和树的形态有关。
当先后插入的关键字有序的时候,就变成了单支树,树的深度为n,那么其平均查找长度为n+12\frac{n+1}{2},(和顺序查找相同),这是最坏的情况。最好的情况是和折半查找的判定树结构类似,那么平均查找长度为log2nlog_2^n成正比。就平均而言,二叉排序树的平均查找长度仍和log2nlog_2^n同数量级

二叉排序树的插入
当树中不存在该节点时则插入。新插入节点一定是叶子节点,并且是查找不成功时查找路径上访问的最后一个节点的左孩子或右孩子

void InsertBST(BSTree &T,ElemType e)
{
	if(!T)
	{
		BSTree S=new BSTree;
		S->data=e;
		S->lchild=S->rchild=NULL;
		T=S;
	}
	else if(e.key>T->data.key) InsertBST(T->rchild,e);
	else InsertBST(T->lchild,e);
}

二叉排序树插入的基本过程是查找,所以和查找的算法一样,时间复杂度是O(log2n)O(log_2^n)

二叉排序树的创建
二叉排序树的创建是从一个空的二叉排序树开始,每次输入一个节点则查找需要插入的位置进行插入。

void CreatBST(BSTree &T)
{
	T=NULL;
	cin>>e;
	while(e.key!=ENDFLAG)
	{
		InsertBST(T,e);
		cin>>e;
	}
}

假设有n个节点,每次插入一个节点的时间复杂度是log2nlog_2^n,那么创建一颗二叉排序树的时间复杂度是nlog2nnlog_2^n
在这里插入图片描述
二叉排序树的删除
假设被删除节点p*p,其双亲为f*f,其左孩子和右孩子分别为PLP_LPRP_R,方便举例,假设p*pf*f的左孩子:
有三种情况:
(1)p*p没有左右孩子,那么删除p*p没有影响,直接修改p*p的指针即可:
f>lchild=NULL;f->lchild=NULL;
(2)若p*p只有左子树或只有右子树,那么直接让他的孩子补上自己的位置即可:f>lchild=p>lchild(f>rchild=pp>rchild)f->lchild=p->lchild(f->rchild=pp->rchild)
(3)当p*p既有左子树又有右子树的时候写出这颗二叉排序树的遍历序列,用它的直接前驱(或直接后继)直接代替要删除的节点,如果直接前驱有左子树,用直接前驱的左子树取代原来直接前驱的位置。
设p的直接前驱为s,s的双亲节点为q:
p>data=s>data;q>rchild=s>lchild;p->data=s->data;q->rchild=s->lchild;
二叉排序树删除和查找一样,时间复杂度为log2nlog_2^n
在这里插入图片描述
7.3.2平衡二叉树树
二叉排序树的查找性能取决于二叉排序树结构,而二叉树的形状取决于数据集
如果有序那么查找时间复杂度为O(n)O(n),如果结构合理查找的时间复杂度为O(log2n)O(log_2^n),事实上,树的高度越小,查找越快。 因此,提出了平衡二叉树,又称AVL树。

平衡二叉树或者是一棵空树,具有下面的特点:
(1)左右子树深度之差的绝对值不超过1
(2)左右子树也是平衡二叉树

**平衡因子:**若将二叉树节点的左右子树深度之差。在平衡二叉树中,平衡因子只可能是-1,0,1。 如果在二叉树中有一个节点的平衡因子的绝对值大于1,则该二叉树就不是平衡的。
在这里插入图片描述
在这里插入图片描述
上面图中,(a)是平衡的,(b)是不平衡的
AVL树的时间复杂度为0(log2nlog_2^n)

2.平衡二叉树的平衡调整方法
方法:插入节点时,按照二叉排序树处理,当破坏了平衡结构的时候,需要进行调整:找到离插入节点最近且平衡因子绝对值超过1的祖先节点,以该节点为根的子树称为最小不平衡树,可将重新平衡的范围局限于这颗子树

很明显的,但把一棵树构造成一棵平衡二叉树,有利于查找的效率
那么如何将一个非平衡二叉树转换成一棵平衡二叉树:
例子:
在这里插入图片描述
很明显的,上面这个是非平衡二叉树,而导致不平衡的是节点9,最初始不平衡应该是这样的(节点8插入之前就已经不平衡了)
在这里插入图片描述
因为节点9是导致不平衡的关键,所以将节点9以及以下的子树都称为最小不平衡子树

例子2:
在这里插入图片描述
一般需要做左旋转或右旋转,如果旋转之后不是一个二叉树了,那么可以多余节点“舍弃”(根节点右子树的最左边的孩子可以插入到左子树的最右边的位子)

7.4散列表的查找
散列查找的思想: 如果查找关键字之间和存储位置有着某种关系,那么在进行查找时,就无需做比较或者只需要做很少的比较就可以找到相应的记录。
通过对元素的关键值进行某种运算,直接求出元素地址,即使用关键字到地址的直接转换,这种方法又称散列法或杂凑法

几个术语:
1、散列函数和散列地址:p=H(key)p=H(key),其中,H为散列函数,p为散列地址
2、散列表:通常散列表的存储空间是一维数组,散列地址是数组的下标
3、冲突和同义词

一般使用除留余数法来创建散列表:H(key)=key%p 其中,表长为m,p一般选取的是不大于表长的最大质数。

7.4.3处理冲突的方法
Hi=(H(key)+di)mH_i=(H(key)+d_i) 模m
1.线性探测法
d=1,2,3,4,....m1d=1,2,3,4,....,m-1
2.二次探测法
d=12,12,22,22,.....+k2,k2d=1^2,-1^2,2^2,-2^2,.....+k^2,-k^2
3.伪随机探测法
di=d_i=伪随机序列

线性探测法的优点:只要散列表未填满,总能找到一个不发生冲突的地址。缺点是会发生“二次聚焦”。
二次探测和伪随机法的优点是不会产生“二次聚焦”,但是不能保证每次一定找到不发生冲突的地址

链地址法:
具有相同散列地址的记录放在同一个链表中,称为同义词链表。有m个散列地址就有m个单链表,用数组存放链表的头指针,凡是散列地址为i的记录都以节点方式插入到响应的单链表中。
在这里插入图片描述
散列表的查找的平均查找长度取决于三个因素:
散列函数、处理冲突的方法和散列表的装填因子
散列表的装填因子α定义为:
α=α=\frac{表中弄填入的记录数}{散列表的长度}
α越小,发生冲突的可能性就越小

计算查找成功的平均查找长度和查找失败时的平均查找长度
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
因此:
ASLsucess=1ni=1nCiASL_{sucess}=\frac{1}{n}\sum_{i=1}^{n}C_i
其中,n为散列表中记录的个数,CiC_i为查找成功需要的比较次数
ASLunsucess=1ri=1rCiASL_{unsucess}=\frac{1}{r}\sum_{i=1}^{r}C_i
其中,r为散列表取值的个数,CiC_i是查找失败时的比较次数

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!