PTA习题解析——目录树
目录树
看到这个问题,我们是一头雾水啊,这讲了个啥?别急,让我们用测试样例模拟一遍。
样例模拟
首先我们先考虑存储数据的方式,根据观察和我们对文件夹的理解,对于一个文件夹而言,与其他文件或文件夹只会有 2 种关系——和我在同一目录、在我的目录,也就是只有同级和下级两种关系。因此我们就很自然地想到孩子兄弟表示法,因为在孩子兄弟对于一个结点也只会有 2 种关系——孩子和兄弟,那我们就用孩子表示在下级目录,兄弟表示在同级目录。
首先我们拥有一个根目录 root,读取第一行数据,表示根目录有个名为 b 的文件,是 root 的孩子,因此根据孩子兄弟表示法,b 应该是 root 的左分支结点。
读取第二行数据,在根目录中有个 c 目录。因此 c 目录是 root 的孩子,并且与 b 文件同级,也就是互为兄弟关系,进行结点的加入。
读取第三行数据,在根目录中有个 ab 目录,并在这个目录下有个 cd 文件。因此 ab 目录是 root 的孩子,并且与 c 目录同级,也就是互为兄弟关系,同时 ab 目录有个左分支 cd,进行结点的加入。
读取第四行数据,在根目录中有个 a 目录,并在这个目录下有个 bc 文件。因此 a 目录是 root 的孩子,并且与 ab 目录同级,也就是互为兄弟关系,同时 a 目录有个左分支 bc,进行结点的加入。
读取第五行数据,在 ab 目录下有个 d 文件。由于 ab 已经存在,因此 d 与 cd 互为兄弟关系,修改 ab 的孩子为 d,d 的兄弟为 cd,进行结点的加入。
重复上述操作完成建树。
把这棵树整理成二叉树的形式。
我们按照前序遍历的顺序读一下这课树,发现在忽略缩进的情况下,读取的顺序和样例的输出数据是一模一样啊,也就是说,只要把这棵树建出来,这个情景我们就解决了。在应用的时候,我们应当积极地考虑使用孩子兄弟表示法建树,因为这种方法建立的是二叉树,我们就可以用二叉树的基操来操作这棵树。
结点结构体定义
typedef struct CSNode
{
string data; //数据域
struct CSNode* firstchild; //指向对应长子结点的指针域
struct CSNode* rightsib; //指向对应右兄弟结点的指针域
int flag_file; //判断是文件还是目录的 flag
}CSNode, * CSTree;
- 此处为什么用 int 类型来当 flag 而不是 bool 类型呢?这是因为改为 int 类型,相当于直接赋予优先级,判断插入位置时直接在同优先级的结点进行查找即可。
建树算法
字符串切片算法
切片算法可以用字符数组实现,也可以用 string 类实现,此处我用字符数组描述。由于我们读入的数据是字符串,因此我们需要先把各个目录分离开来。需要注意的是,虽然文件是特殊数据,但是文件只会出现在字符串串尾,因此只需要一个分支结构单独处理即可。
伪代码
需要强调的是,string 类用来判断字典序和复制操作都可以直接用运算符实现,更为方便。
代码实现
调试结果
伪代码
建树算法只需要再字符串切片算法进行改动即可,把输出语句改为调用结点插入函数即可实现。
代码实现
void createTree(CSTree pre, string str)
{
int idx = 0;
getline(cin, str);
for (int i = 0; i < str.size(); i++)
{
if (str[i] == '\\') //注意用反义字符,不然会报错
{ //只要不在串尾,只会是目录
pre = insertNode(pre, str.substr(idx, i - idx), 1);
idx = i + 1; //移动字符串到下一个目录,即 '\' 之后
}
}
if (idx < str.size()) //文件只出现在字符串尾
{
pre = insertNode(pre, str.substr(idx, str.size() - idx), 0);
}
}
结点插入算法
该算法的目的是向一个树结构中,在正确的位置插入新结点,是解决这个问题的核心。这里要强调一下返回值的重要性,如果不设置返回值,而是把目录引用进去,那么回到调用函数的时候需要自行将指针移动到当前目录,更为繁琐,好的解法是将插入后的接点作为所在的目录,以此为返回值返回函数调用的位置。容易出错的地方是若插入位置是目录的长子结点的话,直接通过前驱指针的后继来操作会插在错误位置导致断链,因此需要设置当前位置指针和前驱指针,就可以规避这个问题。
伪代码
代码实现
CSTree insertNode(CSTree t, string str, int flag) //核心在此
{
CSTree a_node = new CSNode;
CSTree pre = t, ptr;
a_node->data = str; //初始化新结点
a_node->firstchild = a_node->rightsib = NULL;
a_node->flag_file = flag;
if (t->firstchild == NULL) //所在目录没孩子,直接插入结点
{
t->firstchild = a_node;
return t->firstchild;
}
ptr = t->firstchild; //由于根结点本身插入时,是插在长子位,因此另外设置 pre 当前驱结点,ptr 当 pre 的后继,比较好写
while (ptr != NULL && ((ptr->flag_file > a_node->flag_file) || (ptr->flag_file == a_node->flag_file && str > ptr->data)))
{
pre = ptr;
ptr = ptr->rightsib;
}
//要先判空,不然有段错误
if (ptr == NULL) //无处可插入,插在链尾
{
a_node->rightsib = pre->rightsib;
pre->rightsib = a_node;
return a_node; //接下来以 a_node 为根目录操作
}
else if (ptr->data == a_node->data && ptr->flag_file == a_node->flag_file) //目录或文件已存在(第三版就是因为这个出问题)
{
delete a_node; //把申请的新结点打掉
return ptr; //接下来在已有的 ptr 目录下操作
}
else //找到了应该插入的位置
{
if (pre->data == t->data) //插在根目录的长子位
{
a_node->rightsib = pre->firstchild;
pre->firstchild = a_node;
}
else //正常插入
{
a_node->rightsib = pre->rightsib;
pre->rightsib = a_node;
}
return a_node; //接下来以 a_node 为根目录操作
}
}
打印目录树
我们已经知道输出结点的顺序就是先序遍历二叉树的顺序,因此我们只需要添加个缩进的机制就能实现。
void PreOrderTraverse(CSTree T, int space) //从博客上拿来的遍历函数
{ //因为要输出空格,稍微改装下
if (T == NULL)
return;
for (int i = 0; i < space; i++)
{
cout << " ";
}
cout << T->data << endl; //前序遍历
PreOrderTraverse(T->firstchild,space + 2); //下一层多两个空格
//cout << T->data << " " ; //中序遍历
PreOrderTraverse(T->rightsib,space); //兄弟结点不需要多空格
//cout << T->data << " " ; //后序遍历
}
主函数
测试样例
输入样例
15
b
c\
ab\cd
a\bc
ab\d
a\d\a
a\d\z\
b\
c
ab\cd\e
a\bc\f
ab\d\g
a\d\a\h
a\d\z
输出样例
root
a
bc
f
d
a
h
z
a
z
bc
ab
cd
e
d
g
cd
d
b
c
b
c