查找
0.PTA得分截图
1.本周学习总结(0-5分)
1.1 查找的性能指标
ASL成功、不成功,比较次数,移动次数、时间复杂度
- ASL(Average Search Length),即平均查找长度,在查找运算中,由于所费时间在关键字的比较上,所以把平均需要和待查找值比较的关键字次数称为平均查找长度。
- ASL分为查找成功情况下的ASL成功和查找不成功情况下的ASL不成功
- ASL成功表示成功查找到查找表中的元素,平均需要关键字比较次数
- ASL不成功表示没有找到查找表中的元素,平均需要关键字比较次数
1.2 静态查找
分析静态查找几种算法包括:顺序查找、二分查找的成功ASL和不成功ASL。
- 顺序查找
顺序查找是一种最简单的查找方法,它的基本思路是从表的一端向另一端逐个将元素的关键字和给定值k比较,若相等,则查找成功,给出该元素在查找表中的位置;若整个查找表扫描结束后仍未找到关键字等于k的元素,则查找失败。
所以说,Ci(第i个元素的比较次数)在于这个元素在查找表中的位置,如第0号元素就需要比较一次,第一号元素比较2次......第n号元素要比较n+1次。
当待查找元素不在查找表中时,也就是扫描整个表都没有找到,即比较了n次,查找失败
查找代码:
in SeqSearch(RecType R[], int n, KeyType k)
{
int i = 0;
while (i < n && R[i].key != k)//从表头往后找
i++;
if (i >= n)//没找到,返回0
return 0;
else//找到返回逻辑序号i+1
return i + 1;
}
- 二分查找
在有序表中,取中间元素作为比较对象,若给定值与中间元素的关键字相等,则查找成功;
若给定值小于中间元素的关键字,则在中间元素的左半区继续查找;
若给定值大于中间元素的关键字,则在中间元素的右半区继续查找。不断重复上述查找过程,直到查找成功,或所查找的区域无数据元素,查找失败。
折半查找,折半查找又称二分查找,它是一种效率较高的查找方法。但是,折半查找要求线性表是有序表,即表中的元素按关键字有序(递增或递减)有序排列。
代码实现:
int BinarySearch(SeqList list, KeyType kx)
{
//若找到返回该元素在表中的位置,否则返回0
int mid,low=1, high=list.n; //设置初始区间
while(low<=high)
{ /*当查找区间非空*/
mid=(low+high)/2; //取区间中点
if(kx==list.data[mid].key)
return mid; //查找成功,返回mid
else if (kx<list.data[mid].key)
high=mid-1; // 调整到左半区
else low=mid+1; // 调整到右半区
}
return 0; //查找失败,返回0
}
时间复杂度:O(log2n)
1.3 二叉搜索树
二叉搜索树具有下列性质: 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值; 它的左、右子树也分别为二叉排序树。
1.3.1 如何构建二叉搜索树(操作)
结合一组数据介绍构建过程,及二叉搜索树的ASL成功和不成功的计算方法。
如何在二叉搜索树做插入、删除。
- 二叉搜索树的概念
二叉搜索树又称为二叉排序树,它或者是一棵空树,或者具有如下性质的二叉树:
(1) 若它的左子树不为空,则左子树上的所有节点值都小于根节点的值。
(2) 若它的右子树不为空,则右子树上的所有节点值都大于根节点的值。
(3) 它的左右子树也为二叉搜索树。 - 创建二叉搜索树
给出一组元素 38 26 62 94 35 50 28 55
- 把第一个元素作为根节点
- 把第二个元素拿出来与第一个元素做比较,如果比根节点大就放在右边,如果比根节点小就放在左边
- 同理比较第三个元素62
- 插入第四个元素94,先与38比较,进入右子树,然后与62比较
- 按照以上的方式依次把后面的元素插入到树中
1.3.2 如何构建二叉搜索树(代码)
- 类型定义和函数声明
#include<stdio.h>
#include<stdlib.h>
typedef struct BiTNode{
int data;
struct BiTNode *lchild;
struct BiTNode *rchild;
} BiTNode, *BiTree, *Position;
void CreateBiTree(BiTree *T);
void ProOrderTraversal(BiTree T);
Position Find(int tar, BiTree T);
Position Find2(int tar, BiTree T);
Position FindMax(BiTree T);
Position FindMin(BiTree T);
BiTree Insert(int data, BiTree T);
BiTree Delete(int data, BiTree T);
- 在二叉搜索树中查找指定元素
查找的效率取决于树的高度(每递归一次,程序进入树的下一层进行查找)。查找操作的思想也可应用到接下来的插入操作和删除操作。查找程序是用尾递归的方法实现的,而尾递归的程序都可以转换成循环的形式。
//从二叉搜索树T中查找元素,返回其所在结点的地址(尾递归实现)
Position Find(int tar, BiTree T)
{
if (!T)/*没有找到该元素*/
return NULL;
if (tar > T->data)/*该元素在该节点的右子树中*/
return Find(tar, T->rchild);
else if (tar < T->data)/*该元素在该节点的左子树中*/
return Find(tar, T->lchild);
else/*找到该元素*/
return T;
}
//从二叉搜索树T中查找元素,返回其所在结点的地址(循环实现)
Position Find2(int tar, BiTree T)
{
while (T && T->data != tar)
{
if (T->data > tar)
T = T->lchild;
else
T = T->rchild;
}
return T;
}
- 在搜索二叉树中插入元素
插入操作的关键是找到元素应该插入的位置,其思想与Find函数类似。
//插入元素(递归)
//最终返回元素为根节点指针
BiTree Insert(int data, BiTree T)
{
if (!T)//找到插入位置,进行插入
{
T = (BiTree)malloc(sizeof(BiTNode));
T->data = data;
T->lchild = T->rchild = NULL;
}
else if (data > T->data)//进行右子树递归插入
T->rchild = Insert(data, T->rchild);
else if (data < T->data)//进行左子树递归插入
T->lchild = Insert(data, T->lchild);
else//插入失败
printf("元素已经存在,插入失败");
return T;
}
- 在搜索二叉树中删除元素
删除函数是这里面最困难的函数。被删除元素有三种具体情况:叶子结点、度为1的结点和度为2的结点。关键在于将度为2的结点通过其左子树的最大元素或右子树的最小元素替换成度为1的结点而完成删除操作。
//删除元素(递归)
//最终返回元素为根节点指针
BiTree Delete(int data, BiTree T)
{
Position Tmp;
if (!T)
printf("没有找到待删除元素");
else if (data > T->data)//进行右子树递归删除
T->rchild = Delete(data, T->rchild);
else if(data < T->data)//进行左子树递归删除
T->lchild = Delete(data, T->lchild);
//找到对应节点
else if (T->lchild && T->rchild)//被删除节点有左右两个子节点(需要转换成有一个子节点或没有子节点的情况)
{
Tmp = FindMax(T->lchild);
T->data = Tmp->data;
T->lchild = Delete(T->data, T->lchild);
}
else//被删除节点有一个子节点或没有子节点
{
Tmp = T;
if (!T->rchild)//有左节点或没有子节点
T = T->lchild;
else if (!T->lchild)//有右节点或没有子节点
T = T->rchild;
free(Tmp);
}
return T;
}
- 分析代码的时间复杂度
最好:O(log2n)
最坏: O(n) - 为什么要用递归实现插入、删除?递归优势体现在代码哪里?
1.递归的实现明显要比循环简单得多
2.保留父子关系,便于删除和插入顺利找到父亲和孩子
1.4 AVL树
AVL树解决什么问题,其特点是什么?
平衡二叉搜索树,它的特点是在二叉搜索树(BST)的基础上,要求每个节点的左子树和右子树的高度差至多为1。
这个要求使AVL的高度h = logn,底数为2,避免了BST可能存在的单链极端情况(h = n)。
AVL树中任何节点的两个子树的高度最大差别为1
结合一组数组,介绍AVL树的4种调整做法。
*LL的旋转。LL失去平衡的情况下,可以通过一次旋转让AVL树恢复平衡。步骤如下:
1、 将根节点的左孩子作为新根节点。
2、 将新根节点的右孩子作为原根节点的左孩子。
3、 将原根节点作为新根节点的右孩子。
LL旋转示意图如下:
- RR的旋转:RR失去平衡的情况下,旋转方法与LL旋转对称,步骤如下:
1、 将根节点的右孩子作为新根节点。
2、 将新根节点的左孩子作为原根节点的右孩子。
3、 将原根节点作为新根节点的左孩子。
RR旋转示意图如下:
- LR的旋转:LR失去平衡的情况下,需要进行两次旋转,步骤如下:
1、 围绕根节点的左孩子进行RR旋转。
2、 围绕根节点进行LL旋转。
LR的旋转示意图如下:
- RL的旋转:RL失去平衡的情况下也需要进行两次旋转,旋转方法与LR旋转对称,步骤如下:
1、 围绕根节点的右孩子进行LL旋转。
2、 围绕根节点进行RR旋转。
RL的旋转示意图如下:
AVL树的高度和树的总节点数n的关系?
设N(h)为高度为h的AVL树的最小节点数目,则N(h)=N(h-1) + N(h-2) +1; N(0)=0, N(1)=1; 类似斐波那契数列;
插入、查找和删除的性能均为log(n)。
介绍基于AVL树结构实现的STL容器map的特点、用法。
1、 什么是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内部所有的数据都是有序的,后边我们会见识到有序的好处。
map的特点是增加和删除节点对迭代器的影响很小,除了那个操作节点,对其他的节点都没有什么影响。对于迭代器来说,可以修改实值,而不能修改key。
2. map的基本操作函数:
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树区别,其要解决什么问题?
- B-树
也叫B树,即平衡多路查找树,m阶B树表示节点可拥有的最多m个孩子,2-3树是3阶B树,2-3-4树是4阶B树。多叉树可以有效降低树的高度,h=log_m(n),m为log的底数。 - B-树的特点:
- 任意非叶子结点最多只有 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]) 的子树
- 所有叶子结点位于同一层
如:(M=3)
B-树的搜索,从根结点开始,对结点内的关键字(有序)序列进行二分查找,如果
命中则结束,否则进入查询关键字所属范围的儿子结点;重复,直到所对应的儿子指针为
空,或已经是叶子结点;
- B-树的特性:
* 关键字集合分布在整颗树中;
* 任何一个关键字出现且只出现在一个结点中;
* 搜索有可能在非叶子结点结束;
* 其搜索性能等价于在关键字全集内做一次二分查找;
* 自动层次控制;
由于限制了除根结点以外的非叶子结点,至少含有M/2个儿子,确保了结点的至少
利用率,其最底搜索性能为:
其中,M为设定的非叶子结点最多子树个数,N为关键字总数;
所以B-树的性能总是等价于二分查找(与M值无关),也就没有B树平衡的问题;
由于M/2的限制,在插入结点时,如果结点已满,需要将结点分裂为两个各占
M/2的结点;删除结点时,需将两个不足M/2的兄弟结点合并; - B-树的结构体定义
#define MAXN 10
typedef int KeyTypw;
typedef struct node{
int keynum;//节点当前拥有关键字的个数
KeyType key[MAXM];//存放关键字
struct node *parent;//双亲节点指针
struct node *ptr[MAXM];//孩子节点指针数组
}BTNode;
B+树定义,其要解决问题
- B+树是B-树的变体,也是一种多路搜索树:
* 其定义基本与B-树同,除了:
* 非叶子结点的子树指针与关键字个数相同;
* 非叶子结点的子树指针P[i],指向关键字值属于[K[i], K[i+1])的子树
(B-树是开区间);
* 为所有叶子结点增加一个链指针;
* 所有关键字都在叶子结点出现;
如:(M=3)
- B+的特性:
*所有关键字都出现在叶子结点的链表中(稠密索引),且链表中的关键字恰好
是有序的;- 不可能在非叶子结点命中;
- 非叶子结点相当于是叶子结点的索引(稀疏索引),叶子结点相当于是存储
(关键字)数据的数据层; - 更适合文件索引系统;比如对已经建立索引的数据库记录,查找10<=id<=20,那么只要通过根节点搜索到id=10的叶节点,之后只要根据叶节点的链表找到第一个大于20的就行了,比B-树在查找10到20内的每一个时每次都从根节点出发查找提高了不少效率。
1.6 散列查找。
1. 哈希表的设计主要涉及哪几个内容?
散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
2. 构造方法
- 直接取地址法
根据关键字直接加上某个常量作为地址,确定所坐在位置
优点:计算简单,并且不可能有冲突
缺点:关键字分布不是连续的将造成内存单元浪费 - 除留余数法
用关键字k除以某个不大于哈希表长度的m的数p所得的余数作为哈希表地址,即需要一个哈希函数h(k)= k mod p(p最好为素数)
优点:得到的哈希表是大致连续的,且不会超过原来哈希表的长度,可以节省空间
缺点:余数有可能相同,会产生冲突 - 数字分析法
数字分析法是取数据元素关键字中某些取值较均匀的数字位作为哈希地址的方法。即当关键字的位数很多时,可以通过对关键字的各位进行分析,丢掉分布不均匀的位,作为哈希值。它只适合于所有关键字值已知的情况。通过分析分布情况把关键字取值区间转化为一个较小的关键字取值区间。
哈希表的设计
与三个因素有关:
- 哈希表长度
- 采用的哈希函数
结合数据介绍哈希表的构造及ASL成功、不成功的计算
关键字序列:(7、8、30、11、18、9、14)
散列函数:
H(Key) = (key x 3) MOD 7
所以查找成功的计算:
如果查找7,则需要查找1次。
如果查找8,则需要查找1次。
如果查找30,则需要查找1次。
如果查找11,则需要查找1次。
如果查找18,则需要查找3次:第一次查找地址5,第二次查找地址6,第三次查找地址7,查找成功。
如果查找9,则需要查找3次:第一次查找地址6,第二次查找地址7,第三次查找地址8,查找成功。
如果查找地址14,则需要查找2次:第一次查找地址0,第二次查找地址1,查找成功。
所以,ASL成功=(1+2+1+1+1+3+3)/ 7=12/ 7
查找不成功计算
查找地址为0的值所需要的次数为3,
查找地址为1的值所需要的次数为2,
查找地址为2的值所需要的次数为1,
查找地址为3的值所需要的次数为2,
查找地址为4的值所需要的次数为1,
查找地址为5的值所需要的次数为5,
查找地址为6的值所需要的次数为4。
ASL不成功=(3+2+1+2+1+5+4)/ 7=18/ 7
2.PTA题目介绍(0--5分)
介绍3题PTA题目
2.1 是否完全二叉搜索树(2分)
BinTree Insert(BinTree T, int x)//建树函数
if T为空
开辟结点
T->Data = x;
T->Left = T->Right 为 空
else
if x >根结点值
插入到左子树
else if x < 根结点值
插入到右子树
void Output(BinTree T)//输出函数
while (!q.empty())
p = q.front();
q.pop();
if (flag == 0)//控制空格
{
cout << p->Data;
flag = 1;
}
else cout << ' ' << p->Data;
if 左子树不空 输出左子树
if 右子树不空 输出右子树
bool judge(BinTree T)//判断函数
if(根节点不空) 入队根节点;
初始化状态status = true;//如果status==false,则后续所有结点都只能是叶子结点
while(队列不为空)
出队队头结点t;
if(t的左孩子不为空)
if(status == false)
else 标记status = false;
if(t的右孩子不为空)
if(status == false)
t的右孩子入队;
else 标记status = false;
end while
return true;
2.1.2 提交列表
2.1.3 本题知识点
二叉排序树(Binary Sort Tree),又称二叉查找树(Binary Search Tree),亦称二叉搜索树。是数据结构中的一类。在一般情况下,查询效率比链表结构要高。
一棵空树,或者是具有下列性质的二叉树:
(1). 若左子树不空,则左子树上所有结点的值均小于它的根结点的值;
(2). 若右子树不空,则右子树上所有结点的值均大于它的根结点的值;
(3). 左、右子树也分别为二叉排序树;
(4). 没有键值相等的结点.
2.2 航空公司VIP客户查询(2分)
2.2.1 伪代码(贴代码,本题0分)
伪代码为思路总结,不是简单翻译代码。
2.2.2 提交列表
2.2.3 本题知识点
2.3 基于词频的文件相似度(1分)
本题设计一个倒排索引表结构实现(参考课件)。单词作为关键字。本题可结合多个stl容器编程实现,如map容器做关键字保存。每个单词对应的文档列表可以结合vector容器、list容器实现。
2.3.1 伪代码(贴代码,本题0分)
for (int i = 0; i < 个数; i++)do
输入身份证 里程数
if (里程数 < 最小里程数)
更改里程数
end if
调用Insert向H插入结点数;
end for
for(int i = 0; i < 查找次数; i++) do
调用查找函数造H中查找数据
找到数据赋值给ptr
若 ptr == NULL do
else
查找成功
end if
end for
2.3.2 提交列表
2.3.3 本题知识点
- 建立一个哈希表,哈希表中存放的是结点所在的下标(相当于存放链表头指针,只不过这里的指针用下标表示)。
i. 哈希冲突解决方法:拉链法,即在冲突的位置存放一个链表。 - 建立一个存放数据的表(表长同哈希表),该表用作链表。