DS博客作业05--查找
| 这个作业属于哪个班级 |
| ---- | ---- | ---- |
| 这个作业的地址 |
| 这个作业的目标 | 学习查找的相关结构 |
| 姓名 | 黄静 |
0.PTA得分截图
查找题目集总得分,请截图,截图中必须有自己名字。题目至少完成总题数的2/3,否则本次作业最高分5分。没有全部做完扣1分。
1.本周学习总结
1.1 查找的性能指标
ASL:关键字的平均比较次数,也称平均搜索长度,是查找算法的评价指标
在查找运算中时间主要花费在关键字的比较上,把平均需要和给定值k进行比较的关键字次数称为平均查找长度ASL,其定义如下:
ASL分为查找成功情况下的ASL成功和查找不成功情况下的ASL不成功
- ASL成功表示成功查找到查找表中的元素,平均需要关键字比较次数
- ASL不成功表示没有找到查找表中的元素,平均需要关键字比较次数
ASL是衡量查找算法性能好坏的重要指标。一个查找算法的ASL越大,其时间性能越差;反之,一个查找算法的ASL越小,其时间性能越好。
例如:
ASL成功=(1+2+3+4+5+6+7+8+9)/9=5
1.2 静态查找
分析静态查找几种算法包括:顺序查找、二分查找的成功ASL和不成功ASL。
在查找中不涉及表的修改操作,则相应的查找表称为静态查找表。线性表静态查找包括顺序查找,二分查找和分块查找等。
1.2.1 顺序查找
定义
顺序查找是一种最简单的查找方法,它的基本思路是从表的一端向另一端逐个将元素的关键字和给定值k比较,若相等,则查找成功,给出该元素在查找表中的位置;若整个查找表扫描结束后仍未找到关键字等于k的元素,则查找失败。
查找代码
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;
}
查找性能分析
- 查找成功时的顺序查找平均查找长度约为表长的一半
(n+1)/2
,即ASL成功=(n+1)/2
- 若k不在表中,即查找不成功时,需要遍历全表比较n次才能确定查找失败,此时平均查找长度为
n
,即ASL不成功=n - 该顺序查找代码时间复杂度为
O(n)
,其中n为查找表中的元素个数,即时间复杂度为O(n)
1.2.2 折半查找
定义
折半查找又称二分查找,它是一种效率较高的查找方法。但是,折半查找要求线性表是有序表,即表中的元素按关键字有序(递增或递减)有序排列。
折半查找的基本思路是:设R[low···high]是当前查找区间,首先确定该区间的中点位置mid=[(low+high)/2],然后将待查的k值与中点值R
[mid].key`相比较:(假设该表为递增有序排列)
- 若k=R[mid].key,则找到k值,查找成功并返回该元素的逻辑序号
- 若k<R[mid].key,则由表的有序性可知k值位于中值左边区间,即
R[low···mid-1]
中,因此新的查找区间为该左边区间R[low···mid-1]
- 若k>R[mid].key,则由表的有序性可知k值位于中值右边区间,即
R[mid+1···high]
中,因此新的查找区间为该左边区间R[mid+1···high]
针对新的区间进行再一次折半查找,如此循环,直至查找成功或当前区间为空时退出循环。
简单来说,即在区间内将k与当前区间的中点位置的关键字比较,如果二者相同,则查找成功,否则将区间缩小一半,重复查找直到查找成功或当前区间为空
查找代码
int BinSearch(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 + 1;
if (k < R[mid].key)//继续在[low...mid-1]中寻找
high = mid - 1;
else//继续在[mid+1...high]中寻找
low = mid + 1;
}
return 0;
}
//递归算法
int BinSearch(SeqList R, int low, int high, KeyType k)
{
int mid;
if (low <= high)//查找区间存在一个及以上元素
{
mid = (low + high) / 2;
if (R[mid].key == k)//查找成功
return mid + 1;
if (k < R[mid].key)//继续在[low...mid-1]中寻找
BinSearch(R, low, mid - 1, k);
else//继续在[mid+1...high]中寻找
BinSearch(R, mid + 1, high, k);
}
else
return 0;
}
判定树
折半查找过程可以用二叉树来表示,把当前查找区间的中间位置上的元素作为根,由左子表和右子表构造的二叉树分别作为根的左子树和右子树,由此可得到的二叉树称为描述折半查找过程的判定树或比较树。
判定树中查找成功的结点称为内部结点,而查找失败的结点称为外部结点。对于内部结点中的每个单分支结点,添加一个作为它孩子的外部结点使其变为双分支结点,对于内部结点中的每个叶子结点,添加两个作为孩子的外部结点使其变成双分支结点,即构造出了外部结点。判定树刻画了在所有查找情况下进行折半查找的比较过程。
如图所示:
性能分析
- 二分查找ASL成功:总比较次数/内部结点个数
- 二分查找ASL不成功:总比较次数/外部结点个数
- 二分查找的时间复杂度为
O(log2n)
- 关键字比较次数为
log2(n+1)
1.2.3 分块查找
分块查找是一种性能介于顺序查找和折半查找之间的查找方法。
查找思路:
- 将表R[0...n-1]均分为b块
- 表是分块有序,即每一块中的关键字不一定有序,但前一块中的最大关键字必须小于后一块中的最小关键字
- 抽取各块中的最大关键字及其起始位置构成一个索引表IDX[0...b-1],即IDX[i]中存放着第i+1块的最大关键字及该块在表R中的起始位置
(由于表R是分块有序的,所以索引表是一个递增有序表) - 将查找的关键字k与索引表的各个关键字比较,直到找到第一个关键字大于k的元素,再根据IDX找到该关键字所在块的起始地址,并开始顺序查找,若查找不成功,则不存在
即先查找索引表,再在对应分块中查找,索引表可使用折半查找或顺序查找,块内元素无序只能进行顺序查找
1.3 二叉搜索树
二叉查找树(Binary Search Tree),(又称:二叉搜索树,二叉排序树)它或者是一棵空树,或者是具有下列性质的二叉树:
*若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值
*若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值
*它的左、右子树也分别为二叉排序树
上述性质简称二叉排序树性质(BST性质),故二叉排序树实际上是满足BST性质的二叉树,也就是说,二叉排序树在二叉树的基础上增加了节点值的约束。
注意:
- 二叉排序树中没有相同关键字的结点
- 中序遍历二叉排序树得到一个关键字的递增有序序列
1.3.1 二叉搜索树查找
因为二叉排序树可以看作是一个有序表,所以在二叉排序树上进行查找和二分查找类似,也是一个逐步缩小范围的过程。
查找思路
将待查找数据和当前的根节点进行比较
- 若二者相等,则找到该数值
- 如果待查数据小于根节点,则递归查找左子树
- 如果待查数据大于根节点,则递归查找右子树
- 若查找到叶子节点还未找到待查找数值,则不存在
代码实现
BSTNode* SearchBST(BSTNode* bt, KeyType k)
{
if (bt == NULL || bt->key == k)//递归结束条件
return bt;
if (k < bt->key)
return SearchBST(bt->lchild, k);//在左子树上查找
else
return SearchBST(bt->rchild, k);//在右子树上查找
}
//非递归算法
BSTNode* SearchBST(BSTNode* bt, KeyType k)
{
while (bt != NULL)
{
if (k == bt->key)
return bt;//找到k
else if (k < bt->key)
bt = bt->lchild;//在左子树上查找
else bt = bt->rchild;//在右子树上查找
}
return NULL;//没有找到返回NULL
}
1.3.2 构建二叉搜索树
构建思路:
- 首先把第一个元素拿出来作为根节点
- 之后插入的每个结点拿出来与根结点做比较
- 如果比根节点小,当前指针移到左子树
- 如果比根节点大,当前指针移到右子树
- 直到指针为空时插入,再创建一个新结点,由当前指针指向它(元素总是作为树的叶子结点插入)
- 同理,把接下来的元素依次插入到树中即可
代码实现
创建过程就是遍历数组A[i],调用插入函数的过程
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.3.3 二叉搜索树的插入与删除
二叉排序树的插入
在二叉排序树中插入一个关键字为k的结点要保证插入后仍然满足BST性质,是一个边查找边插入的过程,插入的元素一定在叶节点上。
思路:
- 若二叉排序树bt为空,则创建一个key域作为k的结点
- 将k与根节点的关键词比较
- 若二者相等,已存在关键词k,不必插入
- 若k小于根节点,插入根节点的左子树中
- 若k大于根节点,插入根节点的右子树中
- 重复第二步,直到结点为空后插入k完成
代码实现:
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);//插入右子树
}
二叉排序树的删除
在从二叉排序树中删除一个结点时不能直接把以该结点为根的子树都删去,只能删除该结点本身,并且还要保证删除后所得的二叉树仍然满足BST性质。也就是说,在二叉排序树中删去-一个结点相当于删去有序序列(即该树的中序序列)中的一个元素。
删除思路:
删除操作必须首先进行查找,假设在查找结束时p指向要删除的结点,删除结点分为以下几种情况:
-
被删除的结点是叶子结点
- 直接删去该节点,双亲结点中相应指针域的值改为空
- 直接删去该节点,双亲结点中相应指针域的值改为空
-
被删除结点只有左子树或者只有右子树
- 用其左子树或者右子树代替他,其双亲节点相应的指针域改成指向被删除结点的左子树或者右子树
- 用其左子树或者右子树代替他,其双亲节点相应的指针域改成指向被删除结点的左子树或者右子树
-
被删除结点既有左子树,又有右子树
- 以其前驱替代之,再删除该前驱结点(前驱结点是左子树中最大的结点)
- 也可以以其后继替代之,再删除该后继结点(后继结点是右子树中最小的结点)
代码实现:
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;
}
}
1.4 AVL树
1.4.1 AVL树定义
二叉排序树中查找的操作执行时间与树形有关,在最坏情况下执行的时间为O(n),为了避免这种情况发生,人们研究了许多种动态平衡的方法,使得往树中插入或删除结点时通过调整树的形态来保持树的平衡,使之既能保持BST性质不变,又能保证树的高度在任何情况下为log2n,从而确保树上的查找操作在最坏情况下的时间也是O(log2n),平衡的排序二叉树有很多种,AVL树就是其中一种较为著名的平衡的排序二叉树。
在算法中,我们通过平衡因子来具体实现平衡二叉树的定义。这就要求一旦某些结点的平衡因子在插入新结点后不满足要求就要进行调整。某结点的左子树与右子树的高度(深度)差即为该结点的平衡因子(BF,Balance Factor)。平衡二叉树上所有结点的平衡因子只可能是 -1,0 或 1。一棵二叉树所有结点都是平衡的,称之为平衡二叉树。
AVL树具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
1.4.2 AVL树调整
每次向二叉排序树中插入新结点时要保持所有结点满足平衡二叉树的要求。如果在一棵AVL树中插入一个新结点,就有可能造成失衡,此时必须重新调整树的结构,使之恢复平衡。我们称调整平衡过程为平衡旋转。
若向平衡二叉树中插入一个新结点(总是作为叶子结点插入)后破坏了平衡性,首先从该新插入的结向根结点方向查找第一个失衡的结点,然后进行调整。调整的操作可以归纳为下列四种情况:
- LL平衡旋转
- RR平衡旋转
- LR平衡旋转
- RL平衡旋转
LL平衡旋转:这是因为在A结点的左孩子的左子树上插入结点,使得A结点的平衡因子由1变为2而引起的不平衡。需要进行一次顺时针旋转。
调整方法:将A结点的左孩子B右上旋转作为A结点的根结点,A结点右下旋转作为根节点B的右孩子,结点B原右子树变为A结点的左子树。
RR平衡旋转:这是因为在A结点的右孩子的右子树上插入结点,使得A结点的平衡因子由-1变为-2而引起的不平衡。需要进行一次逆时针旋转。
调整方法:将A结点的右孩子B左上旋转作为A结点的根结点,A结点左下旋转作为根节点B的左孩子,结点B原左子树变为A结点的右子树。
LR平衡旋转:这是因为在A结点的左孩子的右子树上插入结点,使得A结点的平衡因子由1增加至2而引起的不平衡。以插入的结点C为旋转轴,先C逆时针旋转,再A顺时针旋转。
调整方法:C向上旋转到A的位置,A作为C的右孩子,C原左孩子作为B的右孩子,C原右孩子作为A的左孩子。
RL平衡旋转:这是因为在A结点的右孩子的左子树上插入结点,使得A结点的平衡因子由-1增加至-2而引起的不平衡。以插入的结点C为旋转轴,先顺时针旋转,再逆时针旋转。
调整方法:C向上旋转到A的位置,A作为C的左孩子,C原左孩子作为A的右孩子,C原右孩子作为B的左孩子。
建立AVL树过程图解:
1.4.3 STL容器map
map是STL的一个关联容器,它提供一对一(第一个称为关键字,第二个称为该关键字的值)的数据处理能力。map内部数据的组织,map内部自建一颗红黑树。
-
定义:map<typename1, typename2> mp; //map需要定义前映射类型(键key)和映射后类型(值value),所以需要在<>内填写两个类型
-
作用:解决映射问题
-
访问://注意:map中的键是唯一的,赋值会被后面的赋值所代替
- 对于一个定义为map<char,int> mp的map,可以通过mp[‘c’]的方式来访问它对应的整数
当建立映射时,就可以直接使用mp[‘c’]=20这样和普通数组一样的方式`
- 通过迭代器访问
C++迭代器是一种检查容器内元素并遍历元素的数据类型,迭代器的作用就相当于去除物品的工具的抽象
c++迭代器interator就是一个指向某STL对象的指针,通过该指针可以简单方便地遍历所有元素
map迭代器的定义与其他STL容器迭代器定义的方式相同:map<typename1, typename2>::iterator it;
typename1和typename2就是定义map时填写的类型,这样就得到了迭代器it。
map迭代器的使用方式和其他的STL容器的迭代器不同,因为map每一对迭代器映射都有两个typename,这决定了必须通过一个int来同时访问键和值。
-
常用函数
-
find() //find(key)返回键为key的映射的迭代器,时间复杂度为O(logN),N为map中映射的个数
-
erase() //有两种用法:删除单个元素,删除一个区间内的所有元素
-
删除单个元素
mp.erase(it), it为需要删除的元素的迭代器,时间复杂度为O(1)
mp.erase(key),key为要删除的映射的键,时间复杂度为O(logN), N为map内元素的个数
删除一个区间的所有元素
mp.erase(first,last),其中first为需要删除的区间起始迭代器,而last则是需要删除的区间末尾迭代器的下一个地址,也即为删除左闭区间[first,last),时间复杂度为O(last-first)
-
szie() //用来获得map中的映射个数,时间复杂度为O(1)
-
clear() //用来清空map中的所有的元素,复杂度为O(N),其中N为map中的元素个数
-
应用
- 需要建立字符(或字符串)与 整数之间映射的题目,使用map可以减少代码量
- 判断大整数或者其他类型数据是够存在的题目,可以把map当bool数组用
- 字符串与字符串的映射也可能会遇到
1.5 B-树和B+树
B-树和AVL树的区别:
BST和AVL树都是内存中,适用小数据量。每个节点放一个关键字,树的高度比较大。
B-树和B+树一个节点可以放多个关键字,降低树的高度。可放外存,适合大数据量查找,如数据库中数据。
1.5.1 B-树
B-树的定义
一棵m阶B-树或者是一棵空树,或者是满足下列要求的m叉树:
1.树中每个结点至多有m棵子树(至多含有m-1个关键字)
2.若根节点不是叶子结点,则根节点至少有两棵子树
3.除根节点以外,所以非叶子节点最少有[m/2]棵子树(即最少含有[m/2-1]个关键字)
4.所有的外部结点在同一层,并且不带信息
B-树的查找
查找思路:
在一棵B-树上顺序查找关键字为k的方法为将k与根节点中的key[i]进行比较:
- 若k=key[i],则查找成功
- 若k<key[1],则沿着指针ptr[0]所指的子树继续查找
- 若key[i]<k<key[i+1],则沿着指针ptr[i]所指的子树继续查找
- 若k>key[n],则沿着指针ptr[n]所指的子树继续查找
- 查找到某个叶结点,若相应指针为空,落入一个外部结点,表示查找失败
B-树的插入
在查找不成功之后,需进行插入。关键字插入的位置必定在叶子结点层,有下列几种情况:
- 该结点的关键字数目n<m-1,不修改指针
- 该结点的关键字数目n=m-1,则需进行结点分裂
- 如果没有双亲结点,新建一个双亲结点,树的高度增加一层
- 如果有双亲结点,将ki插入到双亲结点中
B-树的删除
B-树的删除和插入考虑的相反。
- 结点中关键字的个数>[m/2]-1,直接删除
- 结点中关键字的个数=[m/2]-1
- 要从其左(或右)兄弟结点"借调"关键字
- 若其左和右兄弟结点均无关键字可借(结点中只有最少量的关键字),则必须进行结点的"合并"
B-树删除关键字k分为两种情况:
-
在非叶子结点层上删除关键字k
- 从Pi子树节点借调最大或者最小关键字key代替删除结点;
- pi子树中删除key
- 若子树节点关键字个数 < m/2-1,重复步骤1
- 若删除关键字为叶子结点层,按叶子结点删除操作
-
在叶子结点层上删除关键字k
-
结点b关键字个数大于min,说明删去该关键字后该节点仍满足B树的定义,可直接删去该关键字
-
结点b关键字个数等于min,说明删去该关键字后该节点不满足B树的定义,若可以从兄弟节点借
- 兄弟结点最小关键字上移至双亲结点
- 双亲节点大于删除关键字的关键字下移删除结点
-
结点b关键字个数等于min,兄弟结点关键字个数等于min,兄弟结点没有关键字可以借
- 删除关键字
- 兄弟结点及删除结点,双亲节点中分割二者的关键字合并成一个新的叶子结点
- 若双亲节点的关键字个数小于min,则重复步骤2
-
1.5.2 B+树
定义
索引文件组织中,经常使用B-树的变形B+树,B+树是大型索引文件的标准组织方式。
一棵m阶B+树满足下列条件:
1.每个分支节点至多有m棵子树
2.根节点或者没有子树,或者至少有两棵子树
3.除根节点,其他每个分支节点至少有「m/2]棵子树
4.有n棵子树的节点有n个关键字。
5.所有叶子结点包含全部关键字及指向相应记录的指针
a.叶子结点按关键字大小顺序链接
b.叶子结点时直接指向数据文件的记录
6.所有分支结点(可以看成是分块索引的索引表),包含子节点最大关键字及指向子节点的指针
查找
查找思路:
-
因为所有的叶子结点链接起来成为一个线性链表,可直接总最小关键字开始进行顺序查找所有叶子节点
-
从B+树的根结点开始出发,一直找到叶结点为止
1.6 散列查找
哈希表又称散列表,其基本思路是设要存储的元素个数为n,设置一个长度为m的连续内存单元,以每个元素的关键字ki(i取值为0<=i<=n-1)为自变量,通过一个称为哈希函数的函数h(ki),把ki映射为内存单元的地址(或下标)h(ki),并把该元素存储在这个内存单元中,h(ki)也称为哈希地址。把如此构造的线性表存储结构称为哈希表。哈希表是除顺序表存储结构,链接表存储结构和索引表存储结构之外的又一种存储线性表的存储结构。
注意:哈希表是一种存储结构,它并非适合任何情况,主要适合记录的关键字与存储地址存在某种函数关系的数据。
在构造哈希表时可能存在这样的问题,两个关键字ki和kj有ki不等于kj,但会出现h(ki)=h(kj)的情况,把这种现象叫哈希冲突。哈希冲突是很难避免的。但发生冲突的可能性却有大有小,这会影响哈希查找的性能。
哈希查找性能主要与3个因素有关:
- 与装填因子有关
- 所谓装填因子是指哈希表中已存入的元素数n与哈希地址空间大小m的比值,即a=n/m,且a越小,冲突的可能性就越小(控制在0.6~0.9范围内)
- 与所采用的哈希函数有关
- 若哈希函数选择得当,就可以使哈希地址尽可能均匀地分布在哈希地址空间上,从而减少冲突的发生
- 与解决冲突的方法有关
- 当出现哈希冲突时需要采取解决哈希冲突的方法
哈希函数的构造
直接定址法
直接定址法是以关键字k本身或关键字加上某个常量c作为哈希地址的方法。直接定址法的哈希函数h(k)为h(k)=k+c
优缺点:
- 优点:哈希函数计算简单,适合关键字发布基本连续时
- 缺点:关键字的分布不连续时将造成内存单元的大量浪费
除留余数法
除留余数法是用关键字k除以某个不大于哈希表长度m的数p所得的余数作为哈希地址。除留余数法的哈希函数h(k)通常为`h(k)=k mod p(mod为求余运算,p<=m)
优缺点:
- 优点:计算比较简单,适用范围广,是最经常使用的一种哈希函数
- 缺点:关键要选好p,p最好是质数
- (使得元素集合中的每一个关键字通过该函数转换后映射到哈希表范围内任意地址上概率相等,从而尽可能减少发生冲突的可能性)
数字分析法
该方法是提取关键字中取值较均匀的数字位作为哈希地址。
ASL值计算:
优缺点
它适用于所有关键字值都已知的情况,并需要对关键字中每一位的取值分布情况进行分析。
哈希冲突的解决
开放地址法
开放地址法就是在出现哈希冲突时,再哈希表中找到一个新的空闲位置存放元素,根据开放地址法找空闲单元的方式又分为线性探测法和平方探测法等。
线性探测法
线性探测法是从发生冲突的地址(设为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的有效空间中。
解决冲突过程:
优缺点
- 优点:解决冲突简单
- 缺点:容易产生堆积问题(非同义词冲突)
平方探测法
如果发生冲突的地址为d0,平方探测法的探测序列为:d0+1^2, d0-1^2, d0+2^2, d0-2^2, ······
平方探测法的数学描述公式为 d0=h(k) di=(d0加减i^2) mod m (1<=i<=m-1)
解决过程:
优缺点
- 优点:避免出现堆积问题
- 缺点:不一定能探测到哈希表上的所有单元
拉链法
拉链法是把所有的同义词用单链表连接起来的方法。所有哈希地址为i元素对应的结点构成一个单链表,哈希表地址空间为0~m-1,地址为i的单元是一个指向对应单链表的首结点。
在这种方法中,哈希表的每个单元中存放的不再是元素本身,而是相应同义词单链表的首结点指针。(由于在单链表中可插入任意多个结点,所以此时的装填因子a根据同义词的多少既可以设定为大于一,也可以设定为小于一或等于一,通常a=1)
2.PTA题目介绍
2.1 是否完全二叉搜索树
将一系列给定数字顺序插入一个初始为空的二叉搜索树(定义为左子树键值大,右子树键值小),你需要判断最后的树是否一棵完全二叉树,并且给出其层序遍历的结果。
输入格式:输入第一行给出一个不超过20的正整数N;第二行给出N个互不相同的正整数,其间以空格分隔。
输出格式:将输入的N个正整数顺序插入一个初始为空的二叉搜索树。在第一行中输出结果树的层序遍历结果,数字间以1个空格分隔,行的首尾不得有多余空格。第二行输出YES,如果该树是完全二叉树;否则输出NO。
解题思路
由于题目首先顺序给出一系列数字插入一个初始为空的树中,并且需要我们判断最后的树是否为完全二叉树。所以我们需要先创建一棵空树,把顺序输入的一系列数字插入树中,创建出一棵二叉搜索树,然后再根据完全二叉树的特性判断其是否为完全二叉树。我采用的是完全二叉树经过层次遍历得到的元素序列中,如果出现空结点之后,如果空结点之后全为空结点,则为完全二叉树,否则不是完全二叉树。
所以,在搜索二叉树创建完成后,对其进行层次遍历,如果一直遍历到空结点时的所遍历结点总个数等于给定创建树的一系列元素个数,则证明其为完全二叉树,反之,不是完全二叉树。
伪代码
伪代码为思路总结,不是简单翻译代码。
//CreateBST函数创建二叉树
//InsertBST函数插入结点
//LevelOrder函数层次遍历
int main()
{
定义:树T,数字个数num,存储输入数字序列str[],遍历后空结点前结点数count;
利用while循环将每一个数字插入二叉树中,创建二叉树;
调用函数LevelOrder,count = LevelOrder(T)得到遍历至空结点的结点数;
if count等于num 输出YES;
else 输出NO;
}
//LevelOrder函数层次遍历
int LevelOrder(BSTree bt)//层次遍历
{
初始化队列q;
if 树为空 return 0;
else 根节点bt入队;
while 队列不为空
{
访问队首元素;
if 队首元素为空,flag = 1;
else
{
输出队首元素;
if flag等于0 //还没遍历到空结点
count++;
队首元素左孩子入队;
队首元素右孩子入队;
}
队首元素出队;
}
return count;//返回count值
}
提交列表
本题知识点
- 生成二叉搜索树及二叉搜索树结点的插入
- 二叉树的层次遍历
- 判断是否完全二叉树的方法
- 可根据二叉树的层次遍历至空结点后,后面全为空结点,不再出现数字结点的特性来判断是否完全二叉树
2.2 航空公司VIP客户查询
不少航空公司都会提供优惠的会员服务,当某顾客飞行里程累积达到一定数量后,可以使用里程积分直接兑换奖励机票或奖励升舱等服务。现给定某航空公司全体会员的飞行记录,要求实现根据身份证号码快速查询会员里程积分的功能。
输入格式:
输入首先给出两个正整数N(≤10^5)和K(≤500)。其中K是最低里程,即为照顾乘坐短程航班的会员,航空公司还会将航程低于K公里的航班也按K公里累积。随后N行,每行给出一条飞行记录。飞行记录的输入格式为:18位身份证号码(空格)飞行里程。其中身份证号码由17位数字加最后一位校验码组成,校验码的取值范围为0~9和x共11个符号;飞行里程单位为公里,是(0, 15 000]区间内的整数。然后给出一个正整数M(≤10^5),随后给出M行查询人的身份证号码。
输出格式:
对每个查询人,给出其当前的里程累积值。如果该人不是会员,则输出No Info。每个查询结果占一行。
解题思路
由于该题需要用户身份证号与其里程数相对应,且需要增加记录和查找记录,还有规定的运行时间要求。所以最好使用的方法是哈希查找,由于身份证号高达十多位数,杂乱无序,且部分还含有x,因此现我们使用除留余数法来构造哈希链,哈希函数比较难设计,在查找资料和学习他人代码之后,我发现较好的方法是使用身份证号后5位数来进行除留余数法得到哈希地址,将x当作数字10来处理。将相同身份证号的记录里程数相加,小于最小里程数的记录按照最小里程数计算,所有记录分别插入记录构造哈希链完成后,需要查询的记录按照相同哈希函数计算哈希地址,在哈希链中遍历该地址链表,若找到相同id的输出里程数,否则输出No Info
.
伪代码
/*主函数*/
int main()
{
输入N,min;//N:飞行记录数 min:最小里程
调用CreateHash函数创建哈希表;
输入M;//M:查询记录数
for i = 0 to M
输入id;
调用GetAdr函数计算哈希地址;
调用FindHash函数查找记录
if 有记录
输出对应里程数;
else 输出No Info;
end for;
}
/*创建哈希链*/
void CreateHash(HashTable* ht, int n, int min)
{
链表空间开辟和初始化;
输入数据id,dis;
if dis 小于最小里程数
dis = 最小里程数;
调用GetAdr函数得到该记录对应地址adr;
调用InsertHash函数将记录插入哈希表中;
}
/*计算身份证号所对应的哈希表地址*/
int GetAdr(char id[])
{
取身份证号id最后5位数字计算哈希地址
for i = 13 to 18
if id[i] 等于 x
adr = adr * 10 + 10;
else adr = adr * 10 + (id[i] - '0');
end for;
哈希函数得到adr:adr = adr % MAX;
}
/*插入记录结点*/
void InsertHash(HashTable*& ht, int adr, char id[], int dis)
{
调用FindHash查看是否有过记录,返回ptr;
if 有记录
直接增加里程数ptr->dis = ptr->dis + dis;
else
头插法插入新结点;
}
/*查找是否有过记录*/
HashTable FindHash(HashTable* ht, int adr, char id[])
{
node等于哈希函数adr所对的单链表;
while node不为空
if id与node结点的id相同
return node结点;
else 继续遍历node = node->next;
end while;
没有记录return 0;
}
提交列表
- 一开始使用map库函数的写法,后面两个测试点无法通过,才发现有运行时间要求,且map代码的运行时间超出要求,后来改为使用哈希链的写法
- 哈希链的哈希函数设计很难,对于十几位的身份证号不知道怎么寻找哈希地址,查阅资料和他人代码后才发现可以取最后几位数进行设计,还可以把x定为数字10
- 各个函数设计和接口很难衔接与设计,不断修改与尝试才把函数完全接好
本题知识点
- 哈希链的创建和数据插入,数据查找
- 很大数字的数据处理和哈希函数的设计,哈希地址的计算
- 字符串函数的使用
- 各大函数的设计和接口衔接
- scanf和printf的运行时间小于cin和cout
2.3 基于词频的文件相似度
实现一种简单原始的文件相似度计算,即以两文件的公共词汇占总词汇的比例来定义相似度。为简化问题,这里不考虑中文(因为分词太难了),只考虑长度不小于3、且不超过10的英文单词,长度超过10的只考虑前10个字母。
输入格式:
输入首先给出正整数N(≤100),为文件总数。随后按以下格式给出每个文件的内容:首先给出文件正文,最后在一行中只给出一个字符#,表示文件结束。在N个文件内容结束之后,给出查询总数M(≤10^4),随后M行,每行给出一对文件编号,其间以空格分隔。这里假设文件按给出的顺序从1到N编号。
输出格式:
针对每一条查询,在一行中输出两文件的相似度,即两文件的公共词汇量占两文件总词汇量的百分比,精确到小数点后1位。注意这里的一个“单词”只包括仅由英文字母组成的、长度不小于3、且不超过10的英文单词,长度超过10的只考虑前10个字母。单词间以任何非英文字母隔开。另外,大小写不同的同一单词被认为是相同的单词,例如“You”和“you”是同一个单词。
解题思路
由于该题既需要存储多个文件代码和与之匹配的文件内容,又要计算任意两个文件的相似度,所以我们需要将文件号与文件内容相结合起来,所以想到使用map库。我采用的是将每个单词与出现这个单词的文件的编号相对应存储在map容器中,在查找两个文件相似度时只需遍历所有单词,若该单词对应的文件编号中存在这两个文件,则说明这是二者共有的,相同单词数加一,总单词数加一;若只有其中一个文件,则总单词数加一,最后用相同单词数除以总单词数可得二者相似度。
伪代码
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;
}
提交列表
本题知识点
- 索引表的思路与应用
- map库的使用
- 容器迭代器的使用
- string库函数的使用
- 具有映射关系的可以考虑使用map容器建立索引表