一、单词
(1)为文档中包含的单词生成一个列表?
解答:
方法一:用到标准模板库中的sets和strings
- #include <iostream>
- #include <set>
- #include <string>
-
- using namespace std;
-
- int main(int argc, char **argv)
- {
- set<string> str;
- set<string>::iterator iter;
- string t;
- while(cin >> t)
- {
- str.insert(t);
- }
- for(iter = str.begin(); iter != str.end(); iter++)
- {
- cout << *iter << endl;
- }
- return 0;
- }
(2)为单词进行计数
方法一:用标准模板库中的map将整个计数与每个字符串联系起来
- #include <iostream>
- #include <map>
- #include <string>
-
- using namespace std;
-
- int main(int argc, char **argv)
- {
- map<string, int> m;
- map<string, int>::iterator iter;
- string t;
- while(cin >> t)
- {
- m[t]++;
- }
- for(iter = m.begin(); iter != m.end(); iter++)
- {
- cout << iter->first << " " << iter->second << endl;
- }
- return 0;
- }
为了减少处理时间,可以建立散列表。其中内存分配函数malloc 被改为自定义更高效的 nmalloc和 smalloc
- #include <stdio.h>
- #include <stdlib.h>
- #include <string.h>
- typedef struct node *nodeptr;
- typedef struct node {
- char *word; //单词
- int count; //单词个数
- nodeptr next;
- } node;
- #define NHASH 29989/*圣经中共29131个单词,用跟29131最接近的质数作为散列表大小*/
- #define MULT 31 /*乘数*/
- nodeptr bin[NHASH];//散列表
- unsigned int hash(char *p)//哈希函数,将每个字符串映射成小于NHASH的正整数
- { unsigned int h = 0;
- for ( ; *p; p++)
- h = MULT * h + *p;
- return h % NHASH;
- }
- #define NODEGROUP 1000
- int nodesleft = 0;
- nodeptr freenode;
- nodeptr nmalloc()
- { if (nodesleft == 0) {
- freenode = malloc(NODEGROUP*sizeof(node));
- nodesleft = NODEGROUP;
- }
- nodesleft--;
- return freenode++;
- }
- #define CHARGROUP 10000
- int charsleft = 0;
- char *freechar;
- char *smalloc(int n)
- { if (charsleft < n) {
- freechar = malloc(n+CHARGROUP);
- charsleft = n+CHARGROUP;
- }
- charsleft -= n;
- freechar += n;
- return freechar - n;
- }
- void incword(char *s)//增加与单词相关联的计数器的值,如果之前没有这个词,对计数器初始化
- { nodeptr p;
- int h = hash(s);//找到与单词对应的箱
- for (p = bin[h]; p != NULL; p = p->next)
- if (strcmp(s, p->word) == 0) {//该箱子中若有 该单词,则对应count++ ,否则新建单词指针 (采取头插法)
- (p->count)++;
- return;
- }
- p = nmalloc();//本来用malloc就可以,但优化成了nmalloc
- p->count = 1;
- p->word = smalloc(strlen(s)+1);//本来用malloc就可以,但优化成了smalloc
- strcpy(p->word, s);
- p->next = bin[h];
- bin[h] = p;
- }
- int main()
- { int i;
- nodeptr p;
- char buf[100];
- for (i = 0; i < NHASH; i++)//将每个箱初始化
- bin[i] = NULL;
- while (scanf("%s", buf) != EOF)
- incword(buf);//增加与输入单词相关联的计数器的值
- for (i = 0; i < NHASH; i++)//输出每一个不等于NULL的箱的字符串和个数
- for (p = bin[i]; p != NULL; p = p->next)
- printf("%s %d\n", p->word, p->count);
- return 0;
- }<span style="font-size:18px;"><span style="font-size:18px;">
- </span></span>
二、短语
给定一个文本文件作为输入,插在其中最长的重复子字符串。例如,“Ask not what your country can do for you, but what you can do for your country”中最长的重复字符串是“can do for you”,第二长的是“your country”。
方法一:双重for循环依次比较每个字符串,找到最长重复子字符串
- #include <stdlib.h>
- #include <string.h>
- #include <stdio.h>
-
- //找到两个字符串公共部分的长度
- int comlen(char *p, char *q)
- {
- int i = 0;
- while (*p && (*p++ == *q++))
- {
- i++;
- }
- return i;
- }
-
- int main()
- {
- int i, j;
- int maxi, maxj;
- int currentlen, maxlen = -1;
- char *str = "ask not what your country can do for you, but what you can do for your country";
- int length = strlen(str);
-
- for(i = 0; i < length; i++)
- {
- for(j = i + 1; j < length; j++)
- {
- currentlen = comlen(str + i, str + j);
- if(currentlen > maxlen)
- {
- maxlen = currentlen;
- maxi = i;//i标记了最长公共子串开始的位置
- maxj = j;
- }
- }
- }
-
- for(i = 0; i < maxlen; i++)
- {
- printf("%c", str[maxi + i]);
- }
- printf("n");
- return 0;
- }
方法二:采用后缀数组来处理该问题
我们的程序最多处理MAXN个字符,这些字符存储在数组c中。
#define MAXN 50000
char c[MAXN], *a[MAXN];
我们使用一个称为“后缀数组”的数据结构,这个结构是一个字符指针数组,记为a。读取输入时,我们对a进行初始化,使得每个元素指向输入字符串中的相应字符:
while(ch = getchar() != EOF)
{
a[n] = &c[n];
c[n] = ch;
}
c[n] = 0;
元素a[0]指向整个字符串,下一个元素指向从第二个字符开始的数组后缀,等等。对于输入字符串“banana”,该数组能够表示如下后缀:
char *a="banana"
a[0]=banana
a[1]=anana
a[2]=nana
a[3]=ana
a[4]=na
a[5]=a
如果某个长字符串在数组c中出现了两次,那么它将出现在两个不同的后缀中,因此我们队数组进行排序以寻找相同的后缀。“banana”数组排序为:
a[0]=a
a[1]=ana
a[2]=anana
a[3]=banana
a[4]=na
a[5]=nana
然后就可以扫描数组,通过比较相邻元素来找出最长的重复字符串。
该方法由于排序的存在,时间复杂度为O(nlogn)。
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
//只能比较字符串,两个字符串自左向右逐个字符相比(按ASCII值大小相比较),直到出现不同的字符或遇''为止。
int pstrcmp(const void *p, const void *q)
{
return strcmp(*(char **)p, *(char **)q);
}
int comlen(char *p, char *q)//返回两个参数共同部分的长度
{ int i = 0;
while (*p && (*p++ == *q++))
i++;
return i;
}
#define M 1
#define MAXN 5000000
char c[MAXN], *a[MAXN];
int main()
{
int i, ch, n = 0, maxi, maxlen = -1;
while ((ch = getchar()) != EOF) {
a[n] = &c[n];
c[n++] = ch;
}
c[n] = 0;
qsort(a, n, sizeof(char *), pstrcmp);//快速排序
for (i = 0; i <n-M; i++)
{
if (comlen(a[i], a[i+M]) > maxlen) //比较相邻字符串相同个数
{
maxlen = comlen(a[i], a[i+M]);
maxi = i;
}
}
printf("%.*s\n", maxlen, a[maxi]);
return 0;
}
上面函数要注意下面几点:
1: qsort函数对于字符串进行排序时compare函数的写法
qsort是万能数组排序函数,必须要学会使用,简单的数组自然不用说,这里主要讨论一下字符串数组的使用。
1. 原样输出字符串:
printf("%s", str);
2. 输出指定长度的字符串, 超长时不截断, 不足时右对齐:
printf("%Ns", str); --N 为指定长度的10进制数值
3. 输出指定长度的字符串, 超长时不截断, 不足时左对齐:
printf("%-Ns", str); --N 为指定长度的10进制数值
4. 输出指定长度的字符串, 超长时截断, 不足时右对齐:
printf("%N.Ms", str); --N 为最终的字符串输出长度
--M 为从参数字符串中取出的子串长度
5. 输出指定长度的字符串, 超长时截断, 不足时左对齐是:
printf("%-N.Ms", str); --N 为最终的字符串输出长度
--M 为从参数字符串中取出的子串长度
注意,所谓超长时截断用到的M并不是只在超长时才起作用,而是不管你有没有超长,都必须截取这么长。所以
printf("%-5.2", "123")的输出为:
12空格空格空格
只截取了2个字符,其他的用空格填补,而且左对齐。
6. 上述N,M是可以动态指定的,方法是用*代替M或者N,然后在参数列表里加上一个数字参数。例子:
1)printf("%-*.*s", 5,2,"123");与上面的例子效果一样。
前边*定义的是总的宽度,后边*是指定输出字符个数。分别对应外边参数m和n。
2)printf("%.*s", 2,"123");也可以省略掉前面的那个*号。
3)printf("%*s", 5, "123");表示输出长度为5, 如果超长也不截断,不够的话填补,右对齐。
★d格式符,用来输出十进制整数.
⑴%d,按整型数据的实际长度输出.
⑵%md,m为指定的输出字段的宽度,数据位数小于m,左边补空格,若大于m,按实际长度输出
⑶%ld,输出长整型数据(long)
★o格式符,以八进制输出整数(不带符号,他将符号位也作为八进制数的一部分了)
⑴%o,参考%d的解释.
⑵%lo,参考%ld的解释.
⑶%mo,参考%md的解释.
★x,X格式符,以十六进制输出整数
也是3种参考%d的解释.
★u格式符,用来将unsigned型数据,既无符号数,以十进制形式输出
★c格式符,输出一个字符.
★s格式符,输出一个字符串.
⑴%s,如printf("%s","CHINA")
⑵%ms,输出的字符串占m列,字符串长度小于m,左边补空格,如果超出则全部输出.
⑶%-ms,串小于m,则在m列范围内字符串左靠,右补空格.
⑷%m.ns,输出占m列,但只取字符串左端n个字符.这n个字符输出在m列的右边,然后左边补空格.
⑸%-m.ns,和上面的放下,就是n个字符输出在m列的左侧,右边补空格.n>m,那么m自动取n的值,既保证n个字符正常输出.
printf("%3s,%7.2s,%.4s,%-5.3s\n","CHINA","CHINA","CHINA","CHINA");
★f格式符,用来输出实数,以小数形式输出.
⑴%f,全部输出,而且输出6位小数.
⑵%m.nf,输出数据共占m列,n位小数,如果数据长度小于m那么左边补空格
⑶%-m.nf,和上面的m.nf相反,为左靠齐,右补空格.
★e,E格式符,以指数形式输出实数
⑴%e,不指定输出数据所占的宽度和数字部分的小数位数.
⑵%m.ne和%-m.ne,这里n指小数部分的位数
★g,G格式符,用来输出实数,它根据数值大小,自动选择f格式还是e格式,(选占宽最少的一种),且不输出无意义的0.这种格式用的不多.
对于上面程序的理解
后缀数组举例 如下目标字符串: banana 其长度为6,则后缀数组的长度为6,分别是以b开头的字串(长度为6),以a开头的字串(长度为5),以n开头的字串(长度为4)。。。最后一个是以a开头的字串(长度为1)。
后缀[0] banana
后缀[1] anana
后缀[2] nana
后缀[3] ana
后缀[4] na
后缀[5] a
回到正题,查找一段文字中最长的重复字串。(注意:这不同于算法设计课中常讲的两个字符串的最长公共子序列问题(LCS),LCS问题的最长公共字串可以不是连续的)
最朴素的算法是,让后缀数组之间两两比较,找出最长的公共字串(注意,这里的最长的公共字串必须是以首字符参与匹配的,如果首字母都不匹配,那么长度为0,eg后缀[0]和后缀[1]之间的首字母不匹配,则两者的最长公共字串长度为0.。),但是时间复杂度为O(n^2).
该思想基于以下两个信息:
1)如果存在一个最长的重复字串,那么两个字串均是来自文本串不同的后缀,但这两个后缀有相同的前缀!(这个前缀也就是重复字串了)
2)既然最终结果的后缀肯定拥有相同的前缀,那么我就没有必要让全部后缀之间两两比较,而仅仅比较具有相同的前缀的后缀即可!这可以大大的减少比较的次数,提高效率。
所以,算法的流程是,先将后缀数组字母排序,然后顺次比较(避免了两两比较)即可。
后缀[0] a
后缀[1] ana
后缀[2] anana
后缀[3] banana
后缀[4] na
后缀[5] nana
最终的比较结果是 后缀[1] 和 后缀[2] 之间存在最长公共字串 ana。
后记:
已经多次领教了从后往前寻找算法的优势,EG BM字符串匹配算法,Sunday算法等。这又是一例!
编程珠玑的最后习题部分给出了另外一个问题,如何找到两个不同的字符串中的最长连续字串?
编程珠玑同样利用后缀数组给出了解答,不过答案我看不太明白。
还有一道,如何找到出现次数超过M次的最长连续字串。(M>=2, 当M==2时,就相当于找最长重复字串)
答案是
当M=2时,最长重复字串问题,我们使用的函数是 comlen(a[i], a[i+1]);
当M>2时,我们就需要使用 comlen(a[i], a[i+M-1]);
eg M=3时, 仍按照上面的例子,求出的最长字串为 "a“,长度为1; 因为a[i] 和 a[i+M-1]的最长重复字串肯定在a[i+1]~a[i+M-2]之间也重复了,也就是至少重复了M次。
本例中 a[0] 和 a[2]的重复字串 "a" 在a[1]中也重复了。
后缀[0] a
后缀[1] ana
后缀[2] anana
后缀[3] banana
后缀[4] na
后缀[5] nana
(注意:该问题等价于找一个字符串中的最长回文串)
(该地址给出了一个利用后缀数的解答:http://blog.csdn.net/g9yuayon/archive/2008/06/21/2574781.aspx)
该作者的主要思路就是:分别把一个字符串建立一个后缀树,然后把字符串的对应的倒序字符串再建立一个后缀树,把两个后缀树合并为一个后缀树。判断对应的节点的公共最长前缀。(也就是是找两个结点的最近的祖先结点,祖先结点对应的字符串长度即是回文串的半径!)
建立后缀树的常用技巧是在后缀之后添加结束符。这样可以压缩存储后缀树。如果对两个不同的后缀树的结束符不一样,就可以合并两个不同的后缀树。
广义的后缀树是两个后缀树的直接合并(两个树采用相同的结束符)。这些知识在上面的链接里面都有。
后缀数组和后缀树有何联系?
后缀数和Trie树有何联系?
其实后缀数组本身用处不大,经过排序后用处就大了,比如可以解决上面提到的查找出现超多M次的最长字串问题。
把后缀数组代表的字符串按照压缩存储为trie数,就得到了后缀树!
如果将一个词典典存储为Trie树,那么可以快速的查找某个指定的单词是否在词典中。
如果将一系列单词(比如一篇文章中的所有单词)存储为Trie树,那么可以快速的查找某个指定的单词是否在文章中出现过!进一步可以查找出现过多少次!(通过统计其属于其得叶子节点数)
Trie树可用来查找任意字串是否在
三、生成随机文本
基于字母:下一个字母设置为前一个字母的随机函数,或者下一个字母设置为前k个字母的随机函数。
- /* Copyright (C) 2000 Lucent Technologies */
- /* Modified from markov.c in 'Programming Pearls' by Jon Bentley */
-
- /* markovlet.c -- generate letter-level random text from input text
- Alg: Store text in an array on input
- Scan complete text for each output character
- (Randomly select one matching k-gram)
- */
-
- #include <stdio.h>
- #include <stdlib.h>
-
- char x[5000000];
-
- int main()
- { int c, i, eqsofar, max, n = 0, k = 5;
- char *p, *nextp, *q;
- while ((c = getchar()) != EOF)
- {
- x[n++] = c;
- }
- x[n] = 0;
- p = x;
- srand(1);
- for (max = 2000; max > 0; max--)
- {
- eqsofar = 0;
- 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 = *(nextp+k);
- if (c == 0)
- {
- break;
- }
- putchar(c);
- p = nextp+1;
- }
- return 0;
- }
基于单词:
方法一:最笨的方法是 随机输出字典中的单词;
方法二:稍微好点的方法是读取一个文档,对每个单词进行计数,然后根据适当的概率选择下一个输出的单词;
方法三:如果使用在生成下一个单词时考虑前面几个单词的马尔科夫链,可以得到更加令人感兴趣的文本;
下面是香农的算法:以构建【字母级别的一阶文本 】为例,随机打开一本书并在该页随机选择一个字母记录下来。然后翻到另一页开始读,直到遇到该字母,此时记录喜爱其后面的那个字母。再翻到另外一页搜索上述第二个字母并记录其后面的那个字母。依次类推。对于【字母级别的1阶、2阶文本和单词级别的0阶、1阶文本 】,处理过程类似。
- /* 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("%sn", 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("%sn", skip(phrase, k-1));
- }
- return 0;
- }
课后习题
习题8
找出出现超过M次的最长的字符串。后缀数组a中a[i]~a[i+M]表示的就是M+1个字符串,找出其中这M+1个字符串共有的部分就是超过M次的字符串,同时还要找最长的。而且,这个超过M次的字符串一定出现在最后一个字符串中。否则就不满足条件了。因为快速排序后后缀数组是有序的,我们可以通调用在第一个和最后一个字符串上调用comlen函数来快速的确定这M+1个字符串中共有的字符数。
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
//只能比较字符串,两个字符串自左向右逐个字符相比(按ASCII值大小相比较),直到出现不同的字符或遇''为止。
int pstrcmp(const void *p, const void *q)
{
return strcmp(*(char **)p, *(char **)q);
}
int comlen(char *p, char *q)//返回两个参数共同部分的长度
{ int i = 0;
while (*p && (*p++ == *q++))
i++;
return i;
}
#define M 1
#define MAXN 5000000
char c[MAXN], *a[MAXN];
int main()
{
int i, ch, n = 0, maxi, maxlen = -1;
while ((ch = getchar()) != EOF) {
a[n] = &c[n];
c[n++] = ch;
}
c[n] = 0;
qsort(a, n, sizeof(char *), pstrcmp);//快速排序
for (i = 0; i <n-M; i++)
if (comlen(a[i], a[i+M]) > maxlen) //比较相邻字符串相同个数
{
maxlen = comlen(a[i], a[i+M]);
maxi = i;
}
printf("%.*s\n", maxlen, a[maxi]);
return 0;
}
习题 9
具体见http://blog.csdn.net/wordwarwordwar/article/details/41490013