萌新笔记——C++里创建 Trie字典树(中文词典)(二)(插入、查找、导入、导出)

  萌新做词典第二篇,做得不好,还请指正,谢谢大佬!

  做好了插入与遍历功能之后,我发现最基本的查找功能没有实现,同时还希望能够把内存的数据存入文件保存下来,并可以从文件中导入词典。此外,数据的路径是存在配置文件中的。甚至,还想尝试类似自动补全的功能。当然了,是做一个比较low的补全,比如传入“编程”,能够得到”软件“、”学习“、”学习网站“、”入门“四个字符串。但是传入“编”不会得到“程”,因为“编程”没有录入词典。于是对原有的类进行了修改,并添加了一些函数。

  先放上运行结果吧:

 1 test1
 2 ID : 2    char : 件    word : 编程软件
 3 ID : 3    char : 习    word : 编程学习
 4 ID : 4    char : 站    word : 编程学习网站
 5 ID : 1    char : 门    word : 编程入门
 6
 7 test2
 8 ID : 1    char : 门    word : 编程入门
 9 ID : 3    char : 习    word : 编程学习
10 ID : 4    char : 站    word : 编程学习网站
11 ID : 2    char : 件    word : 编程软件
12 find ID : 3    word : 编程学习

  test1就是上一步里,把word的id打印了出来,并导出到文件中。

  test2是从文件中导入,然后打出结果,并查找“编程学习”,如果存在就打出id。

  首先,新增了一个配置文件类DictionaryConf,如下:

DictionaryConf.h

 1 #ifndef __DICTIONARYCONF_H__
 2 #define __DICTIONARYCONF_H__
 3
 4 #include <string>
 5
 6 namespace ccx{
 7
 8 using std::string;
 9
10 class DictionaryConf
11 {
12     public:
13         DictionaryConf();
14         string getDictionaryPath();
15     private:
16         void GetConf();
17         string _DictionaryPath;
18 };
19
20 }
21
22 #endif

  它的功能很简单,在构造的时候读取文件配置信息,对外只提供一个接口,就是获取一个字符串。它的实现如下:

DictionaryConf.cc

 1 #include "DictionaryConf.h"
 2 #include <json/json.h>
 3 #include <stdlib.h>
 4 #include <fstream>
 5 #include <iostream>
 6
 7 namespace ccx{
 8
 9 using std::ifstream;
10 using std::cout;
11 using std::endl;
12
13 DictionaryConf::DictionaryConf()
14 {
15     GetConf();
16 }
17
18 void DictionaryConf::GetConf()
19 {
20     ifstream ifs;
21     ifs.open("DictionaryConf.json");
22     if(!ifs.good())
23     {
24         cout << "open DictionaryConf.json error" << endl;
25         exit(EXIT_FAILURE);
26     }
27
28     Json::Value root;
29     Json::Reader reader;
30
31     if(!reader.parse(ifs, root, false))
32     {
33         cout << "DictionaryConf json read error" << endl;
34         exit(EXIT_FAILURE);
35     }
36
37     _DictionaryPath = root["DictionaryPath"].asString();
38
39     ifs.close();
40 }
41
42 string DictionaryConf::getDictionaryPath()
43 {
44     return _DictionaryPath;
45 }
46
47 }

  配置文件是写成json格式的,方便读取:

DictionaryConf.json

1 {"DictionaryPath" : "Dictionary.json"}

  然后是修改已有的Dictionary类,增加它的功能(总觉得是不是要变胖接口了0。0)

Dictionary.h

 1 #ifndef __DICTIONARY_H__
 2 #define __DICTIONARY_H__
 3
 4 #include "DictionaryData.h"
 5 #include "DictionaryConf.h"
 6
 7 #include <memory>
 8 #include <vector>
 9 #include <list>
10
11 namespace ccx{
12
13 using std::shared_ptr;
14 using std::vector;
15 using std::list;
16
17 class Dictionary
18 {
19     typedef unordered_map<string, pDictElem>::iterator WordIt;
20     public:
21         Dictionary();
22         void push(const string & word);
23         void push(vector<string> & words);
24         int search(const string & word);//查找,返回词的ID,如果没找到返回-1
25     private:
26         void AddWord(const string & word, int wordId);
27         void splitWord(const string & word, vector<string> & characters);//把词拆成字
28         pDictElem _dictionary;
29         DictionaryConf _conf;
30
31 //遍历用
32     public:
33         void next();
34         string getCurChar();
35         string getCurWord();
36         int getCurWordId();
37         void resetPoint();
38         bool isEnd();
39     private:
40         void nextWord();
41         pDictElem _pcur;
42         WordIt _itcur;
43
44 //用list实现栈,遍历时方便
45         list<WordIt> _stackWord;
46         list<pDictElem> _stackDict;
47
48 //导入导出
49     public:
50         void leading_in();
51         void leading_out();
52 };
53
54 }
55
56 #endif

  这又是一个最终的类。比之前的多了一个查找的函数,一个Add函数,和导入导出的函数。

  Add函数是原来的push改个名字,原因就是从我这里导出到文件的词是包含他的ID的,导入的时候的ID是固定的,所以改成在添加的时候要传入ID。对于提供的接口push,调用了Add函数。

  改动过的Add和push如下:

 1 void Dictionary::AddWord(const string & word, int wordId)
 2 {
 3     vector<string> characters;
 4     splitWord(word, characters);
 5
 6     vector<string>::iterator it_char;
 7     it_char = characters.begin();
 8     pDictElem root;
 9     root = _dictionary;
10     for(; it_char != characters.end(); ++it_char)
11     {
12         WordIt it_word;
13         it_word = root->_words.find(*it_char);
14
15         if(it_word == root->_words.end())
16         {
17             pair<string, pDictElem> temp;
18             temp.first = *it_char;
19             pDictElem dictemp(new DictElem);
20             dictemp->_word = *it_char;
21             dictemp->_wordId = 0;
22             temp.second = dictemp;
23             root->_words.insert(temp);
24             root = dictemp;
25         }else{
26             root = it_word->second;
27         }
28     }
29     if(!root->_wordId)
30     {
31         root->_wordId = wordId;
32     }
33 }

  总感觉29行的if可以去掉呢--!但是没有去试

 1 void Dictionary::push(const string & word)
 2 {
 3     ++(_dictionary->_wordId);
 4     AddWord(word, _dictionary->_wordId);
 5 }
 6
 7
 8 void Dictionary::push(vector<string> & words)
 9 {
10     int size = words.size();
11     for(int i = 0; i < size; ++i)
12     {
13         push(words[i]);
14     }
15 }

  对root的wordID的自增放到了这里。这样,导和的时候就可以调用Add函数了。(好吧我就是懒得重新写一个插入的函数0。0)

  接下来是查找的函数,比较简单,直接把Add拷过来改了一下:

 1 int Dictionary::search(const string & word)
 2 {
 3     vector<string> characters;
 4     splitWord(word, characters);
 5
 6     vector<string>::iterator it_char;
 7     it_char = characters.begin();
 8     pDictElem root;
 9     root = _dictionary;
10     for(; it_char != characters.end(); ++it_char)
11     {
12         WordIt it_word;
13         it_word = root->_words.find(*it_char);
14
15         if(it_word == root->_words.end())
16         {
17             return -1;
18         }else{
19             root = it_word->second;
20         }
21     }
22     return root->_wordId;
23 }

  关于以上,有另外一个想法:不管是在插入还是查找,都是要先“查找”,看看有没有那个节点。对于查找操作,只要返回有或者没有就行了;而对于插入,如果没有就返回它的位置,从那里开始添加节点,这样似乎又可以复用一些代码了  0。0

  最后就是导入和导出了。

  关于这块,写之前一直在纠结,是要遍历所有词,把词完整地存下来。采用json的格式,对于“编程学习”与“编程训练”:

  直接存进文件,后面带上ID的效果:

1 [
2     { "word" : "编程学习" , "id" : 1},
3     { "word" : "编程训练" , "id" : 2)
4 ]

  还是把树按节点存下来,

 1 [
 2     {"key" : "编" , "flag" : 1, "id" : 0 , "value" :
 3     {"key" : "程" , "flag" : 1 , "id" : 0 , "value" :
 4       {"key" : "学" , "flag" : 1 , "id" : 0, "value" :
 5         {"key" : "习" , "flag" : 0 , "id" : 1, "value" : "NULL"
 6         }
 7       },
 8       {"key" : "训" , "flag" : 1 , "id" : 0, "value" :
 9         {"key" : "练" , "flag" : 0 , "id" : 2, "value" : "NULL"
10         }
11       }
12     }
13   }
14 ]

  大致是这样一层嵌套一层的结构。虽然这种结构可以用栈来实现,即把Json::Value型数据入栈,但是后来想想这样虽然一次只存一个结点,好像很节约空间,但是每个节点还要存一些额外的信息,逻辑似乎比较复杂,于是决定用第一种方法来实现,比较简单粗爆。

  代码如下:

 1 void Dictionary::leading_out()
 2 {
 3     Json::Value root;
 4     Json::FastWriter writer;
 5
 6     resetPoint();
 7
 8     while(!isEnd())
 9     {
10         Json::Value elem;
11         elem["Word"] = getCurWord();
12         elem["WordId"] = getCurWordId();
13         root.append(elem);
14         next();
15     }
16
17     string words;
18     words = writer.write(root);
19
20     ofstream ofs;
21     const char * path = _conf.getDictionaryPath().c_str();
22     ofs.open(path);
23     if(!ofs.good())
24     {
25         cout << "open Dictionary.json error(leading_out)" << endl;
26         ofs.open("Dictionary.tmp");
27         if(!ofs.good())
28         {
29             exit(EXIT_FAILURE);
30         }
31     }
32
33     ofs << words;
34     ofs.close();
35 }

  存入文件的效果如下:

1 [{"Word":"编程软件","WordId":2},{"Word":"编程学习","WordId":3},{"Word":"编程学习网站","WordId":4},{"Word":"编程入门","WordId":1}]  

  只有一行(不要在意id顺序)。

  之前想过一种方法能够让它以多行的形式存到文件中,做了如下处理:

 1     .
 2     .
 3     .
 4 ofs << "[" << endl;
 5 int size = root.size();
 6 for(int i = 0; i < size; ++i)
 7 {
 8     string json_file = writer.write(root[i]);
 9     int len = json_file.size();
10     json_file[len - 1] = ‘ ‘;
11     ofs << "\t" << json_file;
12     if(i != size -1)
13     {
14         ofs << "," << endl;
15     }
16     }
17 ofs << endl << "]" << endl;
18 ofs.close();
19     .
20     .
21     .

  一项一项地取出,把最后的‘\n‘替换成空格,然后自己控制输出格式。

  导入格式要与导出格式对应,也使用json,如下:

 1 void Dictionary::leading_in()
 2 {
 3     ifstream ifs;
 4     const char * path = _conf.getDictionaryPath().c_str();
 5     ifs.open(path);
 6     if(!ifs.good())
 7     {
 8         cout << "open Dictionary.json error(leading_in)" << endl;
 9     }else{
10         Json::Value root;
11         Json::Reader reader;
12
13         if(!reader.parse(ifs, root, false))
14         {
15             cout << "json read Dictionary.json error" << endl;
16         }else{
17             int size = root.size();
18             for(int i = 0; i < size; ++i)
19             {
20                 string word = root[i]["Word"].asString();
21                 int wordId = root[i]["WordId"].asInt();
22                 AddWord(word, wordId);
23                 ++(_dictionary->_wordId);
24             }
25         }
26     }
27 }

  这里要说明一下,导出和导入打开文件如果失败了没有exit的原因:

  对于导出,如果打开一个文件失败了直接exit,那内存中的东西就全部丢失了,于是就采用在当路径下创建一个临时文件来存要存的内容。

  对于导入,打开文件失败只是说明路径有问题或者文件本身有问题,修改一下就可以了,或者只是没有导入词典而已,程序中如果已经添加了一些词,此时退出也会使内容丢失。

  当然,目前的机制还是有问题的,比如词ID的设置,如果在内存中已有词的情况下导入新词,可能会使ID错乱。对于导出,或许可以设置一个机制:任何位置要退出的时候都把内容存入临时文件中,可能会更好。

  做到这里,突然又想实现词的补全功能:传入“编程”,能传出“学习”、“学习网站”等的集合,这个做好了再补上吧!

时间: 2024-12-22 14:47:28

萌新笔记——C++里创建 Trie字典树(中文词典)(二)(插入、查找、导入、导出)的相关文章

萌新笔记——C++里创建 Trie字典树(中文词典)(插入、查找、遍历、导入、导出)(上)

写了一个词典,用到了Trie字典树. 写这个词典的目的,一个是为了压缩一些数据,另一个是为了尝试搜索提示,就像在谷歌搜索的时候,打出某个关键字,会提示一串可能要搜索的东西. 首先放上最终的结果: input: 1 编程入门 2 编程软件 3 编程学习 4 编程学习网站 output: 1 char : 件 2 word : 编程软件 3 char : 习 4 word : 编程学习 5 char : 网 6 word : 编程学习网 7 char : 门 8 word : 编程入门 其实这里不应

萌新笔记——C++里将string类字符串(utf-8编码)分解成单个字(可中英混输)

最近在建词典,使用Trie字典树,需要把字符串分解成单个字.由于传入的字符串中可能包含中文或者英文,它们的字节数并不相同.一开始天真地认为中文就是两个字节,于是很happy地直接判断当前位置的字符的ASCII码是否处于0~127之间,如果是就提取一个字符,否则提取两个.在测试分字效果的时候,这种方法出了问题.比如我传一个"abcde一二三四五"进去,abcde可以正常分解成 a b c d e,而后面的"一二三四五"则成了乱码. 于是我开启了谷歌之旅,搜索"

trie/字典树几题

以后有好多次看到这地方就过去了, 未亲自实践过. 不过今晚看了一下,感觉trie的思想还算是比较基础的. 感觉这一个链接讲的不错:http://www.cnblogs.com/BeyondAnyTime/archive/2012/07/16/2592838.html 顺便水了几道题. 具体列表可见:http://vjudge.net/contest/view.action?cid=47036#overview A:水题啦. 稍微理解下trie就能写出来了. 附个自己写的代码,还是用LRJ书上的非

【oiClass1502】查单词(Trie字典树)

题目描述 全国英语四级考试就这样如期来了,可是小y依然没有做好充分准备.为了能够大学毕业,可怜的小y决定作弊.小y费尽心机,在考试的时候夹带了一本字典进考场,但是现在的问题是,考试的时候可能有很多的单词要查,小y能不能来得及呢? 输入 第一行一个整数N,表示字典中一共有多少个单词(N<=10000).接下来每两行表示一个单词,其中:第一行是一个长度<=100的字符串,表示这个单词,全部小写字母,单词不会重复.第二行是一个整数,表示这个单词在字典中的页码.接下来是一个整数M,表示要查的单词数(M

DFA和trie字典树实现敏感词过滤(python和c语言)

现在做的项目都是用python开发,需要用做关键词检查,过滤关键词,之前用c语言做过这样的事情,用字典树,蛮高效的,内存小,检查快. 到了python上,第一想法是在pip上找一个基于c语言的python字典树模块,可惜没找到合适的,如果我会用c写python模块的话,我就自己写一个了,可惜我还不具备这个能力, 只能用python写了,性能差一点就差点吧,内存多一点也无所谓了. 用搜索引擎看CSDN上的网友的用python实现的DFA,再参照自己以前用c语言写过的字典树,有些不大对,就自己写了一

Trie 字典树 删除操作

字典树的删除操作: 1 没找到直接返回 2 找到叶子节点的时候,叶子节点的count标志清零,代表不是叶子节点了 3 如果当前节点没有其他孩子节点的时候,可以删除这个节点 判断是否需是叶子节点,就检查叶子节点的count标志就可以了. 判断是否有其他孩子节点就需要循环26个节点了,如果都为空,那么就没有其他孩子节点了. #include <stdio.h> #include <stdlib.h> #include <iostream> #include <vect

Trie字典树 动态内存

Trie字典树 1 #include "stdio.h" 2 #include "iostream" 3 #include "malloc.h" 4 #include "string.h" 5 6 using namespace std; 7 8 #define MAX_SIZE 26 9 10 typedef struct Trie{ 11 char val; 12 bool isword; 13 struct Trie*

算法导论:Trie字典树

1. 概述 Trie树,又称字典树,单词查找树或者前缀树,是一种用于快速检索的多叉树结构,如英文字母的字典树是一个26叉树,数字的字典树是一个10叉树. Trie一词来自retrieve,发音为/tri:/ “tree”,也有人读为/tra?/ “try”. Trie树可以利用字符串的公共前缀来节约存储空间.如下图所示,该trie树用10个节点保存了6个字符串pool.prize.preview.prepare.produce.progress 在该trie树中,字符串preview,prepa

LeetCode 208.实现Trie(字典树) - JavaScript

??Blog :<LeetCode 208.实现Trie(字典树) - JavaScript> 实现一个 Trie (前缀树),包含 insert, search, 和 startsWith 这三个操作. Trie trie = new Trie(); trie.insert("apple"); trie.search("apple"); // 返回 true trie.search("app"); // 返回 false trie.