之前用C++解决了这个问题,详情可以参考Codeup[100000596]Course List for Student (25)-C++版。
今天打算使用C来解决相同的问题。
相比使用C++的方式,使用C语言需要编写的代码量会更多一些,但是也相比更简单明了。
要想该题目不会执行超时,需要将用户名转换为整型的哈希值,如果采用一般顺序搜索的方式,那么当要搜索的用户名比较多时,那么搜索的时间也会随之增加。对于N个用户,搜索的时间复杂度将为O(N),而使用哈希表的方式则只有O(1)。
要转换为哈希值,为了避免冲突,可以使用下面哈希值的方式。
( 2 6 3 + 2 6 2 + 2 6 1 ) × 10 + 10 (26^{3}+26^{2}+26^{1})\times 10 + 10 (263+262+261)×10+10
为何要采用这样的方式呢,由于用户名是3位大写的字母外加1个数字组成。我们对每位字母按照其字母顺序获取对应的值,比如首位字母A取值为
26
×
26
×
1
26\times 26\times 1
26×26×1,第2位取值只能为
26
×
1
26\times 1
26×1,而最后一位出现的字母A其值只能为1。这样才能避免由于字母相同而顺序不同造成哈希值冲突的问题。
由于最后一位为数字,其取值有10种可能,因此需要将前面字母组合后的值放大10倍才能避免冲突,否则直接相加,那么就会出现OOG1和OOH0哈希值是相同的问题,均为9835。而放大10倍后,那么OOG1的值为 98341,而OOH0为98350。
当然哈希值的方法也可以使用之前文章中10进制的方式,那样仍然也是唯一的,只是空间的利用率只有27%,而这种方式可以充分利用每个值。
下面是第1种哈希值计算的实现方式:
int get_name_id(const char *name) {
int n = 0, k=1;
for (int i = 2; i >= 0; --i) {
n += k * (name[i] - 'A');
k *= 26;
}
n *= 10;
n += name[3] - '0';
return n;
}
我们从低位开始计算每位的幂,然后进行累加从而得到其对应的哈希值。
接着是第2种实现方式:
int get_name_id(const char *name) {
int n = 0;
for(int i = 0; i < 3; i++)
n = 26 * n + (name[i] - 'A');
n = n * 10 + (name[3] - '0');
return n;
}
第2种实现方式相比更为晦涩难以理解。它的下1个值是上1次值的乘积的累加。对于OOG1,其中O的值为14,G为6。因此其过程如下:
第1次: 14 第2次: 14 * 26 + 14 = 378 第3次: (14 * 26 + 14) * 26 + 6 = 14 * 26 * 26 + 14 * 26 + 6 = 9834
其结果与第1种方式得到的结果一致。
下面是我们实现的代码:
#include <stdio.h>
#include <stdlib.h>
typedef struct {
int *s;
int capability;
int length;
} Students;
int get_name_id(const char *name) {
// 获取用户ID
int n = 0;
for (int i = 0; i < 3; i++)
n = 26 * n + (name[i] - 'A');
n = n * 10 + (name[3] - '0');
return n;
}
int int_compare(const void *p1, const void *p2) {
int *a = (int *) p1;
int *b = (int *) p2;
return *a - *b;
}
int main() {
int K, N;
// K是课程数量,N是学生数量
while (scanf("%d %d", &N, &K) != EOF) {
int relation[175771] = {0};
int id = 0;
int n, num;
int s_size = 100;
int size = 50;
char name[5] = {0};
int index = 0;
int *p = NULL;
// 初始化学生结构体数组
Students *students = (Students *) calloc(s_size, sizeof(Students));
Students *stu = NULL;
// 初始化其容纳范围
for (int l = 0; l < s_size; ++l) {
students[l].capability = size;
int *s = (int *) calloc(size, sizeof(int));
students[l].s = s;
}
for (int k = 0; k < K; ++k) {
scanf("%d %d", &n, &num);
for (int j = 0; j < num; ++j) {
//获取学生及选择课程的数量
scanf("%s", name);
id = get_name_id(name);
if (relation[id] == 0) {
index++;
// 记录哈希值对应的索引值
relation[id] = index;
// 超过容量则扩容,大小为原来的一倍
if (index >= s_size - 1) {
stu = (Students *) realloc(students, s_size * 2 * sizeof(Students));
if (stu == NULL) {
printf("Cannot allocate\n");
} else {
// 初始化扩容后结构体的成员
for (int i = index + 1; i < s_size * 2; ++i) {
stu[i].capability = size;
int *s = (int *) calloc(size, sizeof(int));
stu[i].s = s;
}
s_size *= 2;
// 指针指向原有的结构体
students = stu;
}
}
students[index-1].s[students[index-1].length++] = n;
} else {
//对应用户存在
int r = relation[id] - 1;
int length = students[r].length;
if (length >= students[r].capability - 1) {
//扩容其课程容量
int n_size = students[r].capability + 10;
p = (int *) realloc(students[r].s, n_size * sizeof(int));
if (p != NULL) {
students[r].s = p;
students[r].capability = n_size;
} else {
printf("cannot allocate\n");
}
}
students[r].s[students[r].length++] = n;
}
}
}
for (int i = 0; i < N; ++i) {
scanf("%s", name);
id = get_name_id(name);
index = relation[id] - 1;
int length = students[index].length;
printf("%s %d", name, length);
// 按照大小顺序排序课程号
if (length > 1) {
qsort(students[index].s, length, sizeof(students[index].s[0]), int_compare);
}
if(length>0){
printf(" %d",students[index].s[0]);
for (int j = 1; j < length; ++j) {
printf(" %d", students[index].s[j]);
}
}
printf("\n");
}
if (p != NULL) {
free(p);
}
if (stu != NULL) {
free(stu);
}
}
return 0;
}
在这里我们定义了1个结构体Students,其有3个成员,其中s表示课程号的数组,是1个整型指针,而capability表示其容纳能力,可以容纳多少个课程号,而length表示当前已经存入多少个课程号。
当存入的课程号大于其容纳能力,我们需要对课程号的数组进行扩容,此时可以使用realloc函数。
同时我们定义了1个int类型的数组relation用于存储对应哈希值关联的索引值。假如第1个用户名AAA对应的值为2000,而其索引为1。这样,当输入用户AAA时,我们查找到其对应的哈希值为2000,从而查找结构体数组第1个元素即可。
而当输入第2个用户BBB时再计算其对应的哈希值,并记录其索引值为2,以此类推。
当用户数量超过我们设定的初始用户值时,需要对结构体数组进行扩容,其大小为原来的一倍。此时需要初始化扩容后另一半的结构体数组的成员的值。
当用户第1次出现时,将当前课程号记录到其成员s的数组中去。而当用户再次出现时,再次添加对应的课程号。当发现课程号将要大于其容纳能力时,需要对课程号数组进行扩容,从而让其可以容纳更多的元素。
最后是通过测试后的结果。

可以看到使用这种方式虽然编写的代码数量较多,但是思路还是挺清晰的。
对比之前用C++编写的代码的执行效果后发现最后一组占用的内存数量真的太多,已经使用了45MB的内容,从而导致codeup上直接就无法通过,而在PAT上是可以的。因为PAT上内存限制为64MB,而codeup只有32MB。

从上图可以看到,使用C++的版本虽然执行速度没有C版本的快,但是更为节省内存。
经过多次测试发现,可以对上述代码进行下面的优化。
int *relation = (int *)calloc(175771, sizeof(int));
//int relation[175771] = {0};
int s_size = N;
int size = 10;
在这里将原有的哈希表修改为整型指针数组,由于指针类型在64位系统上的大小为8字节,而32位上为4字节,相比整型数组占用的大小与其元素长度有关,对于175771个整型元素(4字节),其在占用的字节大小为175771*4=703084B=687KB,而整型指针只有8字节。
另外,我们对学生结构体指针数组的初始化设定为N而不是100,由于后续会扩容一倍,如果初始值太多而实际输入的学生数量过少会占用过多的内存。同理,课程号的初始值为10,而不是默认的50。
最后是优化的结果:

可以看到基本与C++版本占用的内存一致,但是执行速度比优化前更快了。
当然,我们仍可以继续进行小幅度的优化,比如在扩容学生结构体数组时不是扩大一倍,而是增加让其数量增加10个元素:
int next_size = s_size + 10;
而后续引用到s_size的部分都进行相应的替换,可以得到类似如下的优化结果:

可以看到实际上与之前优化结果相差不大,但是最后一组占用的内存明显减少了44%。
但是减少内存的同时,执行时间也增加了,因为需要扩容的次数增加了。
604

被折叠的 条评论
为什么被折叠?



