前段时间开心网搞了个比赛:http://sns.kaixin001.com/contest/index.php?t=16。因为一直很多事情,所以没有参加,不过我也做了初赛算法部分的第二题,题目如下:
开心网的自助广告系统于2010年8月上线发布,通过该系统,中小企业或创业型团队可以面向开心网不同地区、年龄、性别的用户进行精准且低成本的广告投放,从而帮助其更好地提升品牌价值、开展市场营销活动。
在自助广告系统中,用户的地域、年龄、性别等属性都是重要因素,为了更加精准地区分不同地域的用户,除了用户的既有属性之外,还可以根据用户的IP地址进行地域转换。此转换由于是在用户访问时实时执行,所以对性能的要求非常高。
假设你是开心网的工程师,请实现此种转换。
输入:
两个文本文件,一个是IP库文件(ipbase.txt),所有IP地址均为IPV4 IP地址,文件为GBK编码, 格式是一行一个IP范围与地址的映射。分为三列,第一列是起始IP,第二列是结束IP,第三列是此范围内的IP(含起始IP和结束IP)对应的地址,列之间以制表符分隔。另一个文件是要查询的IP文件(ipcheck.txt) ,每行为一个IP地址。
输出:
每行输出一个查询结果,格式为:查询的IP 对应的地址(以制表符分隔)。
示例:
IP库文件:
61.50.219.42 61.50.221.33 北京 |
61.50.221.34 61.50.221.34 北京 |
61.50.221.35 61.50.221.42 北京 |
222.33.75.18 222.33.75.255 辽宁 |
222.33.76.0 222.33.77.255 辽宁 |
222.33.78.0 222.33.79.129 辽宁 |
222.33.79.130 222.33.79.130 辽宁 |
222.33.79.131 222.33.84.105 辽宁 |
222.33.84.106 222.33.84.106 辽宁 |
222.36.52.37 222.36.52.37 天津 |
222.36.52.38 222.36.52.224 天津 |
222.36.52.225 222.36.52.225 天津 |
222.36.52.226 222.36.64.219 天津 |
222.36.64.220 222.36.64.220 天津 |
要查询的IP文件:
61.50.221.34
222.33.76.2
222.33.75.18
222.33.75.19
得到如下结果:
61.50.221.34 北京
222.33.76.2 辽宁
222.33.75.18 辽宁
222.33.75.19 辽宁
提交代码要求:
Makefile生成的程序文件名必须为ipfinder, 接收两个参数,第一个参数接收IP库文件,第二个参数接收查询文件。在程序所在目录下运行 ./ipfinder ipbase.txt ipcheck.txt可以得到结果。
我下载了他的测试用例,ipbase文档有10000条记录,ipcheck文档有8000条记录,而且据说最后的最大测试量ipbase有10W条记录,ipcheck有300W条记录,首先我想:
1、查询ip无外乎拿一个ip去在ipbase里面找,想想这个过程可以发现,每次我都只能得出一个ip的地址,那么ipcheck的地址内容肯定不需要一次全部存入内存,那一次存多少进去呢?10条?100条?这就是一个需要尝试的地方。
2、ipbase有300W条记录,有两种查询方式,要么一次我全部读入内存然后使用算法查询;要么我一次读取一部分记录进入内存,查询部分后再读另一部分接着查。这里空间换时间,但还得把频繁打开关闭文件的时间也考虑进去,进行一个博弈来选择一种方式。
3、理论上一个程序使用的堆内存(就是C语言里用malloc申请的内存)是可以无限大的,不过就实际情况而言还是控制内存在一定限度内比较好,也就是说想把ipbase的内容全部存入内存还是有一定的困难的,毕竟这里是300W条,改日变成3000W条,3亿条那还不疯掉啊!不过经过我实际测试300W条数据量还是可以一次全部读入内存的,系统表示毫无压力~~
4、假设我一次从ipcheck中查询一条ip地址,那么效率明显要低于我一次查询多条,因为就一次操作假定必须遍历ipbase文件的话获得的信息完全足以查询多条记录,这决定了一次查询最好能够获得多条信息。
5、而在存储ipbase的信息时需要讲究一定的策略,我们知道ip地址有4个字节,每个字节为0~255,如果我们能够给这些ip地址排个序,那么查询时就会方便很多,比如我们把ip地址转换成对应的4个字节的整数排序,查询时求出ipcheck中的ip的整型数值进行二分搜索将会大大减少查询时间,那么我们是否这么排序就可以了呢?或者说排序上还有没有方法进行优化呢?我想了一种简陋的方法,这里就抛砖引玉了,既然每个字节是0~255,那么我是否能够定义一个指针数组存放256个指针指向256个链,然后根据ip地址的首字节放入各个链中再对链进行排序呢?因为链的构成是从无到有的过程,所以每时每刻链都是有序的,那么新来的结点可以使用二分法插入到链中,这样效率应该不错。当然这只是我自己的猜测,虽然后来实现了,不过貌似也不咋地~~有更好方法的朋友欢迎向我提供,我也会亲自实现然后和大家分享。
有了上面的分析,程序写起来应该不会太难,当然也不是那么容易,由思路到代码的过程往往让很多人望而却步。我下面也写了写,不过没有完全实现上面说的,因为时间不够,暂时就放着了,以后有时间再改吧。我下面的代码每次只从ipcheck中读取一个ip,同时把ipbase里的数据全部读入内存,采取的就是我上面第5点设计的方法存储的,时间仓促,虽然自己调试暂时没有发现问题,不过效率很低,如果完全按照我上面的思路来写应该会提高很多,有兴趣的朋友欢迎积极修改啊~~呵呵~
- #include <stdio.h>
- #include <stdlib.h>
- #include <string.h>
- #define NUMBER 256
- #define ADDRLENGTH 50
- //存放IP结点的结构体
- typedef struct ipnode {
- int ip3_sum; //记录IP地址的后3组的和
- char addr[ADDRLENGTH]; //记录地址信息
- struct ipnode *next;
- }IPNODE;
- //头结点结构体
- typedef struct ipline {
- IPNODE *head; //链表头
- int number; //记录该链中有多少个结点
- }IPLINE;
- //定义了256个指针,用于将IP地址分为256组
- IPLINE *ipbase[NUMBER];
- //插入结点
- void InsertIpNode(int local, IPNODE *ipnode)
- {
- IPNODE *cur, *pre;
- cur = ipbase[local]->head;
- pre = cur;
- if (NULL == ipbase[local]->head) {
- ipbase[local]->head = ipnode;
- }
- else {
- //按照结点IP后3组的和由小到大排列插入
- while ((NULL != cur) && (ipnode->ip3_sum > cur->ip3_sum)) {
- pre = cur;
- cur = cur->next;
- }
- ipnode->next = cur;
- if (pre == cur)
- ipbase[local]->head = ipnode;
- else
- pre->next = ipnode;
- }
- }
- //求IP地址后3组的和
- int GetIpSum(char *ip, int *local)
- {
- int i = 0;
- int j = 0;
- int k = 0, sum;
- sscanf(ip, "%d.%d.%d.%d", local, &i, &j, &k);
- sum = k + j * NUMBER + i * NUMBER * NUMBER;
- return sum;
- }
- //将指定IP结点插入到ipbase链表中
- void PutIntoIpbase(char *ip, char *addr)
- {
- int local, sum;
- IPNODE *ipnode;
- sum = GetIpSum(ip, &local);
- ipnode = (IPNODE *)malloc(sizeof(IPNODE));
- ipnode->ip3_sum = sum;
- ipnode->next = NULL;
- strcpy(ipnode->addr, addr);
- InsertIpNode(local, ipnode);
- ipbase[local]->number++;
- }
- //在ipbase链表中查询指定IP
- int FindInIpbase(char *ip)
- {
- int local, sum;
- int begin = 0, end, mid;
- int i;
- IPNODE *point;
- sum = GetIpSum(ip, &local);
- end = ipbase[local]->number;
- if (1 == end)
- printf("%s\t%s\n", ip, ipbase[local]->head->addr);
- else {
- //采用二分查找的方式来解析IP地址
- while (begin <= end) {
- mid = (begin + end) / 2;
- point = ipbase[local]->head;
- for (i = 1; i < mid; i++)
- point = point->next;
- if (sum < point->ip3_sum)
- end = mid - 1;
- else if (sum > point->ip3_sum) {
- if ((NULL != point->next) && (sum < point->next->ip3_sum)) {
- printf("%s\t%s\n", ip, point->addr);
- return 0;
- }
- else
- begin = mid + 1;
- }
- else {
- printf("%s\t%s\n", ip, point->addr);
- return 0;
- }
- }
- point = ipbase[local]->head;
- for (i = 1; i < ipbase[local]->number; i++)
- point = point->next;
- printf("%s\t%s\n", ip, point->addr);
- }
- return 1;
- }
- //初始化ipbase链表组,为头结点分配内存
- void InitIpline(void)
- {
- int i;
- for (i = 0; i < NUMBER; i++) {
- ipbase[i] = (IPLINE *)malloc(sizeof(IPLINE));
- ipbase[i]->head = NULL;
- ipbase[i]->number = 0;
- }
- }
- //读取ipbase.txt文件中的信息
- void ReadIpbase(char **argv)
- {
- FILE *fp;
- char ip1[16];
- char addr[30];
- if (NULL == (fp = fopen(argv[1], "r")))
- exit(0);
- while (EOF != fscanf(fp, "%s %*s %s", ip1, addr))
- PutIntoIpbase(ip1, addr);
- fclose(fp);
- }
- //逐条读取ipcheck.txt文件中的IP地址
- void CheckIplist(char **argv)
- {
- FILE *fp;
- char ip[16];
- if (NULL == (fp = fopen(argv[2], "r")))
- exit(0);
- while (EOF != fscanf(fp, "%s", ip))
- FindInIpbase(ip);
- fclose(fp);
- }
- void InsertIpNode(int local, IPNODE *node);
- int GetIpSum(char *ip, int *local);
- void PutIntoIpbase(char *ip, char *address);
- int FindInIpbase(char *ip);
- void InitIpline();
- void ReadIpbase(char **argv);
- void CheckIplist(char **argv);
- int main(int argc, char *argv[])
- {
- InitIpline();
- ReadIpbase(argv);
- CheckIplist(argv);
- return 0;
- }