ICTCLAS代码学习笔记之CDictionary类

本文详细介绍了ICTCLAS词典的内部实现原理,包括词典类CDictionary的结构和核心成员函数的工作机制,如AddItem、DelItem等。讨论了词典的存储结构优化方案,对比了几种不同的数据结构选择。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

关于ICTCLAS词典的组织
词典相关的操作都在自定义的类CDictionary里,相关文件为CDictionary.h和CDictionary.cpp;
涉及几个结构体变量,只注明了变量没有写构造函数。
struct tagWordResult{
char sWord[WORD_MAXLENGTH]; //!可以用string代替
//The word
int nHandle;
//the POS of the word
double dValue;
//The -log(frequency/MAX)
};
typedef struct tagWordResult WORD_RESULT,*PWORD_RESULT;
/*data structure for word item*/
struct tagWordItem{
int nWordLen; //!可省略
char *sWord; //!可以用string代替,有写拷贝功能性能损失不大
//The word
int nHandle;
//the process or information handle of the word
int nFrequency;
//The count which it appear
};
typedef struct tagWordItem WORD_ITEM,*PWORD_ITEM;
/*data structure for dictionary index table item*/
struct tagIndexTable{ //!索引表,一次性读入应该可以用一个vector<WORD_ITEM>代替
int nCount;
//The count number of words which initial letter is sInit
PWORD_ITEM pWordItemHead;
//The head of word items
};
typedef struct tagIndexTable INDEX_TABLE;
/*data structure for word item chain*/
struct tagWordChain{ //!词链,可以用一个list<WORD_ITEM>代替?
WORD_ITEM data;
struct tagWordChain *next;
/*----Added By Huangjin@ict.ac.cn 2006-5-30----*/
struct tagWordChain()
{
next=NULL;
}
/*-----------------------------------------------*/
};
typedef struct tagWordChain WORD_CHAIN,*PWORD_CHAIN;
/*data structure for dictionary index table item*/
struct tagModifyTable{
int nCount;
//The count number of words which initial letter is sInit
int nDelete;
//The number of deleted items in the index table
PWORD_CHAIN pWordItemHead;
//The head of word items
};
typedef struct tagModifyTable MODIFY_TABLE,*PMODIFY_TABLE;

词典类的声明如下:
class CDictionary
{//!所有的输入参数为char*者都可以使用const string&来代替,至少应该是const char*
//!对返回值所用的char*使用string&来代替
//!对参数为int*的根据情况改为int&或者vector<int>&
//!其他int,double或者char等内置类型一律传值,其他复杂类型如果不涉及修改一律const &
public:
bool Optimum();
bool Merge(CDictionary dict2,int nRatio); //!这里应该用const CDictionary&
bool OutputChars(char *sFilename);
bool Output(char *sFilename);
int GetFrequency(char *sWord, int nHandle);
bool GetPOSString(int nPOS,char *sPOSRet);
int GetPOSValue(char *sPOS);
bool GetMaxMatch(char *sWord, char *sWordRet, int *npHandleRet);
bool MergePOS(int nHandle);
bool GetHandle(char *sWord,int *pnCount,int *pnHandle,int *pnFrequency);
bool IsExist(char *sWord,int nHandle);
bool AddItem(char *sWord,int nHandle,int nFrequency=0);
bool DelItem(char *sWord,int nHandle);
bool Save(char *sFilename);
bool Load(char *sFilename,bool bReset=false);
int GetWordType(char *sWord);
bool PreProcessing(char *sWord,int *nId,char *sWordRet,bool bAdd=false);
CDictionary();
virtual ~CDictionary();
INDEX_TABLE m_IndexTable[CC_NUM]; //!这个换成vector<WORD_ITEM>在构造函数时大小赋为CC_NUM
PMODIFY_TABLE m_pModifyTable;
//The data for modify
protected:
bool DelModified();
bool FindInOriginalTable(int nInnerCode,char *sWord,int nHandle,int *nPosRet=0);
bool FindInModifyTable(int nInnerCode,char *sWord,int nHandle,PWORD_CHAIN *pFindRet=0);
/*----Added By huangjin@ict.ac.cn 2006-5-29----*/
void ClearDictionary(void);
/*-----------------------------------------------*/
};


下面是有必要说明的类成员函数
CDictionary::Load和CDictionary::Save
主要说明内容为词典本身的存储结果,文件为二进制读写格式,存储CC_NUM个索引及相关值。
共有CC_NUM个索引项,对于每个索引的内容:
第一个int型数据的值为当前索引项的词个数m_IndexTable[i].nCount,如果大于0表示该索引项后面有m_IndexTable[i].nCount个词,对于每个词:
前3个int型的数据分别表示每个词的出现频率nFrequency、词条字符串长度nWordLen及其nHandle。如果词条长度大于0则后面连续nWordLen个char型字符为该词的字符
表示。
Load函数另外一个参数bReset主要是控制nFrequency,如果该值为true则nFrequency的值为0
词典的存储过程与读入过程正好相反,需要注意的一点是,存储时要考虑是否有修改词典的部分,即m_pModifyTable的相关内容。主要规则如下:
每个索引的词条个数应该为原有个数+修改表个数-修改表删除个数
写入的顺序为(只针对有修改表的情况)
将修改表中的数据按词条字母序写入,如果词条字符串相等则优先写入hHandle值较小的。
2006-7-31的学习笔记
在家里看代码,心静不下来啊,呵呵。
接着说CDictionary类的相关操作,save函数原来没看太清楚,还得仔细看。
m_pModifyTable的大小要么为0要么与m_IndexTable相同,即CC_NUM大小。如果m_pModifyTable非空则在保存时需要判断。对于第i个索引,写入的个数nCount等于原始的大小m_IndexTable[i].nCount加上新表中的个数并减去新表中删除的个数。后面两个怎么算见后。
如果还没有写够原始的个数m_IndexTable[i].nCount且修改表中还没有写完,则重复下面的操作
如果修改表中的当前词条
2006-8-2 的学习笔记
Save函数不太容易看,先看一下AddItem和DelItem等会对m_pModifyTable有修改的部分再理解Save就容易得多了。
AddItem需要三个参数,分别是要新添的词,其handle和频度。首先进行预处理,如果第一个字是汉字,则返回的是这个汉字的handle及传入词的剩余部分(即要新添的词);如果是分割符则返回的是handle固定为3755且传入词的全部被返回(即要新添的词)(//?3755的含义未知)。如果不是这两种情况则返回false。预处理函数中有一个参数bAdd在当前版本中没有使用。只有预处理成功的词串才会被添加。首先会在原始表即m_IndexTable中查找是否已经有这个词了,如果找到,则对m_IndexTable及m_pModifyTable进行适当的修改并返回表示成功添加的true。这里的修改主要根据在原始表中是否已经删除来区别操作,如果原始表中已删除,则重置这项的频度值,并在修改表中相应位置记录删除个数减1,如果原始表中仍然有词,只要添加相应的频度即可。原始表中没有的情况会相对复杂一些。首先会在修改表中查找,如果找到了则添加相应的位置的频度值并返回true。如果修改表中也没
有,那就要新建一个元素,并将其赋值到修改表的相应项的词串后面了,同时记录该记录的个数加1。其实简单的说就是,如果在原始表中有该项,则设置相应的频率,否则如果在修改表中有该项,则设置相应的频度。如果两者都没有,则在修改表中的新添一个元素代表该项并插入到相应的位置中去。
DelItem的过程可以说是AddItem的逆操作,其参数只需要待删除的词及其handle值。经过同样的预处理之后首先在原始表中查找,如果找到了将原始表中该项的频率值置为-1,在修改表中记录删除个数加一。需要注意的是,如果传入的handle的值为-1即要求忽略handle删除所有相应词的项,需要遍历并修改m_IndexTable和m_pModifyTabale的值。如果原始中没有找到,那么在修改表中做类似的操作。在修改表中的操作会删除所有词相同且handle值相同或者词相同且传入handle值为-1的项。如果在两个表中都没有找到则删除失败返回false。注意,在修改表中做删除操作时并未对修改表中的nCount成员做任何的修改,怀疑为bug//!。
DelModified函数其实就是清空修改表,主要是要释放其中每个项的sWord所占用的buffer。如果使用string及相应的stl类表示则这个函数只需要一条clear语句即可。
IsExist函数用来查找给出的词条及相应的handle是否在原始表和修改表中的存在。
GetHandle函数是一个比较重要的函数,用来查找给定词条相关信息,不仅仅是handle值。对于同一个词,可能有多个handle及相应的频率值。也是先在原始表中查找,如果没有找到在修改表中查找。
FindInOriginalTable是在原始表中查找相应词条信息。传入的参数分别为汉字的内码,词以及handle,返回值为找到的词匹配的位置,就是索引。用的是二分法进行查找,因为m_IndexTable中的词条本身是有序的。对于已删除的词条会做进一步的判断,会找到第一个匹配当前词的项的索引。
FindInModifyTable是类似的一个操作,在修改表中查找相应的词条信息,不同的是返回值的类型为找到词条的前一个位置的指针,而且查找过程中是顺序查找的。
GetWordType用于简单判断输入字的类型,如果输入字符串全为汉字则返回汉字类型,如果第一个是分割符则返回分割符类型,否则返回其他。只有汉字类型和分割符类型是合法的词典项类型。
预处理函数PreProcessing的功能前面已提过,这里需要注意的是,在预处理这个函数中会删除传入词串的首末空格,所以输入参数sWord不能是const string&型。
MergePOS函数是用于将词的词性信息压缩保存在handle里面。在目前的版本中未使用,会同时处理原始表和修改表。对于每一个索引项中的若干词条,如果某个词条与“前一个词”一样且当前词未被删除(即频率值不为-1)则么会标记该词为删除同时在删除表中的相应索引处的delete值加一。如果当前词条为第一个或者比“前一个词”小且未被删除,则置该词条的handle值为传入的参数。对修改
表的操作类似,对于每个索引项,依次处理索引项中的每个词条,如果当前词条比“前一个词”大则令当前词条的handle值为传入的参数,并重置“前一个词”,考察下一个词条;如果与“前一个词”相同,则删除该项。(不可能存在比前一个词小的情况)。该函数始终返回true值。
GetMaxMatch也是一个重要的函数,多次调用。主要是根据传入的词条获取最大匹配的词条及其相应的handle值。首先通过预处理获取传入词条的ID值即内码和剩余的词串(如果第一个字是分割符则为全部词串)以及第一个字。在原始表中查找的时候使用的handle值是-1因此会找到该词在原始表中出现的第一个位置。在找到结果之后会继续往后匹配直到确实找到词条完全匹配的结果之后返回。如果原始表中没有,则在修改表中进行类似的查找,由于修改表为链表形式,因此只能依次往后比较匹配。此函数中最后判断是否在修改表中找到相应结果的语句有问题,if(pCur!=NULL&&CC_Find(pCur->data.sWord,sWordGet)!=pCur->data.sWord)这一句中的红色不等号似乎应该为等号,//!bug吧!。
GetPOSValue用于将传入的词性字符串转换为相应的词性值。其规则为,如果传入字符串长度小于3,则词性值等于第一位乘以256加上第二位的值,如果大于等于3,则应该是由若干个加号连接起来的词性字符串表示,递归调用该函数,词性数值等于加号前的部分所得词性值乘以100以后加上后面的部分。,这里应该加入对传入参数合法性的判断。
GetPOSString为GetPOSValue的逆过程,将传入的词性数值转换为相应的字符串表示输出。该函数始终返回true。这个函数返回的词性字符串可能是上位词性也可能是下位词性(即可能为一个字母表示也可能为两个字母表示)
GetFrequency即获得输入词条及相应的handle的频率值,查表即可,如果没有找到返回0。
Output和OutputChars两个函数分别将词典内容及结构输出到指定的文件中。需要说明的是,在修改模式下不会输出结果(即如果m_pModifyTable不为空的话直接返回)这里打开文件和判断是否为修改模式应该交换一下位置。后者只输出频率大于50的词串,不知道是为了减小输出的文本情况还是另有深意。
Merge是一个将新词典合并到已有词典中的函数,除了词典以外还同时提供一个比例因子nRatio用于控制两个词典信息的权重。只有在原始词典中没有修改信息而且传入词典中没有修改信息时才会执行合并工作。该函数需要改进的第一点就是传入的参数应该使用const&类型。函数的基本思想就是,如果词相同handle值也相同,则将两个词典中该项的频率值相加。如果已有词典中的项较小(词小或者词相同但handle值小)则修改已有词典中该项的频率值,即乘以一个系数nRatio/(nRatio+1),否则为已有词典中的词较大,要求新词典中该项的频率值大于(nRation+1)/10的情况下将这个词通过调用AddItem函数加入到已有词典中,注意传入的频率值为新表中该词的频率值除以(nRatio+1)。
Optimum函数用于优化已有词典m_IndexTable。具体规则有两条,删除频率值为0的词条,删除词条相同但词性一个为另外一个父类的情况(因为子类词类提供更多的信息)。另外就是会删除词性为x,g和qg的词条(不知道为什么)
ClearDictionary是我个人在修改版本中加入的清空m_IndexTable和m_pModifyTable的函数,主要是释放其中动态分配的一些内存。
关于save函数在有修改表下的操作。首先需要说明的是三个计数器的作用。原始表中的m_IndexTable[i].nCount是用来记录其pWordItemHead后一共跟了多少个词条的,并不会进行修改,如果在修改状态下删除了某些词条,则只是简单的将其频率值置为-1不会删除这个词条,因此需要用修改表的相应位置的nDelete记录删除了几个词条。因此,在调用AddItem,DelItemt和MergePOS这三个会造成修改的函数中都会相应的对nDelete进行修改。而修改表中的nCount则是记录修改表中相应索引位置跟了多少个词条(新词),因此只有在AddItem和DelItem中需要修改,前者是新添了一个原始表中没有且现有的修改表中没有的词条时加一,后者是删除了一个修改表中的词条时减一,注意后面这个减一操作在现有的代码中没有,疑为bug//!。在有修改表中的情况下,每个索引的词条数为原始表的nCount加上修改表的nCount再减去nDelete的值,即原始表中置了频率为-1的个数。然后同时走原始表和修改表,哪个小就先写哪个,小即词条小或者词条相同但handle值小。唯一要注意的就是如果原始表中的词项频率值为-1表示已删除不要写入。
关于词典类的总结。
词典类的主要数据存储结构为两个表,原始表m_IndexTable和修改表m_pModifyTable,所有的操作都是围绕这两个数据结构进行的。前者为基本数据,从词典文件中读入,为预定义大小的buffer(因为索引项的个数是固定的),后者为修改数据,只在修改模式下才分配内存并做相关的操作。原始表的索引项是固定的,但是每个索引项的词条数目是不固定的,类似于vector<list<WORD_ITEM>>的结构,其中WORD_ITEM为每个词条项,为词、词的handle及频率frequency。使用list结构不能使用二分查找法,也可以用类似vector<vector<WORD_ITEM>>,因为写入格式中有每个索引项的词条个数这一项,可以预先分配vector的大小,不过vector的vector的情况是否会增加内存分配的开销?m_pModifyTable是一个明显的vector<pair<int nDelete, list<WORD_ITEM>>>结构,在现有的程序中也是使用指针遍历方式进行查找或者比较等。一个折中的策略是vector中都只放指针,即原始表用vector<vector<WORD_ITEM>*>来表示,而修改表用vector<pair<int, list<WORD_ITEM>*>>来表示。这样,对于修改表而言,也会减少一些内存的需求。现有算法中由于删除项目的存在,因此不得不使用一个变量来存储已删除词条的个数辅助为-1的频率值。词典的另一个要求是每个索引项中的词条必须有序,这样主要是方便二分查找,对于输入的handle值为-1的情况也可以依顺序找到第一个匹配的词条。如果换做map一类的结构来存储可以保证有序,查找也很快,就是构造的时候比较慢一些,而且map的键值会比较复杂,因为一是词本身,二是其handle值,而map的映射值为其频率值。需要自定义比较函数,数据结构可能会复杂一些。修改表也要用类似的结构。
方案1:
原始表:vector<vector<WORD_ITEM>>
修改表: vector<pair<int, list<WORD_ITEM>>>
方案2:
原始表:vector<vector<WORD_ITEM>*>
修改表:vector<pair<int, list<WORD_ITEM>*>>>
方案3:
原始表:vector<map<pair<string sword, int nhandle>, int nfrequency>>
修改表:vector<pair<int ndelete, map<pair<string sword, int nhandle>, int nfrequency>>>>
目前考虑用第二种方案。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值