前段时间开心网搞了个比赛: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点设计的方法存储的,时间仓促,虽然自己调试暂时没有发现问题,不过效率很低,如果完全按照我上面的思路来写应该会提高很多,有兴趣的朋友欢迎积极修改啊~~呵呵~

 
  
  1. #include <stdio.h> 
  2. #include <stdlib.h> 
  3. #include <string.h> 
  4.  
  5. #define NUMBER       256 
  6. #define ADDRLENGTH   50 
  7.  
  8. //存放IP结点的结构体 
  9. typedef struct ipnode { 
  10.     int ip3_sum; //记录IP地址的后3组的和 
  11.     char addr[ADDRLENGTH]; //记录地址信息 
  12.     struct ipnode *next; 
  13. }IPNODE; 
  14.  
  15. //头结点结构体 
  16. typedef struct ipline { 
  17.     IPNODE *head; //链表头 
  18.     int number; //记录该链中有多少个结点 
  19. }IPLINE; 
  20.  
  21. //定义了256个指针,用于将IP地址分为256组 
  22. IPLINE *ipbase[NUMBER]; 
  23.  
  24. //插入结点 
  25. void InsertIpNode(int local, IPNODE *ipnode) 
  26.     IPNODE *cur, *pre; 
  27.     cur = ipbase[local]->head; 
  28.     pre = cur; 
  29.     if (NULL == ipbase[local]->head) { 
  30.         ipbase[local]->head = ipnode; 
  31.     } 
  32.     else { 
  33.         //按照结点IP后3组的和由小到大排列插入 
  34.         while ((NULL != cur) && (ipnode->ip3_sum > cur->ip3_sum)) { 
  35.             pre = cur; 
  36.             cur = cur->next; 
  37.         }            
  38.         ipnode->next = cur; 
  39.         if (pre == cur) 
  40.             ipbase[local]->head = ipnode; 
  41.         else 
  42.             pre->next = ipnode; 
  43.     } 
  44.  
  45. //求IP地址后3组的和 
  46. int GetIpSum(char *ip, int *local) 
  47.     int i = 0; 
  48.     int j = 0; 
  49.     int k = 0, sum; 
  50.     sscanf(ip, "%d.%d.%d.%d", local, &i, &j, &k); 
  51.     sum = k + j * NUMBER + i * NUMBER * NUMBER; 
  52.     return sum; 
  53.  
  54. //将指定IP结点插入到ipbase链表中 
  55. void PutIntoIpbase(char *ip, char *addr) 
  56.     int local, sum; 
  57.     IPNODE *ipnode; 
  58.  
  59.     sum = GetIpSum(ip, &local); 
  60.  
  61.     ipnode = (IPNODE *)malloc(sizeof(IPNODE)); 
  62.     ipnode->ip3_sum = sum; 
  63.     ipnode->next = NULL; 
  64.     strcpy(ipnode->addr, addr); 
  65.  
  66.     InsertIpNode(local, ipnode); 
  67.     ipbase[local]->number++; 
  68.  
  69. //在ipbase链表中查询指定IP 
  70. int FindInIpbase(char *ip) 
  71.     int local, sum; 
  72.     int begin = 0, end, mid; 
  73.     int i; 
  74.     IPNODE *point; 
  75.  
  76.     sum = GetIpSum(ip, &local); 
  77.     end = ipbase[local]->number; 
  78.     if (1 == end) 
  79.         printf("%s\t%s\n", ip, ipbase[local]->head->addr); 
  80.     else { 
  81.         //采用二分查找的方式来解析IP地址 
  82.         while (begin <= end) { 
  83.             mid = (begin + end) / 2; 
  84.             point = ipbase[local]->head; 
  85.             for (i = 1; i < mid; i++) 
  86.                 point = point->next; 
  87.             if (sum < point->ip3_sum) 
  88.                 end = mid - 1; 
  89.             else if (sum > point->ip3_sum) { 
  90.                 if ((NULL != point->next) && (sum < point->next->ip3_sum)) { 
  91.                     printf("%s\t%s\n", ip, point->addr); 
  92.                     return 0; 
  93.                 } 
  94.                 else 
  95.                     begin = mid + 1; 
  96.             } 
  97.             else { 
  98.                 printf("%s\t%s\n", ip, point->addr); 
  99.                 return 0; 
  100.             } 
  101.         } 
  102.         point = ipbase[local]->head; 
  103.         for (i = 1; i < ipbase[local]->number; i++) 
  104.             point = point->next; 
  105.         printf("%s\t%s\n", ip, point->addr); 
  106.     } 
  107.     return 1; 
  108.  
  109. //初始化ipbase链表组,为头结点分配内存 
  110. void InitIpline(void
  111.     int i; 
  112.     for (i = 0; i < NUMBER; i++) { 
  113.         ipbase[i] = (IPLINE *)malloc(sizeof(IPLINE)); 
  114.         ipbase[i]->head = NULL; 
  115.         ipbase[i]->number = 0; 
  116.     } 
  117.  
  118. //读取ipbase.txt文件中的信息 
  119. void ReadIpbase(char **argv) 
  120.     FILE *fp; 
  121.     char ip1[16]; 
  122.  
  123.     char addr[30]; 
  124.     if (NULL == (fp = fopen(argv[1], "r"))) 
  125.         exit(0); 
  126.     while (EOF != fscanf(fp, "%s %*s %s", ip1, addr)) 
  127.         PutIntoIpbase(ip1, addr); 
  128.     fclose(fp); 
  129.  
  130. //逐条读取ipcheck.txt文件中的IP地址 
  131. void CheckIplist(char **argv) 
  132.     FILE *fp; 
  133.     char ip[16]; 
  134.     if (NULL == (fp = fopen(argv[2], "r"))) 
  135.         exit(0); 
  136.     while (EOF != fscanf(fp, "%s", ip))  
  137.         FindInIpbase(ip); 
  138.     fclose(fp); 
  139.  
  140. void InsertIpNode(int local, IPNODE *node); 
  141. int GetIpSum(char *ip, int *local); 
  142. void PutIntoIpbase(char *ip, char *address); 
  143. int FindInIpbase(char *ip); 
  144. void InitIpline(); 
  145. void ReadIpbase(char **argv); 
  146. void CheckIplist(char **argv); 
  147.  
  148.  
  149. int main(int argc, char *argv[]) 
  150.     InitIpline(); 
  151.     ReadIpbase(argv); 
  152.     CheckIplist(argv); 
  153.     return 0;