实验——散列表(基于词频的文件相似度)详细过程

一、  实验目的   

1. 掌握散列表相关内容

2. 掌握倒排索引表的应用

二、  实验内容和要求  

1. 问题描述   

      实现一种简单原始的文件相似度计算,即以两文件的公共词汇占总词汇的比例来定义相似度。为简化问题,这里不考虑中文(因为分词太难了),只考虑长度不小于3、且不超过10 的英文单词,长度超过10的只考虑前10个字母。

2. 输入格式                                                                        

      输入首先给出正整数N(≤100),为文件总数。随后按以下格式给出每个文件的内容:首先给出文件正文,最后在一行中只给出一个字符'#',表示文件结束。在N个文件内容结束之后,给出查询总数M(≤10^4 ),随后M行,每行给出一对文件编号,其间以空格分隔。这里假设文件按给出的顺序从1到N编号。

3. 输出格式                                                                        

       针对每一条查询,在一行中输出两文件的相似度,即两文件的公共词汇量占两文件总词汇量的百分比,精确到小数点后1位。注意这里的一个“单词”只包括仅由英文字母组成的、长度不小于3、且不超过10的英文单词,长度超过10的只考虑前10个字母。单词间以任何非英文字母隔开。另外,大小写不同的同一单词被认为是相同的单词,例如“You”和“you”是同一个单词。

4. 输入样例

 

3 
Aaa Bbb Ccc
#
Bbb Ccc Ddd
#
Aaa2 ccc Eee
is at Ddd@Fff
# 
2
1 2
1 3

5. 输出样例

50.0%
33.3%

三、算法设计

1. 主流程设计

int main 
{ 输入文件总数; 读入并存储单词,建立词汇索引表; 输入查询次数; 输入要查询的文件编号; 输出文件相似度;
return 0; }

 

2. 散列表的创建

    设计思路:存储单词以及对应所在的文件的索引,即倒排索引,通过构建针对字符串关键 字的散列表,来快速查找到某个单词。

    具体实现:由于采用每个字母占5位的方法进行移位且输入总规模不超过2MB,按每个单词 最少占4 字节(3个字母+1个分隔符)计算,总词汇表中最多要存50万个单词,则可以建 立一个散列表,规模为500009(大于50万的最小素数),可保证插入不会失败,并初始化 散列表。

 

HashTable CreateTable(int tableSize)  //建立散列表
{ HashTable H; H = (HashTable)malloc(sizeof(struct TableNode)); //H->tableSize = NextPrime(tableSize); //保证散列表最大长度是素数 H->tableSize = MAXTABLESIZE; H->heads = (HList)malloc(H->tableSize * sizeof(struct HashList));//分配链表头结点数组 for (int i = 0; i < H->tableSize; i++) //并初始化表头结点 { H->heads[i].data[0] = 0; H->heads[i].count = 0; H->heads[i].invIndex = NULL; } return H; }

 

3. 创建文件词汇索引表

    设计思路:存储文件到单词的索引,可用文件表即为每个文件建立带头结点的索引链表, 存储每个文件的词汇量以及单词在散列表中的位置。

    具体实现:创建头结点数组F[]并初始化,这样可以在在头结点中存储文件的词汇量,在链 表结点中存储词汇表中单词在散列表中的位置。

 

FList CreateFileIndex(int size)//初始化文件的词汇索引表
{
    FList F;
    F = (FList)malloc(sizeof(struct WordList) * size);
    for (int i = 0; i < size; i++)
    {
       F[i].word = 0;
       F[i].next = NULL;
    }
    return F;
}

 

4. 读入文件中的每个单词

int Get(ElementType word)
{
    输入第一个字符(即用于跳过非字母'\n');
    while (字符为非字母)
    {
        if (字符为文件结束符'#')
            文件结束,结束读入单词;
        else
            继续输入字符(直到输入字符为字母时跳出循环);
    }
    while (字符为字母)
    {
        if (小写字母)
        {
           if (单词中字母个数小于十)//长度超过10的只考虑前10个字母
               将该字母存入数组中;
        }
        else if (大写字母)
        {
           if (单词中字母个数小于十)//长度超过10的只考虑前10个字母
               转换成小写字母存入数组中;
        }
        继续输入字符(直到输入字符为非字母时跳出循环);
    }
    if (单词中字母个数小于三)
        该单词不符合要求,不能存入数组中,都下一个单词;
    else
    {
        单词读入成功,将数组清零;
        成功返回;
    }
}

5. 查询单词

    设计思路:用线性探测法处理冲突。

    具体实现:先在文件词汇索引表中查询对应的文件名(取文件词汇量较少的文件名),找 到对应文件名中的词汇所在位置,根据此单词的位置到散列表中查找单词所在文件列表, 从而判断该单词是否是两文件的公共词汇。

int Find(HashTable H, ElementType key)
{
    初始散列位置;
    while (从该链表的第1个结点开始当未到表尾,并且key未找到时)
    {
        线性探测下一个位置;
        if (位置没有被占用)
            调整为合法地址;
    }
    return 此时指向找到的结点位置,或者为0;
}

6. 自定义strcmp、strcpy函数

    设计思路:根据库函数以及该题要求、需要用库函数的内容功能,对字符串单词进行比较 拷贝。

 

int mystrcmp(char a[], char b[])  //比较
{
     int i;
     for (i = 0; a[i] != '\0' && b[i] != '\0'; i++)
     {
          if (a[i] > b[i])
              return 1;
          else if (a[i] < b[i])
              return -1;
     }
     if (a[i] == '\0' && b[i] == '\0')
         return 0;
     else if (a[i] == '\0')
         return -1;
     else
         return 1;
}

void mystrcpy(char a[], char b[])  //拷贝
{
      int i = 0, j = 0;
      while ((a[i++] = b[j++]) != '\0');
}

 

7. ADT定义

 

#define MAXTABLESIZE 500009  //允许开辟的最大散列表长度
typedef enum { false, true } bool;

typedef struct FileList* FList;
struct FileList {
     int word;    //存储文件词汇量
     FList next;  //单词在散列表中的位置
};

typedef struct WordList* WList;
struct WordList {
     int count;
     WList next;
};

typedef char ElementType[12];  //关键词类型用字符串
typedef struct HashList* HList;
struct HashList {          //链表结点定义
     ElementType data;  //存放单词
     int count;               //单词个数,为0时表示结点为空
     WList invIndex;      //倒排索引
};

typedef struct TableNode* HashTable;  //散列表类型
struct TableNode {  //散列表结点定义
     int tableSize;     //表的最大长度
     HList heads;      //指向链表头结点的数组
};

//自定义strcmp函数,用于比较两个单词(字符串)是否相同
int mystrcmp(char s1[], char s2[]);
//自定义strcpy函数,拷贝单词(字符串)
void mystrcpy(char a[], char b[]);
bool IsLower(char ch);  //是否为小写字母
bool IsUpper(char ch);  //是否为大写字母

/*散列表初始化*/
HashTable CreateTable(int tableSize);

/*建立文件的词汇索引表*/
FList CreateFileIndex(int size);

/*读入文件的每个单词*/
int Get(ElementType word);

/*字符串key移位法映射到整数的散列函数*/
int Hash(const char* key, int tableSize);

/*返回适合key插入的位置*/
int Find(HashTable H, ElementType key);

/*将key插入散列表,同时插入对应的倒排索引表*/
int InsertIndex(int count, HashTable H, ElementType key);

/*将单词在散列表中的位置pos存入文件count对应的索引表*/
void FileIndex(FList file, int count, int pos);

/*计算文件f1和f2的相似度*/
double Similiar(FList file, int f1, int f2, HashTable H);

 

 

8. 算法示例

索引(文件词汇索引表)
文件编号 对应文件单词 对应存储单词 对应存储单词个数
1 Aaa\Bbb\Ccc aaa\bbb\ccc 3
2 Bbb\Ccc\Ddd bbb\ccc\ddd 3
3 Aaa\ccc\Eee\Ddd\Fff aaa\ccc\eee\ddd\fff 5

 

倒排索引(散列表)
文件单词 存储单词 对应文件编号【该单词在文件的位置】 对应出现在的文件个数
Aaa aaa 1[1], 3[1] 2
Bbb bbb 1[2], 2[1] 2
Ccc\ccc ccc 1[3], 2[2], 3[2] 3
Ddd ddd 2[3], 3[4] 2
Eee eee 3[5] 1
Fff fff 3[6] 1

 

四、算法分析

       通过算法过程示例发现,时间复杂度与文件单词的读入和存储线性相关,O(n) = N^2,空间复 杂度为构建散列表和文词汇索引表空间的使用,O(n) = N。

五、总结

       设计算法时,最初尝试先将一个文件的词频统计构建出来,再建立两个至多个文件的词频统计, 但因为一个到多个文件单词的读入存储出现困难、插入单词后查询复杂等因素,最终将自己的 思路弄乱了。通过实验指导书,采用倒排索引的方法重新构建散列表以及文件词汇索引表,思 路才渐渐清晰,但也出现了一些问题,例如读入文件问题,是采用一个一个文件通过读入字符 串即整个文件内容存储单词,还是采用对每个文件中一个一个的字符的读入存储,后来发现对 于一个文件的词频统计,直接读入文件整个字符串内容进行存储处理较为容易,但对于多个文 件的词频统计,依次读入文件的字符构建要求单词进行存储,为之后的插入索引创造便利,使 整个思路更加简便清晰。最后是构建散列表规模导致运行超时的问题,知道要保证散列表最大 长度是素数,但是具体的分析数值也是看了实验指导书得以解决。

 

六、源代码(主要)

int InsertIndex(int count, HashTable H, ElementType key)  //将key插入散列表,同时插入对应的倒排索引表
{
    WList W;
    int pos = Find(H, key);  //检查key是否已经存在
    if (H->heads[pos].count != count) {  //插入散列表
        if (!H->heads[pos].count)
            mystrcpy(H->heads[pos].data, key);  //如果单词没找到,插入新单词
        H->heads[pos].count = count;  //更新文件
        W = malloc(sizeof(struct WordList));  //将文件编号插入倒排索引表
        W->count = count;
        W->next = H->heads[pos].invIndex;
        H->heads[pos].invIndex = W;
        return pos;  //插入成功
    }
    else
        return 0;  //重复单词,不插入
}

void FileIndex(FList file, int count, int pos) {
    FList F;
    if (pos < 0) 
        return ;  //重复的单词不处理
    F = malloc(sizeof(struct FileList));
    F->word = pos;
    F->next = file[count - 1].next;
    file[count - 1].next = F;
    file[count - 1].word++;  //头结点累计词汇量
}

double Similiar(FList file, int f1, int f2, HashTable H)  //计算文件f1和f2的相似度
{
    WList W;
    FList F;
    int i;
    if (file[f1 - 1].word > file[f2 - 1].word)  //使f1的词汇量较小
    {
        i = f1;
        f1 = f2;
        f2 = i;
    }
    i = 0;  //相同词汇量
    F = file[f1 - 1].next;
    while (F)
    {
        W = H->heads[F->word].invIndex;  //找到当前单词在散列表中的位置
        while (W)  //遍历该单词的倒排索引表
        {
            if (W->count == f2)  //f2里也有该单词
                break;
            W = W->next;
        }
        if (W)  //表示f1、f2都有该单词
            i++;
        F = F->next;
    }
    double d = (i / (double)(file[f1 - 1].word + file[f2 - 1].word - i)) * 100;
    return d;
}

 

以上均为原创作品,欢迎大佬前来指正!

posted @ 2021-02-18 21:24  哦呦aholic  阅读(1386)  评论(1编辑  收藏  举报