0.PTA得分截图
1.本周学习总结(0-4分)
1.1 总结查找内容
查找的性能指标ASL
- ASL:关键字的平均比较次数,也叫做平均搜索长度。
- 公式:ASL=∑(i=1->n)p(i)*c(i);其实就是ASL=n个关键字找到时的比较次数之和/n
静态查找
动态查找
- 在查找的基础上,进行插入或是删除的操作。
顺序查找
- 就是按照表的下标顺序,一个个比较直到找到或是遍历结束未找到。(举例如下)
- 顺序表的结构体定义:
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)=n
- 具体数据的asl的计算:
- 成功的ASL=(1+2+3+4+5+6+7)/7=28/7=4(对n个数据的顺序表,成功的ASL=表长的一半)
- 不成功的ASL=7(遍历结束也没有找到,对n个数据的顺序表,不成功的ASL=n)
二分查找
- 在表按数据的大小顺序排序的前提下,进行查找。
- 如对一个有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为止
- 代码的实现:
时间复杂度O(n)=log₂n - 具体数据的asl的计算:(引入判定树进行分析)
判定树
-
构成:以顺序表的中间值作为根,中间值左边为左子树,右边是右子树;判定树的中序遍历得到的是一个从大到小的顺序序列(如果规定左小右大)
-
举例:
- 成功的ASL=(1×1+2×2+3×4)/(1+2+4)=17/7(找到对应次数是对应树节点所在的深度)
- 不成功的ASL=(3×8)/8=3(找不到对应次数是对应范围的根节点所在深度,由上一级判断得到)
-
树中第i层上的记录个数为2^(i-1),查找该层上的每个记录需要进行i次比较;
-
当n比较大时,判定树看成内部节点的总数为n=2^(h)-1,高度为h=log₂(n-1)的满二叉树。
-
比较:二分查找效率较高,成功时最多比较次数为log₂(n+1),,不成功的比较次数最多也是log₂(n+1)
二叉排序树(二叉搜索树)
- 定义:为空或满足条件:若左子树不空,左子树的所有结点的值都要比根节点小,若右子树不空,右子树所有节点的值都比根节点大,且左右子树也都是二叉排序树。(二叉排序树中没有相同的节点)
- 特点:中序遍历二叉排序树会得到一个递增的序列。
- 结构体定义:
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;/*找到则返回*/
}
}
- 删除
- 删除分三种情况:一是被删节点是叶子节点,那么直接删除即可,也就是让它的双亲结点为空;二,若被删节点只有左子树或是只有右子树,即有一子树为空,则让不为空的子树取代被删结点,也就是让它的双亲结点指向它的不空子树;三,若被删结点同时存在左右子树,那么,可用它的前驱结点(左子树中最大的数,即左子树的最右结点)取代它,或是用它的后继节点(右子树中最小的节点,即右子树中最左的节点)取代它。
- 代码实现
- 删除分三种情况:一是被删节点是叶子节点,那么直接删除即可,也就是让它的双亲结点为空;二,若被删节点只有左子树或是只有右子树,即有一子树为空,则让不为空的子树取代被删结点,也就是让它的双亲结点指向它的不空子树;三,若被删结点同时存在左右子树,那么,可用它的前驱结点(左子树中最大的数,即左子树的最右结点)取代它,或是用它的后继节点(右子树中最小的节点,即右子树中最左的节点)取代它。
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;
}
AVL树(平衡二叉树)
- 定义:左右子树都是平衡二叉树,且每个结点需满足“左右子树深度的差的绝对值<=1”。
- 平衡因子:这个节点的左右子树的高度差
- 结构体定义:
typedef struct node
{
KeyType key; //关键字域
int bf; //平衡因子
InfoType data; //其他数据域
struct node *lchild, *rchild;/*左右孩子*/
}BSTNode, * BSTree;
- 当我们在平衡二叉树中插入一个新的节点的时候,可能会使得二叉树失衡,我们把调节平衡的过程称作平衡旋转。以下有四种调整的做法:
-
LL平衡旋转(某节点的左子树的左子树插入一个新节点使得这个节点失衡)
进行顺时针旋转:失衡节点的左孩子上转代替失衡节点,失衡节变为左孩子的右孩子,而原本的右孩子则变为失衡节点的左孩子。简单说,就是失衡节点变为失衡节点的左孩子的右孩子,而原本的右孩子作为失衡节点的左孩子。
-
RR平衡旋转(某节点的右子树的右子树插入一个新节点使得这个节点失衡)
进行逆时针旋转:失衡节点的右孩子上转代替失衡节点,失衡节变为右孩子的左孩子,而原本的左孩子则变为失衡节点的右孩子。简单说,就是失衡节点变为失衡节点的右孩子的左孩子,而原本的左孩子作为失衡节点的右孩子。
-
LR平衡旋转(某节点的左子树的右子树插入一个新节点使得这个节点失衡)
失衡节点左孩子的右孩子取代失衡节点,失衡节点作为它的右孩子,原本的右孩子为失衡节点左孩子,原本左孩子取代这个点的位置。
-
RL平衡旋转(某节点的右子树的左子树插入一个新节点使得这个节点失衡)
失衡节点右孩子的左孩子取代失衡节点,失衡节点作为它的左孩子,原本的左孩子为失衡节点右孩子,原本右孩子取代这个点的位置。
-
B-树和B+树定义。
- 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个外部节点
- 结构体定义:
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+树有两个头节点,一个指数的根节点,一个指最小关键字的叶子节点(即线性链表的头结点);
散列查找
- 哈希表(又称散列表),是一种存储结构,适用于记录的关键字与存储的地址存在某种关系的数据。
- 哈希表的设计:主要是为了解决哈希冲突。在实际应用中,哈希冲突是难以避免的,而它主要与三个因素有关。
- 哈希表长度(哈希表长度与装填因子有关,而装填因子α=存储的关键字个数/哈希表的大小,α越大冲突可能性越大,最大可取值为1,最好控制在0.6~0.9的范围内)
- 采用的哈希函数
- 解决哈希冲突的方法
哈希函数构造方法
- 直接定址法:以关键字本身加上一个常数作为哈希地址的方法,函数表示为h(k)=k+c,(c为常数)
- 优点:计算较为简便且不会造成哈希冲突
- 缺点:如果关键字分布不连续(不集中),较为分散,那么容易造成大量空间的浪费
- 除留余数法:用关键字除以某个不大于哈希表长度的数得到的余数作为地址的方法,函数表示为h(k)=k mod p,(mod表示取余,p<=m)
- 注意,用于取余的数p,最好是素数,这样减少了造成哈希冲突的可能。
- 数字分析法:对关键字进行分析,采取最为合适的方法,对数据进行处理。
- 例如:对于公民的身份证号这一数据来说,因为它的构成与地区、个人生日有关,靠前的数字大多代表了一个地区、或是生日的日期,而在地区相同的人可能会有很多,就生日而言,相同年份出生的人也很多,所以身份证后几位数字,相对于靠前的数字而言,重复的可能性就比较小。这样,我们可能就会采用对后几个数字进行处理的方式来构造哈希函数。(参考pta的“航空公司VIP客户查询”的这道题)
哈希表解决冲突的方法
- 线性探查法:简单来说,就是当这个关键字最初确定的那个地址已经被其他关键字占了的时候,从这个位置开始,逐个往下寻找空地址,遇到的第一个空地址用于存放目前的这个关键字。
- 公式为:
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,······
- 它可以避免出现堆积现象,但是它没有办法探测到哈希表上的所有单元,但至少能探测到一半。
- 公式为:
哈希表相关操作函数
- 结构体定义:
#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*/
}
}
拉链法(把同义词用一个链表连接起来的方法,类似图结构的邻接表,它也有存放头结点的数组)
- 哈希链的结构体定义:
typdef struct HashNode{
int key;/*关键字*/
struct HashNode *next;
}HashNode,*HashTable;
HashTable ht[max];
哈希表和哈希链对比
- 例题:有7个关键字{12,24,23,29,44,80,94},函数为h(k)=k mod 11, 设置哈希表长度为13
- 哈希表:
- 成功的ASL:(1×5+2×2)/7=9/7;(就是关键字对应的探测次数/关键字个数)
- 不成功的ASL:(6+5+4+3+2+1+3+2+1+1+1)/11=29/11;[就是根据题目所给函数的mod的那个p,(取下标为0~p-1的关键字所对应的探测不成功次数相加)/p),其中,不成功次数是这个格子到下一个为空的单元格的距离,取单元格为空的次数为1,下一个单元格为空的单元格对应次数为2,以此类推]
- 哈希链:
- 成功的ASL:(16+21)/7=8/7;(本题,头结点后第1个节点个数为6,所以是16,第二个节点个数为1,所以是21,也可以说是竖着数的层数)(头结点后第n个节点个数*对应的n的值相加/关键字个数)(空的头结点不算在内,只算关键字)
- 不成功的ASL:(15+21)/11=7/11;(就是,计算不为空的链表的结点个数之和/题目所给函数mod的那个p;这是因为,有n个节点的链表,不成功的查找需要进行n次比较)(注意,不计算空链表,在具体代码实现中,为空不会进入查找的比较代码中)
1.2.谈谈你对查找的认识及学习体会。
对查找这块内容来说,新接触到的,比较有意思,是哈希表的使用。而对哈希表这个概念来说,其实我们在学数组的时候好像就有接触到一点,那会儿是利用数组的下标为0或1来标记这个数字是否出现过,下标就代表了对应的数字。这个方法的思路与哈希表其实是相似的。哈希表这一块的内容,其实思路是比较好理解的,但是它的ASL的计算,会容易出错,特别是在计算不成功的ASL的时候,需要记住它的分母是与构造函数中mod的那个值有关的,不要与哈希表大小混在一起了。再有就是,对于数的应用,主要学的是与排序有关的数结构类型,在解题过程中,自己尝试着画出图形模拟构造过程的时候,思路也会慢慢清晰。总的来说,这一部分内容,在对旧知识进行总结的同时,还涉及了新知识的拓展与应用。我能看到的是,存储数据的结构越来越具有特点,思路越来越简单,代码和运行越来越好。这也许也是人们思路的发展历程的另一种体现。最后,我还是很想吐槽,哈希链,这个链表的操作,真的很难找错啊😬,树结构也是,但明显,链结构更不好,因为它永远只会编译出错,一调试就出错,从来不告诉你,大概哪里有问题😭。果然,还是我对链表的操作不够熟悉吧。容易有缺漏。
2.PTA题目介绍(0--6分)
自选3题,必须是编程题,必须包含知识点:
二叉搜索树
哈希表
2.1 7-1 是否完全二叉搜索树 (30分)
2.1.1 该题的设计思路
-
分析:题目要求做的主要有三点:一是构造二叉搜索树,二是判断它是否为完全二叉搜索树,三是对树进行层次遍历。很明显,这道题的结构,是树。
-
首先我们复习一下二叉搜索树
- 它的定义:为空或满足条件:若左子树不空,左子树的所有结点的值都要比根节点小,若右子树不空,右子树所有节点的值都比根节点大,且左右子树也都是二叉排序树。(二叉排序树中没有相同的节点)
- 它的特点是:中序遍历二叉排序树会得到一个递增的序列。
-
完全二叉树:
- 定义:若设二叉树的高度为h,除最后一层外,其它各层 的结点数都达到最大个数,第h层有叶子结点,并且叶子结点都是从左到右依次排布,这就是完全二叉树。
- 特点:层次遍历,遇到第一个空节点时,层次遍历刚好完成了。
-
结合上述分析、对知识点的回顾以及题目的具体要求,我们知道,二叉搜索树是由我们自己构造的,所以在第二步的判断中,我们只需要判断这棵二叉搜索树是不是完全二叉树,而第三步的层次遍历,是我们以及学过的内容。
-
时间复杂度:O(n)
2.1.2 该题的伪代码
- 建树:
建树函数void CreateBST(BinTree& BST, int n)
初始化BST=NULL;
for 0 to n
读取结点值x并调用插入函数插入树中
end for
插入函数void Insert(BinTree& BST, int X)
if 树空
申请树节点并初始化左右孩子,讲节点值存入结构体
end if
else if 插入节点值大于树根节点对应值
调用插入函数,用递归的方法插入左子树中Insert(BST->Left, X);
end else
else if 插入节点值小于树根节点对应值
调用插入函数,用递归的方法插入左子树中Insert(BST->Right, X);
end else
- 判断是否是完全二叉搜索树(在此只需判断是否完全二叉树)
用于判断的函数bool IsCompleteBinaryTree(BinTree& BST, int n)
定义num来计算层次遍历可输出的个数,队列tree存树节点,
if 树空
也算作是完全二叉树,返回true
end if
先把树的根节点放入队列
while 1
取队首node
如果为空则跳出循环
不为空则让左右子树进栈,先左后右
出栈队首并使得输出个数num增1
end while
判断num的值,等于节点个数n则说明在遇到空节点前树节点遍历完成,返回true,反之,则返回false
- 层次遍历与用于判断的函数类似,不过是去掉了计算个数的代码,加上了输出的代码,就不重复写了。
- 代码:
2.1.3 PTA提交列表
- 调试及提交过程遇到的问题:
- 这道题有个坑,就是,它定义的二叉搜索树,是左子树键值大,右子树键值小,而不是我们平时说的左小右大,这个需要注意,在建树的时候需要特别注意,否则最后的输出结果会有问题。
- 还有就是,它的建树,应该是需要分装函数的,把建树和插入分开,我第一次写的时候,是直接把建树的for循环部分放在main函数中,然后设置插入函数,看上去是没有问题,但是VS调试会出错,pta没有编译错误,但是测试点不会全过。我思来想去,这其他的也没什么毛病,找了同学的代码,对比了一下,大体思路都是一样的,然后我学着那份代码,分装了我的函数,其他的判断、插入、层次遍历,一个没改,就可以过了。
- 提交列表编译错误是我误删了符号的意外,其他的部分正确是我错误的修改。
2.1.4 本题设计的知识点
1.二叉搜索树的建立,主要得先明白它的定义和特点,才好结合着写代码。
2.完全二叉树的判断,当然,还是得从定义出发,结合它的特点,对它进行分析。
3.层次遍历,这是之前学过的内容,主要是利用队列的存储结构,进行临时存储和遍历。
2.2 7-4 整型关键字的散列映射 (25分)
2.2.1 该题的设计思路
- 分析:主要是根据除留余数法构造哈希表,然后用线性探测的方法解决哈希冲突。
- 思路,逐个对关键字进行处理,先用除留余数法得到哈希地址,再判断地址是否冲突,冲突则需进行对应处理,但在哈希表中,每个数只能存在一次,不能够有重复的数据。
- 时间复杂度:O(n)
2.2.2 该题的伪代码
- 输出哈希表对应数据下标void Hash(int n, int p)
定义map类数据m标记数据是否出现过,数组h为哈希表
for 1 to n
读入一个数number
if 判断number是否出现过(!m[number])
第一次出现则标记这个数据(m[number]++;)
end if
else
之前出现过,则调用find函数找这个数对应的位置并输出
end else
对number取p的余数得到remainder
if 对应位置为空则不会产生冲突(h[remainder] == 0)
把number存入这个位置并输出此位置
end if
else
for 1 to p
根据线性探测公式得remainder = (number + j) % p;
if 冲突已解决(即(h[remainder] == 0)
存number并输出位置,
跳出循环
end if
end for
end else
end for
- 简单的寻找函数int find(int h[], int x,int p),在哈希表中找x
for 0 to p
if 找到(h[i] == x)
返回位置下标
end if
end for
返回0表示没找到
- 代码:
2.2.3 PTA提交列表
- 调试及提交过程遇到问题:
- 题目给定的N,表示的是关键字的个数,而不是关键字的最大值,而给的取余的数P,比N大,是不确定的,而哈希表长度至少得和P一样大才可。我最开始以为N是关键字的最大值,定义的哈希表就比较小,这就导致了大数据会出错。
- 在写的时候,我忘记了,哈希表中,不能够存在重复的数据,我最开始根本就没有考虑重复数据的问题,所以它的重复数据会都存入,每个数据按不同读入的顺序会有不同的位置下标,这也会出错。在对重复的数据进行处理的时候,我写了一个find函数用来找哈希表中存在的这个关键字,但是我这会儿突然想到,其实可以先算它的余数,然后从这个余数开始遍历寻找位置,没有必要从0开始。
- 我得提醒下自己,在控制输出的时候,千万记得改flag的值,不能再犯这种错误了。
2.2.4 本题设计的知识点
1.主要就是考哈希表,在理解除留余数法和线性探测的基础上做题。用数组代替哈希表的结构体,直接进行模拟,类似之前遇到栈或队列问题也可用数组模拟一样。
2.在建哈希表前,千万记得对表做初始化操作,这才能区分开它是为空或是不为空。
3.对重复数据的标记,可用map容器,也可另外用数组标记。
2.3 7-5(哈希链) 航空公司VIP客户查询 (25分)
2.3.1 该题的设计思路
- 分析:这题,需要存储的是身份证号,以及对应的飞行记录。主要考察的是,对哈希链的操作。涉及了建哈希链、改哈希链数据、找哈希链中的数据。
计算代码的时间复杂度。
2.3.2 该题的伪代码
- 伪代码
建哈希链void Create(HashList* h, int n, int k)
for 0 to n
输入id和距离dist
调用函数插入数据Insert(h, id, dist, k);
end for
插入数据void Insert(HashList* h, char id[], int dist, int k)
调用函数计算哈希地址adr = GetAdr(id);
调用函数找节点p = FindNode(h, id, adr);
if p不为空说明找到
根据里程大小dist,加入p点对应的里程中
end if
else
申请节点空间node
把id赋值给node的对应数据:strcpy(node->number, id);
根据里程大小分情况存入node对应的数据
将node插入哈希链h[adr]中
end else
计算哈希地址int GetAdr(char id[])
初始化adr=0;
for 12 to 16取后6位
adr = adr * 10 + id[i] - '0';
end for
最后一位是x则加10,其他则直接加
adr=adr%max;
返回adr
在哈希链中找用户HashList FindNode(HashList* h, char id[], int adr)
取node = h[adr]->next;
while node不为空
if 找到
返回node节点
end if
else
node下移
end else
end while
返回空
- 代码:
2.3.3 PTA提交列表
- 调试及提交列表所遇到的问题:
- 最开始我纠结于id是要用字符串数组还是string,但是在用string类型的时候,不知道为什么,在写查找函数的时候,比较id的那个条件,我直接写
==
是不可行的,然后我又查了string的函数,还是会出错,我就改用了char,现在想想,可能是因为我的头文件写了string,然后有冲突之类的吧,也有可能是因为我建哈希链的时候没建好。 - 建哈希链的时候,我没有给它申请空间,也没有初始化为空,所以就导致我调试出错。卡在建链上了。
- 中间有一部分是,我忘记了考虑里程的大小情况,只在第一次插入的时候判断它是否比k大,在后续存在节点修改里程的时候,忘记考虑了,所以会导致输出错误的数据。
- 我后面的段错误,我最开始以为是我最大值定义的问题,后来发现,我重复申请了哈希链的空间,还有就是,我建链表的时候,读取id的scanf有问题,本身是地址,是不能加&的,这也导致我出错。
- 最开始我纠结于id是要用字符串数组还是string,但是在用string类型的时候,不知道为什么,在写查找函数的时候,比较id的那个条件,我直接写
2.3.4 本题设计的知识点
1.哈希链的构造,特别注意,建链前需要对哈希链进行初始化和申请空间。
2.哈希链的寻找,和邻接表类似,确定头结点后,遍历头结点后的链表节点,比较对应的数据即可。
3.哈希地址的确定,用的是除留余数法,但也涉及了一点数字分析法,因为身份证号的构成比较特殊且数字较多,需要对其进行一定的分析。