DS博客作业05--查找
这个作业属于哪个班级 | 数据结构--网络2011/2012 |
---|---|
这个作业的地址 | DS博客作业05--查找 |
这个作业的目标 | 学习查找的相关结构 |
姓名 | 王历 |
0.PTA得分截图
1.本周学习总结
1.1 查找的性能指标
ASL成功:
平均需要和给定值k进行比较的关键字次数称为平均查找长度(ASL)
ASL=∑PiCi(i=1~n)
n是查找表中元素个数,
Pi是查找第i个元素的概率(通常假设每个元素的查找概率相等,此时Pi=1/n)
Ci是找到第i个元素所需要的关键字比较次数
一个查找算法的ASL越大,其时间性能越差,反之,若越小其时间性能越好
ASL不成功:
没有找到查找表中的元素,平均需要关键字比较次数
比较次数,移动次数:
对于一组数据当它以哈希表,链表,二叉搜索树储存时,关键字的搜索就要通过将关键字与其中的元素进行比较才能找到
哈希表因会有冲突导致关键字存储位置不是哈希函数所对应的位置,此时的比较次数就会增加
链表因会有冲突,导致关键字不是某一条链表的第一个元素,此时要找到关键字任然要比较,即比较次数要增加
时间复杂度:
顺序查找的时间复杂度:O(n)
折半查找(二分查找)的时间复杂度:O(log2 n),虽然找的高效,但关键字的排序需要是有序的(即适用于顺序表)
二叉排序树的时间复杂度:最好的情况下O(log2 n)与折半查找相似,最坏的情况O(n)
平衡二叉树的时间复杂度:O(log2 n)
1.2 静态查找
静态查找是“真正的查找”。因为在静态查找过程中仅仅是执行“查找”的操作,即查看某特定的关键字是否在表中(判断性查找);检索某特定关键字数据元素的各种属性(检索性查找)。这两种操作都只是获取已经存在的一个表中的数据信息,不对表的数据元素和结构进行任何改变。
- 顺序查找:
顺序查找是按照序列原有顺序对数组进行遍历比较查询的基本查找算法。 - 结构体:
typedef int KeyType;
typedef char* InfoType;
typedef struct node
{
KeyType key;
InfoType data;
} Node[MaxSize];
代码:
int Search(Node B, int n, KeyType k)//在表中查找关键字k,找不到返回-1,找到返回查找地址。n是表中元素个数
{
int i = 0;
while (i < n && B[i].key != k)/*按下标顺序找,直到找到k*/
{
i++;/*不是k则i加一*/
}
if (i >= n)
{
return -1;/*找不到则返回-1*/
}
else
{
return i+1;/*找到的时候不进入循环,所以i需要再增1*/
}
}
- 时间复杂度为:O(n)
- 缺点:查找效率较低,特别是当待查找集合中元素较多时,不推荐使用顺序查找。
- 优点:算法简单而且使用面广。
- 二分查找:
二分查找也称折半查找,它是一种效率较高的查找方法。但是,折半查找要求线性表必须采用顺序存储结构,而且表中元素按关键字有序排列。
代码:
int BinSearch1(SeqList R, int low, int high, KeyType k)
{
int mid;
if (low <= high) //查找区间存在一个及以上元素
{
mid = (low + high) / 2; //求中间位置
if (R[mid].key == k) //查找成功返回其逻辑序号mid+1
return mid + 1;
if (R[mid].key > k) //在R[low..mid-1]中递归查找
BinSearch1(R, low, mid - 1, k);
else //在R[mid+1..high]中递归查找
BinSearch1(R, mid + 1, high, k);
}
else
return 0;
}
- 时间复杂度为:O(logn)
- 优点:折半查找的时间复杂度为O(logn),远远好于顺序查找的O(n)。
- 缺点:虽然二分查找的效率高,但是要将表按关键字排序。而排序本身是一种很费时的运算。既使采用高效率的排序方法也要花费O(nlgn)的时间。
1.3 二叉搜索树
1.3.1 如何构建二叉搜索树(操作)
插入
-
在二叉排序树中插入一个关键字为k的结点要保证插入后仍然满足BST性质,其插入过程为:
-
若二叉排序树bt为空,则创建一个key域作为k的结点,将它作为根结点;
-
若不为空,则与根节点的关键字作比较
-
若相等,则说明树中已经存在该关键字,无需插入,直接返回;
-
若比根节点的值小,就进入根节点的左子树中,否则进入右子树;
-
该结点插入后一定为叶子结点
删除 -
情况1:p节点为叶子结点,直接删去该结点,双亲节点相应指针域修改为NULL;
-
情况2:p结点只有左/右子树,有其左子树或者右子树代替他,双亲节点相应的指针域修改;
-
情况3:p结点既有左子树也有右子树。根据二叉排序树的特点,可以从其左子树中选择关键字最大的结点r(最右结点),用结点r的值代替结点p的值(结点值替换),保存r结点的左孩子,再删去r结点。也可以从右子树中选择关键字最小的结点l(最左结点),用结点l的值代替结点p的值(结点替换),保存l结点的右孩子,再删去结点l。
查找 -
递归查找,查找思路和普通二叉树不同,因为二叉搜索树的左子树中所有值都比根节点小,右子树中所有值都比根节点大,根据这一特点,我们可以将待查找数据和当前的根节点进行比较,如果待查找数据比较小,就有选择的进入左子树进行查找,不用像之前普通二叉树的查找一样,不仅要进左子树还要进右子树中查找。
1.3.2 如何构建二叉搜索树(代码)
结构体:
typedef struct node
{
KeyType key;
InfoType data;
struct node*lchild,*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->key0
return 0;
else if(k<p->key)
return InsertBST(p->lchild,k);
else
return InsertBST(p->rchild,k);
}
构建:
BSTNode *Creat(KeyType A[],int n)
{
BSTNode *bt=NULL;
int i=0;
while(i<n)
{
InsertBST(bt,A[i]);
i++;
}
return bt;
}
删除:
int DeleteBST(BSTree &bt,KeyType k)
{
if(bt==NULL)
return 0;
else
{
if(k<p->key)
return DeleteBST(bt->lchild,k);
else if(k>bt->key0
return DeleteBST(bt->rchild,k);
else
{
Delete(bt);
return 1;
}
}
}
int DeleteBST(BSTree &bt,KeyType k)
{
if(bt==NULL)
return 0;
else
{
if(k<p->key)
return DeleteBST(bt->lchild,k);
else if(k>bt->key0
return DeleteBST(bt->rchild,k);
else
{
Delete(bt);
return 1;
}
}
}
- 时间复杂度:O(n);递归是为了保存父子关系。
1.4 AVL树
在计算机科学中,AVL树是最先发明的自平衡二叉查找树。在AVL树中任何节点的两个子树的高度最大差别为一,所以它也被称为高度平衡树。查找、插入和删除在平均和最坏情况下都是O(log n)。增加和删除可能需要通过一次或多次树旋转来重新平衡这个树。
特点
由于二叉搜索树存在数据分配不平衡的现象,会导致查找时的时间复杂度提高,所以诞生了AVL解决此类问题。
AVL树本质上还是一棵二叉搜索树,它的特点是:
1.本身首先是一棵二叉搜索树。
2.带有平衡条件:每个结点的左右子树的高度之差的绝对值(平衡因子)最多为1。
也就是说,AVL树,本质上是带了平衡功能的二叉查找树(二叉排序树,二叉搜索树)。
结合一组数组,介绍AVL树的4种调整做法。
这四种失去平衡的姿态都有各自的定义:
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=log2(N(h)+1)
时间复杂度:平衡二叉树上最好最坏进行查找关键字的比较次数不会超过平衡二叉树的深度--时间复杂度为 O(log2n)
- 介绍基于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,而且键和值都是唯一的。
头文件:
#include <map>
函数:
begin() 返回指向 map 头部的迭代器
clear() 删除所有元素
count() 返回指定元素出现的次数
empty() 如果 map 为空则返回 true
end() 返回指向 map 末尾的迭代器
erase() 删除一个元素
find() 查找一个元素
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+树
B-树
B-树一个节点可以放多个关键字,降低树的高度。可放外存,适合大数据量查找,一棵m阶B-树符合以下特性:
- 每个结点至多m个孩子结点,至多有m-1个关键字,除根节点外,其他节点至少有m/2个孩子结点,至少有m/2-1个关键字;
- 若根节点不是叶子结点,根节点至少两个孩子结点;
- 叶子结点一定都在最后一层,叶子结点下面还有一层是虚设的外部结点————就是表示查找失败的结点,不含任何信息,在计算B-树高度时要计入最底层的外部结点;
- 结构体定义:
#define MAXM 10
typedef node* BNode;
typedef struct node
{
int keynum;
int key[MAXM];
BNode parent;
BNode ptr[MAXM];
}BTNode;
int m;
int MAX;
int MIN;
B-树和AVL树区别
AVL树结点仅能存放一个关键字,树的敢赌较高,而B-树的一个结点可以存放多个关键字,降低了树的高度,可以解决大数据下的查找
B-树定义
B-tree树即B树,B即Balanced,平衡的意思。因为B树的原英文名称为B-tree,而国内很多人喜欢把B-tree译作B-树,其实,这是个非常不好的直译,很容易让人产生误解。如人们可能会以为B-树是一种树,而B树又是另一种树。而事实上是,B-tree就是指的B树。特此说明。
插入:
(1)如果该结点的关键字个数没有到达2个,那么直接插入即可;
(2)如果该结点的关键字个数已经到达了2个,那么根据B树的性质显然无法满足,需要将其进行分裂
删除:
首先需要明确一点:删除非叶子结点必然会导致不满足B树性质
那么可以这样处理:被删关键字为该结点中第i个关键字key[i],则可从指针son[i]所指的子树中找出最小关键字Y,代替key[i]的位置,然后在叶结点中删去Y。 因此,把在非叶结点删除关键字k的问题就变成了删除叶子结点中的关键字的问题了,
那么B树的删除操作就变成了删除叶子结点中的关键字问题了。
(1)被删关键字Ki所在结点的关键字数目不小于ceil(m/2),则只需从结点中删除Ki和相应指针Ai,树的其它部分不变
(2)被删关键字Ki所在结点的关键字数目等于ceil(m/2)-1,则需调整。
(3)被删关键字Ki所在结点和其相邻兄弟结点中的的关键字数目均等于ceil(m/2)-1,假设该结点有右兄弟,且其右兄弟结点地址由其双亲结点指针Ai所指。则在删除关键字之后,它所在结点的剩余关键字和指针,加上双亲结点中的关键字Ki一起,合并到Ai所指兄弟结点中(若无右兄弟,则合并到左兄弟结点中)。如果因此使双亲结点中的关键字数目少于ceil(m/2)-1,则依次类推。
B+树
一棵m阶b+树满足条件:
- 每个分支节点至多有m棵子树;
- 根结点或者没有子树,或者至少两棵子树;
- 除根结点,其他每个分支结点至少有m/2棵子树;
- 有n棵子树的结点有n个关键字;
- 所有叶子结点包含全部关键字及指向相应记录的指针;叶子结点按关键字大小顺序链接;叶子结点时直接指向数据文件的记录;
- 所有分支结点(可以看成是分块索引的索引表),包含子节点最大关键字及指向子节点的指针;
1.6 散列查找
哈希表
根据设定的哈希函数 H(key)和所选中的处理冲突的方法,将一组关键字映射到一个有限的、地址连续的地址集 (区间) 上,并以关键字在地址集中的“映像”作为相应记录在表中的存储位置,如此构造所得的查找表称之为“哈希表”.
比方说
将关键字序列(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
2.PTA题目介绍
2.1 是否完全二叉搜索树
2.1.1 伪代码
void InserBST(BinTree*& BST,KeyType k)//往二叉树中插入结点k
{
if(BST==NULL) 建立新结点;
else if(k<BST->key) 递归进入右子树;
else 递归进入左子树
}
/*层次遍历二叉树并判断是否为完全二叉树*/
bool Level_DispBST(BinTree* BST)
{
定义变量end表示是否已经找到第一个叶子结点或者第一个只有左孩子结点;
(如果是,则接下来未遍历的结点应该都是叶子结点)
定义flag保存判断结果;
若根结点BST不为空,入队,并输出;
while(队不为空)
出队一个结点p;
若结点p的左,右孩子不为空,依次输出,并入队;
若在剩余节点都应该是叶子结点的情况下p为非叶子节点
不是完全二叉树,flag=flase;
若结点p只有右孩子,没有左孩子
不是完全二叉树,flag=flase;
若结点p为叶子结点或者为第一个只有左孩子的结点
修改end,表示接下来应该都是叶子结点;
end while
return flag;
}
2.1.2 本题知识点
- 完全二叉树的结构
- 二叉树的层次遍历
- 二叉搜索树的插入操作
2.2 航空公司VIP客户查询
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库里的函数
map<string,int>M
string为M数组的下标,int为数组里的数据类型
max(a,b)函数为求a b中的较大值
M.count(id)函数为求数组M中下标为id的数据是否存在
M中的数据不会重复存在,下标为某一确定值时,数据元素也只有一个确定的值
本题使用map里的函数会相对简单,因为身份证号码每个人都是唯一的,所以对于数据的记录较为简单,对于代码方面也简洁,更容易让人读懂
2.3 基于词频的文件相似度
2.3.1 伪代码
int main()
{
定义一个文件列表list保存所有文件;
初始化文件列表;
for i=0 to n
提取每个文件的内容;
end for
for i=0 to m
比较两个文件的单词数量大小,单词数量小的作为模板,计算两文件的重复率;
输出结果;
end for
}
void InserFile(File& F,HashNode* word)//根据单词的首字母插入文件的相对应位置
{
计算该单词应该插入的位置:ard = word->key[0]-'A';
遍历单链F.content[ard]上的单词
若有重复内容 return;
若遍历完毕没有找到重复内容
头插法插入,并使该文件的单词数量+1;
}
void GetFile(FileTable& list, int number)//获取每个文件内容,且将每个小写字母都转换为大写
{
while(1)
若str为'#' ,说明已经到文件内容末尾,break;
while(未遍历到字符串str末尾)
开辟新空间,用于保存新单词word;
while(为字母)
if 单词长度小于10,可以继续加入字母
若为小写字母:word->key += (str[i++]-'a'+'A');
若为大写字母:word->key += str[i++];
else
直接舍弃,i++;
end while
若单词长度小于3,则为不合法单词,舍弃:delete word;
i++;//跳过非字母字符;
end while
end while
}
double File_Similarity(File*file1,File*file2)//计算两文件的重复率
{
定义count保存模板file1的单词个数
定义same记录两文件重复的单词数量,all保存两文件总的单词个数;
for i=0 to 26 //遍历模板的哈希表
p=file1->countent[i].next;//提取其中一个单链表遍历
while(p不为空)
count--;//表示模板中的有一个单词已经遍历过
遍历另一个文件中相对应的位置总寻找是否存在和p相同的单词
若存在 same++;
p=p->next;//遍历单链中下一个单词;
end while
if count==0
表示模板文件中所有单词都遍历完 ,提前退出对模板的遍历,break;
end if
end for
}
2.3.2 本题知识点
- 倒排索引表的构建:由于本题不需要考虑一个文档中存在多个重复单词,所以只需要用set集合来存放文档数据即可;否则可以定义一个二维数组string a[][]或动态数组vettora[]来存放文档信息。
对于字母与非字母的判断:如果是C语言的话,可以在头文件中加入<ctype.h>,如果是C++的话就在头文件中加入,然后用库中的函数isalpha即可快速判断字符是不是字母。 - 对于相同单词不同大小写的情况,这里可以统一转换为大写或小写,方便后续判断。
- 单词的提取:要从主串中提取子串单词,可以使用string库中的substr(子串起始位置,子串结尾位置)来提取单词,但需要知道单词的首字母位置和尾字母位置,所以需要加一条循环语句来找单词的尾字母位置。
- 对于相似单词的判断:可以用string库中的count函数来判断该单词在文档中出现过几次。因为本题不考虑一个文档中存在多个重复单词,所以doc[q].count[it]只会有两个值,如果是1的话说明文档q中存在单词it;如果是0的话说明文档q中不存在单词it。