DS博客作业05--查找
这个作业属于哪个班级 | 数据结构--网络2011/2012 |
---|---|
这个作业的地址 | DS博客作业05--查找 |
这个作业的目标 | 学习查找的相关结构 |
0.PTA得分截图
1.本周学习总结
1.1 查找的性能指标
ASL(Average Search Length)
ASL,是查找算法的查找成功时的平均查找长度的缩写,是为确定记录在查找表中的位置,需和给定值进行比较的关键字个数的期望值。
- ASL:关键字的平均比较次数,也叫做平均搜索长度。
- 公式:ASL=∑(i=1->n)p(i)*c(i);其实就是ASL=n个关键字找到时的比较次数之和/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*/
}
}
- ASL分析
成功:从表的一端开始查找,按顺序依次递增
不成功:需要遍历完整个顺序表才能确定该数据不在顺序表中
- 总结
时间复杂度O(n)=n
查找成功时的平均比较次数约为表长一半
查找不成功时的平均查找长度为:n
二分查找
二分查找也称折半查找,它是一种效率较高的查找方法。但是,折半查找要求线性表必须采用顺序存储结构,而且表中元素按关键字有序排列。
eg. 设一组初始记录关键字序列为(2,4,7,9,10,14,18,26,32,40,50)
- 查找思路
-
在表按数据的大小顺序排序的前提下,进行查找
-
如对一个有n个从小到大排序的的数据的顺序表,查找数据k
①取范围下标,star=0,end=n-1 ②中间值的下标为mid=(star+end)/2 ③比较k和下标为mid的值的大小,如果k小于中间值,则,end=mid-1,如果k大于中间值,则star=mid+1,(加减是因为mid不需要重复判断) ④重复2、3的操作,直到找到k,或是star=end为止
- 二分查找非递归算法
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)
else
}
return 0;
}
- 二分查找递归算法
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;
}
- ASL分析
以当前查找区域的中间位置上的记录作为根节点,左子表作为根节点的左子树,右子表作为根节点的右子树,建立一棵二分查找的判定树。
ASL成功=每一层节点数层次数的总和/总结点数
ASL不成功=其他不存在于树中的每一层节点数(层次数-1)/总结点数
eg. 设一组初始记录关键字序列为(2,4,7,9,10,14,18,26,32,40,50)
成功:查找成功,每个数据查找的次数为该节点在二叉树中所在的高度
不成功:比较到矩形的上层节点就结束
- 总结
时间复杂度:O(log2n)
二分查找查找成功或不成功的平均查找长度均为(h为判定树高度):
和顺序查找对比,二分查找的ASL比顺序查找小,二分查找的查找效率比较高
1.3 二叉搜索树
二叉搜索树又称二叉排序树。
具有如下特性的二叉树:
1.若它的左子树不空,则左子树上所有结点的值均小于根结点的值;
2.若它的右子树不空,则右子树上所有结点的值均大于根结点的值;
3.它的左、右子树也都分别是二叉排序树。
特点:中序遍历二叉排序树会得到一个递增的序列
1.3.1 如何构建二叉搜索树(操作)
- 二叉搜索树 插入操作
思路:
插入关键字k结点要保证插入后仍然满足BST的性质。
插入过程为:
1.若二叉排序树为空,则在给定的一系列关键字序列中,将第一个关键字设为根节点。
2.若不为空,则与根节点的关键字作比较
若比根节点小,作为该根节点的左子树;
若比根节点大,作为该根节点的右子树;
若相等,则无需插入。
注意:插入的元素一定在叶结点上!!!
- 二叉搜索树 删除操作
在删除操作前先用要找到待删除节点的位置
删除二叉树的结点又有三种情况:
1.删除叶子节点,可以直接删除,也就是直接让他的双亲结点为空
2.删除带有一个子节点的节点,则让不为空的子树取代被删结点,也就是让它的双亲结点指向它的不空子树
3.删除带有两个子节点的结点,可用它的前驱结点(左子树中最大的数,即左子树的最右结点)取代它,或是用它的后继节点(右子树中最小的节点,即右子树中最左的节点)取代它
例题:
- 构建过程
创建一棵二叉排序树是从一个空树开始的,每插入一个关键字,就调用一次插入算法把它插入到当前已经生成的二叉排序树中。
- ASL分析
ASL成功:每个数据查找的次数为该节点在二叉树中所在的高度
ASL不成功:
12左,2次,42右,2次;
18右,3,31右,3次;
14左右,4* 2=8次,28左右,4*2=8次。
一共8次查找不成功。
1.3.2 如何构建二叉搜索树(代码)
- 二叉树搜索树结构体定义
typedef int KeyType; //关键字类型
typedef char* InfoType; //其他数据类型
typedef struct node
{
KeyType key; //关键字域
InfoType data; //其他数据域
struct node *lchild, *rchild;/*左右孩子*/
}BSTNode, * BSTree;
- 二叉搜索树的构建
void CreateBST(BinTree& BST, int n)/*建树函数*/
{
int x;
BST = NULL;//树初始化为空
for (int i = 0; i < n; i++)
{
cin >> x;
Insert(BST, x);
}
}
- 二叉搜索树的插入
思路:比较插入关键字与根节点的大小,大的在右子树中找插入位置,小的在左子树中找插入位置,直到找到的插入位置为空。如果是空树可直接插入。
void Insert(BinTree& BST, int X)
{
if (BST == NULL)/*树空*/
{
BST = new TNode;/*申请*/
BST->Data = X;
BST->Left = NULL;
BST->Right = NULL;
}
else if (X > BST->Data)/*大于插左边*/
{
Insert(BST->Left, X);
}
else if (X < BST->Data)/*小于插右边*/
{
Insert(BST->Right, X);
}
}
- 二叉搜索树的查找
类似二分法,但是不需要记录范围的下标,树的结构很巧妙地划分开了大小的区域。左边代表小,右边代表大,只需直接与根节点作比较即可.
BinTree Find(BinTree BST, ElementType X)
{
if (BST == NULL)
{
return NULL;/*为空返回空*/
}
if (X > BST->Data)
{
Find(BST->Right, X);/*大于在右*/
}
else if (X < BST->Data)
{
Find(BST->Left, X);/*小于在左*/
}
else
{
return BST;/*找到则返回*/
}
}
二叉排序树查找的性能分析:
平均查找长度和二叉树的形态有关,即,
最好:log2n(形态匀称,与二分查找的判定树相似)
最坏: (n+1)/2(单支树)
- 二叉搜索树的删除
1.被删除的节点是叶子节点:
直接删去该节点。其双亲节点中相应指针域的值改为“空”
2.被删除的节点只有左子树或者只有右子树:
用其左子树或者右子树代替它 其双亲节点的相应指针域的值改为 “指向被删除节点的左子树或右子树”。
3.被删除的节点既有左子树,也有右子树:
以其前驱替代之,然后再删除该前驱节点。前驱是左子树中最大的节点。
也可以用其后继替代之,然后再删除该后继节点。后继是右子树中最小的节点。
BinTree Delete(BinTree BST, ElementType X)
{
BinTree storage;
if (BST == NULL)/*为空*/
{
printf( "Not Found\n");
return BST;/*输出并返回根节点*/
}
if (X > BST->Data)
{
BST->Right = Delete(BST->Right, X);
}
else if (X < BST->Data)
{
BST->Left = Delete(BST->Left, X);
}
else
{
if (BST->Left && BST->Right)/*左右子树都存在*/
{
storage = FindMin(BST->Right);/*找右子树的最小点*/
BST->Data = storage->Data;
BST->Right = Delete(BST->Right, BST->Data);
}
else
{
storage = BST;
if(BST->Left==NULL)/*左为空*/
{
BST = BST->Right;
}
else/*右为空*/
{
BST = BST->Left;
}
free(storage);/*删除*/
}
}
return BST;
}
- 找最小值:(就是找左子树中最左的节点)
BinTree FindMin(BinTree BST)
{
if (BST == NULL)
{
return NULL;/*为空返回空*/
}
while (BST->Left)/*小在左,左不空*/
{
BST = BST->Left;/*一直找左*/
}
return BST;
}
- 找最大值:(就是找右子树中最右的点)
BinTree FindMax(BinTree BST)
{
if (BST == NULL)
{
return NULL;/*为空返回空*/
}
while (BST->Right)/*大在右,右不空*/
{
BST = BST->Right;/*一直找右*/
}
return BST;
}
Q:为什么要用递归实现插入、删除?递归优势体现在代码哪里?
A:二叉排序树本身就是递归定义的,用递归可以降低时间复杂度使算法高效。
1.4 AVL树(平衡二叉树)
为了提高查找效率,在既能保持BST性质,又能保证树的高度在任何情况下为log2,这样在查找过程中,即使是最坏情况下,执行的时间也还是O(log2),这样的二叉树称为平衡二叉树。
- 定义:左右子树都是平衡二叉树,且每个结点需满足“左右子树深度的差的绝对值<=1”。
- 平衡因子:这个节点的左右子树的高度差
- 结构体定义
typedef struct node
{
KeyType key; //关键字域
int bf; //平衡因子
InfoType data; //其他数据域
struct node *lchild, *rchild;/*左右孩子*/
}BSTNode, * BSTree;
-
四种调整做法
当我们在平衡二叉树中插入一个新的节点的时候,可能会使得二叉树失衡,我们把调节平衡的过程称作平衡旋转。
-
LL平衡旋转(某节点的左子树的左子树插入一个新节点使得这个节点失衡)
进行顺时针旋转:失衡节点的左孩子上转代替失衡节点,失衡节变为左孩子的右孩子,而原本的右孩子则变为失衡节点的左孩子。
简单说,就是失衡节点变为失 衡节点的左孩子的右孩子,而原本的右孩子作为失衡节点的左孩子。
例子:
-
RR平衡旋转(某节点的右子树的右子树插入一个新节点使得这个节点失衡)
进行逆时针旋转:失衡节点的右孩子上转代替失衡节点,失衡节变为右孩子的左孩子,而原本的左孩子则变为失衡节点的右孩子。
简单说,就是失衡节点变为失衡节点的右孩子的左孩子,而原本的左孩子作为失衡节点的右孩子。
例子:
-
LR平衡旋转(某节点的左子树的右子树插入一个新节点使得这个节点失衡)
失衡节点左孩子的右孩子取代失衡节点,失衡节点作为它的右孩子,原本的右孩子为失衡节点左孩子,原本左孩子取代这个点的位置。
例子:
-
RL平衡旋转(某节点的右子树的左子树插入一个新节点使得这个节点失衡)
失衡节点右孩子的左孩子取代失衡节点,失衡节点作为它的左孩子,原本的左孩子为失衡节点右孩子,原本右孩子取代这个点的位置。
例子:
-
例题:
- 总结
AVL树的高度和树的总节点数n的关系:h=log2(N(h)+1)
平衡二叉树上最好最坏进行查找关键字的比较次数不会超过平衡二叉树的深度--时间复杂度为 O(log2n)
最坏的情况下,普通二叉树查找长度为 O(n)
eg.
解析:
根据递推公式F(N)=F(N-1)+F(N-2)+1,得到序列F(1)=1,F(2)=2,F(3)=4,F(4)=7,F(5)=12,F(6)=20.
#include <map>
map映射容器的元素数据是由一个键值和一个映射数据组成的,键值与映照数据之间具有一一映照的关系。map容器的数据结构也采用红黑树来实现的,插入元素的键值不允许重复,比较函数只对元素的键值进行比较,元素的各项数据可通过键值检索出来。由于map与set采用的都是红黑树的结构,所以,用法基本相似。
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 的函数
- AVL树的特点
在AVL树中任何节点的两个子树的高度最大差别为1
1.5 B-树和B+树
B树是一种自平衡的树,能够保持数据有序。这种数据结构能够让查找数据、顺序访问、插入数据及删除的动作,都在对数时间内完成。B树,概括来说是一个一般化的二叉查找树,可以拥有多于2个子节点。与自平衡二叉查找树不同,B树为系统大块数据的读写操作做了优化。B树减少定位记录时所经历的中间过程,从而加快存取速度。
B-树
- 定义:一棵m阶B-树,要么是一棵空树,要么满足以下几点要求
- 每个节点最多有m个孩子节点,最多有m-1个关键字
- 除了根节点,其他节点最少有m/2个孩子节点,至少有(m/2)-1个关键字
- 如果根节点不是叶子节点,那么它至少要有2个孩子节点
- 特点:
- m阶B-树的非根节点,孩子节点最小:(m/2) 最大:(m);关键字个数 最小:[(m/2)-1] 最大:[m-1];
- 根节点至少2个孩子
- 在计算高度的时候,外部节点也要计入在内。(外部节点即失败节点,指向它的指针为空,不存储信息,是虚设的)
- 一棵B-树有n个节点,则它有n-1个外部节点
- 每个结点的结构为:
- n为关键字个数,n+1为孩子指针;
- 结点中按关键字大小顺序排列,ki < ki+1;
- pi为该节点的孩子指针,满足:
- p0指向的结点中关键字小于k0;
- pi指向的结点中关键字大于等于[ki,ki+1];
- p0指向的关键字>kn。
- 结构体定义:
typedef int KeyType; //关键字类型
typedef struct node
{
int keynum;/*节点当前存储的关键字个数*/
KeyType key[MAX]; //关键字域
struct node* parent;/*双亲节点指针*/
struct node* ptr[MAX];/*孩子节点指针数组*/
}BTNode;
-
插入(在查找失败后插入,必须在叶子节点层)
- 若关键字个数<m-1,则不修改指针
- 若关键字个数=m-1,则需要“节点分裂”
例子:
例子:
-
删除
- 若关键字个数>(m/2)-1,直接删除
- 若关键字个数=(m/2)-1:还需判断,如果左(右)的兄弟结点有多余的关键字,可进行“借调”;如果没有(左右兄弟节点关键字刚刚好符合最少关键字个数),则进行“合并”操作。
B+树
- 定义:一棵m阶B+树,需满足以下条件:
- 根节点要么没有子树,要么至少要有2个子树
- 每个节点最多有m棵子树,除根节点,每个节点最少有(m/2)棵子树
- 有n棵子树的节点有n个关键字(关键字与孩子节点的最大值相等)
- 所有的叶子节点包含了所有的关键字并指向对应记录的指针,叶子节点按照关键字大小的顺序链接。
- 所有的分支节点,包含了子节点的最大关键字及指向子节点的指针。
- B+树可进行顺序查找(从最小关键字开始,顺序查找叶子节点构成的线性链表),(这与B-树不同,B-树只能从根节点查找到叶子节点);当然,B+树也能从根节点查找到叶子节点。
B+树和B-树的区别
-
B+树一个节点n个孩子对应n个关键字,孩子个数:(m/2)(m),根节点是2m
-
B-树一个节点n个孩子对应n-1个关键字,孩子个数:[(m/2)-1] [(m-1)],根节点是2(m-1)
-
叶子节点不同:B+树包含了所有关键字,B-树叶子节点关键字与其他节点关键字是不重复的。
-
B+树有两个头节点,一个指数的根节点,一个指最小关键字的叶子节点(即线性链表的头结点);
B-树和AVL树的区别
-
AVL树,该二叉树为了优化查找的性能,会让每个结点的左右子树的高度差维持在1以内,也就是小于等于1。因为我们在二叉查找树上查找数据的时候,是类似二分法的,那么我们每次折半的区间长度就会决定了我们查找的效率,从概率的角度讲,在未知的情况下肯定是一半一半获得的效率最高。所以在二叉查找树中,决定查找效率就是树的高度。
-
B-树,树的结点的高度差会影响结点的查找效率,同时整棵树的高度也会影响高度。在大量操作的情况下,那么从概率的角度出发,我们可以看到越接近叶子结点每层的结点数就越多,那么结点数越多的层数始终会出现一种统计上的优势,即大概率发生,甚至必定发生,显然叶子结点层是拥有最多的结点的层,并且越靠近叶子结点的层数的结点数就越多,那么我们有理由认为查找的结果大概率会发生在叶子结点或者靠近叶子结点的地方。
-
综上所述:B-树是一棵多叉平衡搜索树,旨在比AVL树能够拥有更低的树高,提高查找的效率,但是同AVL树一样,面对插入和删除数据的操作后需要维持平衡。
eg.
1.6 散列查找
哈希表(又称散列表),是一种存储结构,适用于记录的关键字与存储的地址存在某种关系的数据。
基本思路是,设要存储的元素个数为n,设置一个长度为m的连续内存单元,以每个元素的关键字ki(i取值为0~n-1)为自变量,通过一个称为哈希函数的函数h(ki),把ki映射为内存单元的地址(或下标),并把该元素存储在这个内存单元中,这个地址也称为哈希地址。这样构造的线性存储结构就是哈希表。
哈希函数的构造方法
直接定址法
以关键字本身加上一个常数作为哈希地址的方法,函数表示为h(k)=k+c,(c为常数)
- 优点:计算较为简便且不会造成哈希冲突
- 缺点:如果关键字分布不连续(不集中),较为分散,那么容易造成大量空间的浪费
除留余数法
用关键字除以某个不大于哈希表长度的数得到的余数作为地址的方法,函数表示为h(k)=k mod p,(mod表示取余,p<=m)
-
优势:计算简单,适用范围广泛
-
缺点:容易发生哈希冲突。
-
注意,用于取余的数p最好是素数,这样减少了造成哈希冲突的可能。
数字分析法
对关键字进行分析,采取最为合适的方法,对数据进行处理。
-
对于公民的身份证号这一数据来说,因为它的构成与地区、个人生日有关,靠前的数字大多代表了一个地区、或是生日的日期,而在地区相同的人可能会有很多,就生日而言,相同年份出生的人也很多,所以身份证后几位数字,相对于靠前的数字而言,重复的可能性就比较小。这样,我们可能就会采用对后几个数字进行处理的方式来构造哈希函数。
1.6.1 哈希表
哈希表的设计
哈希表的设计:主要是为了解决哈希冲突。
与三个因素有关:
- 哈希表长度(哈希表长度与装填因子有关,而装填因子α=存储的关键字个数/哈希表的大小,α越大冲突可能性越大,最大可取值为1,最好控制在0.6~0.9的范围内)
- 采用的哈希函数
- 解决哈希冲突的方法
哈希表解决冲突的方法
线性探查法
当这个关键字最初确定的那个地址已经被其他关键字占了的时候,从这个位置开始,逐个往下寻找空地址,遇到的第一个空地址用于存放目前的这个关键字。
-
公式
d₀=h(k);(第一次探测代入的是构造的函数) d(i)=[d(i-1)+1]mod m;(其中,1<=i<=m-1,m应该是构造函数里的那个mod的值)
-
优势:解决冲突简单;
-
缺点:容易出现堆积(聚集),或者说叫做非同义词冲突,就是不同的哈希关键字争夺同一个后继地址的问题。
公式
d₀=h(k);(第一次探测代入的是构造的函数)
d(i)=[d₀±i²]mod m;(其中,1<=i<=m-1,)
比如,位置依次为:d₀,d₀+1,d₀-1,d₀+4,d₀-4,······
- 优势:是一种很好的处理冲突的方法,可以避免出现堆积问题;
- 缺点:不一定能探测到哈希表上所有的单元,但最少能探测到一半单元。
哈希表的时间性能
- 查找成功ASL
ASL成功=所有关键字的探测次数总和/有数据的关键字总数
- 查找不成功ASL
在哈希表中查找某个关键字,其哈希地址为d,然后从下标为d的地址单元开始查找,如果该关键字不存在,会一直往后遍历到一个空的位置才会停止。取下标为0~p-1的关键字所对应的探测不成功次数相加)/p,以此类推。
注意:不成功的ASL中,分母是题目中哈希函数的除数
例如,H(k)=k%5,则计算不成功ASL时,分母为5
哈希表的构造例题:
解题步骤:
- 第一步:算出表长,即m=p/装填因子 (p为模)
本题就是,m=7/0.7=10
- 第二步:根据哈希函数
H(key)=(key×3) mod 7
,进行计算填表
(7*3) mod 7=0;
(8*3) mod 7=3;
(30*3) mod 7=6;
(11*3) mod 7=5;
(18*3) mod 7=5;
(9*3) mod 7=6;
(14*3) mod 7=0;
下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|---|
关键字 | 7,14 | 8 | 11,18 | 30,9 |
- 第三步:因为发生了哈希冲突,采用线性探测法方法解决哈希冲突
首先,根据第一步计算表长得到m=10
其次,用线性探测法中除余的是表长 %m
①当进行到18时存在哈希冲突,且d0=h(k)=(18*3)mod7=5(无空位),d1=(5+1)%10=6(无空位),d2=(6+1)%10=7(有空位,填入) 探测次数=3
②同理,进行到9也存在哈希冲突,d0=h(k)=(9*3)mod7=6(无空位),d1=(6+1)%10=7(无空位),d2=(7+1)%10=8(有空位,填入) 探测次数=3
③同理,进行到14也存在哈希冲突,d0=h(k)=(14*3)mod7=0(无空位),d1=(0+1)%10=1(有空位,填入) 探测次数=2
以18为例:
最终的哈希表为:
下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|---|
关键字 | 7 | 14 | 8 | 11 | 30 | 18 | 9 | |||
探测次数 | 1 | 2 | 1 | 1 | 1 | 3 | 3 |
- 第四步:计算ASL成功和不成功
ASL成功=(1*4+2*1+3*2)/7=12/7
ASL不成功=(3+2+1+2+1+5+4+3+2+1)/7=24/7
哈希表相关代码
- 哈希表的结构体定义
#define MaxSize 100 //定义最大哈希表长度
#define NULLKEY -1 //定义空关键字值
#define DELKEY -2 //定义被删关键字值
typedef int KeyType; //关键字类型
typedef char * InfoType; //其他数据类型
typedef struct node
{
KeyType key; //关键字域
InfoType data; //其他数据域
int count; //探查次数域
} HashTable[MaxSize]; //哈希表类型
- 建哈希表
void CreateHT(HashTable ha, KeyType x[], int n, int m, int p) //创建哈希表,x为输入数组,n输入数据个数,m为哈希表长度,这里假设m=p
{
int i;
int num=0;
for (i = 0; i < m; i++)
{
ha[i].key = NULLKEY;/*置为空*/
ha[i].count = 0;
}
for (i = 0; i < n; i++)
{
InsertHT(ha,num, x[i], p);
}
}
- 哈希表的插入
void InsertHT(HashTable ha, int& n, KeyType k, int p)//哈希表插入数据,n表示哈希表数据个数,k插入关键字,p除数
{
int adr;/*存地址*/
adr = k % p;
int time=0;/*探测次数*/
if (ha[adr].key==NULLKEY)/*说明这个位置没有数据*/
{
ha[adr].key = k;
ha[adr].count = 1;/*记为查找一次*/
}
else
{
while (ha[adr].key != NULLKEY)
{
adr = (adr + 1) % p;
time++;/*探测次数加1*/
}
time++;/*找到也加1*/
ha[adr].key = k;
ha[adr].count = time;
}
}
- 哈希表的查找
int SearchHT(HashTable ha, int p, KeyType k)//在哈希表中查找关键字k,找不到返回-1,找到返回查找地址。
{
int time=0;/*次数*/
int adr;
adr = k % p;
time++;
while (ha[adr].key != NULLKEY && ha[adr].key != k)
{
adr = (adr + 1) % p;
time++;
}
uns_count = time;
if (ha[adr].key == k)
{
return adr;/*找到返回地址*/
}
else
{
return -1;/*找不到返回-1*/
}
}
1.6.2 哈希链
哈希链解决冲突的方法
拉链法
把同义词用一个链表连接起来的方法,类似图结构的邻接表,它也有存放头结点的数组
优势:
- 处理冲突简单,无堆积现象,非同义词不会发生冲突;
- 结点空间时动态申请的,空间利用效率高;
- 在链表中,删除结点操作方便。
哈希链的时间性能
- 查找成功ASL
ASL成功=关键字k属于单链表中的第几个结点的和/n
- 查找不成功ASL
ASL不成功=地址i的结点个数和/p(计算不为空的链表的结点个数之和/题目所给函数mod的p)
有n个节点的链表,不成功的查找需要进行n次比较(注意,不计算空链表,在具体代码实现中,为空不会进入查找的比较代码中)
哈希链的构造例题:(此题忽略线性探测法,以链地址法为例)
解题步骤:
- 第一步:根据题目所给的散列表长度,画一个链表
- 第二步:根据哈希函数
H(k)=k mod 7
,进行计算填表,采用拉链法设计哈希链表
25 mod 7=4;
10 mod 7=3;
8 mod 7=1;
27 mod 7=6;
32 mod 7=4;
68 mod 7=7;
- 第三步:计算ASL成功和不成功
ASL成功=(1*5+2*1)/6=7/6
ASL不成功=(1*4+2*1)/7=6/7
哈希链相关代码
- 哈希链的结构体定义
typdef struct HashNode{
int key;/*关键字*/
struct HashNode *next;
}HashNode,*HashTable;
HashTable ht[max];
2.PTA题目介绍
2.1 是否完全二叉搜索树
将一系列给定数字顺序插入一个初始为空的二叉搜索树(定义为左子树键值大,右子树键值小),你需要判断最后的树是否一棵完全二叉树,并且给出其层序遍历的结果。
输入格式:
输入第一行给出一个不超过20的正整数
N
;第二行给出N
个互不相同的正整数,其间以空格分隔。输出格式:
将输入的
N
个正整数顺序插入一个初始为空的二叉搜索树。在第一行中输出结果树的层序遍历结果,数字间以1个空格分隔,行的首尾不得有多余空格。第二行输出YES
,如果该树是完全二叉树;否则输出NO
。输入样例1:
9 38 45 42 24 58 30 67 12 51
输出样例1:
38 45 24 58 42 30 12 67 51 YES
输入样例2:
8 38 24 12 45 58 67 42 51
输出样例2:
38 45 24 58 42 12 67 51 NO
2.1.1 设计思路
题目要求主要分为三点:
- 构造二叉搜索树
- 判断是否为完全二叉搜索树
- 对树进行层次遍历
-
构造二叉搜索树
根据二叉搜索树的定义,若左子树不空,左子树的所有结点的值都要比根节点小;若右子树不空,右子树所有节点的值都比根节点大,且左右子树也都是二叉排序树。同时,中序遍历的二叉搜索树可以得到一个递增的序列。
根据题意,该题定义的二叉搜索树是初始为空的且左子树键值大,右子树键值小。所以,该中序遍历后是一个递减的序列,在构造的时候需要让待插入比当前结点小的结点作为右子树,比当前结点大的作为左子树。
-
判断是否为完全二叉搜索树
根据完全二叉搜索树的定义,若设二叉树的高度为h,除最后一层外,其它各层 的结点数都达到最大个数,第h层有叶子结点,并且叶子结点都是从左到右依次排布,这就是完全二叉树。且层次遍历时遇到第一个空结点层次遍历恰好结束,如果遍历到一个结点只有左孩子,或者左右孩子均为空时,那么剩余未遍历的结点,全都是叶子结点。
-
时间复杂度为O(n)
2.1.2 伪代码
函数:
void Insert(BinTree*& BST, int k);//把结点k插入二叉排列树中
bool IsCompleteBinaryTree(BinTree*& BST, int n);//判断是否为完全二叉树
void LevelOutput(BinTree *BST);//层次遍历输出
构造二叉搜索树://注意:题目要求左子树键值大,右子树键值小
如果BST为空
建立新结点
如果结点k<BST->key
递归进入右子树
如果结点k>BST->key
递归进入左子树
判断是否为完全二叉树://即判断是否所有结点都已经遍历
queue存储树结点的队列
定义变量num来判断是否所有结点都已经遍历
if 树空
也算作是完全二叉树,返回true
end if
先把树的根节点放入队列
while 1
取队首node
如果为空则跳出循环
不为空则让左右子树进栈,先左后右
出栈队首并使得输出个数num增1
end while
判断num的值,等于节点个数n则说明在遇到空节点前树节点遍历完成,返回true,反之,则返回false
层次遍历输出类似于判断是否为完全二叉树的函数,其实两个函数可以结合
2.1.3 具体代码
2.1.4 提交列表
- 调试及提交过程遇到的问题:
- 一开始按照了常规的方法写了代码,但该题是左子树键值大,右子树键值小。
- 除了在判断是否为完全二叉树函数中要判断一次,主函数也要再次判断
- 计算每一层的结点个数容易忽略最左边的情况
2.1.5 本题知识点
-
完全二叉树
完全二叉树是特殊形态的满二叉树,在满二叉树的基础上,它的叶子结点可以不全满,且叶子结点整体偏左。 -
层次遍历
借助队列来储存二叉树每一层的元素并依次输出。 -
二叉搜索树的插入操作
边查找边插入,算法中的根节点指针要用引用类型,这样才能将改变后的值传回给实参。
2.2 航空公司VIP客户查询
不少航空公司都会提供优惠的会员服务,当某顾客飞行里程累积达到一定数量后,可以使用里程积分直接兑换奖励机票或奖励升舱等服务。现给定某航空公司全体会员的飞行记录,要求实现根据身份证号码快速查询会员里程积分的功能。
输入格式:
输入首先给出两个正整数N(≤105)和K(≤500)。其中K是最低里程,即为照顾乘坐短程航班的会员,航空公司还会将航程低于K公里的航班也按K公里累积。随后N行,每行给出一条飞行记录。飞行记录的输入格式为:
18位身份证号码(空格)飞行里程
。其中身份证号码由17位数字加最后一位校验码组成,校验码的取值范围为0~9和x共11个符号;飞行里程单位为公里,是(0, 15 000]区间内的整数。然后给出一个正整数M(≤105),随后给出M行查询人的身份证号码。输出格式:
对每个查询人,给出其当前的里程累积值。如果该人不是会员,则输出
No Info
。每个查询结果占一行。输入样例:
4 500 330106199010080419 499 110108198403100012 15000 120104195510156021 800 330106199010080419 1 4 120104195510156021 110108198403100012 330106199010080419 33010619901008041x
输出样例:
800 15000 1000 No Info
2.2.1 设计思路
题目要求主要为:对哈希链的操作
- 建立哈希链
- 修改哈希链数据
- 查找哈希链数据
建立哈希链,取乘客的身份证号后5位作为哈希地址进行存储,如果不是会员则添加,是会员就看里程数
但这题借用map容器更加方便,但因为身份证最后一位校验码有x,则需要将x转化为数字
时间复杂度:O(n²)
2.2.2 伪代码
方法一:用库函数#include <map>
map容器:
定义vip容器
map<long long, long long>vip
map<long long, long long>::iterator iter
while(n--)
如果最尾巴为x
将ID进行变化ID = ID * 10 + 10
查询距离遍历至结束
如果距离<k
则返回ID和distance两个值的函数
且相加多次距离
方法二:用哈希链
获得哈希表地址:
for i = 12 to i = 16
取后五位数字作为哈希地址
if 最后以为为x
temp乘10再累加
else
直接转换为五位数;
return temp;
在哈希链中寻找用户信息:
node = h[temp]->next;
while 当前用户有信息存储
if 用户信息与存档相匹配
return node;
else 用户信息与存档不相匹配
读取下一个信息;
end while
向哈希链中插入数据:
temp = GetID(ID);//获得信息·
p = Find(h, ID, temp);//核对信息
if 乘客是会员
if 要走的距离小于最小里程数
在本来的里程数上加上k算作最大里程
else
直接在原里程数上增加
end if
end if
else 乘客不是会员
新录入ta的信息成为会员
根据里程大小计算总里程
将新用户信息插入node中
end else
创建哈希链:
for i = 0 to i = n-1
输入ID以及航程dist
Insert(h, ID, dist, k);
end for
2.2.3 具体代码
方法一:用库函数#include <map>
方法二:用哈希链
2.2.4 提交列表
- 调试及提交过程遇到的问题:
- 取后几位作为哈希地址的问题感觉有点难找,取的小了数据运行缓慢且会溢出,然后大了就运算复杂。最后采取了取后5位作为哈希地址,如果最后一位为x则加10
- 建哈希链的时候,需要给它申请空间,初始化为空
2.2.5 本题知识点
- 哈希链的构造创建和寻找,建链前需要对哈希链进行初始化和申请空间。
- 哈希链拉链法的使用
- map库的使用
2.3 基于词频的文件相似度
实现一种简单原始的文件相似度计算,即以两文件的公共词汇占总词汇的比例来定义相似度。为简化问题,这里不考虑中文(因为分词太难了),只考虑长度不小于3、且不超过10的英文单词,长度超过10的只考虑前10个字母。
输入格式:
输入首先给出正整数N(≤100),为文件总数。随后按以下格式给出每个文件的内容:首先给出文件正文,最后在一行中只给出一个字符
#
,表示文件结束。在N个文件内容结束之后,给出查询总数M(≤104),随后M行,每行给出一对文件编号,其间以空格分隔。这里假设文件按给出的顺序从1到N编号。输出格式:
针对每一条查询,在一行中输出两文件的相似度,即两文件的公共词汇量占两文件总词汇量的百分比,精确到小数点后1位。注意这里的一个“单词”只包括仅由英文字母组成的、长度不小于3、且不超过10的英文单词,长度超过10的只考虑前10个字母。单词间以任何非英文字母隔开。另外,大小写不同的同一单词被认为是相同的单词,例如“You”和“you”是同一个单词。
输入样例:
3 Aaa Bbb Ccc # Bbb Ccc Ddd # Aaa2 ccc Eee is at Ddd@Fff # 2 1 2 1 3
输出样例:
50.0% 33.3%
2.3.1 设计思路
本题设计一个倒排索引表结构实现,单词作为关键字。我使用的方法,即将文件按照单词的分类,存储到每个单词的结构中,最后一个结构中保存了该单词所含有的文件。对于每个单词而言,可以使用哈希链来做,不过这里可以用 STL 库的 map 容器来存放。
ps:参考了林智凯学长的博客,其中提供了三种方法,我采用了第二种方法。
2.3.2 伪代码
从单词入手
定义int类型变量的两个文件内编号 file_a,file_b作为待查找的文件编号
定义变量类型为<string,int[]>的map容器构建单词索引表
根据单词在单词索引表中构建映射
for i=0 to 文件组数
输入文件编号
for iterator=map.begin() to !map.end()遍历单词
if 单词在两个文件都出现过
修正重复单词数,合计单词数
else if 单词在其中一个文件中出现过
修正合计单词数
end if
end for
end for
计算输出文件相似度
2.3.3 具体代码
2.3.4 提交列表
-
调试及提交过程遇到的问题:
- 一开始没有很清楚的解题思路,对set和map库函数没有很清楚
- 本题就是B树的延申,通过单词直接找到它出现在那些文件,map 容器主打的特点就是构建一个映射,并且内部实现的结构是红黑树
2.3.5 本题知识点
1.#include <map>容器
元素的遍历 map<string, int[200]>::iterator iterator;//map容器迭代器,遍历时使用
2.isalpha()函数:判断一个字符是否是字母
语法/原型:
int isalpha(int c);
参数 c 表示要检测字符或者 ASCII 码。
返回值:返回非 0(真)表示 c 是字母,返回 0(假)表示 c 不是字母。
3.tolower()函数:把给定的字母转换为小写字母
语法/原型:
int tolower(int c);
参数 c 表示要被转换为小写的字母。
返回值:如果c有相对应的小写字母,则该函数返回c的小写字母,否则c保持不变。返回值是一个可被隐式转换为 char 类型的 int 值。
4.迭代器(STL迭代器)iterator
5.拉链法构造哈希表:这里运用到了拉链法构造哈希表来保存文件内容