3.5 马尔科夫链算法的具体实现(Java环境下)
3.6 马尔科夫链算法的具体实现(C++环境下)
因为C++语言几乎是C的一个超集,只要注意某些写法和规则,C++一样可以以C的形式去使用,实际上在上一篇中,我得所有输入输出就是用的C++中的基本输入输出流。对于C++来讲,更合适的用法应该是定义一些类,建立起程序中需要的各种对象,这样可以隐藏起很多实现细节。在这里,我们更进一步,使用C++的STL(标准模板库),它不仅给我们提供了很多内部机制,更重要的是,它已经被ISO列入到C++的语言定义中了,我们可以放心的使用。
在这里,说到为什么有C++还要学习C,甚至我第一个写的实现就是C。个人来讲,可能是觉得C比较好玩吧,所有的操作都是你可以看到的。我喜欢将C比作一把双刃剑,喜欢那种锋利的感觉(是不是玄幻看多了)。还有可能是学校第一门计算机语言类的课就讲的C,到现在还能记住拿C+OpenCV1.0写的曼德勃罗集合...
STL提供了许多的容器类,例如向量、链表、集合,还包括了许多检索、排序、插入和删除等等的基本算法。利用C++的模板特性,每个STL算法都能用到很多不同的容器类上。容器类的元素可以是用户定义类型或者是内部类型的。这里的容器都被描述为C++模板,可以对特定类型进行实例化。例如,STL里有一个vector容器类,由它可以导出各种具体类型,比如vector<int>;vector<string>等。所有的vector操作,包括排序的标准算法,都可以直接应用到这些数据类型。
在STL里,除了有vector容器(它与Java的vector类差不多),还提供了一个deque容器类。deque(念为deck)是一种双端队列,它正好能符合我们对前缀操作的需要:可以用它存放NPREF个元素,丢掉最前面的元素并在后面添一个新的,这都是O(1)操作。实际上,STL的deque比我们需要的东西更一般,它允许在两端进行压入和弹出,而执行性能方面的保证是我们选择它的原因。
STL还额外提供了一个map容器,其内部用来实现基于平衡的树。在map中可以储存(关键码——值)的数对。map的内部代码实现保证从任何关键码出发提取相关值的操作都是O(logn)的。虽然这种的效率没有散列表高,但是胜在不需要额外写很多代码(虽然我个人还是很喜欢散列表)
当我们拥有了这些强力工具后,就可以进行代码的编写了。首先我们先进行一下声明:
typedef deque<string> Prefix; map<Prefix, vector<string>> statetab;
另外别忘了:
using namespace std;
STL提供了deque的模板,记法deque<string>将它指定为以字符串为元素的deque。由于这个类型将在程序里多次出现,在这里用一个typedef声明,将它另外命名为Prefix。映射类型中将存储前缀和后缀,因为它在程序里只出现一次,我们就没有给它命名。这里声明了一个map类型的变量statetab,它是从前缀到后缀向量的映射。
对于整个程序来说,add函数应该是其中比较不好理解的一部分。
void add(Prefix &prefix, const string &s) { /*判断读入的字符串是否符合要求*/ if (prefix.size() == NPREF) { statetab[prefix].push_back(s); prefix.pop_front(); } prefix.push_back(s); }
这几个非常简单的语句确实做了不少事情。map容器类重载了下标运算符 ([ ]运算符),使它在这里成为一种查询运算。表达式 statetab[prefix]在statetab里完成一次查询,以prefix作为查询的关键码,返回对于所找到的项的一个引用。如果对应的向量不存在,这个操作将建立一个新向量。vector和deque类的push_back函数分别把一个新字符串加到向量或deque的最后;pop_front从deque里弹出头一个元素。
对于使用这种方法来说,实现简单(当然我还是喜欢C),但是速度相比C来说就会差得很远,虽然还不是最慢的。
喜欢C可能还是因为对于基础的东西,喜欢自己一步步去写吧。
Coding time
#include "stdafx.h" #include <vector> #include <string> #include <iostream> #include <deque> #include <cstdio> #include <map> using namespace std; /*deque这个容器可以储存NPREF个元素,并且可以丢掉第一个并在最后添加一个新的元素进去*/ typedef deque<string> Prefix; /*map中可以储存(关键码——值)的数对*/ map<Prefix, vector<string>> statetab; enum { NPREF = 2, /*前缀中词的数量*/ NHASH = 99999, /*哈希表(散列表)大小*/ MAXGEN = 10000, /*录入的最大单词数量*/ MULTIPLIER = 31, BUFSIZE = 100 }; string NONWORD = "\n"; int go_on = 1; void build(Prefix &prefix, istream &in); void add(Prefix &prefix, const string &s); void generate(int nwords); int main() { int nwords = MAXGEN; Prefix prefix; for (int i = 0; i < NPREF; i++) { add(prefix, NONWORD); } build(prefix, cin); add(prefix, NONWORD); generate(nwords); return 0; } void build(Prefix &prefix, istream &in) { string buf; /*读入的是buf(可认为是后缀)*/ while (cin >> buf) { add(prefix, buf); printf("是否继续:1(是) 0(否)"); scanf("%d", &go_on); if (!go_on) { break; } } } void add(Prefix &prefix, const string &s) { /*判断读入的字符串是否符合要求*/ if (prefix.size() == NPREF) { statetab[prefix].push_back(s); prefix.pop_front(); } prefix.push_back(s); } void generate(int nwords) { Prefix prefix; int i; for (i = 0; i < NPREF; i++) { add(prefix, NONWORD); } for (i = 0; i < nwords; i++) { vector<string> &suf = statetab[prefix]; const string &w = suf[rand() % suf.size()]; if (w == NONWORD) { break; } cout << w << endl; prefix.pop_front(); prefix.push_back(w); } }
后记:
对于Awk和Perl语言,我完全没有接触过,因此在这里就不做说明。
对于这么多的语言来讲,程序设计实践这本书中给出了代码运行的各种时间,在下图中我们可以看出:C代码的执行时间要远远快于其他类型的代码。当然,随着时间的发展,cpu的运行速度也越来越快,但这个时间仍然具有参考意义。