一、问题重述:
- 要求编程建立一个文本文件,每个单词不包含空格且不跨行,单词由字符序列构成且区分大小写,统计给定单词在文本文件中出现的总次数,检索输出的某个单词出现在文本中的行号、在该行中出现的次数以及位置。
- 该设计要求可分三个部分实现:其一,建立文本文件,文件名由用户键盘输入;
其二,给定单词的计数,输入一个不含空格的单词,统计输出该单词在文本中的出现次数;其三,检索给定单词,输入一个单词,检索并输出该单词所在的行号,该行中出现的次数以及在该行中的相应位 置。
3.设计数据量大的文本,进行子串的查询处理,分析不同算法运行的时间效率,对所有输出的匹配位置结果进行相互对比验证,以证明算法设计和实现的正确性。分析不同数据规模对不同算法的影响程度。
二、问题分析:
1.本题主要是针对字符串检索,常见的字符串检索算法有:朴素模式匹配(暴力匹配),KMP模式匹配。
2.字符串匹配模式:
(1).朴素模式匹配(算法在下面)
原理:从主串s 和子串t 的第一个字符开始,将两字符串的字符一一比对,如果出现某个字符不匹配,主串回溯到第二个字符,子串回溯到第一个字符再进行一一比对。如果出现某个字符不匹配,
主串回溯到第三个字符,子串回溯到第一个字符再进行一一比对…一直到子串字符全部匹配成功。
(2). KMP(算法在下面)
我们串中的位置指针i,j来说明,第一个位置下标以0开始,如果是人为来寻找的话,肯定不会再把i移动回第1位,因为主串匹配失败的位置(i=3)前面除了第一个A之外再也没有A了,因为我们已经知道前面三个字符都是匹配的!移动过去肯定也是不匹配的!有一个想法,i可以不动,我们只需要移动j即可,如下图:
如果字符匹配,则主串和子串字符同时右移。至于子串回溯到哪个字符,这个问题我们先放一放。
现在发现了不匹配的地方,根据KMP的思想我们要将子串向后移动,现在解决要移动多少的问题。
a.说明一下:一个字符串最长相等前缀和后缀。
在“aba”中,前缀集就是除掉最后一个字符'a'后的子串集合{a,ab},同理后缀集为除掉最前一个字符a后的子串集合{a,ba},那么两者最长的重复子串就是a,k=1;
在“ababa”中,前缀集是{a,ab,aba,abab},后缀集是{a,ba,aba,baba},二者最长重复子串是aba,k=3;
事实上,每一个字符前的字符串都有最长相等前后缀,而且最长相等前后缀的长度是我们移位的关键,因为在P的每一个位置都可能发生不匹配,也就是说我们要计算每一个位置j对应的k,所以用一个数组next来保存,next[j] = k,表示当T[i] != P[j]时,j指针的下一个位置。另一个非常有用且恒等的定义,因为下标从0开始的,k值实际是j位前的子串的最大重复子串的长度。所以next[i]=j,含义是:下标为i 的字符前的字符串最长相等前后缀的长度为j。
b.例如:
子串t= "abcabcmn"的next数组为next[0]=-1(前面没有字符串单独处理)
next[1]=0;next[2]=0;next[3]=0;next[4]=1;next[5]=2;next[6]=3;next[7]=0;
因此,我们子串移动的结果就是让子串的最长相等前缀和主串部分最长相等后缀对齐。
三、代码:
#include<iostream>
#include<string.h>
#include<fstream>
#pragma warning(disable:4996)
#include<conio.h>//catch() 任意键继续
#include<iomanip>
#include<string>
#include<algorithm>
#include<sstream>
using namespace std;
#define MAXSIZE 1000
struct str {
char data[MAXSIZE];
int length;
};
/*
* 朴素模式匹配(暴力匹配)
*/
int simple(str s, str goal, int begin)
{
int i = begin; //指定位置开始查找
int j, k, m, n;
m = goal.length; //模式串长度赋m
n = s.length; //目标串长度赋n
for (;i < n;i++)
{
j = 0;
k = i; //目标串起始位置i送入k
while (j <= m && s.data[k] == goal.data[j])
{
k++;
j++; //继续下一个子符的比较
}
if (j == m) //若相等,则说明找到匹配的子串,返回匹配位置i
return i + 1; //否则从下一个位置重新开始比较
}
return -1;
}
/*
* 查找的单词的最长相等前后缀
* next[i]=j,含义是:下标为i 的字符前的字符串最长相等前后缀的长度为j。
*/
int* GetNext(str goal_s)
{
//由模式串t求出next值
int gls = goal_s.length;//单词长度
int next[MAXSIZE];//目标单词的next数组
int j = 0, k = -1;
next[0] = -1;//第一个字符前无字符串,给值-1
while (j < gls)
{
if (k == -1 || goal_s.data[j] == goal_s.data[k])
{
j++;k++;
if (goal_s.data[j] != goal_s.data[k])
//这里的t.data[k]是t.data[j]处字符不匹配而会回溯到的字符
//为什么?因为没有这处if判断的话,此处代码是next[j]=k;
//next[j]不就是t.data[j]不匹配时应该回溯到的字符位置嘛
next[j] = k;
else
next[j] = next[k];
//
//此时nextval[j]的值就是就是t.data[j]不匹配时应该回溯到的字符的nextval值
//即字符不匹配时回溯两层后对应的字符下标
}
else k = next[k];
//next[k]的值代表的是下标为k的字符前面的字符串最长相等前后缀的长度
//也表示该处字符不匹配时应该回溯到的字符的下标
//这个值给k后又进行while循环判断,此时t.data[k]即指最长相等前缀后一个字符
}
return next;
}
/*KMP算法
*begin为开始搜索的位置
*/
int KMP(str s, str goal_s, int* next, int begin)
{
int sl = s.length;
int gsl = goal_s.length;//要查找的字符长度
int i = begin;
int j = 0;
while (i < sl && j < gsl)
{
if (s.data[i] == goal_s.data[j] || j == -1)
{
i++;j++;
}
else j = next[j];
}
if (j >= gsl)
return(i - gsl + 1);
else
return(-1);//未找到
}
/*
* 对单行内容计数
* choose 选择匹配模式(1-朴素模式匹配 2-KMP)
* 返回次数
*/
int getIndexOfLine(str s, str goal_s, int line, int show = 0, int choose = 2) {
int gls = goal_s.length;//目标单词长度
int* next;
int index[20];
switch (choose)
{
case 1:
{
int result0 = simple(s, goal_s, 0);
if (result0 == -1) {
return -1;
}
int i = 0;
while (result0 != -1) {//搜索到一个后继续往后搜索,直到行末
index[i] = result0;
result0 = simple(s, goal_s, result0 + gls - 1);
i++;
}
break;
}
case 2:
{
next = GetNext(goal_s);//目标的next数组
int result = KMP(s, goal_s, next, 0);
if (result == -1) {
return -1;
}
int i = 0;
while (result != -1) {//搜索到一个后继续往后搜索,直到行末
index[i] = result;
result = KMP(s, goal_s, next, result + gls - 1);
i++;
}
}
break;
}
int k = 0;
if (show == 1)
{
// k = 0;//记录个数
cout << "第" << line << "行:" << ' ';
while (index[k] > 0)
{
cout << index[k] << "\t";
k++;
}
cout << endl;
}
else {
while (index[k] > 0) {
k++;
}
}
return k;
}
/*
* 文本内容查找某单词计数
* 返回总次数
*/
int readFileCount(str goal, int show, int choose)
{
int sum = 0;//记录总次数
int lsum = 0;//记录单行出现的次数
cout << "请输入文件的完整地址路径:" << endl;//(格式:E:\c\count.txt)
string name;
cin >> name;
string a;//保存一行内容
ifstream in;
in.open(name);
int m = 0;//记录第几行
while (getline(in, a))
{
m++;
str tt;//转换成str
tt.length = a.length();
for (int i = 0;i < tt.length;i++) {
tt.data[i] = a[i];
}
lsum = getIndexOfLine(tt, goal, m, show, choose);
if (lsum == -1) {//该行未出现
continue;
}
else {
sum = sum + lsum;
continue;
}
}
if (sum > 0) {
return sum;
}
else {
return 0;
}
}
/*
* 写文件
*/
void writeFile() {
system("cls");
int n;
string file_name;
cout << "请输入文本名称: ";
cin >> file_name;
cout << "请输入文本行数:";
cin >> n;
ofstream outfile;
outfile.open(("E://" + file_name + ".txt").c_str());
cout << "文件创建成功!" << endl;
cout << "请输入" << n << "行单词:" << endl;
string a;
for (int i = 0; i <= n; i++) {
getline(cin, a);
outfile << a << endl;
}
outfile.close();
cout << "单词本已生成!按任意键返回" << endl;
getch();
}
int main() {
cout << "********************欢迎来到文本文件的检索********************" << endl;
cout << "--------------------------------------------------------------" << endl;
while (true)
{
cout << "--------------------------------------------------------------" << endl;
cout << "********1-创建文件********" << endl;
cout << "********2-单词检索********" << endl;
cout << "********3-单词计数********" << endl;
cout << "********4-退出 ********" << endl;
cout << "--------------------------------------------------------------" << endl;
cout << "请选择:" << endl;
int choose;
cin >> choose;
switch (choose)
{
case 1:
writeFile();
break;
case 2:
{
cout << "请输入要查找的内容:" << endl;
string t;
cin >> t;
str tt;
tt.length = t.length();
for (int i = 0;i < tt.length;i++) {
tt.data[i] = t[i];
}
int re = 2;
cout << "--------------------------------------------------------------" << endl;
cout << "请选择检索的算法:(1-朴素模式匹配 2-KMP)" << endl;
cout << "--------------------------------------------------------------" << endl;
int chioce;
cin >> chioce;
switch (chioce)
{
case 1:
re = 1;
break;
case 2:
re = 2;
break;
default:
cout << "无效字符!" << endl;
break;
}
int sum = readFileCount(tt, 1, re);
if (sum == 0) {
cout << "未找到!" << endl;
}
else {
cout << "一共出现了:" << sum << "次" << endl;
}
}
break;
case 3:
{
cout << "请输入要查找的内容:" << endl;
string t;
cin >> t;
str tt;
tt.length = t.length();
for (int i = 0;i < tt.length;i++) {
tt.data[i] = t[i];
}
int sum = readFileCount(tt, 0, 2);
if (sum == 0) {
cout << "未找到!" << endl;
}
else {
cout << "一共出现了:" << sum << "次" << endl;
}
}
break;
case 4:
exit(0);
break;
default:
cout << "输入无效!" << endl;
break;
}
}
}
四、结果: