目录树删除顺序
问题背景
我们需要处理一种特殊的字符串格式,它能表示一个目录树的层级结构。首先,我们需要根据一系列规则,将这些字符串解析并构建成一棵合法的目录树。然后,我们需要模拟一个特定的删除过程,并按顺序记录下被删除的目录名称。
目录结构字符串格式
- 内容: 输入的字符串仅包含数字、字母(大小写敏感)和特殊符号
|-。 - 层次表示: 符号
|-用于表示目录的层级深度。一个目录前的|-序列数量决定了它在树中的层级。 - 目录名: 除去
|-前缀后,剩余的部分即为目录名。
树的构建规则
在解析 dirTreeLines 字符串列表以构建目录树时,必须遵循以下规则:
- 父子关系: 一个子目录挂载在其上方最近的一个、层级比它浅一层的目录之下。
- 子目录顺序: 同一父目录下的多个子目录,其前后顺序由它们在输入列表中的出现顺序决定。
- 异常处理 - 无效父目录: 如果一个非根目录(即带有
|-前缀的行)在输入列表中找不到合法的父目录(例如,一个第4层的目录,其前面最近的上一层是第2层而非第3层),则该行输入将被忽略。 - 异常处理 - 子目录重名: 如果尝试向一个父目录添加一个与现有子目录同名的子目录,则该次添加操作将被忽略,只保留第一次成功添加的那个。
删除规则
删除过程遵循一种严格的自底向上顺序,这本质上是一种树的后序遍历 (Post-order Traversal) :
- 一个目录只有在它的所有子目录都已经被删除之后,才能被删除。
- 如果是叶子目录(没有子目录),则可以直接删除。
任务要求
给定一个表示目录树的字符串列表 dirTreeLines,请先根据“树的构建规则”建立一棵合法的目录树,然后根据“删除规则”依次删除所有目录,并按删除的先后顺序输出它们的名称。
输入格式
-
dirTreeLines: 一个字符串列表,表示目录树的结构。1 <= dirTreeLines.length <= 501 <= dirTreeLines[i].length <= 100- 目录名长度范围为
[1, 10]。 - 用例保证: 输入中有且仅有一个根目录(没有
|-前缀的行)。
输出格式
- 一个字符串序列(列表或数组),表示被依次删除的目录的名称。
样例说明
-
输入:
["|-B","A","|-B","|-|-C","|-|-D","|-|-D","|-|-|-|-D","|-|-E","|-|-|-F","|-lib32"] -
输出:
["C", "D", "F", "E", "B", "lib32", "A"] -
解释:
第一步:构建合法的目录树
我们逐行分析输入,并解释每一行的处理结果:
"|-B": 第 2 层。但此时树为空,没有父目录。忽略。"A": 第 1 层。成为根目录。树: A"|-B": 第 2 层。父目录是A。树: A -> {B}"|-|-C": 第 3 层。父目录是B。树: A -> {B -> {C}}"|-|-D": 第 3 层。父目录是B。C和D是兄弟。树: A -> {B -> {C, D}}"|-|-D": 第 3 层。父目录B下已存在名为D的子目录。忽略。"|-|-|-|-D": 第 5 层。前面最近的上一层是第3层,没有第4层父目录。忽略。"|-|-E": 第 3 层。父目录是B。C,D,E是兄弟。树: A -> {B -> {C, D, E}}"|-|-|-F": 第 4 层。父目录是E。树: A -> {B -> {C, D, E -> {F}}}"|-lib32": 第 2 层。父目录是A。B和lib32是兄弟。树: A -> {B -> {...}, lib32}
最终构建的树结构为:
A |-B | |-C | |-D | '-E | '-F '-lib32第二步:按后序遍历规则删除
- 要删除
A,必须先删除其子目录B和lib32。 - 先处理
B(按输入顺序)。要删除B,必须先删除其子目录C,D,E。 - 先处理
C。C是叶子,删除C。 - 再处理
D。D是叶子,删除D。 - 再处理
E。要删除E,必须先删除其子目录F。 - 处理
F。F是叶子,删除F。 - 现在
E的子目录都已删除,删除E。 - 现在
B的子目录都已删除,删除B。 - 现在轮到
A的下一个子目录lib32。lib32是叶子,删除lib32。 - 现在
A的所有子目录都已删除,删除A。
将删除的名称按顺序组合,即为
["C", "D", "F", "E", "B", "lib32", "A"]。
import java.util.*;
import java.util.stream.Collectors;
/**
* 解决“删除整个目录”问题的方案类。
*
* 核心思想:
* 1. **构建树形结构**:输入是一系列描述目录层级的字符串,这本质上是一个树形结构。
* 首要任务是将这种文本表示转换为一个真正的、易于操作的树数据结构。每个目录都是树上的一个节点 (`DirNode`)。
* 2. **处理输入规则**:在构建树的过程中,需要严格遵守题目给出的规则:
* - 通过 `|-` 前缀的数量来确定目录的深度。
* - 一个目录的父节点是它上方最近的、深度比它小 1 的目录。我们可以用一个类似栈的数组 `pathStack` 来追踪每个深度的最新父节点。
* - 同一个父节点下的子目录不能同名,如果遇到同名,则忽略后来的。使用 `Map` 存储子节点可以轻松实现 O(1) 的同名检查。
* - 没有对应父节点的目录(野节点)需要被忽略。
* 3. **确定删除顺序**:题目要求“如果某目录含有子目录,则需要先删除其子目录”。这正是**树的后序遍历 (Post-order Traversal)** 的定义。在后序遍历中,一个节点总是在其所有子孙节点都被访问之后才被访问。
*
* 算法步骤:
* 1. 创建一个 `DirNode` 内部类来表示目录节点,包含名字、子节点集合等信息。
* 2. 解析 `dirTreeLines` 数组,使用一个 `pathStack` 辅助数组来追踪父节点,从而构建出一棵完整的目录树。在此过程中处理无效行和同名子目录。
* 3. 对构建好的树的根节点执行一次后序遍历。
* 4. 在后序遍历的递归过程中,每当一个节点的所有子树都遍历完毕后,就将该节点的名字添加到结果列表中。
* 5. 最终得到的列表就是符合删除顺序的目录名列表。
*/
public class Solution {
/**
* 内部静态类,用于表示目录树中的一个节点。
*/
static class DirNode {
String name; // 目录名
// 使用 LinkedHashMap 存储子节点。
// Key: 子目录名, Value: 子目录对应的 DirNode 对象。
// 使用 Map 可以 O(1) 检查子目录是否已存在(处理同名忽略规则)。
// 使用 LinkedHashMap 可以保持子目录的插入顺序,符合“按输入先后顺序为准”的要求。
Map<String, DirNode> children = new LinkedHashMap<>();
DirNode(String name) {
this.name = name;
}
}
/**
* 主逻辑方法:解析输入,构建树,然后通过后序遍历得到删除顺序。
* @param dirTreeLines 代表目录树结构的字符串数组。
* @return 按删除顺序列出的目录名列表。
*/
public List<String> deleteTree(String[] dirTreeLines) {
// --- 1. 处理边界情况 ---
if (dirTreeLines == null || dirTreeLines.length == 0) {
return new ArrayList<>();
}
// --- 2. 构建目录树 ---
DirNode root = null;
// pathStack 数组像一个路径栈,pathStack[d] 是在深度为 d 的路径上最近的那个节点。
// 它帮助我们为深度为 d+1 的新节点找到其父节点 pathStack[d]。
// 数组大小设为 dirTreeLines.length + 1 以防止索引越界。
DirNode[] pathStack = new DirNode[dirTreeLines.length + 1];
for (String line : dirTreeLines) {
// 跳过无效的空行
if (line == null || line.isEmpty()) {
continue;
}
// a. 计算当前行的深度和目录名
int depth = calculateDepth(line);
String name = line.substring(depth * 2);
// b. 处理根节点
if (depth == 0) {
// 第一个深度为 0 的行是根节点
if (root == null) {
root = new DirNode(name);
pathStack[0] = root;
}
// 根据题意,有且仅有一个根目录,后续的深度为0的行可视为无效或忽略
continue;
}
// c. 处理子节点
// 根节点必须已存在,且当前深度必须有对应的父节点存在于 pathStack 中
if (root == null || pathStack[depth - 1] == null) {
// 无有效父节点,忽略此行
continue;
}
DirNode parent = pathStack[depth - 1];
// d. 如果父节点下不存在同名子目录,则添加
if (!parent.children.containsKey(name)) {
DirNode newNode = new DirNode(name);
parent.children.put(name, newNode); // 添加到父节点的子目录列表
pathStack[depth] = newNode; // 更新当前深度的最新节点
}
// 如果已存在同名子目录,则忽略
}
// --- 3. 后序遍历树以确定删除顺序 ---
List<String> deletionOrder = new ArrayList<>();
if (root != null) {
postOrderTraverse(root, deletionOrder);
}
return deletionOrder;
}
/**
* 计算行的深度。
* @param line 输入的目录字符串
* @return 0-based 深度
*/
private int calculateDepth(String line) {
int depth = 0;
// 计算行首 "|-" 的数量
// 检查 `line` 字符串,从 `depth * 2` 这个索引位置开始,看看它是否以 `|-` 开头。
// 每成功匹配一次,`depth` 就加一,下一次检查的起始点也相应后移,直到无法匹配为止。
while (line.startsWith("|-", depth * 2)) {
depth++;
}
return depth;
}
/**
* 递归辅助方法,用于后序遍历目录树。
* 后序遍历(左 -> 右 -> 根)的顺序完美匹配了“先删除子目录,再删除父目录”的规则。
*
* @param node 当前遍历的节点
* @param deletionOrder 用于收集删除顺序的结果列表
*/
private void postOrderTraverse(DirNode node, List<String> deletionOrder) {
if (node == null) {
return;
}
// 1. 递归地“删除”(访问)所有子节点
// 遍历 LinkedHashMap 的 values 会保持插入顺序
for (DirNode child : node.children.values()) {
postOrderTraverse(child, deletionOrder);
}
// 2. 当所有子节点都被处理完毕后,再“删除”(访问)当前节点
deletionOrder.add(node.name);
}
}