题意
在操作系统中,数据通常以文件的形式存储在文件系统中。文件系统一般采用层次化的组织形式,由目录(或者文件夹)和文件构成,形成一棵树的形状。文件有内容,用于存储数据。目录是容器,可包含文件或其他目录。同一个目录下的所有文件和目录的名字各不相同,不同目录下可以有名字相同的文件或目录。
为了指定文件系统中的某个文件,需要用路径来定位。在类 Unix 系统(Linux、Max OS X、FreeBSD等)中,路径由若干部分构成,每个部分是一个目录或者文件的名字,相邻两个部分之间用 / 符号分隔。
有一个特殊的目录被称为根目录,是整个文件系统形成的这棵树的根节点,用一个单独的 / 符号表示。在操作系统中,有当前目录的概念,表示用户目前正在工作的目录。根据出发点可以把路径分为两类:
Ÿ 绝对路径:以 / 符号开头,表示从根目录开始构建的路径。
Ÿ 相对路径:不以 / 符号开头,表示从当前目录开始构建的路径。
例如,有一个文件系统的结构如下图所示。在这个文件系统中,有根目录 / 和其他普通目录 d1、d2、d3、d4,以及文件 f1、f2、f3、f1、f4。其中,两个 f1 是同名文件,但在不同的目录下。
对于 d4 目录下的 f1 文件,可以用绝对路径 /d2/d4/f1 来指定。如果当前目录是 /d2/d3,这个文件也可以用相对路径 …/d4/f1 来指定,这里 … 表示上一级目录(注意,根目录的上一级目录是它本身)。还有 . 表示本目录,例如 /d1/./f1 指定的就是 /d1/f1。注意,如果有多个连续的 / 出现,其效果等同于一个 /,例如 /d1///f1 指定的也是 /d1/f1。
本题会给出一些路径,要求对于每个路径,给出正规化以后的形式。一个路径经过正规化操作后,其指定的文件不变,但是会变成一个不包含 . 和 … 的绝对路径,且不包含连续多个 / 符号。如果一个路径以 / 结尾,那么它代表的一定是一个目录,正规化操作要去掉结尾的 /。若这个路径代表根目录,则正规化操作的结果是 /。若路径为空字符串,则正规化操作的结果是当前目录。
Input
第一行包含一个整数 P,表示需要进行正规化操作的路径个数。
第二行包含一个字符串,表示当前目录。
以下 P 行,每行包含一个字符串,表示需要进行正规化操作的路径。
Output
共 P 行,每行一个字符串,表示经过正规化操作后的路径,顺序与输入对应。
输入样例
7
/d2/d3
/d2/d4/f1
…/d4/f1
/d1/./f1
/d1///f1
/d1/
///
/d1/…/…/d2
输出样例
/d2/d4/f1
/d2/d4/f1
/d1/f1
/d1/f1
/d1
/
/d2
提示
1 ≤ P ≤ 10。
文件和目录的名字只包含大小写字母、数字和小数点 .、减号 - 以及下划线 _。
不会有文件或目录的名字是 . 或 … ,它们具有题目描述中给出的特殊含义。
输入的所有路径每个长度不超过 1000 个字符。
输入的当前目录保证是一个经过正规化操作后的路径。
对于前 30% 的测试用例,需要正规化的路径的组成部分不包含 . 和 … 。
对于前 60% 的测试用例,需要正规化的路径都是绝对路径。
分析
T3总是这么多字让人看到崩溃🤯
- 题目分析&代码实现
1. 题目分析
我大概把题目读了两三遍之后才弄清楚了题目的要求。弄清楚之后发现题目并不复杂【T3总是如此】。
我们要解决的就是对一个字符串按要求进行转换,输出住转换结果就可以。
其中的要求可以整理为:
- 转换结果为绝对路径:只包含“/ ”和名字,且第一个一定为“/ ”,最后个一定为名字。
- 若字符串中包含“ / . / ”,则此处本应出现的名字和前一个名字相同,表明无变化。
- 若字符串中包含“ / … / ”,则此处应该出现的名字和上上的名字相同。
- 若字符串中包含“ // ",则只用保留一个“ / ”。
- 若字符串末尾出现“ / ”,则直接去掉。
- 若字符串首个不为“ / ”,则代表之后的目录都是在给出的当前目录之后,代表字符串应该直接接在题目给出的绝对路径之后。
- 若字符串为空,则代表直接输出题目给的当前目录。
整理好以上信息之后,解决问题就很容易了,根据这个信息设计代码就可以。
2. 代码实现
实现过程可以简括为:
- 字符串是否为空,空则直接输出当前目录
- 若不为空,判断字符串首字符是否为“ / ”。若是,则不用改变头部,否则将当前目录加在头部。
- 处理字符串中所有的“ // ”,利用string的find函数,每次定位第一个遇到的“ // ”中第一个字符的下标,删除前面一个“ / ”。直到不再存在“ // ”。
- 处理字符串中所有“ /./ ”,利用string的find函数,每次定位第一个遇到的“ / . / ”中第一个字符的下标,删除前面的“ / . ”。直到不再存在“ / . / ”。
- 处理字符串中所有“ / … / ”,利用string的find函数,每次定位第一个遇到的“ / … / ”中第一个字符的下标。
- 若是在最顶部,直接删除“/ … ”即可。
- 否则找到在它之前的第一个名字,将其和“/ … ”全部删除,整段只保留最后一个“ / ”
- 直到不再存在“ /…/ ”。
- 若字符串末尾存在“ / ”,直接删除
- 输出处理后字符串。
-
需要注意的问题
-
如何获取空字符
这是个棘手的问题,也是这个题目的一个考点。
普通的cin并不会输入任何空格、提行等非字符符号。那么对于string,就只有通过getline这样的输入流操作来实现。
但是有个问题!如果对于普通的数据类型,如int使用cin,而紧接着对string使用getline,你会发现结果和你想的不一样。
原因是,cin从输入流中读取<<之后数据类型的数据,遇到非字符符号后停止,但是输入流中的提行、空格都并没有消失。getline会获取一整行数据,但前提是没有提行。
但是显然,这在这道题中出现了冲突。解决的办法是:利用cin.ignore()将其输入数据之后的提行给忽略,使得接下来的getline可以成功读入数据。
解决办法不止一种,百度即可👐🏼
- 最开始的思路(80分)
这道题最后的ac的代码,是我在调试了很久最初的代码仍然无法拿到100分之后,上网观摩了各路大佬的代码,幡然醒悟之后重新写的。简洁明了,轻松满分【菜鸡跪地】
我最开始的思路就比起来很复杂了。大概是受到目录管理器那道题的影响【在这里就不指路了害怕同样被洗脑233】。
我最开始的想法是:
既然要输出绝对路径,那么关键其实只在于,最后他停在了哪个文件上,因此只需要找到最后的终点。
但很快我发现,这个天真的想法被题目所说的“不同目录存在相同文件名”这个约束给打灭了。因此就顺着这个思路,想到了记录路径。但是这一切都要建立在保存树结构的基础上。因此我就是在处理每个字符串的过程中逐渐完善和回溯目录树,并且忽略到特殊情况。最后得到了到达终点的唯一标识。
实现过程和目录管理器中类链式前向星的做法几乎相同。其实是很简单的,但是代码写起来就很长,和这个做法就比起来看着很蠢【233】。显然是想得太复杂了。
至于为什么是80分,我仍然是不明白,或许是遗漏了什么奇怪的判断条件,但是我实在想不出是什么条件。
附上辛辛苦苦写的80分代码吧⚠️:
//
// main.cpp
// lab1
//
//
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
#include <map>
#include <sstream>
using namespace std;
struct menu //目录节点
{
string name;
map<string,int> child;
int father = 0;
void init(string n,int fa)
{
name = n;
father = fa;
}
};
char mark[2] = {'.','/'};
menu tree[1000];
vector<int> path;
int num = 0,cur = 0,tail = 0; //标记目录数组下标,标记初始化的当前目录
int finding(char c) //在符号数组中寻找
{
for( int i = 0 ; i < 2 ; i++ )
{
if( c == mark[i] )
return i;
}
return -1;
}
void change(string s)
{
int before = cur; //上一个到达的目录,记录该路径最终到达的目录
string now_name;
for( int i = 0 ; i < s.size() ; i++ ) //遍历整个字符串
{
// cout<<i<<" -------- i "<<endl;
switch (finding(s[i])) //确定当前字符类型
{
case -1: //属于文件名字符
{
now_name.clear();
int j = i;
while( finding(s[j]) == -1 && j < s.size()) //记录所有非符号
{
now_name.push_back(s[j]);
j++;
}
//如果当前目录在上一个目录的孩子中不存在,则新建目录
if( tree[before].child.find(now_name) == tree[before].child.end() )
{
tree[++num].init(now_name,before); //新建目录
tree[before].child[tree[num].name] = num; //将该文件名放在父亲目录下
before = num; //当前文件成为新的父目录
}
else //否则直接更新当前目录
before = tree[before].child[now_name];
// cout<<now_name<<" *** "<<i<<" -i- "<<j<<" -j- "<<endl;
//如果是最后一个字符且是非符号,则最后更新的这个文件即为最终到达的文件
if( j == s.size() )
{
tail = before;
i = j;
}
else //否则继续更新遍历索引
i = j - 1;
// cout<<before<<" before "<<tree[before].father<<" father ===="<<endl;
// cout<<endl;
break;
}
case 0: //"."
{
if( i + 1 < s.size() )
{
if( finding(s[i + 1]) == 0 ) //如果下一个也是".",则当前到达目录为上一个目录的父目录
{
if( before > 0 ) //如果上一个到达的目录不是根目录,则上移
before = tree[before].father;
//否则仍然是根目录
// cout<<" ..... "<<before<<" before "<<endl;
}
}
else //如果当前已经是最后一个字符,则上一个到达的文件即为最终到达的文件
tail = before;
break;
}
case 1: //"/"
{
if( i == 0 ) //如果是第一个遇到的/,说明当前目录从root开始
before = 0;
//否则遇到"/"就直接跳过,直到不是"/"
if( i == s.size() - 1 )
tail = before;
// cout<<tail<<" ------ tail "<<endl;
}
default:
break;
}
}
}
void handle(string s) //确认当前目录
{
string now_name;
for( int i = 0 ; i < s.size() ; i++ )
{
if( finding(s[i]) == 1 )
{
if( i == 0 ) //第一个"/",将cur初始化为0,代表根目录
cur = 0;
else
continue;
}
else
{
now_name.clear();
int j = i;
while( finding(s[j]) == -1 && j < s.size()) //记录所有非符号
{
now_name.push_back(s[j]);
j++;
}
//如果上一个目录已经存在这个孩子目录,则直接更新当前目录,否则创建新目录
if( tree[cur].child.empty() )
{
tree[++num].init(now_name,cur); //新建目录
tree[cur].child[tree[num].name] = num; //将该文件名放在父亲目录下
}
cur = tree[cur].child[now_name]; //更新当前目录
if( j == s.size() )
i = j;
else
i = j - 1;
}
}
}
void output() //输出正规路径
{
// cout<<tail<<" ============= tail !! "<<endl;
if( tail == 0 ) //如果最终到达根目录
{
cout<<"/"<<endl; //直接输出“/”
}
else
{
for( int i = tail ; i != -1 ; i = tree[i].father ) //将整个路径反向记录
path.push_back(i);
for( int i = path.size() - 2 ; i >= 0 ; i-- ) //再正向输出(不用输出root)
cout<<"/"<<tree[path[i]].name;
cout<<endl;
}
}
void initialize() //初始化
{
path.clear();
tail = 0;
tree[0].init("root", -1);
}
int main()
{
int p = 0;
cin>>p;
cin.ignore();
string now_menu,route;
getline(cin,now_menu);
handle(now_menu); //确认当前目录
for( int i = 0 ; i < p ; i++ )
{
initialize();
getline(cin,route);
if( !route.empty() ) //路径不空进行转换
change(route); //确认该路径最终到达的文件
else //否则即为当前目录
tail = cur;
output();
}
return 0;
}
总结
- 还是老是把问题复杂化,最后写出来的代码调试起来就很困难,因为复杂化后就很难找到问题。
- string虽然很方便,但是真的有很多幺蛾子😑不过还是学习到了rfind这样的神奇操作
- 小细节往往是一道大题满分的关键!例如这个输入流的考点
代码
//
//
// main.cpp
// lab1
//
//
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
#include <map>
#include <sstream>
using namespace std;
int main()
{
int p = 0,site = 0,site2 = 0;
cin>>p;
string now_menu,route,ans;
cin>>now_menu;
cin.ignore();
for( int i = 0 ; i < p ; i++ )
{
getline(cin,route);
ans.clear();
if( !route.empty() )
{
if( route[0] != '/' ) //如果第一个不是/,说明从当前目录起始,将当前目录路径添加在前
ans = now_menu + '/' + route;
else //否则不变,从根目录起始
ans = route;
//删除“//”
while( (site = ans.find("//")) != -1 ) //出现两个“/”就删除前一个
ans.erase(site, 1);
//删除“/./”
while( (site = ans.find("/./")) != -1 ) //出现“/./”就只留最后一个“/”
ans.erase(site, 2);
//删除“/../"
//出现“/../”就删除它前面的一个目录,因为返回到了该目录的上层
while( (site = ans.find("/../")) != -1 )
{
if( site == 0 ) //说明在最前面,即依然留在根目录,保留一个“/”
ans.erase(site,3);
else //否则找他前面的第一个目录
{
//从“/../”向前反向找第一个“/”,这一定是它前面的第一个目录
site2 = ans.rfind("/",site - 1);
ans.erase(site2, site - site2 + 3 ); //删除直到只剩最后一个“/“
}
}
//删除末尾的“/”
if( ans.size() > 1 && ans[ans.size() - 1] == '/' )
ans.erase(ans.size() - 1,1);
cout<<ans<<endl;
}
else //空路径直接输出当前目录
cout<<now_menu<<endl;
}
return 0;
}