在十五章的最后展示了一个挺复杂的问题,也是利用到了后缀数组的结构,解决的问题是:如何生成随机文本。因为算法的最终确定是“通过文本内容的前K个字符或单词来判断下一个字符或单词是什么”这个思想来进行的,所以书中说这是一个“具有固定转化概率的有限状态马尔科夫链”,但这个术语究竟是什么意思我还没搞清楚。但是总结说来比较简洁的描述方法就是:生成k阶单词或字符级别的随机文本(马尔科夫链)。
在确定了问题和有了相应的解题思路之后我们就可以着手解决这个问题了。源码共展示了三个程序:1,k阶单词级别随机文本。2,k阶单词级别随机文本(哈希加速法)。3.k阶字符级别随机文本。
第一个程序的思路是:1,首先定义比较大的字符数组来存储单词内容,利用scanf()函数读入单词,建立后缀数组。2.后缀数组利用qsort()排序。3.输出文中的前k个单词,设置文本指针为指向字符数组首地址,利用改进的二分查找算法查找在排序后的后缀数组中前k个单词首次出现的位置。4.不断地寻找符合条件的新的位置并利用一个概率算法等概率地选择出其中的一个位置,输出第K+1个单词。5.利用函数重新设置文本的指针指向字符数组的第二个元素,不断循环,终止条件是此次单词为文本的最后一个单词,其后面再无其他单词,故无法继续进行下去,此时终止循环(设置循环次数)。
其中比较重要的改进是对qsort()中的比较函数wordcmp()进行改进,加入了考虑k个单词的条件,其中也是利用到了scanf()函数为每个单词提供的结束符'\0'来判断已经比较过的单词个数。再有就是概率改进的二分查找程序,其改进之处在于“比如,在整数t多次出现在整数数组x[0 ,, n-1]的情况下,找出t第一次出现的位置。”而普通的二分查找算法会返回任意的一个位置,这个改进的原理是修改了程序的循环不变式,让上界u代表的数x[u]>=t,而不是x[u]>t。文中还用到了一个比较小巧的函数char *skip(char *s, int i),其主要功能是返回一个指针,指向s之后的第i个单词(单词级别)。
说明一下其中的概率选择算法,其来源是一道习题,问题是这样的:具体说来,如何在事先不知道文本文件行数的情况下读取该文件,从中随机选择并输出一行?
答案是这样的:
我们总选择第1行,并以概率1/2选择第2行,以概率1/3选择第3行,以此类推。在这一过程结束时,每一行的选中概率是想等的(都是1/n,其中n是文件的总行数):
i=0
while more input lines
with probability 1.0/++i
choice = this input line
print choice
总之这个程序很复杂,值得总结的地方还有很多,今天有点儿着急,所以吸收的还不是很好,程序摹写得也很差劲。所以最后还是要附上源码。
// 生成单词级别的2阶随机文本, k=2
// generate random text from input document
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int k=2;
char inputchars[4300000];
char *word[800000]; //
int nword=0;
int sortcmp(char **p, char **q)
{
return wordcmp(*p, *q);
}
// compare k words in the front
int wordcmp(char *p, char *q)
{
int n=k;
/*
while(*p++ == *q++)
{
if(*p == '\0' && --n ==0)
return 0;
}
*/
for(;*p == *q; p++,q++)
{
if(*p == '\0' && --n ==0)
return 0;
}
return *p-*q;
}
char *skip(char *t, int n)
{
for(;;)
{
if(*t=='\0' && --n==0)
break;
++t;
}
return ++t;
}
int main()
{
int i;
word[0]=inputchars;
while(scanf("%s",word[nword]) != EOF)
{
word[nword+1]=word[nword]+strlen(word[nword])+1;
nword++;
}
// set the last word to '\0' 不明白为什么要设置k位,感觉1位就够了
for(i=0;i<k;i++)
word[nword][i]='\0';
for(i=0;i<k;i++)
printf("%s ",word[i]);
qsort(word, nword, sizeof(char *), sortcmp);
//int l, u, occurence=0, wordsleft, m;
int l, u, wordsleft, m;
char *p=word[0], *q;
//
srand((unsigned)time(NULL));
for(wordsleft=1000;wordsleft>0;--wordsleft)
{
l=-1;
u=nword;
while(l+1 != u)
{
m=(l+u)/2;
if(wordcmp(word[m],p)<0)
l=m;
else
u=m;
}
// 随机选择一行满足条件的文本的算法
int count=0;
for(;;)
{
if(wordcmp(word[u],p)!=0)
break;
//if(rand()%++occurence == 0)
if(rand()%(count+1) == 0)
{
p=word[u];
count++;
}
u++;
}
/*
for(i=0;wordcmp(p, word[u+i])==0;i++)
{
if(rand()%(i+1)==0)
p=word[u+i];
}
*/
q=skip(p,1);
if(*(q+k-1)=='\0')
break;
//putchar(*(q+k));
//printf("%s\n",skip(p,k-1)); // 可以利用这个表达式输出,word[]中各项都是由'\0'结束的单词
printf("%s\n",skip(q,k-1)); // 可以利用这个表达式输出,word[]中各项都是由'\0'结束的单词
p=q;
}
return 0;
}
需要注意的问题:在构造后缀数组时本例是直接向后缀数组中输入内容,因为事先已经在字符数组inputchars[]申请完空间了,所以需要做的就是在word[]字符指针数组inputchars[]字符指针中找到一种关联的关系。
程序的运行效果如下:因为设置了srand()种子,所以运行效果会有不同。
第二个程序利用哈希表的方法实现了程序的加速,散列函数取代二分搜索,使平均的运行时间从O(nlogn)降到了O(n)。程序的整体流程没有发生改变,比较有趣儿的地方是使用了一个哈希表整型数组bin[NHASH]和next[MAXWORD]实现了一个链表的功能,其中的思想我理解了半天才明白过来,也是挺别扭的。bin[]中存储的是当前链表的第一个元素在word[]后缀数组中的下标,而next[]中存储的是当前链表的下一个元素在word[]中的位置。
// 生成单词级别的2阶随机文本, k=2
// generate random text sped up with hash tables
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define NHASH 499979
#define MAXWORD 800000
#define MULT 31
int k=2;
char inputchars[4300000];
char *word[MAXWORD];
int nword=0;
int bin[NHASH];
int next[MAXWORD];
/*
int hash(char *p)
{
int h=0, i;
for(i=0;*p && i<k;i++,p++)
h=h*MULT + *p;
return h%NHASH;
}
*/
unsigned int hash(char *ptr)
{
unsigned int h=0;
char *p=ptr; //定义另一个指针变量,防止原来的指针发生改变
int n;
for(n=k;n>0;++p)
{
h=h*MULT + *p;
if(*p == '\0')
n--;
}
return h%NHASH;
}
int sortcmp(char **p, char **q)
{
return wordcmp(*p, *q);
}
int wordcmp(char *p, char *q)
{
int n=k;
for(;*p == *q; p++,q++)
{
if(*p == '\0' && --n ==0)
return 0;
}
return *p-*q;
}
char *skip(char *t, int n)
{
for(;;)
{
if(*t=='\0' && --n==0)
break;
++t;
}
return ++t;
}
int main()
{
int i, j;
word[0]=inputchars;
while(scanf("%s",word[nword]) != EOF)
{
word[nword+1]=word[nword]+strlen(word[nword])+1;
nword++;
}
for(i=0;i<NHASH;i++)
bin[i]=-1;
for(i=0;i<nword-k+1;i++)
{
j=hash(word[i]);
next[i]=bin[j];
bin[j]=i;
}
for(i=0;i<k;i++)
printf("%s ",word[i]);
int wordsleft, count;
char *phrase=inputchars;
srand((unsigned)time(NULL));
for(wordsleft=1000;wordsleft>0;--wordsleft)
{
count=0;
//for(j=bin[hash(phrase)];j>0 && wordcmp(phrase, word[j])==0;j=next[j])
for(j=bin[hash(phrase)];j>=0;j=next[j])
{
if((wordcmp(phrase, word[j])==0) && (rand()%(++count)==0))
phrase=word[j];
//count++;
}
phrase=skip(phrase, 1);
if(strlen(skip(phrase, k-1))==0)
break;
printf("%s\n",skip(phrase, k-1));
}
return 0;
}
需要注意的问题:1,哈希函数的设计。2,边界的控制,尤其是在初始化bin[]哈希表数组时。3,在哈希表中寻找程序时的判断标准。不应该把wordcmp()写在程序的条件判断部分,因为有可能有hash()值相同而wordcmp()不同,而顺序不一致可能导致有些数据没有访问到。程序的运行效果和上图类似。
最后一个程序没有用到后缀数组,很神奇地在一个char型数组中就完成了所有的工作,这个程序有些难度,我理解了挺长时间,尤其是在边界控制上,真的需要小心翼翼,而且在没有利用后缀数组的前提下在这么短的代码中完成工作,我觉得算法的设计很重要,但整体上的思路还是和上述程序有类似的。
// generate letter-level random text from input text 5阶文本
//
#include <stdio.h>
#include <stdlib.h>
char x[5000000];
int main()
{
int n=0;
char c;
while((c=getchar())!=EOF)
x[n++]=c;
x[n]='\0';
int max, k=5, eqsofar, i;
char *p=x, *q, *nextp;
srand((unsigned)time(NULL));
for(max=2000;max>0;--max)
{
eqsofar=0;
//for(q=x;q<q+n-k;q++)//
for(q=x;q<x+n-k+1;q++)
{
for(i=0;i<k && *(p+i)==*(q+i);i++)
;
if(i==k)
{
if(rand()%(++eqsofar)==0)
nextp=q;
}
/*
//c=*(p+k);
if(c=='\0')
break;
putchar(c);
p=nextp+1;
*/
}
c=*(nextp+k);
if(c=='\0')
break;
putchar(c);
p=nextp+1;
}
return 0;
}
需要注意的问题很多,就不举了。
最后贴上一段程序吧,是第一个程序的源码,变量定义和函数写的需要学习:
/* Copyright (C) 1999 Lucent Technologies */
/* From 'Programming Pearls' by Jon Bentley */
/* markov.c -- generate random text from input document */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
char inputchars[4300000];
char *word[800000];
int nword = 0;
int k = 2;
int wordncmp(char *p, char* q)
{ int n = k;
for ( ; *p == *q; p++, q++)
if (*p == 0 && --n == 0)
return 0;
return *p - *q;
}
int sortcmp(char **p, char **q)
{ return wordncmp(*p, *q);
}
char *skip(char *p, int n)
{ for ( ; n > 0; p++)
if (*p == 0)
n--;
return p;
}
int main()
{ int i, wordsleft = 10000, l, m, u;
char *phrase, *p;
word[0] = inputchars;
while (scanf("%s", word[nword]) != EOF) {
word[nword+1] = word[nword] + strlen(word[nword]) + 1;
nword++;
}
for (i = 0; i < k; i++)
word[nword][i] = 0;
for (i = 0; i < k; i++)
printf("%s\n", word[i]);
qsort(word, nword, sizeof(word[0]), sortcmp);
phrase = inputchars;
for ( ; wordsleft > 0; wordsleft--) {
l = -1;
u = nword;
while (l+1 != u) {
m = (l + u) / 2;
if (wordncmp(word[m], phrase) < 0)
l = m;
else
u = m;
}
for (i = 0; wordncmp(phrase, word[u+i]) == 0; i++)
if (rand() % (i+1) == 0)
p = word[u+i];
phrase = skip(p, 1);
if (strlen(skip(phrase, k-1)) == 0)
break;
printf("%s\n", skip(phrase, k-1));
}
return 0;
}