PAT Basic 1080 MOOC期终成绩 C语言(测试点3)

PAT Basic 1080 MOOC期终成绩 C语言(测试点3)

题目略: https://pintia.cn/problem-sets/994805260223102976/problems/994805261493977088

题解:

题目的要求就不再赘述了,并不难理解,要注意的有:

  • 合格条件是: 编程成绩>=200,总成绩>=60。所以对于编程成绩小于200(包括根本没有编程成绩的),可以直接剔除。所以,省事的做法就是只需要存储有编程成绩的学生,也就是第一部分的数据,对于后面期中和期末新出现的学生都可以直接忽略掉。对于没有期末成绩的,不必额外判断,因为按照成绩计算公式,期中只占40%,不影响最后结果。
  • 最后按总分降序排列,分数相同的按学号升序排列。
  • 总分要注意四舍五入,可以用math.h的round()函数或者加0.5强转int的方法。
  • 没有成绩的要显示-1,初始化的时候可以设定-1为默认值

对于C++,Java或者Python来说,这道题只要用一个map(hashmap))来建立字符串到整数索引的映射,用另外一个数组来顺序存储学生的信息即可。

但是对于C语言来说,没有map这样好用的数据结构,要么把字符串存到线性表里,用binerysearch O(logN)来查找,时间还是可以过的,不会超时,参考一个别人写的题解https://github.com/OliverLew/PAT/blob/master/PATBasic/1080.c

要么就自己构造一个简单的hashmap,查找只需要O(1)的常数时间。我选择了后者,顺便可以学习一下hashmap的基本原理。

第一次尝试:

建立两个数组,一个是map数组,作为散列表,下标代表hash值,元素值为录入学生数据的顺序(从0开始),另一个是arr数组,元素类型是结构体Stu。 凭学号经过BKDRHash()函数得到hash值,并对散列表长度求MOD后得到此字符串对应的在map数组的索引,进而得到arr数组中相对应的索引。

假如: missing这个人算出一个hash值为 12345(随便举例),map[12345]=1,arr[1]就存储missing这个人的信息。

为了保证表示散列表某个单元是否已经被占,我将其初始值设为-1,-1代表此单元为空。

但这个代码只通过了前三个测试点,测试点3大数据量的点一直错误。起初我以为是程序的逻辑有错误,经过多次检查和比较网上其他博客的解法后,断定计算逻辑是没错的。那么问题肯定出在自己构造的hashmap上。由于测试点3数据量较大(10000),我猜是hash冲突了,也就是两个不同的字符串得到hash值MOD SIZE后相等的情况,比如 5%7 == 9%7,实际上这个现象比想象中更常见。在C++和Java中的hashmap实现中,解决hash冲突的方法是把冲突的多个数据存在链表或者二叉树中。此外,解决hash冲突的方法还有开放寻址法。我选择实现开放寻址法来解决hash冲突。代码在后面。

History 卡测试点3

#include 
#include 
#include 
typedef struct Node{
	char name[21];
	int pro;	//编程成绩
	int mid;	//期中	
	int final;	//期末
	int sum;	//总成绩
}Stu;
// BKDR Hash Function
unsigned int BKDRHash(char *str)
{
    unsigned int seed = 131; // 31 131 1313 13131 131313 etc..
    unsigned int hash = 0;
 
    while (*str)
        hash = hash * seed + (*str++);
 
    return (hash & 0x7FFFFFFF) %57713;
}

int comp(Stu *a,Stu *b){
	if(a->sum == b->sum){
		return strcmp(a->name,b->name);
	}else{
		return b->sum - a->sum; // a>b -
	}
}

int main(){
	int P,M,N;
	int map[57713],count=0;	//count是存入的顺序
	Stu arr[10001];			//顺序保存学生的信息
	scanf("%d %d %d",&P,&M,&N);
	for(int i=0;i<57713;i++) map[i]=-1;	//每一个单元都初始化为-1
	for(int i=0;i<P;i++){
		char name[21];
		int program;
		scanf("%s %d",name,&program);
		if(program >= 200){	//编程成绩小于200的直接忽略
			int hash = BKDRHash(name);
			map[hash] = count;	    
			memcpy(arr[count].name,name,sizeof(char)*21);
			arr[count].mid = -1;	//要注意初始化为-1,有可能没有成绩,导致最后输出出错
			arr[count].final = -1;
			arr[count].pro = program>=0&&program<=900? program:-1;
			count++;	//下一个学生
		}
	}
	for(int i=0;i<M;i++){
		char name[21];
		int m;
		scanf("%s %d",name,&m);
		int hash = BKDRHash(name);
		if(map[hash] !=-1 ){	// 如果此学生已经被记录,也就是说明有编程成绩
			arr[map[hash]].mid = m<=100?m:-1;
		} 
		
	}
	for(int i=0;i<N;i++){
		char name[21];
		int f;
		scanf("%s %d",name,&f);
		int hash = BKDRHash(name);
		int idx = map[hash];
		if(idx != -1){	// 如果此学生已经被记录,也就是说明有编程成绩
			arr[idx].final = f<=100?f:-1;	
			if(arr[idx].mid > arr[idx].final){//计算总成绩
				arr[idx].sum = (int)(arr[idx].mid * 0.4 + arr[idx].final*0.6 + 0.5);
			}else{
				arr[idx].sum = arr[idx].final;
			}
		}
	}
	qsort(arr,count,sizeof(struct Node),comp);//按要求排序
	for(int i=0;i<count;i++){
		if(arr[i].sum >= 60)
			printf("%s %d %d %d %d\n",arr[i].name,arr[i].pro,arr[i].mid,arr[i].final,arr[i].sum);
	}
	return 0;
}

第二次尝试

简述一下开放寻址法

开放寻址法的思想非常简单:如果有冲突产生,那么就要尝试寻找新的可用单元。

数学上表达为 h(X) = (Hash(X) + F(i)) mod TableSize 其中F(i)是冲突解决办法。

具体来说,发生了hash冲突时,由于hash(key1)==hash(key2),导致key2要存入的单元被key1占了,就要给key2找一个新家。我们可以去看看下一个单元,也可以每隔S个单元看一下,等等,直到找到可用的空单元。

开放寻址法有多种实现方法,这里也不赘述。我选用的是平方探测法。具体原理来自《数据结构与算法分析 – C语言描述》118–122页。

平方解决函数的定义为:F(i) = F(i-1) + 2i - 1

由于存在冲突的可能,新的散列表里就不能只是存储count值,也要把字符串也存起来,以供查找。于是新的map数组元素类型是Cell结构体, key为学号字符串,val为原来的count值。新元素插入的下标由开放寻址法决定

代码如下

#include 
#include 
#include 

#define SIZE 57713
typedef struct Stu{
	char name[21];
	int pro;
	int mid;
	int final;
	int sum;
}Stu;

typedef struct Cell{
	char key[21];
	int val;
}Cell;

// BKDR Hash Function
unsigned int BKDRHash(char *str)
{
    unsigned int seed = 31; // 31 131 1313 13131 131313 etc..
    unsigned int hash = 0;
 
    while (*str)
        hash = hash * seed + (*str++);
 
    return (hash & 0x7FFFFFFF) % SIZE;
}
/*
此函数利用开放寻址法,实现从散列表中查找的功能。
对于新加入的key,将解决冲突并返回下一个可用的空单元地址。
对于已经存在的key,将返回此key的单元地址。
*/
int find(Cell *map,const char *key){
	int hash = 	BKDRHash(key);
	int step = 0;
	while(map[hash].val != -1 && strcmp(map[hash].key,key)!=0){
		hash += 2*(++step) - 1;
		if(hash >= SIZE)
			hash -= SIZE;
	}
	return hash;
}
/*
此函数利用开放寻址法,实现从散列表中插入的功能。
返回是否插入成功(是否已经存在)
*/
int insert(Cell *map,const char *key, int val){
	int hash = find(map,key);	
	if(map[hash].val == -1){	// 如果find函数返回的是个空单元,说明可以插入;否则将什么也不做
		map[hash].val = val;
		memcpy(map[hash].key,key,sizeof(char)*21);
		return 1;
	}	
	return 0;
}

int comp(Stu *a,Stu *b){
	if(a->sum == b->sum){
		return strcmp(a->name,b->name);
	}else{
		return b->sum - a->sum; 
	}
}

int main(){
	int P,M,N,count=0;
	Cell map[SIZE];	 //新的散列表
	Stu arr[10001];
	scanf("%d %d %d",&P,&M,&N);
	for(int i=0;i<SIZE;i++) 
		map[i].val=-1; 	// -1代表空单元
	
	for(int i=0;i<P;i++){
		char name[21];
		int program;
		scanf("%s %d",name,&program);
		if(program >= 200){
			int flag = insert(map,name,count);	
			memcpy(arr[count].name,name,sizeof(char)*21);
			arr[count].mid = -1;
			arr[count].final = -1;
			arr[count].pro = program>=0&&program<=900? program:-1;
			if(flag) //只有插入成功,才让count递增,题目说保证学号不会重复,所以可以省略这个判断
                count++;	
		}
	}
	
	for(int i=0;i<M;i++){
		char name[21];
		int m;
		scanf("%s %d",name,&m);
		int hashidx = find(map,name);	// hashidx是map数组的索引
		int idx = map[hashidx].val;		// idx是arr数组的索引
		if(idx != -1 ){
			arr[idx].mid = m<=100?m:-1;
		} 
		
	}
	for(int i=0;i<N;i++){
		char name[21];
		int f;
		scanf("%s %d",name,&f);
		int hashidx = find(map,name);
		int idx = map[hashidx].val;
		if(idx != -1){
			arr[idx].final = f<=100?f:-1;	
			if(arr[idx].mid > arr[idx].final){
				arr[idx].sum = (int)(arr[idx].mid * 0.4 + arr[idx].final*0.6 + 0.5);
			}else{
				arr[idx].sum = arr[idx].final;
			}
		}
	}
	qsort(arr,count,sizeof(struct Stu),comp);
	for(int i=0;i<count;i++){
		if(arr[i].sum >= 60)
			printf("%s %d %d %d %d\n",arr[i].name,arr[i].pro,arr[i].mid,arr[i].final,arr[i].sum);
	}
	return 0;
}


测试点 结果 耗时 内存
0 答案正确 3 ms 1792 KB
1 答案正确 3 ms 1824 KB
2 答案正确 2 ms 1892 KB
3 答案正确 12 ms 2304 KB

平时练习多造造轮子,可以加深对一些底层知识的理解。但是赶时间/考试/竞赛/项目 就没必要了~

你可能感兴趣的:(PAT,Basic(乙级))