DS博客作业05--查找
0.PTA得分截图
1.本周学习总结
1.1 查找的性能指标
ASL(Average Search Length),即平均查找长度,在查找运算中,由于所费时间在关键字的比较上,所以把平均需要和待查找值比较的关键字次数称为平均查找长度。
公式:ASL=\(\sum_{i=1}^{n}{p_{i}c_{i}}\)
其中n为查找表中元素个数,\(p_{i}\)为查找第i个元素的概率,通常假设每个元素查找概率相同,\(p_{i}\)=1/n,\(C_{i}\)是找到第i个元素的比较次数。
ASL的计算与移动次数和比较次数有关,决定了该查找算法的时间复杂度
顺序查找
查找方式为从头扫到尾,找到待查找元素即查找成功,若到尾部没有找到,说明查找失败
Ci(第i个元素的比较次数)在于这个元素在查找表中的位置,如第0号元素就需要比较一次,第一号元素比较2次......第n号元素要比较n+1次。所以Ci=i;
当待查找元素不在查找表中时,也就是扫描整个表都没有找到,即比较了n次,查找失败
所以:\(ASL_{成功}=\sum_{i=1}^{n}{p_{i}c_{i}}=\frac{n+1}{2}\)
\(ASL_{不成功}=n\)
折半查找
前提是有序表,利用有序表构建二叉树,中间数为根,左小右大,即为判定树,如下图
查找方式为(找k),先与树根结点进行比较,若k小于根,则转向左子树继续比较,若k大于根,则转向右子树,递归进行上述过程,直到查找成功或查找失败
所以:\(ASL_{成功}=\sum_{i=1}^{h}p_{i}c_{i}=\frac{1}{n}\sum_{i=1}^{h}2^{i-1}\times i=\frac{n+1}{n}\times log_{2}(n+1)-1\approx log_{2}(n+1)-1\)
\(ASL_{不成功}=\sum_{i=1}^{h}p_{i}c_{i}=\frac{1}{n}\sum_{i=1}{n}i=\frac{1}{n}\times\frac{n(n+1)}{2}=\frac{n+1}{2}\)
举个例子:6个数{15,18,30,45,50,66}
\(ASL_{成功}=\frac{1*1+2*2+3*3}{6}\)
\(ASL_{不成功}=\frac{2*1+3*6}{7}\)
比顺序查找快
1.2 静态查找
顺序查找
定义:按照序列原有顺序对数组进行遍历比较查询的基本查找算法
类似与数组查找,一个一个按顺序的找
int SeqSearch(Seqlist R,int n,KeyType k)
{
int i=0;
while(i<n&&R[i].key!=k)i++;//从表头开始找
if(i>=n)return 0;//没找到返回0
else return i+1;//找到返回序号i+1
}
时间复杂度为:O(n)
顺序查找的优缺点:
缺点:查找效率较低,特别是当待查找集合中元素较多时,不推荐使用顺序查找。
优点:算法简单而且使用面广。
二分查找
二分查找也称折半查找(Binary Search),它是一种效率较高的查找方法。但是,折半查找要求线性表必须采用顺序存储结构,而且表中元素按关键字有序排列.
int binarySearch(SeqList R,int n,KeyType k){
int low = 0, high = n-1, mid;
while(low <= high){
mid = (low + high) / 2;
if(R[mid.key] == k)return mid;//找到就返回下标
else if(R[mid].key > k)high = mid - 1;
elss low = mid + 1;
}
return 0;//找不到返回0
}
//递归法
int BinarySearch(SeqList R,int low,int hight,KeyType k)
{
int mid;
if(low<=high){//查找的区间存在一个以上的元素
mid=(low+high)/2;//找中间位置
if(R[mid].key==k)return mid+1;//查找成功,返回序号mid+1
if(R[mid].key>k)BinarySearch(R,low,miid-1,k);//在[low…mid-1]区间递归寻找
else BinarySearch(R,mid+1,high,k); //在[mid+1…high]区间递归寻找
}
else return 0;
}
时间复杂度为:O(logn)
二分查找的优缺点:
优点:折半查找的时间复杂度为O(logn),远远好于顺序查找的O(n)。
缺点:虽然二分查找的效率高,但是要将表按关键字排序。而排序本身是一种很费时的运算。既使采用高效率的排序方法也要花费O(nlgn)的时间。
优化二分--判定树
就是将给定值K与二分查找判定树的根结点的关键字进行比较。若相等,成功。否则若小于根结点的关键字,到左子树中查找。若大于根结点的关键字,则到右子树中查找。
分块查找
分块查找是折半查找和顺序查找的一种改进方法,分块查找由于只要求索引表是有序的,对块内节点没有排序要求,因此特别适合于节点动态变化的情况。
索引表将主表分为4块,每块包含5个元素。要查找主表中的某个元素,需要分为两步查找:
第1步需要确定要查找元素所在的块
第2步在该单元查找指定的元素。
例如,要查找元素62,首先需要将62与索引表中的元素进行比较,因为46<62<77,所以需要在第3个块中查找,该块的起始下标是10,因此从主表中的下标为10的位置开始查找62,直到找到该元素为止。如果在该块中没有找到62,则说明主表中不存在该元素,查找失败。
分块查找介于顺序查找和二分查找之间的一种查找方法。
分块查找的优缺点:
优点:在表中插入或删除一个记录时,只要找到该记录所属的块,就在该块内进行插入和删除运算;因块内记录的存放是任意的,所以插入或删除比较容易,无须移动大量记录。
缺点:分块查找的主要代价是增加一个辅助数组的存储空间和将初始表分块排序的运算。
1.3 二叉搜索树
二叉排序树又称为二叉搜索树,它是一种特殊结构的二叉树,其定义为:二叉树排序树或者是一棵空树,或者是具有如下性质的二叉树:
(1)若它的左子树非空,则左子树上所有结点的值均小于根结点的值;
(2)若它的右子树非空,则右子树上所有结点的值均大于根结点的值;
(3)它的左右子树也分别为二叉排序树
1.3.1 如何构建二叉搜索树(图解)
按照二叉树定义进行二叉搜索树的构造,如图:
ASL计算
每层的节点数\(\times\)查找次数(即高度)进行求和即可得到成功的ASL;
每层中空节点数\(\times\)查找父亲节点的次数(高度-1)进行求和即可得不成功的ASL。
例如图中的ASL计算
\(ASL_{成功}=\frac{1\times1+2\times2+4\times3+3times4}{10}=2.9\)
\(ASL_{不成功}=\frac{5\times3+6\times4}{11}=3.55\)
在二叉搜索树中插入数据
插入的数据一定在叶子节点
在二叉搜索树中删除数据
1.3.2 如何构建二叉搜索树(代码)
构建二叉搜索树
void CreatBST(BSTree BST,int n)
{
int X;
for ( int i=0; i<n; i++ ) {
cin>>X
BST = Insert(BST, X);
}
}
插入数据
BSTree Insert(BSTree BST, KeyType k)
{
if (!BST){
BSTree p = new BSTNode;
p->lchild= NULL;
p->rchild = NULL;
p->data = k;
return p;
}
else if (k < BST->data){
BST->lchild = Insert(BST->lchild, k);
}
else if (k > BST->data){
BST->rchild= Insert(BST->rchild, k);
}
return BST;
}
删除数据
int DeleteBST(BSTree &bt,KeyType k)
{
if(bt==NULL)return 0;//空树
else{
if(k<bt->key)return DeleteBST(bt->lchild,k);//在左侧递归寻找删除结点
else if(k>bt->key)return DeleteBST(bt->rchild,k);//在左侧递归寻找删除结点
else{//找到结点进行删除
Delete(bt);
return 1;
}
}
}
//从二叉树排序树中删除结点p
void Delete(BSTree &p)
{
BSTNode *q;
if(p->rchild==NULL){//p没有右子树
q=p;
p=p->lchild;
delete q;
}
else if(p->lchild==NULL){//p没有左子树
q=p;
p=p->rchild;
delete q;
}
else Delete1(p,p->lchild);//既有左子树又有右子树
}
//既有左子树又有右子树的删除
void Delete1(BSTNode *p,BSTNode *&r)
{
BSTNode *q;
if(r->rchild!=NULL)Delete1(p,r->rchild);//递归找最右下节点
else{
p->key=r->key;
q=r;
r=r->lchild;
delete q;
}
}
时间复杂度:最好:O(logn),最差:O(n)
用递归实现插入、删除的优势
- 保留父子关系,便于删除和插入顺利找到父亲和孩子
- 代码简短,便于书写,树的递归更能掌握
1.4 AVL树
由于二叉搜索树存在数据分配不平衡的现象,会导致查找时的时间复杂度提高,所以诞生了AVL解决此类问题。
AVL树是最先发明的自平衡二叉查找树。在AVL树中任何节点的两个子树的高度最大差别为1,所以它也被称为高度平衡树。增加和删除可能需要通过一次或多次树旋转来重新平衡这个树
构建AVL
AVL树的左右高度差大于一时,会进行以下四种调整
-
LL平衡旋转
-
RR平衡旋转
-
LR平衡旋转
-
RL平衡旋转
AVL树的高度和树的总节点数n的关系:h\(\approx\)\(log_{2}{N(h)+1}\)
map--STL的强大容器
基本概念
map是STL中非常有用的一个容器
和函数类似,map是通过键值key查询对应value
对于数组而言,其下标必须非负整数的数值,且数值不能过大,而map就不同了,map的键值可以是任意类型,包括最常用的string类型,map的value也同样可以为任意类型
用途
map的内部是红黑树实现,其具有自动排序的功能
通过键值排序,以递增的方式排序
若键值为整形,以数值递增排序
若键值为string,以字符串升序排序
相关函数的用法
用法 | 作用 |
---|---|
m.begin(), m.end() | 返回 m的首、尾迭代器 |
m.insert() | (1)可以 m[‘a’] = 1进行赋值;(2)m.insert({‘b’, 2}); |
m.erase(迭代器) / m.erase(键) / m.erase(迭代器范围) | (1)m.erase(it); (2) m.erase(‘b’); (3) m.erase(it, m.end()) |
it->first / it->second | 用于map的遍历:it->first获取key值,it->second获取value值 |
m.find(k); | 返回的键值为k的元素的迭代器 |
count(key) | |
m.empty() | 判断m是否为空,是空返回true,否则返回false |
m.clear() | 清除m中的元素 |
插入
map的插入方式主要是用以下两种,两种方式的区别在于
若存在键值key,则第一种方式insert不能再执行,而第三种可以执行,并且将value覆盖
#include<iostream>
#include<map>
using namespace std;
map<string,string>m;
int main() {
m.insert("abc","efg");
m["www"]="ccc";
return 0;
}
第一种insert是有返回值的,成功插入返回1,插入失败返回0
查找
如何去查找是否存在键值key?
1、m.find(key):返回键值key的迭代器,若没有找到则返回m.end()
【注意】
m.end()并不是m的最后一个元素的迭代器,而是最后一个元素的下一个元素的迭代器
2、m.count(key):返回0,没有key,返回1,有key
#include<iostream>
#include<map>
using namespace std;
map<string,string>m;
int main() {
string key;
cin>>key;
if(m.find(key)!=m.end())cout<<"找到了";
else cout<<"map里没有";
if(m.count(key)==1)cout<<"找到了";
else cout<<"map里没有";
map<string,string>::iterator it;
it=m.find(key);
if(it==m.end())cout<<"map里没有";
else cout<<"找到了";
return 0;
}
删除与清空
如何删除键值key?
有三种方式,一是通过键值删除,二是通过单个元素的迭代器删除,三是通过迭代器的范围
如何清空map容器?
clear即可
#include<iostream>
#include<map>
using namespace std;
map<string,string>m;
int main() {
string key;
cin>>key;
m.erase(key);
map<string,string>::iterator it;
it=m.find(key);
m.erase(it);
m.erase(m.begin(),m.end());
map<string,string>::iterator itl;
map<string,string>::iterator itr;
itl=m.find("123");
itr=m.find("456");
m.erase(itl,itr);
m.clear();
return 0;
}
map的大小
如何知道map中已经有多少个元素了
#include<bits/stdc++.h>
using namespace std;
map<string,string> m;
int main() {
int msize=m.size();
return 0;
}
map的遍历
如何去遍历map中的每一个元素?
有时我们需要去遍历map去寻找一个max_value之类的,map的遍历通过迭代器实现,迭代器++就可以实现去找下一个元素
迭代器的first返回其键值
迭代器的second返回其value
#include<iostream>
#include<map>
using namespace std;
map<string,int>m;
int main() {
string ans;
int maxx=-1e9;
map<string,int>::iterator it;
for(it=m.begin();i!=m.end();i++){
if(it->second>maxx){
maxx=it->second;
ans=it->first;
}
}
cout<<"最大的是 "<<ans<<" 其value是 "<<maxx;
return 0;
}
map的初始值
string的初始值为:空串""
整型的初始值为:0
1.5 B-树和B+树
B-树和AVL树区别
AVL树结点仅能存放一个关键字,树的敢赌较高,而B-树的一个结点可以存放多个关键字,降低了树的高度,可以解决大数据下的查找
B-树定义:一棵m阶B-树或者是一棵空树,或者满足一下要求的树就是B-树
- 每个结点之多m个孩子节点(至多有m-1个关键字);
- 除根节点外,其他结点至少有⌈m/2⌉个孩子节点(至少有⌈m/2⌉-1个关键字);
- 若根节点不是叶子结点,根节点至少两个孩子
B-树的插入
插入要看MAX=m-1(m为阶数)
B-树的删除
删除要看MIN=⌈m/2⌉-1
B+树定义:常被用来对检索时间要求苛刻的场景,适用于大型文件索引
一棵m阶B+树满足条件:
- 每个分支节点至少有m棵子树
- 根节点或没有子树,或者至少有两棵子树
- 除根节点,其他每个分支节点至少有m/2棵子树
- 有n棵子树的节点有n个关键字。
- 所有叶子节点包含全部关键字及指向相应记录的指针(关键字按顺序)
- 所有分支节点包含子节点最大关键字及指向子节点的指针
B树的查询时间复杂度在1到树高之间(分别对应记录在根节点和叶节点),而B+树的查询复杂度则稳定为树高,因为所有数据都在叶节点。
由于键会重复出现,因此会占用更多的空间。但是与带来的性能优势相比,空间劣势往往可以接受,因此B+树的在数据库中的使用比B树更加广泛。
1.6 散列查找
散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
给定表M,存在函数f(key),对任意给定的关键字值key,代入函数后若能得到包含该关键字的记录在表中的地址,则称表M为哈希(Hash)表,函数f(key)为哈希(Hash) 函数。
哈希表的设计主要包括:
- 散列函数f(key)
- 哈希表长度,装填因子有关,因子大冲突越大
- 解决冲突的方法
哈希表构造方法
1.直接取地址法
根据关键字直接加上某个常量作为地址,确定所坐在位置
优点:计算简单,并且不可能有冲突
缺点:关键字分布不是连续的将造成内存单元浪费
2. 除留余数法
用关键字k除以某个不大于哈希表长度的m的数p所得的余数作为哈希表地址,即需要一个哈希函数h(k)= k mod p(p最好为素数)
优点:得到的哈希表是大致连续的,且不会超过原来哈希表的长度,可以节省空间
缺点:余数有可能相同,会产生冲突
3. 数字分析法
将数据的后两位作为哈希地址,构成哈希表
缺点:当数据特别多时,容易已发冲突
解决哈希冲突
1. 开放地址法
- 线性探测法
从发生冲突位置的下一个位置开始寻找空的散列地址。发生冲突时,线性探测下一个散列地址是:Hi=(H(key)+di)%m,(di=1,2,3...,m-1)。闭散列表长度为m。它实际是按照H(key)+1,H(key)+2,...,m-1,0,1,H(key)-1的顺序探测,一旦探测到空的散列地址,就将关键码记录存入
例如设一组初始记录关键字集合为(25,10,8,27,32,68),散列表的长度为8,散列函数H(k)=k mod 7
不成功就是寻找到空位置时查找次数
\(ASL_{成功} = \frac{1\times4+2+3}{6} = 1.5\)
\(ASL_{不成功} = \frac{1+2+1+6+5+4+3}{7} = 3.1\)
2. 拉链法
把所有的同义词用单链表连接起来
例如:{25,10,8,27,32,68}
链表采用头插法
\(ASL_{成功}: \frac{1*5+2*1}{6} = 1.17\)
\(ASL_{不成功}: \frac{0+1+0+1+2+1+1+0}{7} = 0.86\)
2.PTA题目介绍
2.1 是否完全二叉搜索树(2分)
完全二叉树的层次遍历结果中当有第一个空出现后,后面就不会有数据了
非完全二叉树的话,出现空之后,还会有数据
所以只要将前面的进队里,然后再弹出,队列中如果都是空,那么就是完全二叉树,有数据就不是完全二叉树
2.1.1 伪代码
2.1.2 提交列表
2.1.3 本题知识点
- 二叉树的层次遍历可以用于是否是完全二叉树的判断,由遍历结果可以看出,完全二叉树的数据集中在一起,不会出现空的现象
2.2 航空公司VIP客户查询
2.2.1 伪代码
2.2.2 提交列表
cin与cout相比于scanf和printf消耗时间长,改成scanf和printf后,就能过了
2.2.3 本题知识点
- cin cout与scanf printf消耗时间不同
- map查找key的使用count比较方便
2.3 基于词频的文件相似度
2.3.1 伪代码
2.3.2 提交列表
第一次提交时,直接两层循环找相同单词,忘记了map可以自动排序。。。
2.3.3 本题知识点
1.map小标可以为字符型, map可以按照下标自动排序,便于后面相似度计算,mapyyds
2. 分割字符串时,要注意小于三个字母和大于10个字母的单词,尤其是最后一个单词,当时忘记判断了。。。