萌新做詞典第二篇,做得不好,還請指正,謝謝大佬!
做好了插入與遍歷功能之後,我發現最基本的查找功能沒有實現,同時還希望能夠把內存的數據存入文件保存下來,並可以從文件中導入詞典。此外,數據的路徑是存在配置文件中的。甚至,還想嘗試類似自動補全的功能。當然了,是做一個比較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 ]
還是把樹按節點存下來,
(後來發現其實這是一個序列化與反序列化的問題——2016.12.4 23:54 注)
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錯亂。對於導出,或許可以設置一個機制:任何位置要退出的時候都把內容存入臨時文件中,可能會更好。
做到這裡,突然又想實現詞的補全功能:傳入“編程”,能傳出“學習”、“學習網站”等的集合,這個做好了再補上吧!