目录
这道题,我比较感兴趣,是因为之前对网络爬虫十分感兴趣,因此利用python语言学习过相关网络爬虫的知识,也会用它爬取一些图片信息等,而且写了一个可以模拟登入post表单的爬虫,现在还在使用中,嘿嘿~而json文本的处理又是爬虫中很重要的一个知识,其实我也知识对这个略知一二,我没有系统学过JavaScript,不过还是比较了解的,所以选择它来做。并且通过它我也加深了STL容器和相关函数的使用。这道题来自于2017年CCF的9月考的第三题,奈何我菜狗一个,只能到第三题了,哈哈哈,就是如此也是改了好久才把bug统统找出来改正。嗐。讲解完这个题目,我会列出几点STL函数的坑,希望能帮助你下一次不要才进去,然后出不来了(T _ T)
问题描述
JSON (JavaScript Object Notation) 是一种轻量级的数据交换格式,可以用来描述半结构化的数据。JSON 格式中的基本单元是值 (value),出于简化的目的本题只涉及 2 种类型的值:
* 字符串 (string):字符串是由双引号 " 括起来的一组字符(可以为空)。如果字符串的内容中出现双引号 ",在双引号前面加反斜杠,也就是用 \" 表示;如果出现反斜杠 \,则用两个反斜杠 \\ 表示。反斜杠后面不能出现 " 和 \ 以外的字符。例如:""、"hello"、"\"\\"。
* 对象 (object):对象是一组键值对的无序集合(可以为空)。键值对表示对象的属性,键是属性名,值是属性的内容。对象以左花括号 { 开始,右花括号 } 结束,键值对之间以逗号 , 分隔。一个键值对的键和值之间以冒号 : 分隔。键必须是字符串,同一个对象所有键值对的键必须两两都不相同;值可以是字符串,也可以是另一个对象。例如:{}、{"foo": "bar"}、{"Mon": "weekday", "Tue": "weekday", "Sun": "weekend"}。
除了字符串内部的位置,其他位置都可以插入一个或多个空格使得 JSON 的呈现更加美观,也可以在一些地方换行,不会影响所表示的数据内容。例如,上面举例的最后一个 JSON 数据也可以写成如下形式。
{
"Mon": "weekday",
"Tue": "weekday",
"Sun": "weekend"
}
给出一个 JSON 格式描述的数据,以及若干查询,编程返回这些查询的结果。
输入格式
第一行是两个正整数 n 和 m,分别表示 JSON 数据的行数和查询的个数。
接下来 n 行,描述一个 JSON 数据,保证输入是一个合法的 JSON 对象。
接下来 m 行,每行描述一个查询。给出要查询的属性名,要求返回对应属性的内容。需要支持多层查询,各层的属性名之间用小数点 . 连接。保证查询的格式都是合法的。
输出格式
对于输入的每一个查询,按顺序输出查询结果,每个结果占一行。
如果查询结果是一个字符串,则输出 STRING <string>,其中 <string> 是字符串的值,中间用一个空格分隔。
如果查询结果是一个对象,则输出 OBJECT,不需要输出对象的内容。
如果查询结果不存在,则输出 NOTEXIST。
样例输入
10 5
{
"firstName": "John",
"lastName": "Smith",
"address": {
"streetAddress": "2ndStreet",
"city": "NewYork",
"state": "NY"
},
"esc\\aped": "\"hello\""
}
firstName
address
address.city
address.postal
esc\aped
样例输出
STRING John
OBJECT
STRING NewYork
NOTEXIST
STRING "hello"
评测用例规模与约定
n ≤ 100,每行不超过 80 个字符。
m ≤ 100,每个查询的长度不超过 80 个字符。
字符串中的字符均为 ASCII 码 33-126 的可打印字符,不会出现空格。所有字符串都不是空串。
所有作为键的字符串不会包含小数点 .。查询时键的大小写敏感。
50%的评测用例输入的对象只有 1 层结构,80%的评测用例输入的对象结构层数不超过 2 层。举例来说,{"a": "b"} 是一层结构的对象,{"a": {"b": "c"}} 是二层结构的对象,以此类推。
解题思路
借助于我对json的了解,他其实就是一个字典,pthon的字典有很强大功能,而c++没有这种,但是由于多重嵌套的原因,我还是选择用c++来实现,因为担心层数过深,导致效率太低。不过应该是没什么问题其实,如果你喜欢python可以实验一下。
首先就是定义一个object的结构题,我老结构体带师了,哈哈。这个结构体描述的具体是什么?他其实就是{花括号}里面的东西,就是一个object结构。不信你看看。我的object中含有两个map,分别是string映射到string的和string映射到object的。这就分别代表了,一个花括号中的“字符串键-字符串值”和“字符串键-另外一个花括号值”的两种对象。为什么用map来做,map在STL中底层是利用二叉树来存储的,它拥有强大查找功能,其查找的效率为对数级,那么我们的目的不正是查找某一个键对应的东西吗?
然后就是正儿八经的输入。但是难点还是处理字符串啊。按照题目的意思处理字符串。然后是查找算法。具体见代码。
满分代码
#include <iostream>
#include <string>
#include <map>
using namespace std;
struct object { //这就是我们的object类型,它表示一对花括号之间的内容。
bool flag=0;
//这里设计一个红旗,用于快速判断这个花括号有没有内容,只要有内容就为真,利用它,就不要去看 它的字符串键值对和对象键值对了
map<string, string> str_dict;
map<string, object> obj_dict;
};
object json; //初始化我们的json对象
//读n行,把它们组合成一个样本字符串
inline void Getline(string &s, int n) {
//读取n行的json字符串,把它组成一个整体字符串,后面分析
string t; s="";
while (n--) {
getline(cin, t); s+=t;
}
}
//修饰美化字符串,题目给的键和字符串值都是由双引号包围起来,这表示它是字符串,但是我们读取进来不需要双引号,所以把它去掉,然后是字符串中间的"\""和"\\",这只是它表示它是'\'和'"'的标志,但是我们不需要它,把它修正
void prettify(string &s) {
string::size_type l, r;
l=s.find('\"'); r=s.rfind('\"');
s=s.substr(l+1, r-l-1);
string::iterator it=s.begin();
while (it<s.end()-1) {
if ((*it=='\\'&&*(it+1)=='\\')||(*it=='\\'&&*(it+1)=='\"')) it=s.erase(it);
else ++it;
}
}
//根据给定的字符串,这个字符串再最外层包含花括号,即是花括号把内容包括起来,根据字符串对传进来的object引用修改,把它改造成合格的object
void BuildObject(string s, object &obj) {
obj.flag=1; //表示这个object不是空的,便于多层次查找
//进入{}处理对象,去掉最外层的花括号,并在最后加上逗号
//为什么加逗号:键字符串后面会接一个冒号,表示读到这之前的字符串是键;对于不是最后一堆键值对来说,值后面都有一个逗号,这表示前面读到的是值,所以把花括号去掉,然后再加上一个逗号,就可用循环来读取所有的键值对了。
s=s.substr(s.find('{')+1, s.rfind('}')-1)+',';
//计算键值对个数
int num=0;
int f=0;
//这个循环是用来计算在这个object下有多少对键值对,但是一个重要的点是,你必须要警惕值是object的那些键值对的字符串,所以每次碰到一个左花括号就让f加一,右花括号减一,只有f是0,才能计数,这类似于栈的思想
for (string::iterator t=s.begin(); t!=s.end(); ++t) {
if (*t==':'&&!f) ++num;
else if (*t=='{') ++f;
else if (*t=='}') --f;
}
//建立obj
string key_str, val_str;
string::size_type p;
while (num--) { //一次扫码num对键值对
//以冒号结尾是键,这里是认为键中不会有冒号的假设,事实上确实如此,属性一般为单词,合理推断
p=s.find(':');
key_str=s.substr(0, p); s.erase(0, p+1);
f=0; p=0;
//以逗号结尾是值,要小心值是object的嵌套结构,具体解决办法类似计算num的时候
while (1) {
if (s[p]==','&&!f) break;
if (s[p]=='{') ++f;
else if (s[p]=='}') --f;
++p;
}
val_str=s.substr(0, p); s.erase(0, p+1);
//对象里面只有字符串
prettify(key_str);
string::size_type l=val_str.find('{'), r=val_str.rfind('}');
if (l==val_str.npos&&r==val_str.npos) { //值不为对象,是字符串,因为找不到花括号了
prettify(val_str);
obj.str_dict[key_str]=val_str;
}
else { //值中含对象
BuildObject(val_str.substr(l, r-l+1), obj.obj_dict[key_str]);
//递归构造含花括号的值object
}
}
}
void BuildJson(int n) { //这是总的构造函数
string s; Getline(s, n);
BuildObject(s, json);
}
//查找函数,“.”结尾的字符串,以这个点为分割,把它查找的不同层次划分出来,依次进入查找
void Check(string &key, object json) {
while (key!="") {
string::size_type p=key.find('.');
string k=key.substr(0, p);
key.erase(0, p+1);
if (json.str_dict[k]!=""&&key=="") {
cout<<"STRING "<<json.str_dict[k]<<endl;
return;
} else if (json.obj_dict[k].flag) {
if (key=="") cout<<"OBJECT"<<endl;
else Check(key, json.obj_dict[k]);
}
else {cout<<"NOTEXIST"<<endl; return;}
}
}
int main() {
int n, m; cin>>n>>m; getchar();
BuildJson(n);
string query_key;
while (m--) {
cin>>query_key;
query_key+=".";
Check(query_key, json);
}
return 0;
}
一些STL使用的坑
这里提出一些STL使用的坑,太苦了我了。嗐。
substr
如果它传的是一个参数,他表示它从这个参数位置截取到最尾巴。这个参数可意识string::size_type类型的变量,也就是类似于整数,用于下标遍历的;也可以是字符串的迭代器。
如果它传的是两个参数,这是我要着重强调的,因为我就是一直卡在这个坑里,找不出bug。它如果传两个参数,有两种情况。第一种是它如果传的是两个整数,也就是string::size_type类型时候,第一个变量,表示从某一个位置开始截取,第二个参数表示截取几个字符!这两个参数不是代表了一个区间不是区间!如果你以为这是区间,碰巧你的第一个参数可能是0,那么刚好后面一个参数既可以是区间的右坐标,也可以是字符个数,因为它们大小相同,然而这只是巧合,你却很难发现,这意味有一些情况结果符合预期,你会蒙蔽。还有一种情况,即是传参数是两个迭代器,那这就是截取一个区间了。
map的迭代器的问题
注意map是已经排好序的容器(sorted),它自动排序,这源于它底层利用二叉平衡树的原因。这种已序容器,它的迭代器不是随即迭代器,是双向迭代器。同样情况还有set,multiset,map,multimap,list,随机迭代器可以进行迭代器运算,就是说迭代器iterator可以加上一个数,然后到对应位置,但是map不行,它的迭代器只能++,--,!=,==等运算,不能+2,-3,<,<=这样的运算。这是不合法的,原因是没有定义这样的运算,这也是STL迭代器类型不同造成的。
所以一个要注意的点是循环中,如下
for (string::iterator t=s.begin(); t!=s.end(); ++t)
这个代码中注意到是t!=s.end(),而不是小于,这个比较细节,不然你可能异常了,也不知道哪里报错。
当然也不需要我提醒,大概大部分人都知道,++t比t++效率要高,原因是后者需要创建一个临时变量保存原来的值,因为后者用来赋值做右值式子表示是加之前的值。--的运算类似。
差不多就这么多,感谢你的阅读,觉得写的不错可以点赞,点一波关注~我会把我的刷题过程中碰到觉得值得的题目updown的~