博客作业-查找
0pta截图
1.本周学习总结(0-5分)
1.1 查找的性能指标
- ASL成功
平均需要和给定值k进行比较的关键字次数称为平均查找长度(ASL)
ASL=∑PiCi(i=1~n)
n是查找表中元素个数,
Pi是查找第i个元素的概率(通常假设每个元素的查找概率相等,此时Pi=1/n)
Ci是找到第i个元素所需要的关键字比较次数
一个查找算法的ASL越大,其时间性能越差,反之,若越小其时间性能越好
不成功
没有找到查找表中的元素,平均需要关键字比较次数
-
比较次数,移动次数
对于一组数据当它以哈希表,链表,二叉搜索树储存时,关键字的搜索就要通过将关键字与其中的元素进行比较才能找到
哈希表因会有冲突导致关键字存储位置不是哈希函数所对应的位置,此时的比较次数就会增加
链表因会有冲突,导致关键字不是某一条链表的第一个元素,此时要找到关键字任然要比较,即比较次数要增加 -
时间复杂度
顺序查找的时间复杂度:O(n)
折半查找(二分查找)的时间复杂度:O(log2 n),虽然找的高效,但关键字的排序需要是有序的(即适用于顺序表)
二叉排序树的时间复杂度:最好的情况下O(log2 n)与折半查找相似,最坏的情况O(n)
平衡二叉树的时间复杂度:O(log2 n)
1.2 静态查找
分析静态查找几种算法包括:顺序查找、二分查找的成功ASL和不成功ASL。
-
一般线性表的顺序查找:
ASL成功=(n+1)/2
ASL不成功=(n+1) -
有序顺序表的顺序查找
ASL成功=(n+1)/2
ASL不成功=n/(n+1)+n/2 -
二分查找
二分查找的ASL成功与ASL不成功通过画出对应查找序列的判定树,进而进行计算。 -
索引查找
ASL=I块内+I块间
其中块间可以使用二分查找
(1)ASL = (b+1)/2 + (s+1)/2 块内和块间均使用顺序查找,由于n = s*b,可以通过构建方程,得到当s = n^(1/2)。
ASL最小为n^(1/2)+1
(2)ASL = (s+1)/2 + ceil(log2(b+1)) 块内顺序,块间二分
1.3 二叉搜索树
1.3.1 如何构建二叉搜索树(操作)
创建代码
结合一组数据介绍构建过程,及二叉搜索树的ASL成功和不成功的计算方法。
ASL成功=(∑第i层的结点个数i)/总结点个数
以上例子ASL成功=(11+22+33+42)/8
ASL不成功=(∑第i层结点的空子树个数i)/总空结点个数
以上例子ASL不成功=(21+43+4*4)/9
如何在二叉搜索树做插入、删除。
- 插入
比较插入关键字与树中结点比较,当其中已有关键字与它相等,则不需要插入,当一个结点比它大,且左子树为空时,直接插入其左子树,当一个结点比它小,且右子树为空时,直接插入其右子树 - 删除
先对二叉树中结点关键字与要删除的比较,找到删除节点位置后,再调用删除函数删除
1.3.2 如何构建二叉搜索树(代码)
1.如何构建、插入、删除及代码。
typedef fstruct node //元素类型
{
KeyType key;//关键字项
InfoType data;//其他数据域
struct node *lchild,*rchild;//左右孩子指针
}BTSNode;
bool CreateBST(KeyType A[],int n)//创建二叉排序树
{
BSTNode * bt=NULL;//初始时bt为空
int i=0;
while(i<n)
{
InsertBST(bt,a[i]);//将关键字a[i]插入二叉排序树bt中
i++;
}
return bt;//返回建立的二叉排序树的跟指针
}
bool InsertBST(BSTNode *&bt,KeyType k)
//在二叉排序树bt中,插入一个关键字为k的结点,若插入成功则返回真,否则返回假
{
if(bt==NULL)//原树为空,新插入的结点为根节点
{
bt=new BSTNode;
bt->key=k;bt->lchild=bt->rchild=NULL;
return trye;
}
else if(bt->key==k)//树中存在相同关键字的结点,返回假
return false;
else if(k<bt->key)
return InsertBST(bt->lchild,k);//插入左子树中
else
return InsertBST(bt->rchild,k);//插入右子树中
}
void delete(BSTNode *&p)//删除结点p
{
BSTNode *q;
q=p;//然q指向p结点
p=p->rchild;
free(q);//释放结点q的空间
}
bool DeleteBST(BSTNode *&bt,KeyType k)
{
if(bt==NULL)//空树删除失败,返回为假
return false;
else
{ if(k>bt->key)//递归在左子树中删除k结点
return DeleteBST(bt->lchild,k);
else if(k<bt->key)//递归在右子树中删除k结点
return DeleteBST(bt->rchild,k);
else//找到了要删除的结点bt
{
Delete(bt);
return true;
}
}
}
2.分析代码的时间复杂度
- 二叉树
插入:O(n);查找:O(n);删除:O(n) - 二叉平衡树
插入:O(log2 n);查找:O(log2 n);删除:O(log2 n)
3.为什么要用递归实现插入、删除?递归优势体现在代码哪里? - 插入:
与顺序表比,不需要大量移动数组;与链表比,不需要更改指针关系,不需要考虑头插法和尾插法 - 删除:
递归查找才能把父子关系保留 - 优势:
1、递归的利用可以使算法或数据结构大大简化,代码简洁明了,
2、相同一个具有该特性的课题采用递归或其他算法,所要求的预定义及相应的结果都将不一样,
3、用了递归可能使用减少部份定义,代码实现部份大大减少
1.4 AVL树
AVL树解决什么问题,其特点是什么?
- AVL树是二叉搜索树的优化版,又称平衡二叉搜索树,高度平衡树。当一棵二叉搜索树的结点一直单边插入时,这时候它的查找效率趋近于O(n),非常慢。
- 而AVL树的特点是:“AVL树中任何结点的两个子树的高度最大差别为1” ,这样就克服了结点单边存储而导致查找效率低下的问题。即平衡因子=某一结点左右子树的高度差,所以平衡因子取值范围:-1 0 1
一般情况下,一颗平衡二叉树总是二叉排序树,因为脱离二叉排序树来讨论平衡二叉树是没有意义的,所以每个节点是平衡的。
所有左子树的节点都比根节点的值小,所有右子树节点的值都比根结点值大,这样在查询一个数的时候,可以每次根据根的信息决定左走还是右走
而这个定义是递归定义的,左子树的左子树所有点比左子树根小,左子树的右子树比左子树的根大,对于每一棵子树都是这样
如下图为AVL树
如下图不是AVL树
结合一组数组,介绍AVL树的4种调整做法。
LR调整
LL调整
RR调整
RL调整
AVL树的高度和树的总节点数n的关系?
高度为 h 的 AVL 树,节点数 N 最多2^h − 1; 最少N(h)=N(h− 1) +N(h− 2) + 1
2) 节点最多的时候是满二叉树,如果认为第一层的高度为0,那么节点数最多应该是2^(h+1) -1
介绍基于AVL树结构实现的STL容器map的特点、用法。
Map是STL的一个关联容器,翻译为映射,数组也是一种映射。如:int a[10] 是int 到 int的映射,而a[5]=25,是把5映射到25。数组总是将int类型映射到其他类型。这带来一个问题,有时候希望把string映射成一个int ,数组就不方便了,这时就可以使用map。map可以将任何基本类型(包括STL容器)映射到任何基本类型(包括STL容器)。
map提供关键字到值的映射 ,其中第一个可以称为关键字,每个关键字只能在map中出现一次,第二个称为该关键字的值,由于这个特性.普通 int 数组是 map<int ,int > a。字符到整型的映射,就是 map<char ,int > a,而字符串到整型的映射,就必须是 map<string , int > a。map的键和值也可以是STL容器,如 map< set
- 使用map:map对象是模板类,需要关键字和存储对象两个模板参数:
std:map<int,string> personnel;
这样就定义了一个用int作为索引,并拥有相关联的指向string的指针。为了使用方便,可以对模板类进行一下类型定义,
typedef map<int,CString> mapc
mapc enumMap; - 函数
begin() 返回指向map头部的迭代器
end() 返回指向map末尾的迭代器
rbegin() 返回一个指向map尾部的逆向迭代器
rend() 返回一个指向map头部的逆向迭代器
lower_bound() 返回键值>=给定元素的第一个位置
upper_bound() 返回键值>给定元素的第一个位置
empty() 如果map为空则返回true
max_size() 返回可以容纳的最大元素个数
size() 返回map中元素的个数
clear() 删除所有元素
count() 返回指定元素出现的次数
equal_range() 返回特殊条目的迭代器对
erase() 删除一个元素
swap() 交换两个map
find() 查找一个元素
get_allocator() 返回map的配置器
insert() 插入元素
key_comp() 返回比较元素key的函数
value_comp() 返回比较元素value的函数
1.5 B-树和B+树
B-树和AVL树区别,其要解决什么问题?
平衡多路查找树,m阶B树表示节点可拥有的最多m个孩子,2-3树是3阶B树,2-3-4树是4阶B树。多叉树可以有效降低树的高度,h=log_m(n),m为log的底数。
- 特点
任意非叶子结点最多只有 M 个儿 子, M>2
根结点的儿子数为 [2, M]
除根结点以外的非叶子结点的儿子数为 [M/2, M]
每个结点存放至少 M/2-1 (向上取整)和至多 M-1 个关键字, M> 2
非叶子结点的关键字个数 = 指向孩子的指针个数 -1 ;
非叶子结点的关键字: K[1], K[2], …, K[M-1] , K[i] < K[i+1]
非叶子结点的指针: P[1], P[2], …, P[M] ;其中 P[1] 指向关键字小于 K[1] 的子树, P[M] 指向关键字大于 K[M-1] 的子树,其它 P[i] 指向关键字属于 (K[i-1], K[i]) 的子树
所有叶子结点位于同一层
在普通平衡二叉树中,插入删除后若不满足平衡条件则进行旋转 操作,而在B树中,插入删除后不满足条件则进行分裂及合并操作。所以,B树并不需要把节点一次性加载到内存,而B树的查找过程是一个顺指针查找节点和节点中查找关键字的交叉过程。
B-树定义。结合数据介绍B-树的插入、删除的操作,尤其是节点的合并、分裂的情况
- 插入
1、如果该结点的关键字个数没有到达m-1个,那么直接插入即可;
2、如果该结点的关键字个数已经到达了m-1个,那么根据B树的性质显然无法满足,需要将其进行分裂。分裂的规则是该结点分成两半,将中间的关键字进行提升,加入到父亲结点中,但是这又可能存在父亲结点也满员的情况,则不得不向上进行回溯,甚至是要对根结点进行分裂,那么整棵树都加了一层。
- 删除
1、如果该结点拥有关键字数量仍然满足B树性质,则不做任何处理;
2、如果该结点在删除关键字以后不满足B树的性质(关键字没有到达ceil(m/2)-1的数量),则需要向兄弟结点借关键字,这有分为兄弟结点的关键字数量是否足够的情况。
(1)如果兄弟结点的关键字足够借给该结点,则过程为将父亲结点的关键字下移,兄弟结点的关键字上移;
(2)如果兄弟结点的关键字在借出去以后也无法满足情况,即之前兄弟结点的关键字的数量为ceil(m/2)-1,借的一方的关键字数量为ceil(m/2)-2的情况,那么我们可以将该结点合并到兄弟结点中,合并之后的子结点数量少了一个,则需要将父亲结点的关键字下放,如果父亲结点不满足性质,则向上回溯;
B+树定义,其要解决问题
B-树的变体,也是一种多路搜索树,与B-树的区别是:
1、非叶子结点的子树指针与关键字个数相同,即n 个 key 值的节点指针域为 n 而不是 n+1
2、非叶子结点的子树指针 P[i] , 指向关键字值属于 [K[i], K[i+1]) 的子树( 左闭右开,B树是全开区间)
为所有叶子结点增加一个链指针
3、B+树的key 的副本存储在内部节点,真正的 key 和 data 存储在叶子节点上 。
- B+树的两个明显特点:
1、数据只出现在叶子节点
2、所有叶子节点增加了一个链指针
1.6 散列查找。
哈希表的设计主要涉及哪几个内容?
-
定义:哈希表又叫散列表,是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
给定表M,存在函数f(key),对任意给定的关键字值key,代入函数后若能得到包含该关键字的记录在表中的地址,则称表M为哈希(Hash)表,函数f(key)为哈希(Hash) 函数 -
基本概念:
若关键字为k,则其值存放在f(k)的存储位置上。由此,不需比较便可直接取得所查记录。称这个对应关系f为散列函数,按这个思想建立的表为散列表。
对不同的关键字可能得到同一散列地址,即k1≠k2,而f(k1)=f(k2),这种现象称为冲突(英语:Collision)。具有相同函数值的关键字对该散列函数来说称做同义词。综上所述,根据散列函数f(k)和处理冲突的方法将一组关键字映射到一个有限的连续的地址集(区间)上,并以关键字在地址集中的“像”作为记录在表中的存储位置,这种表便称为散列表,这一映射过程称为散列造表或散列,所得的存储位置称散列地址。
若对于关键字集合中的任一个关键字,经散列函数映象到地址集合中任何一个地址的概率是相等的,则称此类散列函数为均匀散列函数(Uniform Hash function),这就是使关键字经过散列函数得到一个“随机的地址”,从而减少冲突。
散列函数能使对一个数据序列的访问过程更加迅速有效,通过散列函数,数据元素将被更快地定位。
实际工作中需视不同的情况采用不同的哈希函数,
通常考虑的因素有:
1、计算哈希函数所需时间
2、关键字的长度
3、哈希表的大小
4、关键字的分布情况
5、记录的查找频率 -
哈希函数的构造方法:
1、直接定地址法:以关键字k本身或关键字加上某个常量c作为哈希地址的方法:h(k)=k+c;
2、除留取余法:取关键字被某个不大于散列表表长m的数p除后所得的余数为散列地址。即 H(key) = key MOD p,p<=m。不仅可以对关键字直接取模,也可在折叠、平方取中等运算之后取模。对p的选择很重要,一般取素数或m,若p选的不好,容易产生同义词。p取不大于m的素数时效果最好
此方法计算比较简单,适用范围广,是最经常使用的一种哈希函数
3、数字分析法:分析一组数据,比如一组员工的出生年月日,发现出生年月日的前几位数字大体相同,这样的话,出现冲突的几率就会很大,但是发现年月日的后几位表示月份和具体日期的数字差别很大,如果用后面的数字来构成散列地址,则冲突的几率会明显降低。因此数字分析法就是找出数字的规律,尽可能利用这些数据来构造冲突几率较低的散列地址。
4、平方取中法:当无法确定关键字中哪几位分布较均匀时,可以先求出关键字的平方值,然后按需要取平方值的中间几位作为哈希地址。这是因为:平方后中间几位和关键字中每一位都相关,故不同关键字会以较高的概率产生不同的哈希地址。
5、折叠法:将关键字分割成位数相同的几部分,最后一部分位数可以不同,然后取这几部分的叠加和(去除进位)作为散列地址。数位叠加可以有移位叠加和间界叠加两种方法。移位叠加是将分割后的每一部分的最低位对齐,然后相加;间界叠加是从一端向另一端沿分割界来回折叠,然后对齐相加
6、随机数法:选择一随机函数,取关键字的随机值作为散列地址,即H(key)=random(key)其中random为随机函数,通常用于关键字长度不等的场合。 -
处理冲突
1、开放定址法:
线性探测法:Hi=(H(key) + di) MOD m,i=1,2,…,k(k<=m-1),其中H(key)为散列函数,m为散列表长,di为增量序列,可有下列三种取法:
(1). di=1,2,3,…,m-1,称线性探测再散列;
(2). di=12,-12,22,-22,⑶2,…,±(k)2,(k<=m/2)称二次探测再散列;(平方探测)
(3). di=伪随机数序列,称伪随机探测再散列。
2、拉链法:把所有的同义词用单链表连接起来的方法,所有哈希地址为i的元素对应的节点构成一个单链表,哈希表地址空间为0~m-1,地址为i的单元是一个指向对应单链表的首节点,在者种方法中,哈希表的每个单元中存放的不再是元素本身,而是相应同义词单链表的首结点指针,由于在单链表中可以插入任意多个结点,所以此时填装因子>=1,通常取1
与开放定址法相比的优点:
(1)、拉链发处理冲突简单,且无堆积现象,即非同义词绝不会发生冲突,因此平均查找长度较短
(2)、由于拉链法中各单链表上的结点空间是动态申请的,所以它更适用于造表前无法确定的表的长度
(3)、开放定址法为为减少冲突要求填装因子a较小,故当数据规模较大时会浪费很多空间,而拉链法中可取a>=1;且元素较大时拉链法中增加的指针域可忽略不计,因此节省空间
(4)、在用拉链法构造的哈希表中,删除结点的操作更加易于实现。
缺点:
指针需要额外的空间,故当数据规模较小时,开放定址法较为节省空间,若将节省的空间指针用来扩大哈希表规模,可使填装因子变小,这又减少了开放定址法中的冲突,从而提高了平均查找速度 -
结合数据介绍哈希表的构造及ASL成功、不成功的计算:
结合数据介绍哈希链的构造及ASL成功、不成功的计算
2.PTA题目介绍(0--5分)
介绍3题PTA题目
2.1 是否完全二叉搜索树(2分)
本题务必结合完全二叉搜索树经过层次遍历后在队列的特点去设计实现。结合图形介绍。
如上图所示,由完全二叉搜索树的特点可知,若其经过中序遍历后存入队列后,中间数据不应该会有空,是一个完整的队列。所以由此可知,上图所示的树不是一棵完全二叉搜索树。
由刚才的分析可知,上图所示的树是一个完全二叉搜索树
2.1.1 伪代码(贴代码,本题0分)
伪代码为思路总结,不是简单翻译代码。
//层次遍历
当结点T不为空时
q.push(T)
while 队列q不为空时
{
p=q.front();
出队
输出p->data
if p->left
then 左孩子入队
if p->right
then 右孩子入队
}
//判断
当T结点不空时,q.push(T);
while T!=NULL
{
取队头元素;出队;
if T不为空
then 将左右孩子入队
end if
while 队头不为空
{
取队头并判断后一个元素是否为空
出队
若后一个为空,则不是完全二叉搜索树
否则时完全二叉搜索树
}
}
2.1.2 提交列表
2.1.3 本题知识点
1、使用了队列来存储树中的结点
2、入队列适用q.push();
3/取队头元素使用q.front();
4、将队列元素弹出使用q.pop();
5、采用层次遍历
2.2 航空公司VIP客户查询(2分)
2.2.1 伪代码(贴代码,本题0分)
伪代码为思路总结,不是简单翻译代码。
map<string, int>M;
for i=0 to n-1 i++
输入身份证,已行驶里程数据
if 里程小于最小里程k
then 让里程=k
end if
if 该身份证已由会员记录
then 原来的里程数再加上刚才输入的(M[id1]+=len)
end if
else 未被登记为会员
则现在输入身份证及里程信息(M[id1]=len)
end for
for i=0 to m-1 i++
输入身份证
若信息库(M)里由=有该身份证信息就输出里程信息
若信息库M中未找到该身份证信息
则输出“NO INFO
2.2.2 提交列表
本题因为将map数组也定义成了m导致两个不同的变量用了同一个名字,计算机无法分辨
后面更改一下数组名就行了
2.2.3 本题知识点
运用了map库里的函数
map<string,int>M
string为M数组的下标,int为数组里的数据类型
max(a,b)函数为求a b中的较大值
M.count(id)函数为求数组M中下标为id的数据是否存在
M中的数据不会重复存在,下标为某一确定值时,数据元素也只有一个确定的值
本题使用map里的函数会相对简单,因为身份证号码每个人都是唯一的,所以对于数据的记录较为简单,对于代码方面也简洁,更容易让人读懂
本题与7-3QQ账号注册查询类似
2.3 基于词频的文件相似度(1分)
本题设计一个倒排索引表结构实现(参考课件)。单词作为关键字。本题可结合多个stl容器编程实现,如map容器做关键字保存。每个单词对应的文档列表可以结合vector容器、list容器实现。
2.3.1 伪代码(贴代码,本题0分)
伪代码为思路总结,不是简单翻译代码。
for i=1 to N i++
while 读取一行的内容,当内容为#就停止循环
令top=0;
for j=0 to s.length() j++
if 内容不是字母即为分割符
then if 单词长度小于3 则不记录
end if
continue
end if
if 单词长度大于10
then 值记录前10
end if
else 加结束符 单词数+1
top=0
end if
else 是字母,大写该小写
需要队组后一个单词判断
计算相似度
for i=0 to m-1 i++
输入两个单词 x y
map<string,int>::iterator it1,it2;//两个迭代器,类似指针
分子,分母=两个文件所有单词-相同的
用for 循环队两个文件进行遍历
if (it1->first<it2->first)it1++;//it1字母小了, 向后移动
else if it2字母小了, 向后移动
else 一样
同时移动
2.3.2 提交列表
2.3.3 本题知识点
使用了map函数
string s 字符串型变量
利用getchar()吸收回车
map<string,int>::iterator it1,it2;//两个迭代器,类似指针