BUAA数据结构大作业2023

BUAA数据结构大作业2023

题目(大规模文档去重分析)

问题描述

谷歌、百度等大型搜索引擎需要定期抓取全球网站的网页数据,并建立索引用于支持关键词的快速查询。通常搜索引擎需要判断抓取的网页是否在已经索引的数据集中,来决定是否更新索引,从而提高搜索引擎的更新效率。这需要高效文本去重算法进行支持。

Simhash是Google用来处理海量文本去重的算法(参考论文《Detecting Near-duplicates for web crawling》),借助于文本向量的构建以及局部敏感哈希技术,可以将每个网页文本生成一串二进制编码(文本指纹fingerprint),再利用高效的索引方法,加速大规模文本数据集中相似文本的检测。网页指纹生成原理如下图所示:

在这里插入图片描述

1.首先统计网页(Doc)特征向量feature中每个特征的权重(w1, w2,…wn)(如何确定特征及其权重详见下面的具体实现方法);

2.依据每个特征对应的哈希值(hashvalue,一个由01组成的长度为M的串,其中1表示1,0表示-1,M为指纹长度)得到一组符号化权重值。如图所示,“100110”为长度为6的哈希串,“100110 w1”将得到一组(6个)符号化w1值“w1 -w1 -w1 w1 w1 -w1”。

3.特征向量所有特征对应位置符号化权重值相加,将得到一组整数,如图所示“13, 108, -22, -5, -32, 55”。

4.获取上步得到的一组整数的符号值(1表示正数,0表示负数和零),将其组成符号值串,即为该网页的指纹(fingerprint)。如图所示,“13, 108, -22, -5, -32, 55”其符号值串为“110001”,也就是网页(Doc)的指纹。

基于Simhash原理实现一个相似网页(文本)检测工具,方法如下:

1.获取网页特征向量。对所有网页(文本)的非停用词(stopword)英文单词进行词频(出现次数)统计,并将单词词频由高到低进行排序(频度相同时,按字典序),取前N个单词构成网页特征向量feature=(word1, word2 , … , wordN)。N为特征向量维度,或长度。

注意:英文单词为仅由字母构成的字符串,不区分大小写。统计时要将大写字母转换为小写字母。在自然语言处理中,停用词(stop-word)指的是文本分析时不会提供额外语义信息的词的列表,如英文单词a,an,he,you等就是停用词。

2.统计每个网页(文本)的特征向量中每个特征(单词)的频度(权重),得到特征向量对应的权重向量weight=(w1, w2 , … , wN)。

3.每个特征wordi均有一个对应哈希值串hashi,每个网页的特征向量对应的权重向量中权重wi(i=1,2,…,N)按对应哈希值串hashi进行符号取值,得到一组由wi和-wi组成的符号化权重值向量SignWeighti。(权重值为0时符号化后值仍为0)。

4.计算网页指纹fingeprint。对每个网页,特征向量的所有特征对应的符号化权重向量对应位置值累加,并对累加结果,大于0置1,小于等于0置0,得到网页(文本)指纹(fingerprint)(一个由01组成的长度为M的编码串串):

在这里插入图片描述

其中:

Sign(X) = { 1 | 0,当X>0时为1,X<=0时为0}

5.按上面方法计算新抓取的网页的指纹。

6.基于文本指纹相似度可以对新抓取的网页与已有的网页数据集进行相似比较。指纹相似度可通过汉明距离(Hamming distance)进行计算:两个文本指纹的汉明距离是指其二进制编码串中01取值不同的数量。举例如下:文本指纹10101 和 00110 从第1位开始依次有第1、第4、第5位不同,海明距离为3。

基于汉明距离可以筛选出大概率相同的网页文本,一般超过某个阈值(在此,阈值设置为3)则判定为不相似,小于等于阈值判定为相似。

7.按输出形式要求,依次将新抓取网页与已有相似网页(指纹汉明距离阈值为3以内的)按汉明距离由小到大网页输出到屏幕和指定文件中。

输入形式

从命令行输入特征向量长度N以及指纹长度M。

具体形式如下:

simtool N M

其中simtool为网页相似检测程序。其根据当前目录下的停用词文件“stopwords.txt”、哈希值文件“hashvalue.txt”、已有网页数据文件“article.txt”、待查重(即新提取)的网页数据文件“sample.txt”,按上面要求依次对sample.txt中每个网页文档在article.txt文件中查找相似网页,并按输出要求输出检测结果。

注意:

1.在课程网站“课件下载”区提供了project2023.zip,其中包括英文停用词表“stopwords.txt”文件(文件中只包含单词,不含其解释,且已按字典序排序)和哈希值文件“hashvalue.txt”。该hashvalue.txt文件中包含了10000(行)x128(列)由01组成的数据,每一行为一个哈希值串,若程序命令行输入的特征向量N为1000和指纹长度M为16时,则取该文件前1000行,每行取前16列作为实际哈希值表在程序中使用。哈希值文件的规模决定了本题的特征向量最大长度不超过10000,指纹最大长度不超过128。

2.由于Windows系统下文本文件中的’\n’回车符在(评测环境)Linux系统下会变为’\r’和’\n’2个字符,建议用fscanf(fp,”%s”,…)来处理停用词文件中英文单词。

3.为了简化相似检测程序的实现,已从互联网上爬取(Web Crawling)相关网页(文档)的工作已经完成,并将爬取的网页文档数据已存入一个文本文件(aritcle.txt)中,其中每个网页第一行为网页标识号(如XX-XXXX,可按字符串来输入),然后为网页内容,网页文档间以换页符\f分隔。在课程网站下载区提供了一个用于测试的article.txt文件。sample.txt文件中每个网页的标识号为“Sample-XXX”,其它格式同article.txt文件。

输出形式

按下面形式依次输出sample.txt文件中每个网页在article.txt中找到的相似网页信息到文本文件result.txt中:

X1

0:ID01 ID02 …

1: ID11 ID12 …

2: ID21 ID22 …

3: ID31 ID32 …

X2

其中X1,X2…为sample.txt中网页标识号,次序同原文件中序;“0:ID01 ID02 …”表示在article.txt中与相应网页指纹汉明距离为0的所有网页标识号,按article.txt文件中出现序排列,中间以一个空格分隔若不存在相关网页,则无汉明距离为0的信息输出,即无“0:…”一行信息输出,其它部分含义相同。汉明距离相同的最后一个网页标识后也有一个空格分隔符。行末换行时输出字符“\n”即可。

同时,将sample.txt文件中第一个网页相似检测结果信息输出到屏幕上,即输出下面信息到屏幕上:

X1

0:ID01 ID02 …

1: ID11 ID12 …

2: ID21 ID22 …

3: ID31 ID32 …

样例输入

假设simtool.exe为网页相似检测程序,以下面方式运行该程序:

simtool 1000 16

(运行程序前,从课程网站下载区下载project2023.zip文件,其中包括:article.txt, sample.txt, hashvalue.txt, stopwords.txt, results(example).txt)

说明:若本地编程环境为dev-C++,可点击菜单Execute\Parameters…,在下面对话框中输入相应命令行参数。

在这里插入图片描述

样例输出

假设simtool.exe为网页相似检测程序,以下面方式(特征向量长度为1000,指纹长度为16)运行该程序:

simtool 1000 16

程序运行后,屏幕上输出的结果为:

在这里插入图片描述

所生成的结果文件“result.txt”内容应与下载区文件“result(example).txt”完全相同。

若以下面方式(特征向量长度为1000,指纹长度为32)运行该程序:

simtool 1000 32

程序运行后,屏幕上输出的结果为:

在这里插入图片描述

样例说明

以上面屏幕输出为例,其中第一行Sample-1为sample.txt中第一个网页标识号;第二行开始输出汉明距离(值为0,1,2,3)及在文件article.txt中与给定网页汉明距离为相应值的网页编号,网页编号间以一个空格分隔。若article.txt中不存在与给定网页汉明距离为某个值的网页,则该行不输出,如上面第2个屏幕输出,由于不存在汉明距离为1和3的网页,则该行不输出。输出文件result.txt中信息含意与此类同。

问题分析

首先建议多读几遍题,弄清楚本题到底什么意思,以及给的那几个文件都是干嘛的。如果理解了题意,可以改写题面如下:

  1. 从从命令行读入参数N和M;
  2. 建立停用词表;
  3. 统计article.txt所有文章中出现的合法(即不在停用词表中的单词)单词的频率,对其排序,取其前n个单词记为 w o r d 1 , w o r d 2 , . . . , w o r d n word_{1}, word_{2}, ..., word_{n} word1,word2,...,wordn
  4. 取hashvalue.txt文件的前N行M列组成一张表,该表第i行对应 w o r d i word_i wordi 的哈希值;
  5. 对sample.txt和article.txt中的每一篇文章,分别统计 w o r d 1 , w o r d 2 , . . . , w o r d n word_{1}, word_{2}, ..., word_{n} word1,word2,...,wordn出现的次数,利用公式算出每一篇文章的指纹向量;
  6. 针对sample.txt中的每一篇文章的指纹,算出其与article中每一篇指纹的汉明距离,并输出。

本题工程量不小,我在此记录下来的是我从0开始一步步分析与优化的思路:

程序框架的建立

我的想法是按照上面的分析思路,确定出程序的框架,然后在再根据这个框架去写对应的函数,程序能正常跑之后再去优化函数细节。首先要在主函数中从命令行读入N和M,有关命令行的读入,可以参考这篇文章,同时,为了方便,将M和N定义为全局变量:

# include <stdio.h>
# include <stdlib.h>
# include <string.h>

/*----------------------常量与全局变量------------------------*/
int M, N;  // 记录命令行参数

FILE* article;
FILE* sample;
FILE* hashvalue;
FILE* stopwords;


/*----------------------函数------------------------*/


//主函数
int main(int argc, char* argv[])
{
    article = fopen("article.txt","r");
    sample = fopen("sample.txt","r");
    hashvalue = fopen("hashvalue.txt","r");
    stopwords = fopen("stopwords.txt","r");
    
    // N = atoi(argv[1]);
    // M = atoi(argv[2]);
    scanf("%d%d", &N, &M);
    // 为方便调试,可以先从控制台读入,最后把代码改成命令行读入即可

    return 0;
}

而后我们要用合适的数据结构存储停用词表,并依照此表,写一个函数用于判断输入的单词是否在停用词表中,并以此对article.txt中的单词进行词频统计,找出并记录其前n个单词:

// 主函数中:
    build_stopwords_list(stopwords);  // 建立停用词表(无论用什么数据结构存储该表,我们都把它定义为全局变量)


    char tmp_word[15];   
    while(get_all_words_in_article(article, tmp_word) != EOF) {
        if (is_legal(tmp_word)) {
            // 把该单词加到词频统计表中
            // 若该词已经在词频表中了,其次数+1
            // 这两个函数需要我们后面自己设计
        }
    }
    rewind(article);  // 将文件指针移至文件初


    {
        // 对词频表进行排序,取其前n个单词
        // 具体的实现方法肯定和我们选取的数据结构有关
    }

接着,我们要根据hashvalue.txt文件的内容建立这n个单词对应的哈希值的哈希表,然后统计出article和sample中的这n个单词出现的次数,算出每一篇文章的指纹向量:

// 主函数中
    build_hashvalue_list(hashvalue);  // 建立哈希值表


    {
        //计算article和sample中每一篇文章的指纹向量
    }
    print_ans();

问题难点分析

其实我们面对的最主要的困难在于数据结构如何选取,大概有这么几方面:

  1. 停用词表的建立,我们希望is_legal函数输入任意一个单词,判断其是否在停用词表中;
  2. 对article.txt中所有单词进行词频统计后再排序。当我们读入了一个合法的单词时,要判断词频统计的表中是否已经有该词了,通常来讲,这点会极其耗费时间,因为常规无序的存储要想判断这点只能把表遍历一遍,所以我们要用某种特定的数据结构存储统计的单词(最后用的字典树)。
  3. 取出article.txt中词频前n个单词后,在对article和sample中的每一篇文章处理时,我们要实现这样一个功能:输入一个单词,判断该单词是否是这n个单词中的一个。我们当让可以把这n个单词遍历一遍,但那样的话肯定会消耗巨大的时间。

其实针对第一个问题,要实现停用词表的建立与is_legal函数的功能并不困难,因为题目已经把stopwods排序了,我们可以用二分查找来搜索每个单词是否在停用词表内,由于停用词一共就300多个,所以每次查找最多也只需要9次,实现代码如下:

// 全局变量
char stopwords_list[320][15];  // 停用词表


int build_stopwords_list(FILE* stopwords)
// 建立停用词表
{
    int num = 0;
    while (fscanf(stopwords, "%s", stopwords_list[num++]) != EOF) {}
    return num;
}


int cmp_is_legal(const void* a, const void* b)
{
    return strcmp((char*)a, (char*)b);
}


int is_legal(char* word, int stop_word_num)  
// 判断该词是否在停用词表中
{
    if (!(bsearch(word, stopwords_list, stop_word_num, sizeof(stopwords_list[0]), cmp_is_legal)))
        return 1;
    return 0;
}

但是article中的单词太多了,我实现之后跑了一下程序,发现即使每次判断都是常数时间(<=9),但依然会带来巨大的时间开销,所以似乎最开始的思路要改一改,我们可以先不管什么单词都读进来,存到某个数据结构中,最后取其前n个单词时,再去判断该单词是否在停用词表中,这样做就不用每个词都比较一遍了,如此便解决了这个问题,接下来的问题就是第二点和第三点了。

具体处理过程

无论如何,我们都要读取article.txt中的所有单词,为此可以写一个函数:

int get_all_words_in_article(FILE* fp, char* dst)  
// 读取article中的所有单词,每次读一个,结尾返回EOF
{
    int i = 0;
    char c;
    while ((c = fgetc(fp)) != EOF) {
        if (isalpha(c) != 0) {
            dst[i++] = tolower(c);
        } else if (i) {
            dst[i] = '\0';
            return 0;
        }
    }
    return EOF;
}

该函数的调用方法在上面的程序框架中已经写了(其实上面的框架只是我个人最初的想法,后面肯定有些细节之处要变动,就比如现在已经确定is_legal函数不在每次读进来个单词之后就调用了)。

字典树的创建

现在要选取合适的数据结构建立article.txt中所有单词组成的词频表,经过上网搜索与寻求学长的帮助后,最后打算用字典树来实现这个词频统计的功能。字典树的介绍可以参考这些文章:

当然,看完之后还是不太懂也挺正常,要是你们的大作业也用到了字典树(应该是大概率),可以去问问梦拓学长和助教,并自己上网搜搜相关的资料。总之,一定把原理搞懂了再来实现代码。而想要运行时间快,建议用数组来实现,我参考了上面的第二篇文章,但是要稍加改动,因为我们的目的不是建造一棵树然后给定某个单词去查找这个单词出现了多少次(换句话说这篇文章里的find函数没啥用),而是要找出出现次数前n个单词,换句话说,我们要记录下来都出现过什么单词,即实现对输入过的单词的遍历。为此,我们可以定义一种结构,记录单词与其出现的次数,然后开一个结构数组去存储:

/*----------------------类型定义------------------------*/
typedef struct word {
    int time;
    char str[15];
} word;


/*----------------------常量与全局变量------------------------*/
word word_frequency_list[MAX_SIZE]; // 词频表
int trie[MAX_SIZE][26]; //字典树(词频统计的)
int pos;  // 单词位置

同时设计insert_to_trie函数实现单词插入:

void insert_to_trie(char* str)
// 向字典树中插入一个单词
{
    if (strlen(str) < 15)  // 防止文章中识别不出来的单词的影响(比如cqioawcwaecbwuesvcwseev这种,或者邮箱号那种)
    {
        int p = 0;
        for (int i = 0; str[i]; i++) {
            int n = str[i] - 'a';
            if (trie[p][n] == 0) {
                trie[p][n] = pos++;
            }
            p = trie[p][n];
        }
        if (!word_frequency_list[p].time) {        
            strcpy(word_frequency_list[p].str, str);
        }        
        word_frequency_list[p].time++;
    }
}

这样在主函数中读入单词,调用insert,最后qsort一下就OK了:

int cmp_for_frelist(const void* a, const void* b)
{
    word* e1 = (word*)a;
    word* e2 = (word*)b;
    if (e1 -> time != e2 -> time) return e2 -> time - e1 -> time;
    return strcmp(e1 -> str, e2 -> str);
}


// 主函数中
    char tmp_word[100];   
    while(get_all_words_in_article(article, tmp_word) != EOF) {
        insert_to_trie(tmp_word);  // 将该单词插入字典树        
    }
    rewind(article);  // 将文件指针移至文件初

    qsort(word_frequency_list, pos, sizeof(word), cmp_for_frelist);  // 词频排序

之后我们遇到了第三个问题,即用什么数据结构去存储前N个单词?我们要选取一种便于查找某个单词是否在这N个单词中的结构。为了测试一下程序到现在运行状况如何,我这里先直接用数组去存,后面优化时再更换数据结构:

// 全局变量
char feature_word[MAX_SIZE][15];


// 主函数中
// 取出词频表中前n个不在停用词表中的单词
int feature_num = 0;
for (int i = 0; ; i++) {        
    if (is_legal(word_frequency_list[i].str, stopword_num)) {   
        strcpy(feature_word[feature_num++], word_frequency_list[i].str);
        if (feature_num == N) break;
    }        
}

然后我们可以打印输出一下feature_word中的元素,发现确实能输出看起来挺正常的单词,虽然有的词比较奇怪(比如单个s这种),但是转念一想,这么大个程序想要精准识别挺难的,也没必要,所以暂且就认为我们到目前为止的操作一切正常。

现在我们该来考虑下怎么存储这n个单词了,我想到要用哈希表(有关哈希表的内容,大家可以自己去网上搜索一下,而且ds课后面有一节应该也回讲哈希)。

这里原本我写了好多内容,讨论了如何去建立哈希表,后来在梦拓学长的点拨下,知道了通常是这么做的:将a-z编号为1-26,然后将单词按位转化为一串数,该串数是26进制的,再把它转化为10进制的数,最后模上一个大质数(学长告诉我模9007),就对应到哈希表中的元素了(采用拉链法来解决哈希冲突)。

此时,前面测试用的feature_word等变量可以在程序中删除了

哈希表的创建

首先定义特征单词的结构类型:

/*----------------------类型定义------------------------*/
typedef struct feature_word
{
    char str[15];  // 单词
    char hashvalue[129];  // 单词对应的哈希值
    struct feature_word *next;
} feature_word, * wptr;


/*----------------------常量与全局变量------------------------*/
wptr head[9007];

而后定义函数用于创建新单词结点:

wptr newnode()
{
    return (wptr)malloc(sizeof(feature_word));
}


wptr getnode(char *str)
{
    wptr p = newnode();
    p -> next = NULL;
    strcpy(p -> str, str);   
    return p;
}

计算哈希值的函数设计如下:

int hashCode(char *str)
// 计算哈希值函数
{
    int tem = 0;
    int len = strlen(str);
    for(int i = 0;i < len;i++)
    	tem = (tem * 26 + str[i] - 'a' + 1) % 9007;
    return tem;
}

在哈希表中插入一个单词的操作如下:

void insert_to_hash(char *str)
// 映射函数
{
    int val = hashCode(str);
    wptr p = getnode(str);
    p -> next = head[val];
    // 此处修改p的hashvalue域,一会再写
    head[val] = p;
    return ;
}

寻找单词对应的结点的函数如下:

wptr find_in_hash(char *str)
// 寻找单词对应的结点的函数
{
    int val = hashCode(str);
    for(wptr i = head[val]; i ; i = i -> next)
        if(strcmp(str,i -> str) == 0)
            return i;
    return NULL; // 没找到
}

我们后面寻找时,只需要找到该结点,然后就能得到其hashvalue域的值。

我们在生成哈希表时,还要修改对应的hashvalue域,故在主函数中书写如下:

    // 取出词频表中前n个不在停用词表中的单词
    int feature_num = 0;
    char tmp_hash[129] = {0};
    for (int i = 0; ; i++) {        
        if (is_legal(word_frequency_list[i].str, stopword_num)) {   
            for (int j = 0; j < M; j++) {  // 读hashvalue.txt中该行的前M个字符
                tmp_hash[j] = fgetc(hashvalue);
            }
            tmp_hash[M] = '\0';                       
            
            insert_to_hash(word_frequency_list[i].str, tmp_hash);  // 插入哈希表
            fgets(tmp_hash, 128, hashvalue); // 将文件指针移动至下一行首
            feature_num++;
            if (feature_num == N) break;
        }        
    }

其中insert_to_hash函数改写如下:

void insert_to_hash(char *str, char* tmp_hash)
// 映射函数
{
    int val = hashCode(str);
    wptr p = getnode(str);
    p -> next = head[val];
    strcpy(p -> hashvalue, tmp_hash);
    head[val] = p;
    return ;
}

这样,我们就完成了本题的全部核心部分,接下来就是要根据这张哈希表的内容,去计算每一篇文章的指纹了。

乱七八糟小细节

接下来的活没什么技术含量,但确实是体力活,我们要读取每一篇文章,针对其中的每一个词,判断其是否在这张哈希表中,若在,则按照要求修改当前文章的指纹向量,最后得到总的指纹向量作为本文的指纹。

为了读取方便,我们写一个通用函数,其接受一个当前的文件指针,读取该文件中当前文章的标识符:

int get_identifier(FILE* fp, char *dst)
{
    if (fgets(dst, 128, fp) != NULL) return 0;
    return EOF;

}

同时再设计一个函数,接受一个当前的文件指针,读取该文件中当前文章的标识符:

int get_one_word_in_cur_article(FILE* fp, char* dst)  
// 读取article中的所有单词,每次读一个,结尾返回EOF
{
    int i = 0;
    char c;
    while ((c = fgetc(fp)) != '\f') {
        if (isalpha(c) != 0) {
            dst[i++] = tolower(c);
        } else if (i) {
            dst[i] = '\0';
            return 0;
        }
    }
    return '\f';
}

比较阴险的是,article.txt的最后一篇文章没有\f这个标识符,所以循环会一直卡死在那跑不出来,同时由于fgets会读进来换行符,所以每篇文章末尾\f之后的\n会被读成下一篇文章的identifier,我自己de了快半个小时才发现这俩问题… 解决方法到不难,稍微改写一下get_one_word_in_cur_article函数即可:

int get_one_word_in_cur_article(FILE* fp, char* dst)  
// 读取article中的所有单词,每次读一个,结尾返回EOF
{
    int i = 0;
    char c;
    while ((c = fgetc(fp)) != '\f' && c != EOF) {
        if (isalpha(c) != 0) {
            dst[i++] = tolower(c);
        } else if (i) {
            dst[i] = '\0';
            return 0;
        }
    }
    if (c == EOF) 
        return EOF;
    
    fgetc(fp);  // 把最后的换行符吃掉
    return '\f';
}

同时,由于sample中的每一篇文章都要和article中的所有文章去比较,所以可以定义一种文章结构类型,如下:

/*----------------------类型定义------------------------*/
typedef struct passage {
    char identifier[16];  // 本文的标识符
    int feature[129];  // 特征指纹
} passage;


/*----------------------常量与全局变量------------------------*/
passage article_data[100000];

我们优先把article.txt中的所有文章的指纹算出来存储到article_data结构数组中:

// 主函数中
    int article_num = 0;
    while(get_identifier(article, article_data[article_num].identifier) != EOF) {
        char temp_word[15];
        wptr temp_pos;
        int flag = 0;
        while((flag = get_one_word_in_cur_article(article, temp_word)) != '\f') {
            if (flag == EOF) break;
            if ((temp_pos = find_in_hash(temp_word)) != NULL) {
                for (int i = 0; i < M; i++) {
                    if ((temp_pos -> hashvalue[i] == '0')) {                        
                        article_data[article_num].feature[i]--;
                    }else {
                        article_data[article_num].feature[i]++;                        
                    }
                }
            }
        }
        // 已经算出来了,大于0的位置1,其余置0
        for (int i = 0; i < M; i++) {
            if (article_data[article_num].feature[i] > 0) 
                article_data[article_num].feature[i] = 1;
            else 
                article_data[article_num].feature[i] = 0;
        }
        article_num++;
        if (flag == EOF) break;
        
    }

此时可以把article_data中每个元素的两个域都打印出来,发现确实可行,说明我们的代码到目前为止都是正确的(太不容易了!)。

接下来逐一地对sample中的每一篇文章进行操作,对于sample.txt中的每一篇文章,我们要记录它的标识符,算出它的指纹,并由此计算出它和article.txt中的每篇文章的指纹的汉明距离,并以此排序,所以可以定义一种结构体如下:

/*----------------------类型定义------------------------*/
typedef struct hanmming_distance {  // 每一篇汉明距离的结构定义,属于辅助result而定义的
    char article_identifier[16];
    int distance;
} hanmming_distance;

typedef struct result {  // 结果类型
    char sample_identifier[16];  // 本文标识符
    int sample_feature[129];  // 本文的指纹向量
    hanmming_distance gather[10000];  // 汉明距离
} _result;


/*----------------------常量与全局变量------------------------*/
_result result;

然后在主函数中读取sample.txt中的每一篇文章,计算其指纹,并存储到result中,和上面的思路差不多,如下:

    while(get_identifier(sample, result.sample_identifier) != EOF) {
        char temp_word[15];
        wptr temp_pos;
        int flag = 0;
        while((flag = get_one_word_in_cur_article(sample, temp_word)) != '\f') {
            if (flag == EOF) break;
            if ((temp_pos = find_in_hash(temp_word)) != NULL) {
                for (int i = 0; i < M; i++) {
                    if ((temp_pos -> hashvalue[i] == '0')) {                        
                        result.sample_feature[i]--;
                    }else {
                        result.sample_feature[i]++;                        
                    }
                }
            }
        } 
        // 已经算出来了,大于0的位置1,其余置0
        for (int i = 0; i < M; i++) {
            if (result.sample_feature[i] > 0) 
                result.sample_feature[i] = 1;
            else 
                result.sample_feature[i] = 0;
        }

        // 以下去计算本文的指纹和article中的各片的汉明距离



        if (flag == EOF) break;
    }

其中“计算本文的指纹和article中的各片的汉明距离”,我们可以设计一个函数去计算:

int calculate_hamming_distance(int fingerprint1[], int fingerprint2[])
{
    int res = 0;
    for (int i = 0; i < M; i++) {
        if (! (fingerprint1[i] == fingerprint2[i])) {
            res++;
        }
    }
    return res;
}

在主函数“// 以下去计算本文的指纹和article中的各片的汉明距离”处补充代码如下:

        // 以下去计算本文的指纹和article中的各片的汉明距离
        for (int i = 0; i < article_num; i++) {
            strcpy(result.gather[i].article_identifier, article_data[i].identifier);  // 记录标识符
            result.gather[i].distance = calculate_hamming_distance(result.sample_feature[i], article_data[i].feature);  // 记录汉明距离
        }

此时我打印了一下结果发现不能把所有文章都读进来,一打开给的sample.txt文件发现文章前面还有几行空行。。。。。。只能说这设计的是真狗啊,为此,还得修改一下get_identifier函数:

int get_identifier(FILE* fp, char *dst)
{
    while (fgets(dst, 128, fp) != NULL) {
        if (dst[0] != '\n') return 0;
    }
    return EOF;
}

记录之后我们按照汉明距离对各篇文章进行排序,为此还需再设计一个cmp函数:

int cmp_for_distance(const void *a, const void *b)
{
    hanmming_distance* e1 = (hanmming_distance*) a;
    hanmming_distance* e2 = (hanmming_distance*) b;
    if (e1 -> distance != e2 -> distance) return e1 -> distance - e2 -> distance;
    return strcmp(e1 -> article_identifier, e2 -> article_identifier);
}

而后在主函数循环中进行排序:

qsort(result.gather, article_num, sizeof(result.gather[0]), cmp_for_distance);

而后我们输出检测一下(还没有按照原题目中要求格式输出):

        for (int i = 0; i < article_num; i++) {
            if (result.gather[i].distance > 3) break;
            printf("%d: %s\n", result.gather[i].distance, result.gather[i].article_identifier);
        }

发现:对应的文章输出完全正确!但是顺序不一样,究其原因,是因为我们在cmp_for_distance函数中比较顺序时,当两者的汉明距离相同时,我们直接比较了标识符的字典序,这是不合理的,比如1-79和1-148这种就无法正确排序,为此,我们得想办法解决这个问题。单从标识符去判断应该也行,但我写到这懒了,懒得去想了,于是我的解决办法就是读article的文章时,在结构体中加一个域表示这是第几篇文章,于是passage结构体改写如下:

typedef struct passage {
    char identifier[16];
    int feature[129];
    int order;
} passage;

并且在读article的时候加一句article_data[article_num].order = article_num;

同时,对hanmming_distance结构体(result的一部分)也加一个order域:

typedef struct hanmming_distance {  // 每一篇汉明距离的结构定义,属于辅助result而定义的
    char article_identifier[16];
    int distance;
    int order;  // 文章的顺序
} hanmming_distance;

在读sample的循环中,对其中的每一篇article的数据,我们也给其order赋值,然后再排序:

        // 以下去计算本文的指纹和article中的各片的汉明距离
        for (int i = 0; i < article_num; i++) {
            result.gather[i]. order = i;
            strcpy(result.gather[i].article_identifier, article_data[i].identifier);  // 记录标识符
            result.gather[i].distance = calculate_hamming_distance(result.sample_feature, article_data[i].feature);  // 记录汉明距离
        }
        qsort(result.gather, article_num, sizeof(result.gather[0]), cmp_for_distance);

同时改写cmp_for_distance函数如下:

int cmp_for_distance(const void *a, const void *b)
{
    hanmming_distance* e1 = (hanmming_distance*) a;
    hanmming_distance* e2 = (hanmming_distance*) b;
    if (e1 -> distance != e2 -> distance) return e1 -> distance - e2 -> distance;
    return e1 -> order - e2 -> order;
}

我们再按照刚才的格式,读入1000和16,输出一sample1的结果如下:

0: 1-1
1: 1-111
1: 1-327
1: 1-901
1: 1-917
2: 1-79
2: 1-148
2: 1-200
2: 1-235
2: 1-319
2: 1-353
2: 1-380
2: 1-381
2: 1-391
2: 1-508
2: 1-511
2: 1-531
2: 1-571
2: 1-577
2: 1-614
2: 1-616
2: 1-838
2: 1-842
2: 1-872
2: 1-959
3: 1-10
3: 1-43
3: 1-51
3: 1-62
3: 1-71
3: 1-77
3: 1-91
3: 1-93
3: 1-96
3: 1-103
3: 1-114
3: 1-115
3: 1-116
3: 1-132
3: 1-155
3: 1-156
3: 1-158
3: 1-165
3: 1-170
3: 1-193
3: 1-203
3: 1-206
3: 1-217
3: 1-225
3: 1-226
3: 1-233
3: 1-241
3: 1-246
3: 1-253
3: 1-296
3: 1-315
3: 1-328
3: 1-345
3: 1-368
3: 1-400
3: 1-407
3: 1-446
3: 1-453
3: 1-458
3: 1-480
3: 1-494
3: 1-496
3: 1-502
3: 1-509
3: 1-510
3: 1-512
3: 1-522
3: 1-543
3: 1-552
3: 1-553
3: 1-559
3: 1-572
3: 1-595
3: 1-597
3: 1-666
3: 1-674
3: 1-675
3: 1-679
3: 1-695
3: 1-696
3: 1-712
3: 1-727
3: 1-729
3: 1-730
3: 1-743
3: 1-796
3: 1-797
3: 1-801
3: 1-813
3: 1-830
3: 1-835
3: 1-846
3: 1-849
3: 1-851
3: 1-868
3: 1-873
3: 1-883
3: 1-892
3: 1-902
3: 1-905
3: 1-923
3: 1-944
3: 1-945
3: 1-955
3: 1-963
3: 1-964
3: 1-972
3: 1-975
3: 1-976
3: 1-990

当读入1000和32时,结果如下:

0: 1-1
2: 1-901

完全符合!,历经千辛万苦,终于把程序的主体敲完了,剩下的就是按照格式去输出了!

调整输出格式

由于我们把每一篇sample的所有输出相关的数据都记录到result变量里了,之后article_num变量我们没有设成全局变量,所以输出的时候可以设计一个print_result函数,把article_num传进去(也可以把article_num设计成全局变量),把result中对应的数据输出即可。

这里也是有一点麻烦的~~(这大作业真是处处麻烦)~~,因为有可能某个汉明距离没有文章与其对应,就不要打印,比如样例2输入的1000 32,所以我就直接分段输出了(先完成任务再说,优化的事情先搁一边):

void print_result(int article_num)
{
    printf("%s", result.sample_identifier);  // 打印sample的标识符
    int now_lacation = 0;  // 记录当前打印到第几个数据
    if (result.gather[now_lacation].distance == 0) {       
        printf("%d:", 0);
        for (; now_lacation < article_num; now_lacation++) {
            // 由于我们读标识符的时候用的fgets把末尾的换行符也读进来了,所以要先把其换行符去掉,下面同理
            result.gather[now_lacation].article_identifier[strlen(result.gather[now_lacation].article_identifier) - 1] = 0;
            if (result.gather[now_lacation].distance == 0) 
                printf("%s ", result.gather[now_lacation].article_identifier);
            else {
                printf("\n");
                break;  
            }                                      
        }
    }

    if (result.gather[now_lacation].distance == 1) {       
        printf("%d:", 1);
        for (; now_lacation < article_num; now_lacation++) {
            result.gather[now_lacation].article_identifier[strlen(result.gather[now_lacation].article_identifier) - 1] = 0;
            if (result.gather[now_lacation].distance == 1) 
                printf("%s ", result.gather[now_lacation].article_identifier);
            else {
                printf("\n");
                break;  
            }                                      
        }
    }

    if (result.gather[now_lacation].distance == 2) {       
        printf("%d:", 2);
        for (; now_lacation < article_num; now_lacation++) {
            result.gather[now_lacation].article_identifier[strlen(result.gather[now_lacation].article_identifier) - 1] = 0;
            if (result.gather[now_lacation].distance == 2) 
                printf("%s ", result.gather[now_lacation].article_identifier);
            else {
                printf("\n");
                break;  
            }                                      
        }
    }

    if (result.gather[now_lacation].distance == 3) {       
        printf("%d:", 3);
        for (; now_lacation < article_num; now_lacation++) {
            result.gather[now_lacation].article_identifier[strlen(result.gather[now_lacation].article_identifier) - 1] = 0;
            if (result.gather[now_lacation].distance == 3) 
                printf("%s ", result.gather[now_lacation].article_identifier);
            else {
                printf("\n");
                break;  
            }                                      
        }
    }
    if (result.gather[now_lacation].distance > 3) return ;  // 大于3的不打印
}

之后在主函数中调用print_result,输入1000 16,打印出sample1的数据如下:

Sample-1
0:1-1
1:1-11 1-327 1-901 1-917
2:1-7 1-148 1-200 1-235 1-319 1-353 1-380 1-381 1-391 1-508 1-511 1-531 1-571 1-577 1-614 1-616 1-838 1-842 1-872 1-959
3:1-1 1-43 1-51 1-62 1-71 1-77 1-91 1-93 1-96 1-103 1-114 1-115 1-116 1-132 1-155 1-156 1-158 1-165 1-170 1-193 1-203 1-206 1-217 1-225 1-226 1-233 1-241 1-246 1-253 1-296 1-315 1-328 1-345 1-368 1-400 1-407 1-446 1-453 1-458 1-480 1-494 1-496 1-502 1-509 1-510 1-512 1-522 1-543 1-552 1-553 1-559 1-572 1-595 1-597 1-666 1-674 1-675 1-679 1-695 1-696 1-712 1-727 1-729 1-730 1-743 1-796 1-797 1-801 1-813 1-830 1-835 1-846 1-849 1-851 1-868 1-873 1-883 1-892 1-902 1-905 1-923 1-944 1-945 1-955 1-963 1-964 1-972 1-975 1-976 1-990

对比发现,成功了!!!!!!!!!!!!!!!!!!

然后打印全部的数据,发现后面几篇文章是有问题的(晕),我把情况和身边的同学吐槽,经过他的点拨,我明白了原来是我的result变量在每次输出之后没有清零,故而算指纹的时候就错乱了,所以在每次读入之后都把result变量置0,用memset函数:

memset(&result, 0, sizeof(result));

此时输出,发现了一些神奇的数据(左边是标准答案,右边是我的输出):

在这里插入图片描述

有一些数据的末尾被吞了!好家伙,有的标识符读进来了换行符,有的没有是吧,这数据给的真是一坨*,但没办法,我们在清除末尾的换行符的时候只好判断一下再清除了,改写print_result如下:

void print_result(int article_num)
{
    printf("%s", result.sample_identifier);  // 打印sample的标识符
    int now_lacation = 0;  // 记录当前打印到第几个数据
    if (result.gather[now_lacation].distance == 0) {       
        printf("%d:", 0);
        for (; now_lacation < article_num; now_lacation++) {
            // 由于我们读标识符的时候用的fgets把末尾的换行符也读进来了,所以要先把其换行符去掉
            if (result.gather[now_lacation].article_identifier[strlen(result.gather[now_lacation].article_identifier) - 1] == '\n')
                result.gather[now_lacation].article_identifier[strlen(result.gather[now_lacation].article_identifier) - 1] = 0;
            if (result.gather[now_lacation].distance == 0) 
                printf("%s ", result.gather[now_lacation].article_identifier);
            else {
                printf("\n");
                break;  
            }                                      
        }
    }

    if (result.gather[now_lacation].distance == 1) {       
        printf("%d:", 1);
        for (; now_lacation < article_num; now_lacation++) {
            if (result.gather[now_lacation].article_identifier[strlen(result.gather[now_lacation].article_identifier) - 1] == '\n')
                result.gather[now_lacation].article_identifier[strlen(result.gather[now_lacation].article_identifier) - 1] = 0;
            if (result.gather[now_lacation].distance == 1) 
                printf("%s ", result.gather[now_lacation].article_identifier);
            else {
                printf("\n");
                break;  
            }                                      
        }
    }

    if (result.gather[now_lacation].distance == 2) {       
        printf("%d:", 2);
        for (; now_lacation < article_num; now_lacation++) {
            if (result.gather[now_lacation].article_identifier[strlen(result.gather[now_lacation].article_identifier) - 1] == '\n')
                result.gather[now_lacation].article_identifier[strlen(result.gather[now_lacation].article_identifier) - 1] = 0;
            if (result.gather[now_lacation].distance == 2) 
                printf("%s ", result.gather[now_lacation].article_identifier);
            else {
                printf("\n");
                break;  
            }                                      
        }
    }

    if (result.gather[now_lacation].distance == 3) {       
        printf("%d:", 3);
        for (; now_lacation < article_num; now_lacation++) {
            if (result.gather[now_lacation].article_identifier[strlen(result.gather[now_lacation].article_identifier) - 1] == '\n')
                result.gather[now_lacation].article_identifier[strlen(result.gather[now_lacation].article_identifier) - 1] = 0;
            if (result.gather[now_lacation].distance == 3) 
                printf("%s ", result.gather[now_lacation].article_identifier);
            else {
                printf("\n");
                break;  
            }                                      
        }
    }
    if (result.gather[now_lacation].distance > 3) return ;  // 大于3的不打印
}

终于把所有数据都对上了!,写了快3天的大作业,刚才输出成那样,刚才差点崩溃了都。

交的时候把scanf读入改成命令行读入即可,然而,交上去就发现。。。出错了。。。。。。。。。。。真的崩溃了。明明文本全都对了,为什么还过不了!于是求助助教,经过了一番周折,在题干中发现了输出格式的问题:他要把所有结果输出到rersult.txt中,并且在控制台输出sample1的结果。要我说,在本题结尾搞这一出,属实有病。。。

此功能比较简单,输出时改一下就好了,无非加了一个文件指针,print_result函数多加一个文件指针的输入即可:

void print_result(int article_num, FILE* fp)
{
    fprintf(fp, "%s", result.sample_identifier);  // 打印sample的标识符
    int now_lacation = 0;  // 记录当前打印到第几个数据
    if (result.gather[now_lacation].distance == 0) {       
        fprintf(fp, "%d:", 0);
        for (; now_lacation < article_num; now_lacation++) {
            // 由于我们读标识符的时候用的fgets把末尾的换行符也读进来了,所以要先把其换行符去掉
            if (result.gather[now_lacation].article_identifier[strlen(result.gather[now_lacation].article_identifier) - 1] == '\n')
                result.gather[now_lacation].article_identifier[strlen(result.gather[now_lacation].article_identifier) - 1] = 0;
            if (result.gather[now_lacation].distance == 0) 
                fprintf(fp, "%s ", result.gather[now_lacation].article_identifier);
            else {
                fprintf(fp, "\n");
                break;  
            }                                      
        }
    }

    if (result.gather[now_lacation].distance == 1) {       
        fprintf(fp, "%d:", 1);
        for (; now_lacation < article_num; now_lacation++) {
            if (result.gather[now_lacation].article_identifier[strlen(result.gather[now_lacation].article_identifier) - 1] == '\n')
                result.gather[now_lacation].article_identifier[strlen(result.gather[now_lacation].article_identifier) - 1] = 0;
            if (result.gather[now_lacation].distance == 1) 
                fprintf(fp, "%s ", result.gather[now_lacation].article_identifier);
            else {
                fprintf(fp, "\n");
                break;  
            }                                      
        }
    }

    if (result.gather[now_lacation].distance == 2) {       
        fprintf(fp, "%d:", 2);
        for (; now_lacation < article_num; now_lacation++) {
            if (result.gather[now_lacation].article_identifier[strlen(result.gather[now_lacation].article_identifier) - 1] == '\n')
                result.gather[now_lacation].article_identifier[strlen(result.gather[now_lacation].article_identifier) - 1] = 0;
            if (result.gather[now_lacation].distance == 2) 
                fprintf(fp, "%s ", result.gather[now_lacation].article_identifier);
            else {
                fprintf(fp, "\n");
                break;  
            }                                      
        }
    }

    if (result.gather[now_lacation].distance == 3) {       
        fprintf(fp, "%d:", 3);
        for (; now_lacation < article_num; now_lacation++) {
            if (result.gather[now_lacation].article_identifier[strlen(result.gather[now_lacation].article_identifier) - 1] == '\n')
                result.gather[now_lacation].article_identifier[strlen(result.gather[now_lacation].article_identifier) - 1] = 0;
            if (result.gather[now_lacation].distance == 3) 
                fprintf(fp, "%s ", result.gather[now_lacation].article_identifier);
            else {
                fprintf(fp, "\n");
                break;  
            }                                      
        }
    }
    if (result.gather[now_lacation].distance > 3) return ;  // 大于3的不打印
}

然后主函数中判断一下去输出即可,然后就会发现,还是不对(崩溃中)。经过助教的提醒,知道了评测机时Linux系统,他的换行符比较特别。。。所以我们读标识符的时候用fscanf读,最后输出的时候带上\n即可。同时,大数据的文章较多,我们原来的数组开的不够大——往死了开就行。

完整代码

#pragma GCC optimize ("O3")
#pragma once
#pragma pack (16)

# include <stdio.h>
# include <stdlib.h>
# include <string.h>


/*----------------------类型定义------------------------*/
typedef struct word {
    int time;
    char str[50];
} word;

typedef struct feature_word
{
    char str[50];  // 单词
    char hashvalue[129];  // 单词对应的哈希值
    struct feature_word *next;
} feature_word;

typedef feature_word* wptr;

typedef struct passage {
    char identifier[16];
    int feature[129];
    int order;
} passage;

typedef struct hanmming_distance {  // 每一篇汉明距离的结构定义,属于辅助result而定义的
    char article_identifier[16];
    int distance;
    int order;  // 文章的顺序
} hanmming_distance;

typedef struct result {  // 结果类型
    char sample_identifier[16];  // 本文标识符
    int sample_feature[129];  // 本文的指纹向量
    hanmming_distance gather[10000000];  // 汉明距离
} _result;


/*----------------------常量与全局变量------------------------*/
# define MAX_SIZE 1000000

int M, N;  // 记录命令行参数

// 文件指针
FILE* article;
FILE* sample;
FILE* hashvalue;
FILE* stopwords;
FILE* out;

char stopwords_list[320][15];  // 停用词表

word word_frequency_list[MAX_SIZE]; // 词频表
int trie[MAX_SIZE][27]; //字典树(词频统计的)
int pos;  // 单词位置

wptr head[9007];
passage article_data[1000000];

_result result;


/*----------------------函数------------------------*/
int build_stopwords_list(FILE* stopwords)  
// 建立停用词表
{   
    
    int num = 0;
    while (fscanf(stopwords, "%s", stopwords_list[num++]) != EOF) {}    
    return num;
}


int cmp_is_legal(const void* a, const void* b)  
{
    return strcmp((char*)a, (char*)b);
}


int is_legal(char* word, int stop_word_num)  
// 判断该词是否在停用词表中
{
    if (!(bsearch(word, stopwords_list, stop_word_num, sizeof(stopwords_list[0]), cmp_is_legal)))
        return 1;
    return 0;
}


int get_all_words_in_article(FILE* fp, char* dst)  
// 读取article中的所有单词,每次读一个,结尾返回EOF
{
    int i = 0;
    char c;
    while ((c = fgetc(fp)) != EOF) {
        if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) {
            dst[i++] = (c >= 'a' && c <= 'z') ? c : c + 'a' - 'A';
        } else if (i) {
            dst[i] = '\0';
            return 0;
        }
    }
    return EOF;
}


void insert_to_trie(char* str)
// 向字典树中插入一个单词
{
    if (strlen(str) < 50)  // 防止文章中识别不出来的单词的影响(比如cqioawcwaecbwuesvcwseev这种)
    {
        int p = 0;
        for (int i = 0; str[i]; i++) {
            int n = str[i] - 'a';
            if (trie[p][n] == 0) {
                trie[p][n] = pos++;
            }
            p = trie[p][n];
        }
        if (!word_frequency_list[p].time) {        
            strcpy(word_frequency_list[p].str, str);
        }        
        word_frequency_list[p].time++;
    }
}


int cmp_for_frelist(const void* a, const void* b)
{
    word* e1 = (word*)a;
    word* e2 = (word*)b;
    if (e1 -> time != e2 -> time) return e2 -> time - e1 -> time;
    return strcmp(e1 -> str, e2 -> str);
}


wptr newnode()
{
    return (wptr)malloc(sizeof(feature_word));
}


wptr getnode(char *str)
{
    wptr p = newnode();
    p -> next = NULL;
    strcpy(p -> str, str);   
    return p;
}


int hashCode(char *str)
// 计算哈希值函数
{
    int tem = 0;
    int len = strlen(str);
    for(int i = 0;i < len;i++)
        tem = (tem * 26 + str[i] - 'a' + 1) % 9007;
    return tem;
}


void insert_to_hash(char *str, char* tmp_hash)
// 映射函数
{
    int val = hashCode(str);
    wptr p = getnode(str);
    p -> next = head[val];
    strcpy(p -> hashvalue, tmp_hash);
    head[val] = p;
}


wptr find_in_hash(char *str)
// 寻找单词对应的结点的函数
{
    int val = hashCode(str);
    for(wptr i = head[val]; i ; i = i -> next)
        if(strcmp(str,i -> str) == 0) 
            return i;
    return NULL; // 没找到
}


int get_identifier(FILE* fp, char *dst)
{
    if (fscanf(fp, "%s", dst) != EOF) return 0;
    return EOF;

}


int get_one_word_in_cur_article(FILE* fp, char* dst)  
// 读取article中的所有单词,每次读一个,结尾返回EOF
{
    int i = 0;
    char c;
    while ((c = fgetc(fp)) != '\f' && c != EOF) {
        if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) {
            dst[i++] = (c >= 'a' && c <= 'z') ? c : c + 'a' - 'A';
        } else if (i) {
            dst[i] = '\0';
            return 0;
        }
    }
    if (c == EOF) 
        return EOF;
    
    fgetc(fp);  // 把最后的换行符吃掉
    return '\f';
}


int calculate_hamming_distance(int fingerprint1[], int fingerprint2[])
{
    int res = 0;
    for (int i = 0; i < M; i++) {
        if (! (fingerprint1[i] == fingerprint2[i])) {
            res++;
        }
    }
    return res;
}


int cmp_for_distance(const void *a, const void *b)
{
    hanmming_distance* e1 = (hanmming_distance*) a;
    hanmming_distance* e2 = (hanmming_distance*) b;
    if (e1 -> distance != e2 -> distance) return e1 -> distance - e2 -> distance;
    return e1 -> order - e2 -> order;
}


void print_result(int article_num, FILE* fp)
{
    fprintf(fp, "%s\n", result.sample_identifier);  // 打印sample的标识符
    int now_lacation = 0;  // 记录当前打印到第几个数据
    if (result.gather[now_lacation].distance == 0) {       
        fprintf(fp, "%d:", 0);
        for (; now_lacation < article_num; now_lacation++) {
            if (result.gather[now_lacation].distance == 0) 
                fprintf(fp, "%s ", result.gather[now_lacation].article_identifier);
            else {
                fprintf(fp, "\n");
                break;  
            }                                      
        }
    }

    if (result.gather[now_lacation].distance == 1) {       
        fprintf(fp, "%d:", 1);
        for (; now_lacation < article_num; now_lacation++) {
            if (result.gather[now_lacation].distance == 1) 
                fprintf(fp, "%s ", result.gather[now_lacation].article_identifier);
            else {
                fprintf(fp, "\n");
                break;  
            }                                      
        }
    }

    if (result.gather[now_lacation].distance == 2) {       
        fprintf(fp, "%d:", 2);
        for (; now_lacation < article_num; now_lacation++) {
            if (result.gather[now_lacation].distance == 2) 
                fprintf(fp, "%s ", result.gather[now_lacation].article_identifier);
            else {
                fprintf(fp, "\n");
                break;  
            }                                      
        }
    }

    if (result.gather[now_lacation].distance == 3) {       
        fprintf(fp, "%d:", 3);
        for (; now_lacation < article_num; now_lacation++) {
            if (result.gather[now_lacation].distance == 3) 
                fprintf(fp, "%s ", result.gather[now_lacation].article_identifier);
            else {
                fprintf(fp, "\n");
                break;  
            }                                      
        }
    }
    if (result.gather[now_lacation].distance > 3) return ;  // 大于3的不打印
}



/*---------------主函数-------------------*/
int main(int argc, const char* argv[])
{
    article = fopen("article.txt","r");
    sample = fopen("sample.txt","r");
    hashvalue = fopen("hashvalue.txt","r");
    stopwords = fopen("stopwords.txt","r");
    out = fopen("result.txt", "w");
    N = atoi(argv[1]);
    M = atoi(argv[2]);
    // scanf("%d%d", &N, &M);

    // 为方便调试,可以先从控制台读入,最后把代码改成命令行读入即可

    int stopword_num = build_stopwords_list(stopwords);  // 建立停用词表(无论用什么数据结构存储该表,我们都把它定义为全局变量)
    


    pos = 1;
    char tmp_word[100];  
    
    while(get_all_words_in_article(article, tmp_word) != EOF) {
        insert_to_trie(tmp_word);  // 将该单词插入字典树        
    }
    rewind(article);  // 将文件指针移至文件初
    
    qsort(word_frequency_list, pos, sizeof(word), cmp_for_frelist);  // 词频排序
    
    // 取出词频表中前n个不在停用词表中的单词
    int feature_num = 0;
    char tmp_hash[129] = {0};
    for (int i = 0; ; i++) {        
        if (is_legal(word_frequency_list[i].str, stopword_num)) {   
            for (int j = 0; j < M; j++) {  // 读hashvalue.txt中该行的前M个字符
                tmp_hash[j] = fgetc(hashvalue);
            }
            tmp_hash[M] = '\0';                       
            
            insert_to_hash(word_frequency_list[i].str, tmp_hash);  // 插入哈希表
            fgets(tmp_hash, 128, hashvalue); // 将文件指针移动至下一行首
            feature_num++;
            if (feature_num == N) break;
        }        
    }

    int article_num = 0;
    while(get_identifier(article, article_data[article_num].identifier) != EOF) {
        article_data[article_num].order = article_num;
        char temp_word[20];
        wptr temp_pos;
        int flag = 0;
        while((flag = get_one_word_in_cur_article(article, temp_word)) != '\f') {
            if (flag == EOF) break;
            if ((temp_pos = find_in_hash(temp_word)) != NULL) {
                for (int i = 0; i < M; i++) {
                    if ((temp_pos -> hashvalue[i] == '0')) {                        
                        article_data[article_num].feature[i]--;
                    }else {
                        article_data[article_num].feature[i]++;                        
                    }
                }
            }
        }
        // 已经算出来了,大于0的位置1,其余置0
        for (int i = 0; i < M; i++) {
            if (article_data[article_num].feature[i] > 0) 
                article_data[article_num].feature[i] = 1;
            else 
                article_data[article_num].feature[i] = 0;
        }
        article_num++;
        if (flag == EOF) break;
        
    }

    
    int num = 0;
    while(get_identifier(sample, result.sample_identifier) != EOF) {

        char temp_word[50];
        wptr temp_pos;
        int flag = 0;
        while((flag = get_one_word_in_cur_article(sample, temp_word)) != '\f') {
            if (flag == EOF) break;
            if ((temp_pos = find_in_hash(temp_word)) != NULL) {
                for (int i = 0; i < M; i++) {
                    if ((temp_pos -> hashvalue[i] == '0')) {                        
                        result.sample_feature[i]--;
                    }else {
                        result.sample_feature[i]++;                        
                    }
                }
            }
        } 
        // 已经算出来了,大于0的位置1,其余置0
        for (int i = 0; i < M; i++) {
            if (result.sample_feature[i] > 0) 
                result.sample_feature[i] = 1;
            else 
                result.sample_feature[i] = 0;
        }
        
        // 以下去计算本文的指纹和article中的各片的汉明距离
        for (int i = 0; i < article_num; i++) {
            result.gather[i]. order = i;
            strcpy(result.gather[i].article_identifier, article_data[i].identifier);  // 记录标识符
            result.gather[i].distance = calculate_hamming_distance(result.sample_feature, article_data[i].feature);  // 记录汉明距离
        }
        qsort(result.gather, article_num, sizeof(result.gather[0]), cmp_for_distance);
        if (num == 0) {
            print_result(article_num, stdout);
        }
        print_result(article_num, out);
        memset(&result, 0, sizeof(result));
        num++;
        if (flag == EOF) break;  
    }
    
    
    fclose(out);
    fclose(article);
    fclose(sample);
    fclose(hashvalue);
    fclose(stopwords);

    return 0;
}

此代码跑大数据约7.4s。

优化思路(肯定还有很多别的思路,笔者最后优化到2s左右就没继续优化)

思路一

在.c文件头部加上这三行代码:

#pragma GCC optimize ("O3")
#pragma once
#pragma pack (16)

我也不清楚这里面的原理,但是看网上说这么写会优化一下时间,就这样吧。

思路二

程序中用到的isalpha和tolower需要引入头文件ctype.h,加载头文件需要一些时间,所以我们自己去实现这两个函数的功能(非常简单),把头文件去掉。

思路三

在处理停用词时,我们原来的思路是先根据stopwords.txt文件建立停用词表,然后读article.txt的所有单词时不管什么单词都读进来,最后取前n个单词的时候都读到字典树里,然后排序,最后取前N个单词的时候每次都判断一下该词是否在停用词表中,这里我们用到了二分查找。

我改进如下:不建立停用词表,读完article.txt中所有单词后,把所有停用词都在字典树里找一遍,如果当前字典树里有这个停用词,就把其time域置0,这样我们取前N个单词的时候就不用二分查找去判断该词是否在停用词表中了,这样我们就固定查找320次(停用词个数),每次查找所需最多搜索次数为该单词的长度(停用词都很短,次数不多),比原来的思路能优化不少。

具体实现时,把build_stopwords_list、 cmp_is_legal、 is_legal删除掉,新写一个在字典树中查找单词的函数:

word* find_in_trie(char str[]){  
    //查找以某个字符串为前缀的单词位置
	int p = 0;
	for(int i = 0; str[i]; ++i){
		int n = str[i] - 'a';
		if(trie[p][n] == 0){
			return NULL;
		}
		p = trie[p][n];		
	}
	return &word_frequency_list[p];
}

同时在主函数中,读完article.txt的单词后,排序前的那段代码改写如下:

    char stop_word[50];
    while(fscanf(stopwords, "%s", stop_word) != EOF) {
        word* tmp_pos = find_in_trie(stop_word);
        if (tmp_pos != NULL) {
            tmp_pos -> time = 0;
        }
    }

现在再去交,大概是6.92-7s之间的时间,确实比原来优化了不少。

思路四

在我一开始写print_result函数时,构造的结构体是瞎构造——不管汉明距离是几都一股脑地存进去,然后每次都qsort一遍。输出时也不长脑袋,硬是把相同的代码写了4遍。当时我就说这里可以优化,现在就来优化他。

其实我们不需要再排序了!只要开4个数组,分别记录汉明距离为0,1,2,3的文章的标识符输出时对这4个数组输出即可,于是改造的结构体如下:

typedef struct result {  // 结果类型
    char sample_identifier[16];  // 本文标识符
    int sample_feature[129];  // 本文的指纹向量
    char gather[4][1000000][50];  // 距离为0,1,2,3的文章的标识符
    int cnt[4];

} _result;

原来的hanming_distance被删掉了,所以在主函数中,在对每篇sample中记录其于各篇article的汉明距离,以及输出时,也要改写:

// 主函数中
        for (int i = 0; i < article_num; i++) {
            int dis = calculate_hamming_distance(result.sample_feature, article_data[i].feature);
            if (dis <= 3)
                strcpy(result.gather[dis][result.cnt[dis]++], article_data[i].identifier);
        }
        
        if (num == 0) {
            print_result(article_num, stdout);
        }
        print_result(article_num, out);
        memset(&result, 0, sizeof(result));

其中print_result改写如下:

void print_result(int article_num, FILE* fp)
{
    fprintf(fp, "%s\n", result.sample_identifier);  // 打印sample的标识符
    for (int i = 0; i < 4; i++) {
        if (!result.cnt[i]) continue;
        int num = result.cnt[i];
        fprintf(fp, "%d:", i);
        for (int j = 0; j < num; j++) {
            fprintf(fp, "%s ", result.gather[i][j]); 
        }
        fprintf(fp, "\n");
    }
}

然后就会发现,时间变成了11s+。。。我想可能是原来数组开的是1000000的长度,现在我们开了个三维数组,相当于100倍,那memset可能会耗时很多,所以我把他开成50000,发现直接窜到4s了!真是令我大开眼界——原来memset才是那个时间杀手!

思路五

由思路四启发,想到其实我们用memset也是当时图省事,现在这种情况,我们对result置0,其实只要对其cnt和sample_identifier域置0即可,所以我们自己写一个函数替代memset:

void set_zero()
{
    for (int i = 0; i < 4; i++) {
        result.cnt[i] = 0;
    }
    for (int i = 0; i < 129; i++) {
        result.sample_feature[i] = 0;
    }
}

此时测评机上时间基本在2.6s-2.7s范围内。

思路六

在写读入单词的通用函数时,我们是这么写的:

int get_all_words_in_article(char* dst)  
// 读取article中的所有单词,每次读一个,结尾返回EOF
{
    int i = 0;
    char c;
    while ((c = fgetc(fp)) != EOF) {
        if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) {
            dst[i++] = (c >= 'a' && c <= 'z') ? c : c + 'a' - 'A';
        } else if (i) {
            dst[i] = '\0';
            return 0;
        }
    }
    return EOF;
}

要进行两次判断,但我们都知道文章里小写字母肯定最多,大写字母肯定最小,于是可以改成这样优化判断次数:

int get_all_words_in_article( FILE* fp, char* dst)  
// 读取article中的所有单词,每次读一个,结尾返回EOF
{
    int i = 0;
    char c;
    while ((c = fgetc(fp)) != EOF) {
        if (c >= 'a' && c <= 'z') {
            dst[i++] = c;
        } else if (c >= 'A' && c <= 'Z') {
            dst[i++] = c + 'a' - 'A';
        } else if (i) {
            dst[i] = '\0';
            return 0;
        }
    }
    return EOF;
}

同样对get_one_word_in_cur_article改写如下:

int get_one_word_in_cur_article(FILE* fp, char* dst)  
// 读取article中的所有单词,每次读一个,结尾返回EOF
{
    int i = 0;
    char c;
    while ((c = fgetc(fp)) != '\f' && c != EOF) {
        // while(fread(&c, CHAR_SIZE, 1, fp) != 0) {
        if (c >= 'a' && c <= 'z') {
            dst[i++] = c;
        } else if (c >= 'A' && c <= 'Z') {
            dst[i++] = c + 'a' - 'A';
        } else if (i) {
            dst[i] = '\0';
            return 0;
        }
    }
    if (c == EOF) 
        return EOF;   
    fgetc(fp);  // 把最后的换行符吃掉
    return '\f';
}

思路七

使用fread快读函数(助教提示的),可以参考这些文章:

我们先来改第一次读article时的代码,可以定义一个全局变量数组记录article.txt的所有字节:

/*----------------------常量与全局变量------------------------*/
char ARTICLE[10000000];
int article_len;  // article.txt的长度
int article_loc;  // 当前遍历到的下标

同时要注意主函数中用rb方式来读取:

// 主函数中
    article = fopen("article.txt","rb");
    sample = fopen("sample.txt","r");
    hashvalue = fopen("hashvalue.txt","r");
    stopwords = fopen("stopwords.txt","r");
    out = fopen("result.txt", "w");

    article_len = fread(ARTICLE, sizeof(char), 10000000, article);

改写get_all_words_in_article如下:

int get_all_words_in_article(char* dst)  
// 读取article中的所有单词,每次读一个,结尾返回EOF
{
    int i = 0;
    char c;
    while ((article_loc < article_len)) {
        c = ARTICLE[article_loc++];
    // 原来是 while ((c = fgetc(fp)) != EOF) {
        if (c >= 'a' && c <= 'z') {
            dst[i++] = c;
        } else if (c >= 'A' && c <= 'Z') {
            dst[i++] = c + 'a' - 'A';
        } else if (i) {
            dst[i] = '\0';
            return 0;
        }
    }
    return EOF;
}

现在交上去已经是2.4秒了。但我们还有第二次读入article.txt和读入sample.txt没有修改,而且article.txt已经不用再次调用fread读入了,也不用rewind了——我们已经存到数组里了,所以只要把article_loc置0,再遍历一次即可。但是这也是有很大麻烦的,因为我们虽然可以通过\f来断开文章,但是却很难完美地在数组中找出其标识符。幸好我们最开始用函数去封装了读单词的函数,所以只要设计相同的接口去读入就OK,于是可以先改写读入article.txt中的每篇文章的标识符与单词的代码如下(注意我们要设计无论什么情况下,返回值和原函数都相同的函数,这样主函数中就直接调用api就行):

int get_identifier_in_article(char *dst)  // 读入article.txt当前的标识符
{
    int i = 0;
    char c;
    while (article_loc < article_len) {
        c = ARTICLE[article_loc++];
        if ((c >= 'a' && c <= 'z') || (c == 'A' || c == 'Z') || c >= '0' || c == '9' || c == '-') {
            dst[i++] = c;
        } else if (i) {
            dst[i] = '\0';
            return 0;
        }
    }
    return EOF;
}


int get_one_word_in_article(char *dst)  // 读入article.txt当前文章中的一个单词
{
    int i = 0;
    char c;
    while (article_loc < article_len && (c = ARTICLE[article_loc++]) != '\f') { 
        if (c >= 'a' && c <= 'z') {
            dst[i++] = c;
        } else if (c >= 'A' && c <= 'Z') {
            dst[i++] = c + 'a' - 'A';
        } else if (i) {
            dst[i] = '\0';
            return 0;
        }
    }
    if (c == '\f') return '\f';
    return EOF;
}

读sample时同理,现在基本上是2.2s左右了。

思路八

减少函数调用——写函数可读性强,但是会额外耗费时间(网上有解释),我一开始为了方便优化调试而写了一堆函数,现在可以删去一些不必要的函数,比如建哈希表时,new_node函数就完全没必要,可以删去;计算汉明距离的函数也非必须,可以删去而在主函数中计算,这两处修改完毕后,再交最好的成绩已经是2.0x秒了(可能和当前judge平台人数有关,所以大作业可以等哪天人少的时候去交)。

于是继续搜索那些函数没必要,发现set_zero是真没必要——甚至连返回值都没有,故删去。同样的,还有print_result/insert_to_trie、find_in_trie函数。

思路九

删掉fclose,现在是2.04s。

思路十

我们用字典树去存储单词数据时,实际上相当于给每一个前缀一个编号,但问题是不是所有前缀都是单词,也就导致了我们记录的word_frequency_list其实是很稀疏的,排序的时候把很多time为0(即不是单词的前缀)排了,可能会浪费很多时间,所以我的想法是在“给每一个前缀一个编号”的基础上,再给“每一个是单词的编号”一个编号,用一个int型数组去存(这个数组就相当于映射函数),同时令设置一个all_word_num(同pos一样,应当初始化为1)变量去记录所有单词的数量,最后对数组的前all_word_num个去排序就行。

/*----------------------常量与全局变量------------------------*/
int contrast[MAX_SIZE];  // 单词的映射“函数”
int all_word_num;

// 主函数中单词插入字典树时
    while(get_all_words_in_article(tmp_word) != EOF) {
        // insert_to_trie(tmp_word);  // 将该单词插入字典树        
        if (strlen(tmp_word) < 50)  // 防止文章中识别不出来的单词的影响(比如cqioawcwaecbwuesvcwseev这种)
        {
            int p = 0;
            for (int i = 0; tmp_word[i]; i++) {
                int n = tmp_word[i] - 'a';
                if (trie[p][n] == 0) {
                    trie[p][n] = pos++;
                }
                p = trie[p][n];
            }
            if (!contrast[p]) {  
                contrast[p] = all_word_num++;      
                strcpy(word_frequency_list[contrast[p]].str, tmp_word);
            }        
            word_frequency_list[contrast[p]].time++;
        }
    }

/*----------------------函数------------------------*/
word* find_in_trie(char str[])
{  //查找以某个字符串为前缀的单词位置
	int p = 0;
	for(int i = 0; str[i]; ++i){
		int n = str[i] - 'a';
		if(trie[p][n] == 0){
			return NULL;
		}
		p = trie[p][n];		
	}
	return word_frequency_list + contrast[p];
}

(此思路改写后效率提升并不大,可能是因为单词太多,在循环中添加一条判断的代价都很大)

思路十一

原来输出时,我们用fprintf输出,且需要对result的gather域中的四个数组进行遍历,于是乎我们就想,能不能用fwrite输出(和fread属于同一类函数)、能不能不循环?于是我就想,可以在把各篇sample的标识符存到result中时就构造出输出的格式,输出的时候直接一个fwrite就输出了,于是我们改写result中的gather数组,让它记录整个输出的格式(每两个标识符之间手动添加一个空格),而cnt域则记录整个输出字符串的长度,于是记录标识符时改写入下:

/*----------------------类型定义------------------------*/
typedef struct result {  // 结果类型
    char sample_identifier[16];  // 本文标识符
    int sample_feature[129];  // 本文的指纹向量
    char gather[4][100000];  // 距离为0,1,2,3的文章的标识符
    int cnt[4];  // 当前位数

} _result;


// 主函数中
            if (dis <= 3) {  // 如果汉明距离小于3
                int i_len = strlen(article_data[i].identifier);      
                strcpy(result.gather[dis] + result.cnt[dis], article_data[i].identifier);
                result.gather[dis][result.cnt[dis] + i_len] = ' ';
                result.gather[dis][result.cnt[dis] + i_len + 1] = '\0';
                result.cnt[dis] += i_len + 1;
            }    

输出时(以输出到out(result.txt)为例)改写如下:

// 主函数中
		fprintf(out, "%s\n", result.sample_identifier);  // 打印sample的标识符
        for (int i = 0; i < 4; i++) {
            if (!result.cnt[i]) continue;
            fprintf(out, "%d:", i);
            fwrite(result.gather[i], sizeof(char), result.cnt[i], out);
            fprintf(out, "\n");
        }

(此思路对效率也没有实质性的提升,因为strcpy函数调用也很费时间)

思路十二

原来已经把ctype.h这个没什么用的头文件去掉了,但是仍然保留了string.h,在整个程序中我们用到了该库的三个函数strlen,strcmp,strcpy。可以通过自定义函数去掉这个头文件:

# define my_strlen(a, len1) for (len1 = 0; a[len1]; len1++)


# define my_strcmp(s1, s2, res) int ii = 0;\
    while ((*(s1 + ii) !=  '\0') && *(s1 + ii) == *(s2 + ii)) ii++;\
    if (s1[ii] < s2[ii]) res = -1;\
    else if (s1[ii] > s2[ii]) res = 1;\
    else res = 0; 


void my_strcpy(char s1[], char s2[])
{
    int i;
    for (i = 0; s2[i]; i++) {
        s1[i] = s2[i];
    }
    s1[i] = '\0';
}

其中前两个函数使用宏定义——因为宏属于字符替换,程序运行时不算调用函数,速度更快。注意的是宏函数传进去了返回值len1和res,所以调用的时候不是赋值而是类似于通过指针修改值的感觉,比如下例:

int cmp_for_frelist(const void* a, const void* b)
{
    word* e1 = (word*)a;
    word* e2 = (word*)b;
    if (e1 -> time != e2 -> time) return e2 -> time - e1 -> time;
    int res;
    my_strcmp(e1 -> str, e2 -> str, res);
    return res;
}

趁着周日早上没人起床,多交几次,突破了2s大关,1.987s,大功告成!

修改后的完整代码

#pragma GCC optimize ("O3")
#pragma once
#pragma pack (16)

# include <stdio.h>
# include <stdlib.h>

/*----------------------封装了string.h头文件------------------------*/
void my_strcpy(char s1[], char s2[])
{
    int i;
    for (i = 0; s2[i]; i++) {
        s1[i] = s2[i];
    }
    s1[i] = '\0';
}


# define my_strlen(a, len1) for (len1 = 0; a[len1]; len1++)


# define my_strcmp(s1, s2, res) int ii = 0;\
    while ((*(s1 + ii) !=  '\0') && *(s1 + ii) == *(s2 + ii)) ii++;\
    if (s1[ii] < s2[ii]) res = -1;\
    else if (s1[ii] > s2[ii]) res = 1;\
    else res = 0; 

/*----------------------类型定义------------------------*/
typedef struct word {
    int time;
    char str[50];
} word;

typedef struct feature_word
{
    char str[50];  // 单词
    char hashvalue[129];  // 单词对应的哈希值
    struct feature_word *next;
} feature_word;

typedef feature_word* wptr;

typedef struct passage {
    char identifier[16];
    int feature[129];
} passage;


typedef struct result {  // 结果类型
    char sample_identifier[16];  // 本文标识符
    int sample_feature[129];  // 本文的指纹向量
    char gather[4][100000];  // 距离为0,1,2,3的文章的标识符
    int cnt[4];  // 当前位数

} _result;


/*----------------------常量与全局变量------------------------*/
# define MAX_SIZE 1000000

int M, N;  // 记录命令行参数

// 文件指针
FILE* article;
FILE* sample;
FILE* hashvalue;
FILE* stopwords;
FILE* out;

word word_frequency_list[MAX_SIZE]; // 词频表
int trie[MAX_SIZE][27]; //字典树(词频统计的)
int contrast[MAX_SIZE];  // 单词的映射“函数”
int all_word_num;  // article.txt中所有单词的数目
int pos;  // 单词位置


wptr head[9007];  // 哈希表
passage article_data[200000];  // article中各篇文章的数据(标识符,指纹)

_result result;  // 输出结果保存于此

char ARTICLE[44500000];  // article.txt的缓冲区
int article_len;  // article.txt的长度
int article_loc;  // 当前遍历到的下标

char SAMPLE[650000];  // sample.txt的缓冲区
int sample_len;  // sample.txt的长度
int sample_loc;  // 当前遍历到的下标


/*----------------------函数------------------------*/
word* find_in_trie(char str[])
// 查找以某个字符串为前缀的单词位置
{  
	int p = 0;
	for(int i = 0; str[i]; ++i){
		int n = str[i] - 'a';
		if(trie[p][n] == 0){
			return NULL;
		}
		p = trie[p][n];		
	}
	return word_frequency_list + contrast[p];
}


int get_all_words_in_article(char* dst)  
// 读取article中的所有单词,每次读一个,结尾返回EOF
{
    int i = 0;
    char c;
    while ((article_loc < article_len)) {
        c = ARTICLE[article_loc++];
    // 原来是 while ((c = fgetc(fp)) != EOF) {
        if (c >= 'a' && c <= 'z') {
            dst[i++] = c;
        } else if (c >= 'A' && c <= 'Z') {
            dst[i++] = c + 'a' - 'A';
        } else if (i) {
            dst[i] = '\0';
            return 0;
        }
    }
    return EOF;
}


int cmp_for_frelist(const void* a, const void* b)
// 字典树中所有单词排序的比较函数
{
    word* e1 = (word*)a;
    word* e2 = (word*)b;
    if (e1 -> time != e2 -> time) return e2 -> time - e1 -> time;
    int res;
    my_strcmp(e1 -> str, e2 -> str, res);
    return res;
}


int hashCode(char *str)
// 计算哈希值函数
{
    int tem = 0;
    int len;
    my_strlen(str, len);
    for(int i = 0;i < len;i++)
        tem = (tem * 26 + str[i] - 'a' + 1) % 9007;
    return tem;
}


void insert_to_hash(char *str, char* tmp_hash)
// 映射函数
{
    int val = hashCode(str);
    wptr p = (wptr) malloc(sizeof(feature_word));
    p -> next = NULL;
    my_strcpy(p -> str, str);
    p -> next = head[val];
    my_strcpy(p -> hashvalue, tmp_hash);
    head[val] = p;
}


wptr find_in_hash(char *str)
// 寻找单词对应的结点的函数
{
    int val = hashCode(str);
    for(wptr i = head[val]; i ; i = i -> next) {
        int tmp_cmp;
        my_strcmp(str, i -> str, tmp_cmp);
        if(!tmp_cmp) 
            return i;
    }
        
    return NULL; // 没找到
}


int get_identifier_in_sample(char *dst)
// 读取sample中当前文章的标识符
{
    int i = 0;
    char c;
    while (sample_loc < sample_len) {
        c = SAMPLE[sample_loc++];
        if ((c >= 'a' && c <= 'z') || (c == 'A' || c == 'Z') || c >= '0' || c == '9' || c == '-') {
            dst[i++] = c;
        } else if (i) {
            dst[i] = '\0';
            return 0;
        }
    }
    return EOF;
}


int get_identifier_in_article(char *dst)  
// 读入article.txt当前的标识符
{
    int i = 0;
    char c;
    while (article_loc < article_len) {
        c = ARTICLE[article_loc++];
        if ((c >= 'a' && c <= 'z') || (c == 'A' || c == 'Z') || c >= '0' || c == '9' || c == '-') {
            dst[i++] = c;
        } else if (i) {
            dst[i] = '\0';
            return 0;
        }
    }
    return EOF;
}


int get_one_word_in_article(char *dst)  
// 读入article.txt当前文章中的一个单词
{
    int i = 0;
    char c;
    while (article_loc < article_len && (c = ARTICLE[article_loc++]) != '\f') { 
        if (c >= 'a' && c <= 'z') {
            dst[i++] = c;
        } else if (c >= 'A' && c <= 'Z') {
            dst[i++] = c + 'a' - 'A';
        } else if (i) {
            dst[i] = '\0';
            return 0;
        }
    }
    if (c == '\f') return '\f';
    return EOF;
}


int get_one_word_in_sample(char *dst)  
// 读入sample.txt当前文章中的一个单词
{
    int i = 0;
    char c;
    while (sample_loc < sample_len && (c = SAMPLE[sample_loc++]) != '\f') { 
        if (c >= 'a' && c <= 'z') {
            dst[i++] = c;
        } else if (c >= 'A' && c <= 'Z') {
            dst[i++] = c + 'a' - 'A';
        } else if (i) {
            dst[i] = '\0';
            return 0;
        }
    }
    if (c == '\f') return '\f';
    return EOF;
}


/*---------------主函数-------------------*/
int main(int argc, const char* argv[])
{
    article = fopen("article.txt","rb");
    sample = fopen("sample.txt","rb");
    hashvalue = fopen("hashvalue.txt","r");
    stopwords = fopen("stopwords.txt","r");
    out = fopen("result.txt", "w");

    article_len = fread(ARTICLE, sizeof(char), 44500000, article);
    sample_len = fread(SAMPLE, sizeof(char), 650000, sample);
    N = atoi(argv[1]);
    M = atoi(argv[2]);
    // scanf("%d%d", &N, &M);
    // 为方便调试,可以先从控制台读入,最后把代码改成命令行读入即可
    pos = 1;
    char tmp_word[100];  
    
    all_word_num = 1;
    while(get_all_words_in_article(tmp_word) != EOF) {
        // 将该单词插入字典树   
        int tmp_len;
        my_strlen(tmp_word, tmp_len);     
        if (tmp_len < 50)  // 防止文章中识别不出来的单词的影响(比如cqioawcwaecbwuesvcwseev这种)
        {
            int p = 0;
            for (int i = 0; tmp_word[i]; i++) {
                int n = tmp_word[i] - 'a';
                if (trie[p][n] == 0) {
                    trie[p][n] = pos++;
                }
                p = trie[p][n];
            }
            if (!contrast[p]) {  
                contrast[p] = all_word_num++;      
                my_strcpy(word_frequency_list[contrast[p]].str, tmp_word);
            }        
            word_frequency_list[contrast[p]].time++;
        }
    }
    article_loc = 0;
    // 在字典树中“删除停用词”
    char stop_word[50];
    while(fscanf(stopwords, "%s", stop_word) != EOF) {
        word* tmp_pos = find_in_trie(stop_word);
        if (tmp_pos != NULL) {
            tmp_pos -> time = 0;
        }
    }

    qsort(word_frequency_list, all_word_num, sizeof(word), cmp_for_frelist);  // 词频排序
    
    // 取出词频表中前n个不在停用词表中的单词
    int feature_num = 0;
    char tmp_hash[129] = {0};
    for (int i = 0; i < N; i++) {       
        for (int j = 0; j < M; j++) {  // 读hashvalue.txt中该行的前M个字符
            tmp_hash[j] = fgetc(hashvalue);
        }
        tmp_hash[M] = '\0';                       
        
        insert_to_hash(word_frequency_list[i].str, tmp_hash);  // 插入哈希表
        fgets(tmp_hash, 128, hashvalue); // 将文件指针移动至下一行首
        feature_num++;      
    }

    // 读article的每篇文章的数据
    int article_num = 0;
    while(get_identifier_in_article(article_data[article_num].identifier) != EOF) {
        char temp_word[20];
        wptr temp_pos;
        int flag = 0;
        while((flag = get_one_word_in_article(temp_word)) != '\f') {
            if (flag == EOF) break;
            if ((temp_pos = find_in_hash(temp_word)) != NULL) {
                for (int i = 0; i < M; i++) {
                    if ((temp_pos -> hashvalue[i] == '0')) {                        
                        article_data[article_num].feature[i]--;
                    }else {
                        article_data[article_num].feature[i]++;                        
                    }
                }
            }
        }
        // 已经算出来了,大于0的位置1,其余置0
        for (int i = 0; i < M; i++) {
            if (article_data[article_num].feature[i] > 0) 
                article_data[article_num].feature[i] = 1;
            else 
                article_data[article_num].feature[i] = 0;
        }
        article_num++;
        if (flag == EOF) break;
        
    }
   
    // 读sample的每篇文章的数据
    int num = 0;
    while(get_identifier_in_sample(result.sample_identifier) != EOF) {
        char temp_word[50];
        wptr temp_pos;
        int flag = 0;
        while((flag = get_one_word_in_sample(temp_word)) != '\f') {
            if (flag == EOF) break;
            if ((temp_pos = find_in_hash(temp_word)) != NULL) {
                for (int i = 0; i < M; i++) {
                    if ((temp_pos -> hashvalue[i] == '0')) {                        
                        result.sample_feature[i]--;
                    }else {
                        result.sample_feature[i]++;                        
                    }
                }
            }
        } 
        // 已经算出来了,大于0的位置1,其余置0
        for (int i = 0; i < M; i++) {
            if (result.sample_feature[i] > 0) 
                result.sample_feature[i] = 1;
            else 
                result.sample_feature[i] = 0;
        }
        
        for (int i = 0; i < article_num; i++) {
            int dis = 0;
            for (int j = 0; j < M; j++) {
                if (result.sample_feature[j] != article_data[i].feature[j])
                    dis++;
            }
            // 存到结果变量中
            if (dis <= 3) {
                int i_len;
                my_strlen(article_data[i].identifier, i_len);
                my_strcpy((result.gather[dis] + result.cnt[dis]), article_data[i].identifier);
                result.gather[dis][result.cnt[dis] + i_len] = ' ';
                result.gather[dis][result.cnt[dis] + i_len + 1] = '\0';
                result.cnt[dis] += i_len + 1;                
            }               
        }

        // 输出
        if (num == 0) {
            fprintf(stdout, "%s\n", result.sample_identifier);  // 打印sample的标识符
            for (int i = 0; i < 4; i++) {
                if (!result.cnt[i]) continue;
                fprintf(stdout, "%d:", i);
                fwrite(result.gather[i], sizeof(char), result.cnt[i], stdout);
                fprintf(stdout, "\n");               
            }   
        }

        fprintf(out, "%s\n", result.sample_identifier);  // 打印sample的标识符
        for (int i = 0; i < 4; i++) {
            if (!result.cnt[i]) continue;
            fprintf(out, "%d:", i);
            fwrite(result.gather[i], sizeof(char), result.cnt[i], out);
            fprintf(out, "\n");
        }

        // 置0
        for (int i = 0; i < 4; i++) {
            result.cnt[i] = 0;
        }
        for (int i = 0; i < 129; i++) {
            result.sample_feature[i] = 0;
        }

        num++;
        if (flag == EOF) break;  
    }
    return 0;
}
  • 4
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
BUAA数据结构大作业涉及到了优化print_result函数和实现Trie树。在优化print_result函数时,原始的结构体并没有根据汉明距离进行区分,而是将所有的结果一起存储并每次都进行排序。此外,在输出时也没有进行代码的重用,而是重复写了多段相同的代码。这种实现方式显然可以进行优化。 关于Trie树的实现,一开始的印象是它完全由链式结构组成,但后来发现数组也可以用来实现Trie树。然而,在完成大作业时,由于时间紧迫,我并没有深入理解这个方法,只是简单地照着网上的模板进行了插入和查找操作。 对于BUAA数据结构大作业,我建议你先理解Trie树的原理,并且如果你的大作业中使用到了Trie树(应该是很有可能的),你可以咨询梦拓学长和助教,同时也可以在网上搜索相关资料。在实现代码之前,一定要确保自己理解了原理。如果你希望代码的运行速度更快,我建议你使用数组来实现Trie树。你可以参考上面提到的第二篇文章,稍加改动,因为我们的目的不是只建立一棵树来查找特定单词的出现次数,而是要找出出现次数前n个单词。因此,我们需要记录所有出现过的单词,并能够遍历它们。为此,可以定义一个结构体来记录单词和出现次数,并创建一个结构体数组来存储它们。同时,使用一个数组来实现字典树的词频统计。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *3* [BUAA数据结构大作业2023](https://blog.csdn.net/weixin_50567399/article/details/131394979)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *2* [2022BUAA数据结构期末大作业的一些想法](https://blog.csdn.net/m0_62558898/article/details/125564521)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值