【C++】文本文件单词统计、定位

文本文件单词统计
1.1 【问题描述】
编写一个文本文件单词统计的程序,包括建立文件、单词统计、单词查询、单词定位的功能。
1.2 【基本要求】
程序应先询问用户的ID号(ID 号包括两个大写字母和4 位数字),例如:
请输入用户ID号:AB1234
程序应对输入的ID 号验证,符合ID 号要求的格式,然后程序提示四种选择:
(1) 建立文件
(2) 单词统计
(3) 单词查询及定位
(4) 退出
注意:
i) 文件至少包含50个英文单词(一定出现重复的单词,且一定包含数字)
ii) 文档不规范,单词之间可能不只有一个空格,并且有加引号的一些单词“jiaozi”
加引号的单词算单词,但数字不算单词
iii) 逐行扫描文本文件,计算文档中单词总数,及各个单词出现的频次,并且按照单词首字母abcd……
的顺序排列,显示并生成soft.txt文件
iv) 查询任意给出的单词是否存在,如果不存在,在屏幕上输出“查无此词! ”;如果存在,显示单词
出现的总次数,并且详细列出其出现的位置。
例如: 请您输入待查询的词: of
单词of 共出现了2次;
第1次出现在第1行,第5个位置;
第2次出现在第3行,第1个位置。
请您输入待查询的词:

读完题后,我们可以把这个题目分成五个小模块,每个模块都可以用一个函数来实现,题目要求的单词查询和单词定位在一个函数内,但是我们会分开讲解这两部分功能。

image-20210527205856180

接下来说说每个功能的实现:

ID格式判断

这个很简单,直接上代码

//ID格式判断函数
bool idcheck(string str)
{
    if(str.size() != 6) return false; //如果输入字符串长度不是6,直接返回false

    for (int i = 0; i < str.size(); i++) //若长度是6,逐个字符遍历
    {
        if(i < 2)
        {
            if(!isupper(str[i])) return false; //如果前两位不是大写字母,返回false
        }
        else if(i >=2 && i < 6)
        {
            if(!isdigit(str[i])) return false; //如果后四位不是数字,返回false
        }
    }
    return true; //如果上述条件都满足,返回true
}

因为题目要求ID长度为6,所以如果长度不是6直接返回,然后判断前两位是否是字母,后4位是否是数字。这个很简单,不再赘述。

建立文件

这里需要用到C++IO库中文件输入输出的知识,头文件fstream定义了三个类型来支持文件IO:ifstream从一个给定文件读取数据,ofstream向一个给定文件写入数据,以及fstream可以读写给定文件。本题中用到的是前两个类型ifstream和ofstream,顾名思义,i是input,代表输入,f是file,代表文件,用这个两个类型定义对象后,他们分别是默认从与之关联的文件中读和写的。ifstream是读,ofstream是写。

这里我们只需分别定义:

ifstream fin; //创建读文件的ifstream对象
ofstream fout; //创建写文件的ofstream对象

输出时的格式:

fstream的对象名 << "要输出的内容" << endl;

接下来我们要做的就是让ifstream对象和ofstream对象与相应的文件关联,关联的方式也很简单,直接用ifstream/ofstream对象的open()函数即可。函数参数为要关联的文件地址。我这里读入的文件名为Happiness.txt,路径在D盘编译环境的目录下。输出文件题目要求为soft.txt,我们直接用这个名字即可。open()函数要关联的对象如果不存在,会自动创建该文件。

还有一点要注意的是:访问文件一定要检查是否打开成功,如果没有打开成功就进行后续操作可能会产生意想不到的后果。然后就是对文件执行完相应的操作后,要用close函数关闭文件。

最后建立文件的功能集成到一个buildFile函数中,代码如下:

//建立文件
void bulidFile()
{
    fin.open("D:\\Happiness.txt"); //这里的路径可以根据具体文件的路径来修改
    if(!fin.is_open())
    {
        cout << "打开目标文件失败!" << endl;
        return;
    }

    //创建我们将要输出数据的文件
    //这里的路径可以根据具体文件的路径修改,如果目标文件不不存在将会创建该文件
    fout.open("D:\\soft.txt");
    if(!fout.is_open())
    {
        cout << "创建soft文件失败!" << endl;
        return;
    }

}

单词统计

单词统计的方法也不难想,要统计单词出现的频率,很直接的我们就想到了散列表(哈希表)因为一个string类型的数对应一个int类型的数用散列表可以很容易存储,C++中用unordered_map表示散列表。我们只需在读入单词的时候让键为该单词所对应的值+1即可。

我们定义一个散列表wordmap来存储单词出现的次数unordered_map<string, int> wordmap

unordered_map<string, int> wordmap; //记录单词出现次数的散列表
wordmap[word] ++;//这个单词的出现次数+1

wordmap[word] ++;的意思就是键为word的值自增,其中word是string类型的对象

一个重要的问题是:我们是按行读入的,那怎么样把单词拿出来单独判断呢?逐个字符判断的话非常不方便,而且还需要考虑空格问题。此时我们需要从string中读单词,我们可以使用C++IO库中的strIng流。

sstream头文件定义了三个类型文件来支持内存IO,这些类型可以向string写入数据,从string读取数据,就像string是一个IO流一样。

istringstream从string读取数据,ostringstream向string写入数据,而stringstream既可以从string读数据也可以向string写数据。

本题中只用istringstream对象便足以,我们需要创建一个istringstream对象并将其绑定到我们保存行的字符串,然后便可以从行中读取内容了。代码如下:

string line, word; //定义行和读入的单词
while(getline(fin, line)) //fin是我们之前创建的ifstream对象,用来从文件中读数据
{
	istringstream oneline(line); //将istringstream对象online绑定到读入的行line
    while(online >> word) //借助istringstream对象online从string对象line中读取单词
    {
        进行相应的单词处理、统计操作
    }
}

解决了单词出现次数以及读入的问题,接下来要解决的是输出每个单词出现的次数。

因为单词是以key的形式存在散列表中的,输出不是很方便,这里我们用一个动态数组vector来输出。我们可以在读入单词的时候先判断一下该单词是否在散列表wordmap中出现过,如果没有出现过,就将其加入数组中(为什么要判断该单词是否出现过呢?因为我们输出单词清单时要求不能有重复,每个单词只能出现一次)。

动态数组的定义:vector<string> wordlist

wordlist.push_back(word)就是向动态数组中加入一个单词word。

当读完所有的文本之后我们遍历输出vector即可。

//遍历输出wordlist中的内容
    for(auto c : wordlist)
    {
        fout << c << " 出现了 "<< wordmap[c] << "次" << endl;
    }
/*auto是C++11标准中的编译器自动推断类型,意思就是编译器会自动把auto换成wordlist中元素的类型,当要遍历的对象类型名很长时,写auto就很方便,当然,这里的auto也可以换成string,因为wordlist中存的就是string类型的数,同理,如果vector存的是int,auto也可以换成int*/

//写成这样也可以
for(string c : wordlist)
    {
        fout << c << " 出现了 "<< wordmap[c] << "次" << endl;
    }

单词的出现次数我们搞定了,接下来我们应该考虑一下读入单词时的处理,因为题目对单词的格式进行了一些限制:单词之间不止一个空格,且带引号的单词算单词,但数字不算单词,单词之间不止一个空格好说,按行读入的函数getline直接将一行读入,这个问题自动解决,我们要处理的就剩下带引号的单词这个问题。老规矩,也是把这个处理函数写成一个单词检查函数wordcheck()。先看下代码,结合代码理解。

单词格式判断
//判断一个字符串是否是单词函数
bool wordCheck(string str)
{
    for(int i = 0; i < str.size(); i ++ )
    {
        //第一种情况,带引号的单词
        if(str[0] == '\"') //对带引号的单词进行特判
        {
            for(int j = i + 1; j < str.size() - 1; j ++ ) //引号是单词开头的引号
            {
                if(!isalpha(str[j])) return false; //从引号下一个字符开始遍历
            }
        }
        else //第二种情况,不带引号的单词
        {
            if(!isalpha(str[i])) return false;
        }
    }
    return true;
}

wordcheck函数的返回值是布尔类型,如果传入的字符串符合单词的格式,返回true,否则返回false。

我们只需要判断传入字符串(单词)的第一位是否是引号,如果是引号,就从引号的下一位开始扫描,扫到最后一个引号的前一个位置为止,判断这些位置是否存在不是字母的情况。这里会用到字符函数,如下表。

image-20210527220632420

到此为止,我们解决了单词的格式判断问题,还有一个要处理的地方,我们在输出单词出现次数的时候,因为要按小写abcd的形式排列,所以就不能出现大写字母,我们需要一个能把大写字母转换成小写字母的函数lowerword()。

大写转小写函数
//大小写转换函数
string lowerword(string str)
{
    string res;
    for(int i = 0; i < str.size(); i ++)
    {
        str[i] = tolower(str[i]);
        res += str[i];
    }
    return res;
}

这个没什么好说的,逐个字符扫描,用库函数tolower()即可,最后返回处理后的字符串。

然后,还有一个点,输出单词时要按abcd顺序输出,所以我们需要对vector数组中的元素按字典序进行排序,这里我们可以利用头文件<algorithm>中的sort()函数。

//将wordlist中的单词按字典序排序
    sort(wordlist.begin(), wordlist.end());

呼~,到这里,单词统计功能就说完了。

单词查询

在我们做好单词统计的前期工作后,单词查询就变得非常简单了,直接从之前建立的散列表wordmap中查找输入的单词即可。

	cout << "请您输入待查询的单词:" << endl;
    string word;
    cin >> word;
    if(wordmap.count(word)) //如果该单词在散列表中,进行相关操作
    {
        cout << "单词" << word << "共出现了" << wordmap[word] << "次;" << endl;
    }

count()函数是unordered_map自带的函数,功能如下:

image-20210527222438632

说白了就是检查给定键是否在散列表中,如果在表中就返回1,否则返回0。

单词定位

题目要求输出查询单词的行号和具体位置,行号的话我们类比前面的单词出现次数,再定义一个散列表就行了,分别存单词和行号,然后再定义一个散列表来存行号和每行的内容。

但是,存单词和对应的行号用散列表真的合适吗?嘿嘿,既然我都这么问了,显然不合适,因为同一个单词可能在多行出现,此时,一个单词对应多个行号,即一个键对应多个值,此时散列表就不再适合存了,因为unordered_map要求一键对应一值,且键不允许重复。此时,我们应该用头文件<map>中的multimap来存单词和所在行号。multimap是允许一键对应多个值的,且multimap内部是有序的(其底层是红黑树,红黑树是一种弱化版的平衡二叉树)。

multimap<string, int> linemap; //记录单词对应的行号的散列表,用multimap是因为一个单词可能在多行出现
unordered_map<int, string> sentencemap; //记录每行的编号和内容
sentencemap.insert(make_pair(numline, line));//把每行编号和内容存到sentence中
linemap.insert(make_pair(res, numline)); //记录单词出现的行号,有可能出现一键多值的情况

单词存入上述两个容器中的过程在单词统计过程中实现,这里不再详细展开。

解决了存储同一单词对应多个行的问题,接下来的问题是我们如何访问一个键对应的多个值呢?也就是说我们如何从multimap中找到同一个单词对应的多个行呢?

解决这个问题需要用到迭代器的知识,大家只需要知道迭代器就是用来访问容器的一种工具,知道它的功能是什么就OK了,不要被它长长的定义格式吓到。

定义迭代器并赋值:

multimap<string, int>::iterator firstbucket; //这里用multimap是因为其有自动按键排序的功能
firstbucket = linemap.find(word); //find函数返回一个指向第一个具有键word元素的迭代器

firstbucket可以理解为一个链表(但它不是链表,指的是这种思想),它代表的是某个单词第一次出现的行号,继续向后遍历它,可以得到某个单词第二次、第三次···第N次出现的行号。

输出multimap中一键对应多值有两种方法:

//输出multimap中一键对应多值的方法一
		multimap<string, int>::iterator firstbucket; //这里用multimap是因为其有自动按键排序的功能
		firstbucket = linemap.find(word); //find函数返回一个指向第一个具有键word元素的迭代器
        for(int k = 0; k != linemap.count(word); k ++, firstbucket ++)
        {   //count函数求出某个键出现的次数
            //firstbucket->second为单词出现的行数序号
			cout << firstbucket->first << "——" << firstbucket->second << endl;
		}


//输出multimap中一键对应多值的方法二
        multimap<string, int>::iterator beg,end;
        //equal_range函数返回一个迭代器的pair对象,first成员等价于lower_bound(key),second成员等价于upper_bound(key)
        /*  lower_bound(key)返回一个迭代器,指向键不小于k的第一个元素
      upper_bound(key)返回一个迭代器,指向键不大于k的第一个元素*/
        beg = linemap.equal_range(word).first;
        end = linemap.equal_range(word).second;

        for(firstbucket = beg; firstbucket != end; firstbucket ++)
        {
            cout << firstbucket->first << "——" << firstbucket->second << endl;
        }

具体可以参考:C++multimap查找一个key对应的多个value

解决了找到同一单词对应的多个行的问题,接下来的问题就是如何定位单词在某一行的具体位置。

很直接的一个想法就是根据单词找到对应的行,然后扫描该行,如果该行存在一个词与输入的单词相同,记录下标,然后输出即可。但是,如果该行存在多个词与输入的单词相同会发生什么呢?

int index = 0,location = 0;
while(读入单词)
{
	index ++;
	if(word == 行中的某个单词)
	{
		location = index;
	}
}
cout << "单词在该行的第" << location << "个位置" << endl;

按照上面的代码,index会被更新多次(这个次数为该单词在同一行出现的次数),前面index的值会被后面index的值覆盖掉,所以最后输出的index是该单词在该行最后一次出现的位置。

举个例子:

测试文本的第27行中单词“and”出现了2次,第一次在第2个位置,第二次在第7个位置

li

  • 22
    点赞
  • 109
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值