目录树删除顺序

86 阅读8分钟

目录树删除顺序

问题背景

我们需要处理一种特殊的字符串格式,它能表示一个目录树的层级结构。首先,我们需要根据一系列规则,将这些字符串解析并构建成一棵合法的目录树。然后,我们需要模拟一个特定的删除过程,并按顺序记录下被删除的目录名称。

目录结构字符串格式

  • 内容: 输入的字符串仅包含数字、字母(大小写敏感)和特殊符号 |-
  • 层次表示: 符号 |- 用于表示目录的层级深度。一个目录前的 |- 序列数量决定了它在树中的层级。
  • 目录名: 除去 |- 前缀后,剩余的部分即为目录名。

树的构建规则

在解析 dirTreeLines 字符串列表以构建目录树时,必须遵循以下规则:

  1. 父子关系: 一个子目录挂载在其上方最近的一个、层级比它浅一层的目录之下。
  2. 子目录顺序: 同一父目录下的多个子目录,其前后顺序由它们在输入列表中的出现顺序决定。
  3. 异常处理 - 无效父目录: 如果一个非根目录(即带有 |- 前缀的行)在输入列表中找不到合法的父目录(例如,一个第4层的目录,其前面最近的上一层是第2层而非第3层),则该行输入将被忽略
  4. 异常处理 - 子目录重名: 如果尝试向一个父目录添加一个与现有子目录同名的子目录,则该次添加操作将被忽略,只保留第一次成功添加的那个。

删除规则

删除过程遵循一种严格的自底向上顺序,这本质上是一种树的后序遍历 (Post-order Traversal)

  • 一个目录只有在它的所有子目录都已经被删除之后,才能被删除。
  • 如果是叶子目录(没有子目录),则可以直接删除。

任务要求

给定一个表示目录树的字符串列表 dirTreeLines,请先根据“树的构建规则”建立一棵合法的目录树,然后根据“删除规则”依次删除所有目录,并按删除的先后顺序输出它们的名称。


输入格式

  • dirTreeLines: 一个字符串列表,表示目录树的结构。

    • 1 <= dirTreeLines.length <= 50
    • 1 <= 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 层。父目录是 BCD 是兄弟。树: A -> {B -> {C, D}}
    • "|-|-D": 第 3 层。父目录 B 下已存在名为 D 的子目录。忽略
    • "|-|-|-|-D": 第 5 层。前面最近的上一层是第3层,没有第4层父目录。忽略
    • "|-|-E": 第 3 层。父目录是 BC, D, E 是兄弟。树: A -> {B -> {C, D, E}}
    • "|-|-|-F": 第 4 层。父目录是 E树: A -> {B -> {C, D, E -> {F}}}
    • "|-lib32": 第 2 层。父目录是 ABlib32 是兄弟。树: A -> {B -> {...}, lib32}

    最终构建的树结构为:

    A
    |-B
    |  |-C
    |  |-D
    |  '-E
    |    '-F
    '-lib32
    

    第二步:按后序遍历规则删除

    1. 要删除 A,必须先删除其子目录 Blib32
    2. 先处理 B (按输入顺序)。要删除 B,必须先删除其子目录 C, D, E
    3. 先处理 CC 是叶子,删除 C
    4. 再处理 DD 是叶子,删除 D
    5. 再处理 E。要删除 E,必须先删除其子目录 F
    6. 处理 FF 是叶子,删除 F
    7. 现在 E 的子目录都已删除,删除 E
    8. 现在 B 的子目录都已删除,删除 B
    9. 现在轮到 A 的下一个子目录 lib32lib32 是叶子,删除 lib32
    10. 现在 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);
    }
}