DS博客作业05--查找
| 这个作业属于哪个班级 |
| ---- | ---- | ---- |
| 这个作业的地址 |
| 这个作业的目标 | 学习查找的相关结构 |
| 姓名 | 王鑫 |
0.PTA得分截图
1.本周学习总结
本周学习的是查找,查找我们已经存储好的数据。因为数据的存储方式有很多种,我们要根据不同的数据存储的方式来判别什么样的数据查找方式适合我们需要查找的数据。
这样可以让查找的效率大大的提升。在数据的量很大的时候,就很节省很多时间。
1.1查找的性能指标
查找的效率我们要怎么看呢,我们一般通过看本次查找的ASL(Average Search Length),叫做关键字的平均比较次数,也称平均搜索长度
n:记录的个数
Pi:查找第i个记录的概率(通常认为Pi=1/n)
Ci:找到第i个几率所需要的比较次数
1.2静态查找
一般就只是查询和检索操作
而动态查找就是在发现没有我们要找的数据的时候进行插入这个数据,这样称为动态查找。
1.2.1顺序查找
用于顺序表,像数组一样我们顺着下标往下找,直到我们找到key。
从表的一端开始,顺序扫描线性表,依次将扫描到的关键字和给定值k相比较,若当前扫描到的关键字与k相等,则查找成功;若扫描结束后,仍未找到关键字等于k的记录,则查找失败。
这样我们的ASL成功为
查找不成功的时候,我们每次的查找次数都是n次这样我们ASL不成功为:n
1.2.2二分查找
二分查找也称为折半查找,每次都找中间的下标的数字来对比key在哪一部分,这样一般一半的查找可以节省很多时间。
这个查找在学习C语言的时候我们也有学习。
但是因为这样查找的特性,我们的数据必须是按关键字值的递增或递减顺序排列
我们对判定二分查找的ASL的方法,判定树。这样的树,把中间的数据作为根,大于根往右子树走,小于根往左走。左边的也是最中间的作为根,这样直到最后下面一层都是NULL,找不到。
算ASL成功的时候,我们就是在第几层就第几层的比较次数。
ASL成功为:
ASL不成功为:
1.2.3分块查找
分块查找是折半查找和顺序查找的一种改进方法,折半虽然非常大方便,但是它的局限性也非常大,用折半法的数据必须按照关键码顺序排序。而顺序查找可以解决表元素动态变化的要求,但查找的效率很低。这时候我们中间的方法就出现了。
将查找表分为若干个子块。块内元素可以无序,但块之间是有序的,即第一个块中的最小关键字小于第二个块中的所有记录的关键字,第二个块中的最大关键字小于第三个块中的所有记录的关键字,以此类推。在建立一个索引表,索引表中的每个元素含有各块的最大关键字和各块中第一个元素的地址,索引表按关键字有序排列。
1.3二叉搜索树
二叉查找树(Binary Search Tree),(又:二叉搜索树,二叉排序树)它或者是一棵空树,或者是具有下列性质的二叉树: 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值; 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值; 它的左、右子树也分别为二叉排序树。二叉搜索树作为一种经典的数据结构,它既有链表的快速插入与删除操作的特点,又有数组快速查找的优势;所以应用十分广泛,例如在文件系统和数据库系统一般会采用这种数据结构进行高效率的排序与检索操作。
二叉树的中序遍历得到的结果就是从小到大的的递增的序列。这个也是二叉搜索树的特点。
用我的说法来说这个二叉排序树就是,每个结点都满足:左子树都比它小,右子树的所有结点都比它大。
我们要怎么才能做到这样的树呢,我们下面就来创建一颗二叉排序树。
1.3.1 如何构建二叉搜索树(操作)
我们现在来创建一个二叉搜索树
我们先得到一组数据,这个数据要能创建比较平衡的二叉树。
下面是一组数据,我们按照顺序来进行创建二叉树这组数据就可以创建比较平衡的二叉树,要是这一组数据是递增或递减的,这样就会一直是一条边,就是只有一条边的树。
第一个的根结点是这组数据的第一个数字12
然后我们看看第二个数字,是5,这个数字是比12小的,我们就在左子树上创建一个节点5
我们每次插入新的数字都是要作为叶子节点插进来的,查到其他地方就会错误。
第三个数字是18,我们先和12对比,更大,右子树,发现右子树为空,我们刚好作为叶子插入。
再和之前一样我们继续插入,2我们对比12更小,往左子树走。左子树有东西,再对比,更小,再是左子树,为空,作为叶子节点插入。
再看看下一位数字,9,和之前一样的方法,和最后作为5的右子树插入。
我们把所有的数字一个个的插入,得到最后的结果就是这样
计算一下这个二叉排序树的成功和不成功的ASL
成功的ASL:分子由每层结点的数量和所在层数乘积的和组成,分母就是我们结点的数量
不成功的ASL:分子由每个结点被完善的节点和它们上一层节点的乘积之和,分母及时这些被完善的数量
如何插入节点
插入节点和建树的时候一样,都是在叶子节点的插入。建树的时候就是一个一个新节点插入树
我们现在再插入一个14
现在和12对比,更大右子树,然后和18对比再往左走,然后比15小我们往左走就看见空就可以作为叶子结点插入。
如何删除节点
如果我们要删除的节点正处于叶子结点,我们就可以直接删除,如果我们要删除的节点不是叶子结点。
1)被删除的节点只有左子树的时候
我们直接把删除的节点的右子树或者左子树代替它的位置。如我们现在删除一个只有子树的节点17
因为17只有一个左子树,所以我们直接把15的一边连过去。
2)被删除的既有左子树又有右子树
这时候我们有两种选择。一个是找左子树的最大,二是找右子树最小的。
第一种选择
第二种选择
1.3.2如何构建二叉搜索树(代码)
二叉搜索树的结点类型
typedef struct node
{
KeyType key;
//关键字项
InfoType data;
//其他数据域
struct node* Ichild, * rchild; //左右孩子指针
}BSTNode, * BSTree;
插入的代码
插入的时候一定要空的时候,作为一个叶子插入这棵树。
int InsertBST(BSTree& p, KeyType k)
{
if (p == NULL) // 原树为空
{
p = new BSTNode;
p->key = k; p->lchild = p->rchild = NULL; return 1;
}
else if (k == p->key)//相同关键词
return 0;
else if (k < p->key)
return InsertBST(p->lchild, k)
else
return InsertBST(p->rchild, k);//插入到右子树
}
我们插入成功就会返回1,0代表没有插入成功,意味着我们树上已经有相同的数据
删除数据的代码
我们先找到key在哪
int DeleteBST(BSTree& bt, KeyType k)
{
if (bt == NULL) return 0; / 空树删除失败
else
{
if (k < bt->key) return DeleteBST(bt->lchild, k);
// 递归在左子树中删除为k的节点
else if (k > bt->key) return DeleteBST(bt->rchild, k);
// 递归在右子树中删除为k的节点
else
{
Delete(bt);
//删除*bt节点
return 1;
}
}
}
如果没有找到地方就会返回0找到的话我们继续
判断删除的结点是什么样的情况,我们要运用什么样的方式来删除,如果只是有其中一个子树,我们就直接把子树和父亲的关系建立起来,然后删除key的点即可。
void Delete(BSTreee& p)
从二叉排序树中删除* p节点
{
BSTNode* q;
if (p->rchild == NULL)
//*p节点没有右子树的情况
{
q = p; p = p->lchild; delete q;
}
else if (p->1child == NULL)
//*p节点没有左子树
{
q = p; p = p->rchild; delete q;
}
else Delete1(p, p->lchild);
//*p节点既有左子树又有右子树的情况
}
如果是下一种两个都有的情况我们就要运用递归的方式来删除节点,因为这样我们才可以把节点的父子关系保留下来。
void Delete1(BSTNode* p, BSTNode*& r) // 被删节点:p, p的左子树节点 :
{
BSTNode* q;
if (r->rchild != NULL)
Delete1(p, r->rchild);
// 递归找最右下节片
else // 找到了最右下节点 * r
{
// 将 * r的关键字值赋给 * p
p->key = r->key;
q = r; r = r->lchild; delete q;
}
}
我们先找最右的节点(即最大的数字),然后我们要用r->rchild来找空,这样找到的时候就能直接改动最大的节点。
我们找到最大后,修改点的key为最大点,再删除最大节点(删除方法和之前只有一个子树的方法一致)
1.4AVL树
AVL树也叫平衡二叉树,是平衡状态的二叉搜索树。
当我们的数据很大或者顺序比较单一时,我们形成的二叉搜索树就会比较失衡,一边庞大,一边萎小。这时候我们就创造了平衡二叉树,也就是AVL树。这个树很平衡,数据集中,大程度上利用了树的结构,树的高度都比较低,这样查找的效率就会很高。
AVL树本质上还是一棵二叉搜索树,它的特点是:
- 本身首先是一棵二叉搜索树。
- 带有平衡条件:每个结点的左右子树的高度之差的绝对值(平衡因子)最多为1。
也就是说,AVL树,本质上是带了平衡功能的二叉查找树(二叉排序树,二叉搜索树)。
平衡因子:该节点左子树与右子树的高度差
在AVL树上的查找最坏的情况也均为O(log2n),这个就是我们AVL树的最大便利之处。
1.4.2AVL树的调整(旋转)
AVL树的插入很多时候需要我们进行调整,才可以保持我们的AVL树不会失衡(即平衡因子的绝对值不大于1),这时候的调整分为四种调整类型
我们现在就用一组数组来创建一个AVL树,从而更加了解AVL树吧
我们先运用我们在前面二叉搜索树的知识来创建节点,但是我们在创建节点的时候还要再看看有没有结点失衡
像前两个节点的时候,两个平衡因子都没有大于1
第三个节点的时候,我们发现有节点失衡了
**根据判断这个类型是LL型,所以我们找到失衡点和旋转点。
- 失衡点就是平衡因子=2的点
- 而我们这个旋转类型为LL型,所以我们的旋转点就是失衡点的儿子节点**
把失衡的点变成旋转点的里一个儿子节点
我们直接展示旋转后的结果
旋转完后,我们的节点都没有失衡了
这样我们继续插入,直到 60 的时候我们发现又一次失衡了,这次我们的调整的类型为RR型
和LL型很相识,RR型的旋转节点也是失衡节点是儿子节点。但本次失衡的点有两个,这种时候我们就要选择离插入的点最近的那个失衡点作为要调整对象。
RR型调整和LL型差不多,我们要把失衡的点变成旋转点的儿子节点。
旋转后的结果
我们继续插入 11
这时其实都是满足的,所以我们继续往下插入
插入 8
这时候就发现有点失衡了,而这次的失衡类型和之前不一样了,是LR型。
这种的我们旋转的点就和之前的不一样了,我们旋转的是失衡点的孙子节点。
把孙子节点替代原来的节点,而我们原来的失衡点就根据我们树的情况插入。
旋转后的结果
我们继续插入 35 32 直到发现不平衡
这次的调整类型为RL型,这个和LR有相似之处。
把孙子节点替代失衡的节点,而我们原来的失衡点就根据我们树的情况插入
旋转后的结果
这样我们这一棵树就完成了,这棵树就是一颗AVL树,这样的树每个结点的平衡因子都满足条件。我们这些过程中正好包括我们的四种调整类型
AVL树的高度和树的总节点数n的关系
平衡二叉树上最好最坏进行查找关键字的比较次数不会超过平衡二叉树的深度。o(logzn)。
最坏的情况下,普通二叉排序树查找长度为0(n)
STL容器map的特点、用法
Map是STL的一个关联容器,它提供一对一(其中第一个可以称为关键字,每个关键字只能在map中出现一次,第二个可能称为该关键字的值)的数据处理能力,由于这个特性,它完成有可能在我们处理一对一数据的时候,在编程上提供快速通道。map内部自建一颗红黑树(一种非严格意义上的平衡二叉树),这颗树具有对数据自动排序的功能,所以在map内部所有的数据都是有序的
使用map要包含map类所在的头文件
#include<map>
//map对象是模板类,需要关键字和存储对象两个模板参数:
std:map<int,string> personnel;
map的基本操作
begin() 返回指向map头部的迭代器
clear() 删除所有元素
count() 返回指定元素出现的次数
empty() 如果map为空则返回true
end() 返回指向map末尾的迭代器
equal_range() 返回特殊条目的迭代器对
erase() 删除一个元素
find() 查找一个元素
get_allocator() 返回map的配置器
insert() 插入元素
key_comp() 返回比较元素key的函数
lower_bound() 返回键值>=给定元素的第一个位置
max_size() 返回可以容纳的最大元素个数
rbegin() 返回一个指向map尾部的逆向迭代器
rend() 返回一个指向map头部的逆向迭代器
size() 返回map中元素的个数
swap() 交换两个map
upper_bound() 返回键值>给定元素的第一个位置
value_comp() 返回比较元素value的函数
1.5 B-树和B+树
每次学习新的知识都是为了更好的解决问题,在我们之前学习的AVL树虽然查找的效率很快,但是面对庞大的数据库,这样的树还是不适用。我们就学习一种更加适合庞大的数据库的树。——B树 这样的树,我们就会一个节点放多个关键字,这样就能把树的高度压缩的更短。适合大数据查找。
硬盘中的节点也是B树结构。B树利用多个分支(称为子树)的节点,减少获取几率时所经历的节点数,从而达到节省存取时间的目的。
1.5.1 B-树(又称为多路平衡查找树)
一棵m阶B树(balanced tree of order m)是一棵平衡的m路搜索树。它或者是空树,或者是满足下列性质的树:
- 根结点至少有两个子女;
- 每个非根节点所包含的关键字个数 j 满足:┌m/2┐ - 1 <= j <= m - 1;
- 除根结点以外的所有结点(不包括叶子结点)的度数正好是关键字总数加1,故内部子树个数 k 满足:┌m/2┐ <= k <= m ;
- 所有的叶子结点都位于同一层。
B树的创建与插入
因为B树节点有好几个关键字和B树的特点,所以在创建B树的时候,会有一些节点的分裂与合并。
我们先找一组数据来创建一个3阶的B树。==因为B树的阶数确定,所以我们的最多的关键数确定,j=2。
数据如下
和我们上面的数据基本一致
因为B树是节点内多个关键字,所以我们插入的时候先放在一个节点内,直到我们这个节点的关键字大于M-1
我们这时候就要把中间的 15 提出来做为一个独立的节点,然后 30 和 2 分裂成两个节点。
我们继续插入,我们插入都是要插入到叶子节点去的。不能插到非叶子节点
这时候我们可以继续插入,直到达到分裂条件
这时候发现我们的点分裂完还是不满足条件,我们就继续分裂
**继续插入我们最后一个数字 35 **
B树的删除
当我们要删除一些关键词的时候,因为B树的性质所以我们要注意关键词会不会小于最少的要求。当小于最小的数字的时候,这时候我们就要合并来保持B树的平衡。
B-树的删除:和插入的考虑相反,
- 结点中关键字的个数>[m/2]-1,直接删除
- 结点中关键字的个数=[m/2]-1要从其左(或右)兄弟结点“借调”关键字
若其左和右兄弟结点均无关键字可借(结点中只有最少量的关键字),则必颁进行结点的“合并”。
我们先删除第一个数字 35 ,这个数据在叶子节点上,叶子节点上的关键词>最小,我们就可以直接删除。
我们现在删除第二个数字,45 这个就不是叶子节点,所以我们删除的时候发现删除后就不满足最小。这个时候向孩子节点借就会导致孩子节点也不满足所以我们这时候把孩子和自己合成一个再删除 45 。
要是兄弟节点足够借,我们就要借过来。
删除 8
B+树
B+树是B树的变形,B+是大型索引文件的标准组织方式。
一颗m阶B+树满足下列条件:
- 每个分支节点至多有m课子树。
- 根结点或者没有子树,或者至少有两颗子树。
- 除根节点,其他每个分支节点至少有[m/2]课子树。
- 有n棵子树的节点有n个关键字。
- 所有叶子节点包含全部关键字及指向相应记录的指针
叶子节点按关键字大小顺序连接
叶子节点是直接指向数据文件的记录 - 所有分支节点(可看成是分块索引的索引表)
包含子节点最大关键字及指向子节点的指针
B+树可以顺序检索,但是B树不可以,因为它最下面的叶子节点是一条链式结构
1.6 散列查找。
之前的查找怎么样都要一些时间,而有些却很快就能找到我们需要的东西,这是为什么?
要是我们能够不花费时间,直接能够用访问地址就能找到我们需要的数据。这时候我们的散列查找法就是很好的方法。
我们上学期学习的哈希数组就是一种散列查找(哈希查找)。
哈希表
哈希表(Hash Table)又称散列表,是除顺序表存储结构、链接表存储结构和索引表存储结构之外的又一种存储线性表的存储结构。
注意∶哈希表是一种存储结构,它并非适合任何情况,主要i合记录的关键字与存储地址存在某种函数关系的数据。
当两个关键词根据哈希函数转化地址时一样的,我们称之为哈希冲突
为了减少哈希冲突我们设计哈希表的时候就要将装填因子(数据数量/表长)控制在0.6~0.9之间。
除留余数法
除留余数法是用关键字k除以某个不大于哈希表长度m的数p所得的余数作为哈希地址的方法。
给希函数h(k)为:
h(k)=k mod p (mod为求余运算,p≤m)
p最好是质数(素数)。
开放定址法
当产生哈希冲突的时候,我们就要想办法来放冲突的两个元素。下面有两种方法
(1)线性探查法
当我们哈希冲突的时候,我们就往后放,直到有位置。
我们创建一个哈希表来了解。数据如下:
我们对 13取余
前面这几个数字都没有产生哈希冲突,探查次数就是一次。现在我们加入下一个数字 29发现这个数字产生了哈希冲突。所以我们就要往后走直到我们到下标为6才插入,这时候我们对比了4次,所以探测次数为4。
我们继续插入,发现77哈希冲突,77mod13=12.下标为12的已经有数据了,但是这已经是表的尽头了,我们就要回去到下标为0。这时探测两次。
(2)平方探测法
平方探查法是一种较好的处理冲突的方法,可以避免出现堆积现象。它的缺点是不能探查到哈希表上的所有单元,但至少能探查到一半单元。
上一个是往后移动,我们这次就根据平方来移动。+1,-1,+4,-4.....
数据如下:
第一次遇见冲突,29,我们探测三次就可以了
哈希表的ASL
我们以第一个哈希表为例:
我们查找成功的ASL为:
分子为:我们所有探测成功的次数
分母为:我们探测的数据个数
**查找不成功的ASL为:
- 分子为:我们所有不成功的次数
我们查找不成功有两种情况:
- 地址内无内容
- 地址内有内容,但是!=k,我们就要往下找,直到为空
- 分母为:取余的数字
哈希链的ASL
用拉链法也是处理哈希冲突的一种方法,我们用一个链表数组来存储,同一个地址的数据就用一条链连接起来。
我们用上个数据来做一个哈希链,也是对13取余
得到一个哈希链:
查找成功的ASL:
分子:各层成功的数量*层数
分母:数据的个数
**查找不成功的ASL:
不成功的情况就是:当我们发现地址的内容不是我们的k,(当这个节点为空的时候,我们默认没有不用探查)
2.PTA题目介绍
2.1是否完全二叉搜索树
经过老师的介绍,完全二叉树的特点是只有最后一层有的空的情况。当我们层次遍历树的时候,最后一层才有空结点且在右边反映到结果上就是我们的所有空结点都在最后。
我们实现在代码层面,就是当层次遍历的每层LastNode为NULL时,我们的栈内不能有非空结点
2.1.1伪代码
主函数
for i->num
调用InserterBST函数,插入数据
调用PrintBT函数来
if(是完全二叉树)
printf("YES");
else
Printf("NO");
函数Printf
队列Q
if (bt不为空)
进队
end if
while(队Q不为空)
curnode=队头
if (curnode != NULL)
输出节点
end if
出栈队头
if curnode不为空
两个孩子进栈
end if
if curnode为空&&队头不为空
return false
end if
2.1.2提交列表
2.1.3本题知识点
- 完全二叉树的特点,用于解题
- 二叉树的中序遍历得到的结果是从小到大的,根据这个特性,我们可以检验二叉搜索树的建立是否正确。
2.2(哈希链) 航空公司VIP客户查询
2.2.1伪代码
//主函数
调用CreateHash函数,建立哈希表
for i->要查询的数量
输入身份证号
adr=函数(取地址)
调用函数FindHash
if 有相同的会员号
输出公里数
else
输出“No Info”
end for
//建函数CreateHash
for i->所有人
输入会员号和公里
adr=函数(取地址)
if 地址内为空
插入会员号和公里数
end if
else
while 本地址内所有的会员号
if 找到相同的
H[i].dis+=公里
end if
else
没找到
return -1
end else
2.2.2提交列表
2.2.3本题知识点
- 用map会超时,用链表的时间对更低,适合本题
- 哈希链表的长度要适用于数据
2.3(自建倒排索引表) 基于词频的文件相似度
2.3.1伪代码
for i->num //做单词个数的循环
输入 str
while str不为# //本次的单词输入没有停止
if 是字母,单词长度<10
转换成统一字母大小写
end if
else
if长度<2
加入map容器
end if
清空字符
end else
输入新字符
end while
end for
//计算单词的重复率
for 从begin->end
if 是重复单词
num_count++//重复词汇++
num_sum++//总数++
end if
else if 非重复单词
总数++
end else
end for
输出单词重复率 cout/sum;
2.3.2提交列表
2.3.3本题知识点
- 本题使用的是map容器,总的感觉很方便,直接使用就可以
- 学习新的东西很好,但一时间没办法很熟悉。