查找
| 这个作业属于哪个班级 | 数据结构--网络2011/2012 |
| ---- | ---- | ---- |
| 这个作业的地址 | DS博客作业05--查找 |
| 这个作业的目标 | 学习查找的相关结构 |
|姓名|喻文康|
0.PTA得分截图
1.本周学习总结(0-5分)
1.1 查找的性能指标
ASL成功、不成功,比较次数,移动次数、时间复杂度
ASL:查找算法的查找成功时的平均查找长度的缩写,是为确定记录在查找表中的位置,需和给定值进行比较的关键字个数的期望值。
P(i)=查找表中第i个记录的概率
C(i)=关键字与给定值相等的第i个记录时,和给定值 进行过比较的关键字个数。
用于静态查找表中顺序表的查找
对于含有n个记录的表,查找成功时的平均查找长度为
ASL成功(待查找的数字肯定在散列表中才会查找成功)
查找数字A的长度 = 需要和散列表中的数比较的次数;
ASL不成功(待查找的数字肯定不在散列表中)
【解题的关键之处】根据哈希函数地址为MOD7,因此任何一个数经散列函数计算以后的初始地址只可能在0~6的位置
比较次数,移动次数
对于一组数据当它以哈希表,链表,二叉搜索树储存时,关键字的搜索就要通过将关键字与其中的元素进行比较才能找到
哈希表因会有冲突导致关键字存储位置不是哈希函数所对应的位置,此时的比较次数就会增加
链表因会有冲突,导致关键字不是某一条链表的第一个元素,此时要找到关键字任然要比较,即比较次数要增加
时间复杂度
顺序查找的时间复杂度:O(n)
折半查找(二分查找)的时间复杂度:O(log2 n),虽然找的高效,但关键字的排序需要是有序的(即适用于顺序表)
二叉排序树的时间复杂度:最好的情况下O(log2 n)与折半查找相似,最坏的情况O(n)
平衡二叉树的时间复杂度:O(log2 n)
1.2 静态查找
-
顺序查找
在顺序查找(Sequence Search)表中,查找方式为从头扫到尾,找到待查找元素即查找成功,
若到尾部没有找到,说明查找失败。所以说,Ci(第i个元素的比较次数)在于这个元素在查
找表中的位置,如第0号元素就需要比较一次,第一号元素比较2次......第n号元素要比较
n+1次。所以Ci=i;所以
可以看出,顺序查找方法查找成功的平均 比较次数约为表长的一半。当待查找元素不在查找
表中时,也就是扫描整个表都没有找到,即比较了n次,查找失败
-
二分查找(也叫折半查找)
首先待查找表是有序表,这是折半查找的要求。在折半查找中,用二叉树描述查找过程,查
找区间中间位置作为根,左子表为左子树,右子表为右子树,,因为这颗树也被成为判定树
(decision tree)或比较树(Comparison tree)。查找方式为(找k),先与树根结点进行比
较,若k小于根,则转向左子树继续比较,若k大于根,则转向右子树,递归进行上述过程,
直到查找成功或查找失败。在n个元素的折半查找判定树中,由于关键字序列是用树构建的,
所以查找路径实际为树中从根节点到被查结点的一条路径,因为比较次数刚好为该元素在树中
的层数。所以Pi为查找k的概率,level(Ki)为k对应内部
结点的层次。而在这样的判定树中,会有n+!种查找失败的情况,因为将判定树构建为完全二
叉树,又有n+1个外部结点(用Ei(0<=i<=n)表示),查找失败,即为从根结点到某个外部结点
也没有找到,比较次数为该内部结点的结点数个数之和,所以,
qi表示查找属于Ei中关键字的概率,level(Ui)表示Ei对应外部结点的层次。所以,在一颗有
n个结点判定树中,总数,所以判定树高度为的满二叉树,
第i层上结点个数为,查找该层上的结点需要进行i次比较,因此,在等概率情况下ASL为
1.3 二叉搜索树
1.3.1 如何构建二叉搜索树(操作)
如何在二叉搜索树做插入、删除。
二叉排序树就纯粹就是序号的排序,然后把序号排成二叉形式的排序,排序多之就像树叉,所以叫二叉排序树。
二叉构造排序树:①首先先要插入法构造, 先插入数字6用圈圈起来,
②第二个结点 4 比 6 来的小 所以插入在 6 的左子树。
那么接下来找第三个结点 8 比 6 来的大 所以就要插入在 6 的右子树,
第四个结点 5 比6 来得小 先进入左子树然度后跟 4比较专5 比4 大 所以插入在 4 的右子树。
以此类推 排序最后的二叉排序树读出来就是:中序遍历就是 【34 5 6 7 8 9】, 那先序遍历就是:【 6 4 3 5 8 7 9 】。
二叉搜索树做插入
新插入的结点一定是一个新添加的叶子结点,如下图
虽然上面两种插入结果得到的二叉树都符合二叉查找树的性质,但是不满足“新插入的结点
一定是一个新添加的叶子结点”,因为有了这个特点插入的操作变得相对简单
二叉搜索树做删除
删除节点的操作相对查找和插入要相对复杂一些,主要考虑以下三种情况(前两种情况操作较为简单,第三种较为复杂)
在删除操作前先用要找到待删除节点的位置(这里使用的递归,也可以改成迭代)
情形一:删除叶子节点
因为删除叶子节点不会破坏BST的结构,删除叶子节点的操作较为简单,步骤如下
1、判断待删除节点的左右子树是否为空,如果都为空那么就是叶子节点
2、判断待删除节点是待删除节点父节点的右子树还是左子树,将对应的指针赋值NULL
3、free待删除节点
情形二:删除带有一个子节点的节点
上图写了四种,但对待删除节点来说只有两种,只有左子树,或只有右子树,两种情况
的处理方式基本相同,都是将待删除节点的左/右子树 赋值给 待删除节点的父节点的左/右子树
情形三:删除带两个节点的节点
因为删除节点会有破坏 BST 正确结构的风险,删除带两个节点的节点操作显得较为复杂,首
先需要找到待删除节点的 后继节点 和 该后继节点的父节点,(一个节点的后继节点是指,这
个节点在中序遍历序列中的下一个节点,相应的,前驱节点是指这个节点在中序遍历序列中的上
一个节点),删除节点的后继节点一定是删除节点右子树的最左侧节点,这篇随笔采用的方式是
后继节点替代待删除节点的方式而不是前驱节点替代删除节点,需要考虑的情况如下
1、后继节点为待删除节点的子节点
在后继节点为待删除节点的子节点的前提下,该后继节点有右子树和没有右子树的操作是相同的,
都是将 后继节点 替代 待删除节点,并将待删除节点的左子树 赋值给 后继节点的左子树
2、后继节点不为待删除节点的子节点
这里后继节点还要在分为后继节点有子节点和没有子节点的情况
(1)后继节点没有右子节点
删除后:
(2)后继节点有右子节点
删除后:
与上面的后继节点没有右子节点相比需要增加一个操作,需要将后继节点的右子树 赋值给 后继节
点的父节点的左子树
1.3.2 如何构建二叉搜索树(代码)
1.二叉搜索树的创建
BSTNode* CreatBST(KeyType A[], int n)//返回树根指针
{
BSTNode* bt = NULL;//初始时bt为空树
int i = 0;
while (i < n)
{
InsertBST(bt, A[i]);//将结点A[i]插入树中
i++;
}
return bt;//返回建立的二叉排序树的根指针
}
1.二叉搜索树的插入
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 == bt->key)//二者相等,已存在,不用插入
return 0;
else if (k < p->key)
return InsertBST(p->lchild, k);//插入左子树
else return InsertBST(p->rchild, k);//插入右子树
}
1.二叉搜索树的删除
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);//删除结点
return 1;
}
}
}
//从二叉树排序树中删除结点p
void Delete(BSTree& p)
{
BSTNode* q;
if (p->rchild == NULL)//被删除结点没有右子树的情况
{
q = p;
p = p->lchild;
delete q;
}
else if (p->lchild == NULL)//被删除结点没有左子树的情况
{
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;
}
}
2.分析代码的时间复杂度
二叉树:插入:O(n);查找:O(n);删除:O(n)
二叉平衡树:插入:O(log2 n);查找:O(log2 n);删除:O(log2 n)
3.为什么要用递归实现插入、删除?递归优势体现在代码哪里?
插入:与顺序表比,不需要大量移动数组;与链表比,不需要更改指针关系,不需要考虑头插法和尾插法
删除:递归查找才能把父子关系保留
优势:
1、递归的利用可以使算法或数据结构大大简化,代码简洁明了,
2、相同一个具有该特性的课题采用递归或其他算法,所要求的预定义及相应的结果都将不一样,
3、用了递归可能使用减少部份定义,代码实现部份大大减少
1.4 AVL树
AVL是自平衡二叉搜索树,满足两个条件,第一个条件是:本身是二叉搜索树;第二个条件是,
所有结点的左右子树高度差不超过1。
平衡二叉树(AVL树)在符合二叉查找树的条件下,还满足任何节点的两个子树的高度最大差为1。
下面的两张图片,左边是AVL树,它的任何节点的两个子树的高度差<=1;右边的不是AVL树,其根
节点的左子树高度为3,而右子树高度为1;
如果在AVL树中进行插入或删除节点,可能导致AVL树失去平衡,这种失去平衡的二叉树可以概括为
四种姿态:LL(左左)、RR(右右)、LR(左右)、RL(右左)。它们的示意图如下:
这四种失去平衡的姿态都有各自的定义:
-
LL:LeftLeft,也称“左左”。插入或删除一个节点后,根节点的左孩子(Left Child)的左孩子
(Left Child)还有非空节点,导致根节点的左子树高度比右子树高度高2,AVL树失去平衡。
-
RR:RightRight,也称“右右”。插入或删除一个节点后,根节点的右孩子(Right Child)的右
孩子(Right Child)还有非空节点,导致根节点的右子树高度比左子树高度高2,AVL树失去平衡。
-
LR:LeftRight,也称“左右”。插入或删除一个节点后,根节点的左孩子(Left Child)的右孩
子(Right Child)还有非空节点,导致根节点的左子树高度比右子树高度高2,AVL树失去平衡。
-
RL:RightLeft,也称“右左”。插入或删除一个节点后,根节点的右孩子(Right Child)的左孩
子(Left Child)还有非空节点,导致根节点的右子树高度比左子树高度高2,AVL树失去平衡。
AVL树的高度和树的总节点数n的关系
高度为 h 的 AVL 树,节点数 N 最多2^h − 1; 最少N(h)=N(h− 1) +N(h− 2) + 1
节点最多的时候是满二叉树,如果认为第一层的高度为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 ,string> a,而且键和值都是唯一的。
使用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+树
-
区别:平衡二叉树(AVL树):是一种二叉排序树(BST),其中每个节点的左子树和
右子树的高度差至多等于1.也就是说AVL树的平衡因子只可能是1,0,-1。
多路查找树(B-树):是一种平衡的多路查找树,2-3树和2-3-4树都是B树的特例。节
点最大的孩子称为B-树的阶(order),因此,2-3树是3阶B树,2-3-4树是4阶B树。 -
B-树的定义
节点的孩子节点的最大数称为阶用m表示
所有的叶子节点在同一层,并且不带信息
每个节点最多含有m颗子树,最多含有m-1个关键字
根节点不是终端节点那么根节点至少有两个子树
除根节点以外其他非叶子节点至少有m/2向上取整个子树
每个非叶子节点的结构为:n,p0,k1,p1,k2,p2,k3,p3…kn,pn
其中n为关键字个数 m/2-1<n<m-1,ki为关键字ki<ki+1,pi为该节点的孩子节点的指针
pi所指的节点的、关键字大于等于ki小于ki+1,pn所指的关键字大于kn。
插入
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+树(m阶)
每个分支至多有m棵子树
根节点没有子树或者至少有两颗
除了根节点其他节点至少有m/2向上取整个子
有n棵子树的节点有n个关键字
叶子节点包含全部关键字,以及只想相应记录的指针,而叶子节点按关键字大小顺序链接( 每个叶子节点可以看成
是一个基本索引块,它的指针指向数据文件中的记录)
所有分支节点(可以看成是索引的索引)中仅仅包含他的各个子节点中最大关键字以及指向子结点的指针。
解决问题:
B+树的磁盘读写代价更低。B+树的内部结点并没有指向关键字具体信息的指针,其内部结点比B树小,盘块能容纳
的结点中关键字数量更多,一次性读入内存中可以查找的关键字也就越多,相对的,IO读写次数也就降低了。而IO
读写次数是影响索引检索效率的最大因素。B+树的查询效率更加稳定。
1.6 散列查找。
-
哈希表又称散列表,其基本思路是设要存储的元素个数为n,设置一个长度为m的连续内存单元,以每个元素的关键
字ki(i取值为0<=i<=n-1)为自变量,通过一个称为哈希函数的函数h(ki),把ki映射为内存单元的地址(或下标)
h(ki),并把该元素存储在这个内存单元中,h(ki)也称为哈希地址。把如此构造的线性表存储结构称为哈希表。哈
希表是除顺序表存储结构,链接表存储结构和索引表存储结构之外的又一种存储线性表的存储结构。
比方说
将关键字序列(7、8、30、11、18、9、14)散列存储到散列表中。H(key) = (keyx3) MOD 7。装填(载)因子为0.7
哈希表:
查找成功的平均查找长度= (1+1+1+1+3+3+2)/7 = 12/7
查找不成功的平均查找长度 = (3+2+1+2+1+5+4)/7 = 18/7
哈希链:
查找成功的平均查找长度= (14+23)/7=10/7
查找不成功的平均查找长度 = (11+23)/7=1 -
哈希冲突的解决
开放地址法
开放地址法就是在出现哈希冲突时,再哈希表中找到一个新的空闲位置存放元素,根据开放地址法找空闲单元的方式又分
为线性探测法和平方探测法等。
线性探测法
线性探测法是从发生冲突的地址(设为d0)开始,依次探测d0的下一个地址(当到达下标为m-1的哈希表表尾时,下一个探
测地址为表首地址0),直到找到一个空闲单元为止(当m>=n时一定能够找到一个空闲单元)。
线性探测法的数学递推描述公式为:d0=h(k) di=[d(i-1)+1] mod m (1<=i<=m-1) ,其中模m是为了保证找到的位置
在0~m-1的有效空间中。
2.PTA题目介绍(0--5分)
2.1 是否完全二叉搜索树(2分)
2.1.1 伪代码(贴代码,本题0分)
定义一个二叉树T;
定义一个队列Q;
定义数组A;
If(树为空)返回真 end if;
定义节点p且指向树根节点;
初始化队列;
p入队;
While(队列不为空){
出队;
对头元素赋值给p;
If(p为空) break;end if
p的左子树入队;
p的右子树入队;
}end
While(队列不为空){
出队;
队头指针指向p;
数值赋给数组A;
If(p)为空 返回错误;end if
}
If(数组为升序)
返回真;
Else
返回假;
End if
2.1.2 提交列表
2.1.3 本题知识点
- 生成二叉搜索树及二叉搜索树结点的插入
- 二叉树的层次遍历
- 判断是否完全二叉树的方法
可根据二叉树的层次遍历至空结点后,后面全为空结点,不再出现数字结点的特性来判断是否完全二叉树
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 提交列表
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分)
2.3.1 伪代码(贴代码,本题0分)
int main()
{
使用map库定义单词索引表Word_table和容器迭代器iterator;
/*存储数据*/
for i = 1 to 文件总数 num
输入字符串str;
/*分割字符串,处理单词*/
while str 不等于 "#"
遍历字符串
if 该字符是字母
if 组合单词word长度小于10
加入组合word组成完整单词;
else
if word长度不小于3
加入map容器单词索引表:Word_table[word][i] = 1;
清空字符串
end while
再次输入新字符串str;
end for;
/*计算相似度*/
for i = 0 to 查询总数M
输入文件编号file1,file2; 单词总数sum = 0,相同单词数count = 0;
使用容器迭代器遍历Word_table中所有单词
if 两个文件都出现
sum加一,count加一;
else if 出现一个文件
sum加一;
计算相似度count除以sum并输出;
end for;
}
2.3.2 提交列表
2.3.3 本题知识点
map函数的使用
string函数的使用
容器迭代器的使用