DS博客作业-查找

这个作业属于哪个班级 数据结构--网络2011/2012
这个作业的地址 DS博客作业05--查找
这个作业的目标 学习查找的相关结构
姓名 骆梦钒

0.PTA得分截图

查找题目集总得分,请截图,截图中必须有自己名字。题目至少完成总题数的2/3,否则本次作业最高分5分。没有全部做完扣1分。

1.本周学习总结(0-5分)

1.1 查找的性能指标

(1)ASL(Average Search Length),即平均查找长度,在查找运算中,由于所费时间在关键字的比较上,所以把平均需要和待查找值比较的关键字次数称为平均查找长度。

其中n为查找表中元素个数,Pi为查找第i个元素的概率,通常假设每个元素查找概率相同,Pi=1/n,Ci是找到第i个元素的比较次数。

(2)成功情况下(概率相等)的平均查找长度ASL成功是指找到查找表中任一记录平均需要的关键字比较次数。

(3)ASL的计算与移动次数和比较次数有关,决定了该查找算法的时间复杂度。

1.2 静态查找

分析静态查找几种算法包括:顺序查找、二分查找的成功ASL和不成功ASL。

(1)在顺序查找(Sequence Search)表中,查找方式为从头扫到尾,找到待查找元素即查找成功,若到尾部没有找到,说明查找失败。所以说,Ci(第i个元素的比较次数)在于这个元素在查找表中的位置,如第0号元素就需要比较一次,第一号元素比较2次......第n号元素要比较n+1次。所以Ci=i;所以

可以看出,顺序查找方法查找成功的平均 比较次数约为表长的一半。当待查找元素不在查找表中时,也就是扫描整个表都没有找到,即比较了n次,查找失败

(2)折半查找,首先待查找表是有序表,这是折半查找的要求。查找方式为(找k),先与树根结点进行比较,若k小于根,则转向左子树继续比较,若k大于根,则转向右子树,递归进行上述过程,直到查找成功或查找失败。在n个元素的折半查找判定树中,由于关键字序列是用树构建的,所以查找路径实际为树中从根节点到被查结点的一条路径,因为比较次数刚好为该元素在树中的层数。所以

Pi为查找k的概率,level(Ki)为k对应内部结点的层次。而在这样的判定树中,会有n+!种查找失败的情况,因为将判定树构建为完全二叉树,又有n+1个外部结点(用Ei(0<=i<=n)表示),查找失败,即为从根结点到某个外部结点也没有找到,比较次数为该内部结点的结点数个数之和,所以

qi表示查找属于Ei中关键字的概率,level(Ui)表示Ei对应外部结点的层次。

(3)哈希表中的ASL 查找成功的平均查找长度是指查找到哈希表中已有关键字的平均探测次数。而查找不成功的平均查找长度是指在哈希表中找不到待查的元素,最后找到空位置元素的探测次数平均值。

1.3 二叉搜索树

1.3.1 如何构建二叉搜索树(操作)

(1)创建与计算

给11个数据元素有序表(2,3,10,15,20,25,28,29,30,35,40)采用折半查找。则ASL成功和不成功分别是多少?
首先进行创建:
·把中间元素作为根节点

·把第二个元素拿出来与第一个元素做比较,如果比根节点大就放在右边,如果比根节点小就放在左边

·同样道理比较第三个元素,按照以上的方式依次把后面的元素插入到树中

查找成功时总会找到途中某个内部结点,所以成功时的平均查找长度为即25查找一次,成功,10,30要查找2次,成功,2,15,28,35要查找3次,成功,3,20,29,40要查找4次,成功。
而不成功的平均查找长度为
因为内部结点都能查找成功,而查找不成功的就是那些空的外部结点,所以到查询到2的左孩子,15的左孩子,28的左孩子,35的左孩子,3的左右孩子,20的左右孩子,29的左右孩子,40的左右孩子时,都是查找不成功的时候。

(2)如何在二叉搜索树做插入。

由于二叉搜索树的特殊性质确定了二叉搜索树中每个元素只可能出现一次,所以在插入的过程中如果发现这个元素已经存在于二叉搜索树中,就不进行插入。
否则就查找合适的位置进行插入。
第一种情况:root为空
直接插入,return true

第二种情况:要插入的元素已经存在
如上面所说,如果在二叉搜索树中已经存在该元素,则不再进行插入,直接return false;
第三种情况:能够找到合适位置
继续使用(1)的数据,插入数据18

Root->25 18<25 root=root->left
Root->10 18>10 root=root->right
Root->15 18>15 root=root->right
Root->20 18<20 root=root->left
Root=NULL

(3)如何在二叉搜索树做删除

对于二叉搜索树的删除操作,主要是要理解其中的几种情况,写起来还是比较简单的。
当然一开始还是需要判断要删除的节点是否存在于我们的树中,如果要删除的元素都不在树中,就直接返回false;否则,再分为以下四种情况来进行分析:
·要删除的节点无左右孩子
·要删除的节点只有左孩子
·要删除的节点只有右孩子
·要删除的节点有左、右孩子
对于第一种情况,我们完全可以把它归为第二或者第三种情况;
如果要删除的节点只有左孩子,那么就让该节点的父亲结点指向该节点的左孩子,然后删除该节点,返回true

如果要删除的节点只有右孩子,那么就让该节点的父亲结点指向该节点的右孩子,然后删除该节点,返回true

对于上面这两种情况我们还应该在之前进行一个判断,就是判断这个节点是否是根节点,#####如果是根节点的话,就直接让根节点指向这个节点的左孩子或右孩子,然后删除这个节点。

最后一种也是最麻烦的一种就是要删除的节点的左右孩子都存在。
找到该节点的右子树中的最左孩子(也就是右子树中序遍历的第一个节点) 把它的值和要删除的节点的值进行交换
`然后删除这个节点即相当于把我们想删除的节点删除了,返回true;

1.3.2 如何构建二叉搜索树(代码)

1.如何构建、插入、删除及代码。

(1)构建
BinTree CreatBinTree()
{
	BinTree T;
	ElemType ch;
	cin >> ch;
	if (ch == 0)
		T = NULL;
	else
	{
		T = (BinTree)malloc(sizeof(TNode));
		T->data = ch;
		T->left = CreatBinTree();//递归创建左子树
		T->right = CreatBinTree();//递归创建右子树
	}
	return T;
}
(2)插入

与find类似,要找到元素应该插入的位置

BinTree InsertX2BST(BinTree BST, ElemType x)
{
	if (!BST)
	{
		BST = (BinTree)malloc(sizeof(TNode));
		BST->data = x;
		BST->left = BST->right = NULL;
	}
	if (BST->data > x)
		BST->left = InsertX2BST(BST->left, x);
	else if(BST->data<x)
		BST->right = InsertX2BST(BST->right, x);
	return BST
}
(3)删除

有三种情况需要考虑:
(1)要删除的是叶结点。
(2)要删除的结点只有一个孩子。
(3)要删除的结点有两个孩子。此时,可以取左子树的最大值,或取右子树的最小值去替代要删除的结点

BinTree DeleteX_inBST(BinTree BST, ElemType x)
{
	if (!BST)
	{
		printf("树为空\n");
		return BST;
	}
	BinTree tmp;
	if (BST->data > x)
		BST->left = DeleteX_inBST(BST->left, x);
	else if (BST->data < x)
		BST->right = DeleteX_inBST(BST->right, x);
	else
	{
		if (BST->left && BST->right)
		{
			tmp = FindMin_inBST_(BST->right);
			BST->data = tmp->data;
			BST->right = DeleteX_inBST(BST->right, x);
		}
		else
		{
			tmp = BST;
			if (BST->left == NULL)
				BST = BST->right;
			else
				BST = BST->left;
		}
		free(tmp);
	}
	return BST:
}

2.分析代码的时间复杂度

给定值的比较次数等于给定值节点在二叉排序树中的层数。
如果二叉排序树是平衡的,则n个节点的二叉排序树的高度为Log 2n+1,其查找效率为O(Log 2n),近似于折半查找。
如果二叉排序树完全不平衡,则其深度可达到n,查找效率为O(n),退化为顺序查找。一般的,二叉排序树的查找性能在O(Log 2n)到O(n)之间。
因此,为了获得较好的查找性能,就要构造一棵平衡的二叉排序树。

3.为什么要用递归实现插入、删除?递归优势体现在代码哪里?

(1)递归就是有去(递去)有回(归来)
有去:是指把问题分解成无数的小问题,一层一层地解决,最终到达临界点之后,即解决完最后一个需要调用自身函数处理的问题之后,有回:将解决的结果原路返回到原点,原问题解决。
递归的基本思想,是把规模较大的一个问题,分解成规模较小的多个子问题去解决,而每一个子问题又可以继续拆分成多个更小的子问题。

(2)优势
·保留父子关系,便于删除和插入顺利找到父亲和孩子
·代码更简洁清晰,可读性更好 递归可读性好,在树的算法中使用递归显然简单的多。

1.4 AVL树

平衡二叉搜索树(Self-balancing binary search tree)又被称为AVL树(有别于AVL算法),且具有以下性质:它是一 棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。

(1)AVL树解决什么问题,其特点是什么?

AVL树解决了二分搜索树的不足以及BST可能导致的长链问题。
它的特点是AVL树中任何节点的两个子树的高度最大差别为1。

(2)结合一组数组,介绍AVL树的4种调整做法。

如果在AVL树中进行插入或删除节点,可能导致AVL树失去平衡,这种失去平衡的二叉树可以概括为四种姿态:LL(左左)、RR(右右)、LR(左右)、RL(右左)。
·新节点插入较高左子树的左侧——左左:右单旋

·新节点插入较高右子树的右侧——右右:左单旋

·新节点插入较高左子树的右侧——左右:先左单旋再右单旋

·新节点插入较高右子树的左侧——右左:先右单旋再左单旋

(3)AVL树的高度和树的总节点数n的关系?

设N(h)为高度为h的AVL树的最小节点数目,则N(h)=N(h-1) + N(h-2) +1; N(0)=0, N(1)=1,类似斐波那契数列

(4)介绍基于AVL树结构实现的STL容器map的特点、用法。

·map的插入:

在没有指定比较函数时,map元素的插入位置是按key的升序插入到红黑树中的。有三种插入方式。

map<int,int> M;
M.insert(pair<int,int>(1,2));//第一种:用insert函数插入pair数据
M.insert(map<int,int>::value_type(3,4));//第二种:用insert函数插入value_type数据
M[5]=6;//第三种:用数组方式插入数据

·反向遍历:

map<int, int>::reverse_iterator  iter;
    for(iter = M.rbegin(); iter != M.rend(); iter++)
    {
        cout<<iter->first<<" "<<iter->second<<endl;

}

·数据查找(key值匹配):

M.count(3);//查找成功返回1,失败返回0
M.find(3)==M.end();//查找成功返回数据所在位置的迭代器,失败返回等于end函数返回的迭代器

1.5 B-树和B+树

(1)B-树和AVL树区别,其要解决什么问题?

·区别:

B-树是一棵多叉平衡搜索树,旨在比AVL树能够拥有更低的树高,提高查找的效率,
但是同AVL树一样,面对插入和删除数据的操作后需要维持平衡,这可能带来一些得不偿失的情况。其次B-树可以被采用在外存的数据查询上,因为树高比较低,这样就可以减少磁盘的I/O次数。

·解决问题:

B树这种数据结构可以用来描述外部存储。这种数据结构常被应用在数据库和文件系统的实现上。

(2)B-树定义。结合数据介绍B-树的插入、删除的操作,尤其是节点的合并、分裂的情况

·定义:

B树(英语:B-tree)是一种自平衡的树,能够保持数据有序。概括来说是一个一般化的二叉查找树(binary search tree),可以拥有多于2个子节点。
一颗m阶的B树定义如下:

1)每个结点最多有m-1个关键字。
2)根结点最少可以只有1个关键字。
3)非根结点至少有Math.ceil(m/2)-1个关键字。
4)每个结点中的关键字都按照从小到大的顺序排列,每个关键字的左子树中的所有关键字都小于它,而右子树中的所有关键字都大于它。
5)所有叶子结点都位于同一层,或者说根结点到每个叶子结点的长度都相同。

·操作:

1)插入

分两种情况:

<1>若该结点的关键字个数<m-1,直接在最底层插入

<2>若该结点的关键字个数>=m-1,以中间关键码(m/2上取整)为界将结点一分为二,产生一个新结点,并把中间关键码插入到父结点(k-1层)中。同理,父亲节点也可能需要分裂,最终可能导致树长高一层。

2)删除

也分为两种情况

<1>删除非终端节点:首先找到要删除的关键码所在结点,用指针Ai(右指针) 所指子树中最小关键码Y代替Ki,然后在相应终端结点中删去Y,这样就转为删除终端结点中的关键码的问题了。

<2>删除终端节点:

·被删关键码所在结点中的关键码个数>=[m/2],说明删去该关键字后该结点仍满足B-树的定义。这种情况最为简单,只需从该结点中直接删去关键字即可。

·被删关键码所在结点中关键码个数n=[m/2]-1,说明删去该关键字后该结点将不满足B-树的定义,需要调整。
调整过程分为两种:

①如果其左右兄弟结点中有“多余”的关键字,即与该结点相邻的右(左)兄弟结点中的关键字数目大于[m/2]-1。父母中紧靠被删关键字的下来,兄弟中紧靠被删关键字的关键字上取

②被删关键码所在结点和其相邻的左右兄弟节点中的关键码个数均等于[m/2]-1,左右兄弟都不够借。父母节点中与被删的关键字紧靠的关键字下来,然后去和兄弟节点合并

(3)B+树定义,其要解决问题

B+树是一种树数据结构,B+树的特点是能够保持数据稳定有序,其插入与修改拥有较稳定的对数时间复杂度。B+树元素自底向上插入,这与二叉树恰好相反。
一棵m阶的B+树定义如下:
·每个结点至多有m个子女;
·除根结点外,每个结点至少有[m/2]个子女,根结点至少有两个子女;
·有k个子女的结点必有k个关键字。
B+树的查找与B树不同,当索引部分某个结点的关键字与所查的关键字相等时,并不停止查找,应继续沿着这个关键字左边的指针向下,一直查到该关键字所在的叶子结点为止。

数据库索引用B+树。

1.6 散列查找。

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

1、哈希表的构造

(1)直接定址法
(2)数字分析法
(3)平方取中法
(4)折叠法
(5)除留取余法
(6)随机数法

2、结合数据介绍哈希表的构造及ASL成功、不成功的计算

例:
将关键字序列(7、8、30、11、18、9、14)散列存储到散列表中。散列表的存储空间是一个下标从0开始的一维数组。散列函数为: H(key) = (keyx3) MOD 7,处理冲突采用线性探测再散列法,要求装填(载)因子为0.7。
(1) 请画出所构造的散列表;
(2) 分别计算等概率情况下查找成功和查找不成功的平均查找长度。
解:
(1)散列表:
α = 表中填入的记录数/哈希表长度 ==> 哈希表长度 = 7/0.7 = 10
H(7) = (7x3) MOD 7 = 0 H(8) = (8x3) MOD 7 = 3 H(30) = (30x3)MOD 7 = 6
H(11) = (11x3) MOD 7 = 5 H(18) = (18x3) MOD 7 = 5 H(9) = (9x3) MOD 7 = 6 H(14) = (14x3) MOD 7 = 0
按关键字序列顺序依次向哈希表中填入,发生冲突后按照“线性探测”探测到第一个空位置填入。

(2)查找成功的平均查找长度
(待查找的数字肯定在散列表中才会查找成功)
查找数字A的长度 = 需要和散列表中的数比较的次数;
步骤:
比如 查找数字:8
则 H(8) = (8x3) MOD 7 = 3
哈希表中地址3处的数字为8,进行了第一次比较:8 = 8,则查找成功,查找长度为1;
比如查找数字:14
则 H(14) = (14x3) MOD 7 = 0
哈希表中地址0处的数字为7,进行第一次比较:7≠14
哈希表中地址1处的数字为14,进行第二次比较:14=14 ,则查找成功,查找长度为2。

所以总的查找成功的平均查找长度= (1+1+1+1+3+3+2)/7 = 12/7。

(3)查找不成功的平均查找长度
(待查找的数字肯定不在散列表中)
【解题的关键之处】根据哈希函数地址为MOD7,因此任何一个数经散列函数计算以后的初始地址只可能在0~6的位置
查找0~6位置查找失败的查找次数为:
地址0,到第一个关键字为空的地址2需要比较3次,因此查找不成功的次数为3.
地址1,到第一个关键字为空的地址2需要比较2次,因此查找不成功的次数为2.
地址2,到第一个关键字为空的地址2需要比较1次,因此查找不成功的次数为1.
地址3,到第一个关键字为空的地址4需要比较2次,因此查找不成功的次数为2.
地址4,到第一个关键字为空的地址4需要比较1次,因此查找不成功的次数为1.
地址5,到第一个关键字为空的地址9,因此查找不成功的次数为5.
地址6,到第一个关键字为空的地址9,因此查找不成功的次数为4.
于是得到如下数据:

则查找不成功的平均查找长度 = (3+2+1+2+1+5+4)/7 = 18/7。

3、对散列表函数产生冲突的解决办法

(1)开放地址法(也叫开放寻址法):实际上就是当需要存储值时,对Key哈希之后,发现这个地址已经有值了,这时该怎么办?不能放在这个地址,不然之前的映射会被覆盖。这时对计算出来的地址进行一个探测再哈希,比如往后移动一个地址,如果没人占用,就用这个地址。如果超过最大长度,则可以对总长度取余。这里移动的地址是产生冲突时的增列序量。
(2)再哈希法:在产生冲突之后,使用关键字的其他部分继续计算地址,如果还是有冲突,则继续使用其他部分再计算地址。这种方式的缺点是时间增加了。
(3)链地址法:链地址法其实就是对Key通过哈希之后落在同一个地址上的值,做一个链表。其实在很多高级语言的实现当中,也是使用这种方式处理冲突的,
(4)建立一个公共溢出区:这种方式是建立一个公共溢出区,当地址存在冲突时,把新的地址放在公共溢出区里。

2.PTA题目介绍(0--5分)

介绍3题PTA题目

2.1 是否完全二叉搜索树(2分)

2.1.1 伪代码(贴代码,本题0分)

建立好二叉搜索树以后的难点应该就是判断是否是完全二叉树了。完全二叉树的定义是:深度为n的二叉树,其全部深度n-1的节点为满,而深度为n的节点只能聚集在左侧。那么我们可以层次遍历找到第一个NULL节点,记录遍历的节点个数,如果其个数为n的话说明为完全二叉树,否则不是。这里可以应用完全二叉树的定义。

node* Insert(node* root, int x)//插入法创建树
{
	if root为空
	{
		新建root结点
		root->data=x
		root->left=root->right=NULL//左右孩子为空
		return root
	}
	else
	{
		if x小于root->data
			root->right=Insert()//递归
		else if x大于root->data
			root->left=Insert()
	}
	return root
}
void layershow(node* root)//层次遍历
{
	新建队列que
	if root
	{
		输入root->data
		出队
		if root左孩子不为空
		{
			输入“ ”root->left->data
			root->left入队
		}
		if root右孩子不为空
		{
			输入" "root->right->data
			root->right入队
		}
	}
}
int check(node *root)//判断
{
	新建队列que
	root入队
	while root等于que.front()不为空
	  do
	   root->left入队
	   root->right入队
	   出队
	   num加一
	  end
    if num等于n
		return 1
	return 0
}

2.1.2 提交列表

2.1.3 本题知识点

(1)在创建二叉树时利用插入法而不是直接建立
(2)插入函数中运用递归将数据插入树中
(3)建立队列,通过入队出队将树的数据储存下来,再进行判断
(4)利用层次遍历找到第一个NULL节点,记录遍历的节点个数(定义num),如果其个数为n(即num=n)的话说明为完全二叉树,否则不是。

2.2 航空公司VIP客户查询(2分)

2.2.1 伪代码(贴代码,本题0分)

·思路:(1)建立一个哈希表,哈希表中存放的是结点所在的下标(相当于存放链表头指针,只不过这里的指针用下标表示)。
i. 哈希冲突解决方法:拉链法,即在冲突的位置存放一个链表。
(2)建立一个存放数据的表(表长同哈希表),该表用作链表。
·伪代码

int hash(char* number)//哈希函数,对身份证号进行散列
{
	long long res=0
	for i=0 to 17 //最后一位是前17位的校验值,可以不考虑,并不影响唯一性
		res = 10*res+(number[i]-'0')
	return res%MAXLENGTH
}
void insert(char* number, int ditance)// 将飞行记录信息插入到表中
{
	int h = hash(number);//对身份证进行散列
	for i等于head[h] i不等于-1 i等于records[i].next
		if strcmp(records[i].number, number)//连接
		{
			records[i].distance += distance//飞行距离
				return;
		}
	end
	records[count].distance += distance//records当做链表使用,count表示records表中已经插入的结点数目
	strncpy(records[count].number, number, 18)
	records[count].next = head[h];//哈希表,存放链表头结点在records表中下标
	head[h]=count++//结点数目加一
}
int find(char* number)// 根据身份证号查找飞行记录
{
	int h=hash(number)//对身份证散列后的数字
	for i等于head[h] i不等于-1 i等于records[i].next
		if strcmp(records[i].number,number为空) 返回i
	end
	返回-1
}

2.2.2 提交列表

2.2.3 本题知识点

(1)哈希表的运用。利用哈希表来存放结点所在下标。
(2)用到指针便于传递数据,建立多条链。
(3)关于字符串的知识点,有利用字符串的有关操作,例如,strcmp()和strncpy()。
(4)运用memset函数将变量初始化指定的值。

2.3 基于词频的文件相似度(1分)

2.3.1 伪代码(贴代码,本题0分)

·思路:实现一种简单原始的文件相似度计算,即以两文件的公共词汇占总词汇的比例来定义相似度。为简化问题,这里不考虑中文(因为分词太难了),只考虑长度不小于3、且不超过10的英文单词,长度超过10的只考虑前10个字母。
·伪代码

int main()
{
	输入n
	for i=1 to n
	//使用字符接收每一输入字符,当字符不为英语字母时,用string复制字符数组中的字符,直至输入#结束
		while ch=tolower(getchar())不等于‘#’
			do
				if ch大等于‘a’且ch小等于‘z’
					if sn小于10 s[sn++]=ch//长度小于10的单词
				else
					s[sn]=0
					if sn大于2 mp[i][s]=1
					sn=0
		end
	    for map<string,bool>::iterator it=mp[i].begin it不等于mp[i].end()
			//直接使用map来进行操作 
			for j=0 to i
				com[i][j]=com[j][i]+=mp[j][it->first]
		end
		com[i][i]=num[i]=mp[i].size()
	end
	输入m
	for i=0 to m
		输入a,b
		输出com[a][b]*100.0/(num[a]+num[b]-com[a][b])

}

2.3.2 提交列表

2.3.3 本题知识点

(1)运用map统计,不过注意一点map在查询时会改变其size().
(2)要将单词长度不同的分开讨论
(3)用字符数组存入每个字符
(4)利用二位数组存储所有单词

posted on 2021-06-14 16:24  诗酒趁年华灬  阅读(54)  评论(1编辑  收藏  举报