目录删除

104 阅读6分钟

目录删除

问题背景

我们需要处理一种特殊的字符串格式,它能表示一个目录树的层级结构,类似于 Windows 系统中 tree /f 命令的输出。我们的任务是根据一系列规则,删除这个目录树中的特定目录,并统计被删除目录的总数。

目录结构字符串格式

  1. 内容: 输入的字符串仅包含数字、字母(大小写敏感)和特殊符号 |-

  2. 层次表示:

    • 符号 |- 用于表示目录的层级深度。一个目录前的 |- 序列数量决定了它在树中的层级。
    • 没有 |- 前缀的是根目录(第1层)。一个 |- 表示第2层,两个 |- 表示第3层,以此类推。
  3. 父子关系:

    • 每个子目录都挂载在其上方最近的一个、层级比它浅一层的目录之下。

删除规则

一个目录能够被删除,当且仅当它同时满足以下两个条件:

  1. 名称匹配: 该目录的名称必须在给定的待删除目录名列表 delDirs 中。
  2. 叶子节点: 该目录必须是一个叶子目录(即没有任何子目录)。

特别注意(连锁反应):

当一个目录的所有子目录都被删除后,它自身也会变成一个叶子目录。此时,如果它的名称也恰好在 delDirs 列表中,那么它也符合被删除的条件,删除操作会继续向上进行。

任务要求

给定一个表示目录树的字符串列表 dirTreeLines 和一个待删除目录名的列表 delDirs,请重复应用上述删除规则,直到没有任何目录可以被删除为止。最终,返回被删除的不同路径的目录总数。


输入格式

  • delDirs: 第一个参数,一个字符串列表,包含所有待删除的目录名。

    • 1 <= delDirs.length <= 100
    • 1 <= delDirs[i].length <= 100
  • dirTreeLines: 第二个参数,一个字符串列表,表示目录树的结构。

    • 1 <= dirTreeLines.length <= 50
    • 1 <= dirTreeLines[i].length <= 100
  • 用例保证:

    • 输入的目录结构是合法的。
    • 有且仅有一个根目录。
    • 除根目录外,所有目录都有对应的父目录。

输出格式

  • 一个整数,表示最终被删除的目录的总个数。如果没有任何目录被删除,则输出 0

样例说明

样例 1

  • 输入:

    • delDirs = ["B", "Cpp"]
    • dirTreeLines = ["A", "|-B", "|-|-Cpp", "|-|-B", "|-|-|-B", "|-lib32"]
  • 输出: 4

  • 解释:

    1. 构建目录树:

      • /A (根)
      • /A/B
      • /A/B/Cpp
      • /A/B/B
      • /A/B/B/B
      • /A/lib32
    2. 模拟删除过程 (多轮进行) :

      • 第 1 轮: 寻找初始的、符合删除条件的叶子目录。

        • /A/B/Cpp: 是叶子目录,名称 CppdelDirs 中 -> 删除 (计数: 1)。
        • /A/B/B/B: 是叶子目录,名称 BdelDirs 中 -> 删除 (计数: 2)。
        • /A/lib32: 是叶子目录,但名称 lib32 不在 delDirs 中 -> 不删除。
      • 第 2 轮: 因 /A/B/B/B 被删除,/A/B/B 现已成为叶子目录。

        • /A/B/B: 是新的叶子目录,名称 BdelDirs 中 -> 删除 (计数: 3)。
      • 第 3 轮: 因 /A/B/Cpp/A/B/B 都被删除,/A/B 现已成为叶子目录。

        • /A/B: 是新的叶子目录,名称 BdelDirs 中 -> 删除 (计数: 4)。
      • 结束: /A 仍然有子目录 /A/lib32,不是叶子目录,流程结束。

    3. 最终结果: 共删除了 4 个目录。

样例 2

  • 输入:

    • delDirs = ["A", "Java"]
    • dirTreeLines = ["A", "|-a", "|-|-A", "|-A", "|-|-A"]
  • 输出: 3

  • 解释:

    1. 构建目录树:

      • /A (根)
      • /A/a
      • /A/a/A
      • /A/A
      • /A/A/A
    2. 模拟删除过程:

      • 第 1 轮:

        • /A/a/A: 是叶子,名称 AdelDirs 中 -> 删除 (计数: 1)。
        • /A/A/A: 是叶子,名称 AdelDirs 中 -> 删除 (计数: 2)。
      • 第 2 轮: /A/a/A/A/A/A 被删除后,/A/a/A/A 成为新的叶子目录。

        • /A/a: 是叶子,但名称 a 不在 delDirs 中(大小写敏感)-> 不删除。
        • /A/A: 是叶子,名称 AdelDirs 中 -> 删除 (计数: 3)。
      • 结束: 根目录 /A 仍有子目录 /A/a,不是叶子目录,无法删除。

    3. 最终结果: 共删除了 3 个目录。

import java.util.*;
import java.util.stream.Collectors;

public class Solution {
    /**
     * 内部静态类,用于表示目录树中的一个节点。
     */
    static class DirNode {
        String name;       // 目录名
        DirNode parent;    // 指向父节点的引用
        List<DirNode> children = new ArrayList<>(); // 存储所有直接子节点
        boolean isDeleted = false; // 标记该节点是否已被删除

        DirNode(String name, DirNode parent) {
            this.name = name;
            this.parent = parent;
        }

        /**
         * 判断当前节点是否为叶子节点。
         * 一个节点是叶子,当且仅当它的所有子节点都已经被标记为删除。
         * 如果一个节点原本就没有子节点,它也是叶子。
         * @return 如果是叶子节点,返回 true;否则返回 false。
         */
        boolean isLeaf() {
            for (DirNode child : children) {
                // 只要有一个子节点尚未被删除,当前节点就不是叶子节点
                if (!child.isDeleted) {
                    return false;
                }
            }
            return true; // 所有子节点都已被删除(或没有子节点)
        }
    }

    /**
     * 主函数,实现删除逻辑并返回删除的目录数。
     * @param delDirs 要删除的目录名列表
     * @param dirTreeLines 代表目录树结构的字符串列表
     * @return 被删除的目录总数
     */
    public int deleteDirectories(String[] delDirs, String[] dirTreeLines) {
        // --- 1. 准备工作 ---

        // 将要删除的目录名放入 HashSet,以便进行 O(1) 的快速查找
        Set<String> namesToDelete = new HashSet<>(Arrays.asList(delDirs));
        
        // 处理边界情况
        if (dirTreeLines == null || dirTreeLines.length == 0 || namesToDelete.isEmpty()) {
            return 0;
        }

        // --- 2. 构建目录树 ---

        // allNodes 列表用于存储树中所有的节点,方便后续进行多轮遍历
        List<DirNode> allNodes = new ArrayList<>();
        // pathStack 数组像一个路径栈,pathStack[d] 存储深度为 d 的最新节点
        DirNode[] pathStack = new DirNode[dirTreeLines.length]; // 深度不可能超过行数

        // 处理根节点(总是在第一行,深度为 0)
        String rootName = dirTreeLines[0];
        DirNode root = new DirNode(rootName, null);
        allNodes.add(root);
        pathStack[0] = root;

        // 遍历剩余的行,构建树的其余部分
        for (int i = 1; i < dirTreeLines.length; i++) {
            String line = dirTreeLines[i];
            
            // 计算当前行的深度。"|-" 长度为2。
            // lastIndexOf 找到最后一个 "|-" 的起始位置。
            int depth = line.lastIndexOf("|-") / 2 + 1;
            
            // 截取掉前缀,得到目录名
            String name = line.substring(depth * 2);

            // 父节点是上一深度的最新节点
            DirNode parent = pathStack[depth - 1];
            
            // 创建新节点
            DirNode newNode = new DirNode(name, parent);
            
            // 建立父子关系
            parent.children.add(newNode);
            
            // 更新路径栈和节点列表
            pathStack[depth] = newNode;
            allNodes.add(newNode);
        }

        // --- 3. 迭代删除 ---

        int deletedCount = 0; // 记录删除的目录总数
        boolean deletedInThisPass; // 标记在一轮完整的遍历中是否发生了删除

        // 使用 do-while 循环,至少执行一次,只要上一轮有删除操作就继续下一轮
        do {
            deletedInThisPass = false; // 每轮开始前重置标志
            
            // 遍历所有节点,尝试删除
            for (DirNode node : allNodes) {
                // 检查是否满足所有删除条件:
                // 1. 节点尚未被删除
                // 2. 节点的名字在待删除列表中
                // 3. 节点当前是一个叶子节点(即其所有子节点都已被删除)
                if (!node.isDeleted && namesToDelete.contains(node.name) && node.isLeaf()) {
                    node.isDeleted = true; // 标记为已删除
                    deletedCount++;       // 总数加 1
                    deletedInThisPass = true; // 标记本轮发生了删除
                }
            }
        } while (deletedInThisPass); // 如果本轮有删除,则可能导致新的叶子节点产生,需要再来一轮

        // 4. 返回结果
        return deletedCount;
    }
}

public class Main {
    public static void main(String[] args) {
        Solution solver = new Solution();
        
        // 样例1
        System.out.println("--- 样例 1 ---");
        String[] delDirs1 = {"B", "Cpp"};
        String[] dirTreeLines1 = {
            "A",
            "|-B",
            "|-|-Cpp",
            "|-|-B",
            "|-|-|-B",
            "|-lib32"
        };
        int result1 = solver.deleteDirectories(delDirs1, dirTreeLines1);
        System.out.println("输出: " + result1); // 预期: 4

        System.out.println();
        
        // 样例2
        System.out.println("--- 样例 2 ---");
        String[] delDirs2 = {"A", "Java"};
        String[] dirTreeLines2 = {
            "A",
            "|-a",
            "|-|-A",
            "|-A",
            "|-|-A"
        };
        int result2 = solver.deleteDirectories(delDirs2, dirTreeLines2);
        System.out.println("输出: " + result2); // 预期: 3
    }
}