3.3 在C语言下构造马尔科夫链数据结构

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/Coulson_Zhao/article/details/79949669

3.3 在C语言下构造马尔科夫链数据结构

    在文章的开头,我们要强调一下文章的目的和整体脉络,以便在长文中丢失自我。

1.目的

    我们给定一个输入(前缀),算法将自动匹配它的后缀,并根据选定的后缀更新前缀再重选后缀直至文档结束

2.脉络

    对于我们期望的这种数据结构,我们要明白的是,它要服务马尔科夫链算法。回忆这个算法,我们不难发现算法中需要通过前缀去寻找后缀。而这之中,状态(State)又是一个很重要的存在。因为散列表(也就是哈希表)中对应的元素就是“状态”。

    由此,我们可以先定义状态,它是和散列表相对应的,我们可以通过散列函数来定位到“状态”;(lookup函数)

    状态中包含着前缀(两个单词)和数量未明确的后缀(后缀是一个单词);(结构体)

    随着我们对文本的读取,我们需要增加“状态“的数量,也就是增加前缀和其对应的后缀。(add与addsuffix函数)

3.主体

    我们首先定义一些常量:

enum
{
	NPREF = 2,			/*前缀中词的数量*/
	NHASH = 4093,		/*哈希表(散列表)大小*/
	MAXGEN = 10000		/*录入的最大单词数量*/
};

    我们设定一个很大的数组,这是因为我们预计程序可能会读取很大的文件,或许是一整本书。我们也选择了一个很大的散列表,这样,即使有10,000个前缀(词对)散列表中的平均链长依旧会很短(大概是两到三个前缀那么长)。数组越大,链的期望长度越短,查询进行的也就更快。过短的散列表会使得链长过长,查询时间变慢;过长的散列表则会导致储存空间的溢出。

    对于整个输入来讲,我们希望其能达到如下的目标:

    1.散列表中元素以状态(state)类型进行储存

    2.状态中包含前缀以及数量不能确定的后缀(Suffix)

    3.每个前缀和其对应的后缀都可以很好的联系在一起

    因此,我们定义如下几个结构体:

typedef struct State State;
typedef struct Suffix Suffix;
/*散列表中的元素*/
struct State
{
	/*前缀数量是确定的*/
	char *pref[NPREF];
	/*状态中要可以包含后缀及指向下一个前缀*/
	Suffix *suf;
	State *next;
};
/*后缀*/
struct Suffix
{
	char *word;
	/*可以构成多个后缀*/
	Suffix *next;
};
/*这个就是散列表*/
State *statetab[NHASH];

    为了方便理解,我们给出示意图:


    我们需要一个作用于前缀的散列函数,而这里要注意的是,前缀的形式是字符串数组。这就要求当输入是字符串数组的时候,散列函数任然可以给出一个对应于散列表的值。而我们的散列函数是:

/*散列表键值计算函数*/
unsigned int hash_number(char *s[NPREF])
{
	unsigned int h;
	unsigned char *p;
	int i;

	h = 0;
	for (i = 0; i < NPREF; i++)
	{
		for (p = (unsigned char *)s[i]; *p != '\0'; p++)
		{
			h = MULTIPLIER * h + *p;
		}
	}
	/*返回哈希表中的键值*/
	return h;
}

    可以看到,散列表中h的计算方法是十分具有唯一性的。也就是说,对于每一个不同的前缀,我们都会有一个唯一的h与其相对应,即使碰巧h相同,散列表中的状态也不会很多(一般不会超过5个)

    因此,在寻找前缀位置时,散列表的存在,就会让速度变得十分快捷。

State *lookup(char *prefix[NPREF], int create)
{
	int i, h;
	State *sp = NULL;

	h = hash_number(prefix);
	for (sp = statetab[h]; sp != NULL; sp = sp->next)
	{
		for (i = 0; i < NPREF; i++)
		{
			/*如果输入的prefix和哈希表中对应的sp->pref[i]不相同*/
			/*证明不是要找的前缀*/
			/*注意前缀中含有两个单词*/
			if (strcmp(prefix[i], sp->pref[i]) != 0)
			{
				break;
			}
		}
		if (i == NPREF)
		{
			return sp;
		}
	}
	/*在哈希表中没有与输入前缀相对应的部分时*/
	if (create)
	{
		sp = (State *)malloc(sizeof(State));
		for (i = 0; i < NPREF; i++)
		{
			sp->pref[i] = prefix[i];
		}
		sp->suf = NULL;
		/*将新生成的状态(State)加入到散列表中(哈希表)*/
		sp->next = statetab[h];
		statetab[h] = sp;
	}
	return sp;
}

    以上那些,都是建立在散列表已经形成的基础上的,那么如何构建一个散列表。就需要我们的build函数等了。

void build(char *prefix[NPREF], FILE *f)
{
	char buf[100], fmt[10];
	sprintf(fmt, "%%%ds", sizeof(buf) - 1);
	/*将文本中符合“fmt”定义的单词读取到buf中*/
	/*fmt定义为最大99个字符,因为基本所有单词不会超过20个字符*/
	while (fscanf(f, fmt, buf) != EOF)
	{
		/*使用字符串复制函数strdup*/
		add(prefix, strdup(buf));
	}
}
void add(char *prefix[NPREF], char *suffix)
{
	/*定位到prefix,并且把后缀suffix加入到prefix对应的sp中*/
	State *sp;
	sp = lookup(prefix, 1);
	addsuffix(sp, suffix);
	/*前缀的更新*/
	memmove(prefix, prefix + 1, (NPREF - 1) * sizeof(prefix[0]));
	prefix[NPREF - 1] = suffix;
}

    build函数是一个很精巧的函数(尤其注意在遍历文档(*f)中和add等函数的联动,运用散列表达到相当棒的效果)。它遍历文章,寻找与给定前缀相匹配的后缀。

    在遍历文档时,只要给出了文档的前两个单词,函数之间的联动就可以将整个文档形成散列表:

    1.给定前两个单词时,散列表为空,文档指针在开头。

    2.读入两个单词作为prefix后,将其后面的一个单词作为后缀,并更新前缀。同时,散列表中将会留有这个前缀和其对应的后缀

    3.循环2过程直到结尾

    注意a:当遇到完全相同的前缀时(散列函数值相同、英文相同),由于散列表中已经有这个前缀,相当于更新了这个前缀的后缀

    注意b:当遇到相同的前缀时(散列函数值相同、英文不同),这个新的前缀仍会被生成到散列表的对应位置上。只不过散列表的对应位置上将会出现两个状态(参看lookup函数)

    当完成以上所有步骤时,有关马尔科夫链的数据结构就搭建完成了,理解需要很长时间。下面是结构的代码,下一篇CSDN文章中将会给出具体的主函数实现。

    下面是数据结构,而具体的带着主函数的实现将在下一篇文章中讲到:

    算法实现

#include "stdafx.h"
#include "stdlib.h"
#include "string.h"
#include <iostream>

using namespace std;

enum
{
	NPREF = 2,			/*前缀中词的数量*/
	NHASH = 4093,		/*哈希表(散列表)大小*/
	MAXGEN = 10000,		/*录入的最大单词数量*/
	MULTIPLIER = 31,
	BUFSIZE = 100
};
char fmt[] = "%99s";

typedef struct State State;
typedef struct Suffix Suffix;
/*散列表中的元素*/
struct State
{
	/*前缀数量是确定的*/
	char *pref[NPREF];
	/*状态中要可以包含后缀及指向下一个前缀*/
	Suffix *suf;
	State *next;
};
/*后缀*/
struct Suffix
{
	char *word;
	/*可以构成多个后缀*/
	Suffix *next;
};
/*这个就是散列表*/
State *statetab[NHASH];

/*哈希表键值计算函数*/
unsigned int hash_number(char *s[NPREF])
{
	unsigned int h;
	unsigned char *p;
	int i;

	h = 0;
	for (i = 0; i < NPREF; i++)
	{
		for (p = (unsigned char *)s[i]; *p != '\0'; p++)
		{
			h = MULTIPLIER * h + *p;
		}
	}
	/*返回哈希表中的键值*/
	return h;
}

State *lookup(char *prefix[NPREF], int create)
{
	int i, h;
	State *sp = NULL;

	h = hash_number(prefix);
	for (sp = statetab[h]; sp != NULL; sp = sp->next)
	{
		for (i = 0; i < NPREF; i++)
		{
			/*如果输入的prefix和哈希表中对应的sp->pref[i]不相同*/
			/*证明不是要找的前缀*/
			/*注意前缀中含有两个单词*/
			if (strcmp(prefix[i], sp->pref[i]) != 0)
			{
				break;
			}
		}
		if (i == NPREF)
		{
			return sp;
		}
	}
	/*在哈希表中没有与输入前缀相对应的部分时*/
	if (create)
	{
		sp = (State *)malloc(sizeof(State));
		for (i = 0; i < NPREF; i++)
		{
			sp->pref[i] = prefix[i];
		}
		sp->suf = NULL;
		/*将新生成的状态(State)加入到散列表中(哈希表)*/
		sp->next = statetab[h];
		statetab[h] = sp;
	}
	return sp;
}

/*读取文档并且构建前缀、后缀*/

void build(char *prefix[NPREF], FILE *f)
{
	char buf[100], fmt[10];
	sprintf(fmt, "%%%ds", sizeof(buf) - 1);
	/*将文本中符合“fmt”定义的单词读取到buf中*/
	/*fmt定义为最大99个字符,因为基本所有单词不会超过20个字符*/
	while (fscanf(f, fmt, buf) != EOF)
	{
		/*将符合“fmt”标准的单词复制到buf中*/
		/*使用字符串复制函数strdup*/
		add(prefix, strdup(buf));
	}
}

/*添加函数,将会把整篇文档都加到散列表中*/
/*遍历文档,文章中所有前缀都能涉及*/
void add(char *prefix[NPREF], char *suffix)
{
	/*定位到prefix,并且把后缀suffix加入到prefix对应的sp中*/
	State *sp;
	sp = lookup(prefix, 1);
	addsuffix(sp, suffix);
	/*前缀的更新*/
	memmove(prefix, prefix + 1, (NPREF - 1) * sizeof(prefix[0]));
	prefix[NPREF - 1] = suffix;
}

/*为某一个特定的前缀添加后缀*/
void addsuffix(State *sp, char *suffix)
{
	Suffix *suf;
	suf = (Suffix *)malloc(sizeof(Suffix));
	suf->word = suffix;
	suf->next = sp->suf;
	sp->suf = suf;
}
int main()
{
    return 0;
}


阅读更多
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页