PAT甲级[1039]Course List for Student (25)-C版

之前用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倍才能避免冲突,否则直接相加,那么就会出现OOG1OOH0哈希值是相同的问题,均为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%。
但是减少内存的同时,执行时间也增加了,因为需要扩容的次数增加了。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值