leetcode
leetcode 501 ~ 550
设计内存文件系统

设计内存文件系统

难度:

标签:

题目描述

代码结果

运行时间: 31 ms, 内存: 16.5 MB


/*
 * LeetCode Problem 588: Design In-Memory File System
 * 
 * The task is to design a file system that can create directories and files, add content to a file, and read content from a file.
 * The following operations should be supported:
 * - ls(path): List the content in the directory specified by the path. If the path is a file, return the file name only.
 * - mkdir(path): Create a directory at the specified path.
 * - addContentToFile(filePath, content): Add content to the file at the specified path. If the file does not exist, create it.
 * - readContentFromFile(filePath): Return the content of the file at the specified path.
 */
 
import java.util.*;
import java.util.stream.Collectors;
 
public class FileSystem {
    private class Node {
        boolean isFile = false;
        String content = "";
        TreeMap<String, Node> children = new TreeMap<>();
    }
 
    private Node root;
 
    public FileSystem() {
        root = new Node();
    }
 
    public List<String> ls(String path) {
        Node node = root;
        List<String> result = new ArrayList<>();
        if (!path.equals("/")) {
            String[] parts = path.split("/");
            node = Arrays.stream(parts)
                          .skip(1)
                          .reduce(root, (current, part) -> current.children.get(part), (a, b) -> b);
            if (node.isFile) {
                result.add(parts[parts.length - 1]);
                return result;
            }
        }
        result.addAll(node.children.keySet());
        return result;
    }
 
    public void mkdir(String path) {
        Node node = root;
        String[] parts = path.split("/");
        Arrays.stream(parts)
              .skip(1)
              .forEach(part -> node = node.children.computeIfAbsent(part, k -> new Node()));
    }
 
    public void addContentToFile(String filePath, String content) {
        Node node = root;
        String[] parts = filePath.split("/");
        node = Arrays.stream(parts)
                      .skip(1)
                      .limit(parts.length - 2)
                      .reduce(root, (current, part) -> current.children.computeIfAbsent(part, k -> new Node()), (a, b) -> b);
        Node fileNode = node.children.computeIfAbsent(parts[parts.length - 1], k -> new Node());
        fileNode.isFile = true;
        fileNode.content += content;
    }
 
    public String readContentFromFile(String filePath) {
        Node node = root;
        String[] parts = filePath.split("/");
        node = Arrays.stream(parts)
                      .skip(1)
                      .reduce(root, (current, part) -> current.children.get(part), (a, b) -> b);
        return node.content;
    }
}
 

解释

方法:

这道题使用前缀树 (Trie) 来实现一个内存文件系统。思路是: 1. 用一个 TrieNode 类来表示前缀树的节点,包含一个 defaultdict 类型的 child 字段存储子节点,一个 contents 字段存储文件内容。 2. FileSystem 类表示整个文件系统,包含一个 TrieNode 类型的 root 字段作为前缀树的根节点。 3. 对于不同的操作: - ls:从根节点开始,沿着给定路径逐层搜索对应的节点。如果该节点是文件,则返回文件名;如果是目录,则返回目录下的所有文件和子目录名,并按字典序排序。 - mkdir:从根节点开始,沿着给定路径逐层插入对应的节点,缺失的中间目录也会被创建。 - addContentToFile:类似 mkdir,但如果最后的节点已经存在则追加内容,否则创建新文件并写入内容。 - readContentFromFile:从根节点开始搜索给定路径对应的节点,返回其 contents 字段的内容。

时间复杂度:

ls: O(L + klogk),其中 L 是路径长度,k 是目录下子节点数量 mkdir、addContentToFile、readContentFromFile: O(L),其中 L 是路径长度

空间复杂度:

O(N),其中 N 是文件系统中的总节点数

代码细节讲解

🦆
为什么在设计内存文件系统时选择使用前缀树(Trie)而不是其他数据结构,如哈希表或平衡树?
前缀树(Trie)适合用于存储和检索字符串数据集中的键,特别是当这些键具有共同前缀时。在文件系统中,文件路径可以视为由多个部分组成的字符串,且这些部分常常在不同路径中重复出现。使用前缀树可以高效地共享和管理这些共同前缀,从而节省空间并提高检索效率。相比之下,哈希表虽然检索快速,但不支持按前缀有序地枚举键,且每个键都需要独立存储,不利于路径的共享存储;平衡树可以保持键的有序性,但在处理具有公共前缀的长路径时,效率不如前缀树。
🦆
在`ls`操作中,当路径指向一个文件时,返回的是文件名而不是文件内容,这种设计的原因是什么?
`ls`操作在Unix和类Unix系统中通常用于列出目录内容,包括子目录和文件名,而不是显示文件内容。模仿这种行为,当`ls`操作指向文件时返回文件名而非内容,可以保持与传统文件系统的操作习惯一致,并且使得`ls`操作的输出保持一致性(总是返回文件或目录名列表)。这样设计也有助于区分查看目录内容和读取文件内容的操作,提高用户使用的直观性。
🦆
在执行`mkdir`操作时,如何处理已经存在的目录路径?是否会重新创建或者覆盖,还是简单地忽略?
在执行`mkdir`操作时,如果目录路径中的某个部分已经存在,该操作不会对已存在的目录进行重新创建或覆盖,而是简单地忽略这一部分,继续在该路径下创建不存在的目录。这种方式避免了数据的不必要覆盖,同时也反映了Unix和类Unix系统中`mkdir`命令的常见行为,即可以安全地重复执行而不会导致错误或数据损失。
🦆
对于`addContentToFile`方法,如果路径中的某些目录尚未创建,是否会自动创建这些目录,还是会抛出错误?
在`addContentToFile`方法中,如果文件所在的路径中包含尚未创建的目录,这些目录会被自动创建。这种设计使得文件的添加操作更为灵活和容易,用户无需事先创建所有必需的目录结构即可直接添加或更新文件内容。这避免了操作的复杂性并增强了用户体验。

相关问题

LRU 缓存

请你设计并实现一个满足  LRU (最近最少使用) 缓存 约束的数据结构。
实现 LRUCache 类:
  • LRUCache(int capacity)正整数 作为容量 capacity 初始化 LRU 缓存
  • int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1
  • void put(int key, int value) 如果关键字 key 已经存在,则变更其数据值 value ;如果不存在,则向缓存中插入该组 key-value 。如果插入操作导致关键字数量超过 capacity ,则应该 逐出 最久未使用的关键字。

函数 getput 必须以 O(1) 的平均时间复杂度运行。

 

示例:

输入
["LRUCache", "put", "put", "get", "put", "get", "put", "get", "get", "get"]
[[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]]
输出
[null, null, null, 1, null, -1, null, -1, 3, 4]

解释
LRUCache lRUCache = new LRUCache(2);
lRUCache.put(1, 1); // 缓存是 {1=1}
lRUCache.put(2, 2); // 缓存是 {1=1, 2=2}
lRUCache.get(1);    // 返回 1
lRUCache.put(3, 3); // 该操作会使得关键字 2 作废,缓存是 {1=1, 3=3}
lRUCache.get(2);    // 返回 -1 (未找到)
lRUCache.put(4, 4); // 该操作会使得关键字 1 作废,缓存是 {4=4, 3=3}
lRUCache.get(1);    // 返回 -1 (未找到)
lRUCache.get(3);    // 返回 3
lRUCache.get(4);    // 返回 4

 

提示:

  • 1 <= capacity <= 3000
  • 0 <= key <= 10000
  • 0 <= value <= 105
  • 最多调用 2 * 105getput

LFU 缓存

请你为 最不经常使用(LFU)缓存算法设计并实现数据结构。

实现 LFUCache 类:

  • LFUCache(int capacity) - 用数据结构的容量 capacity 初始化对象
  • int get(int key) - 如果键 key 存在于缓存中,则获取键的值,否则返回 -1
  • void put(int key, int value) - 如果键 key 已存在,则变更其值;如果键不存在,请插入键值对。当缓存达到其容量 capacity 时,则应该在插入新项之前,移除最不经常使用的项。在此问题中,当存在平局(即两个或更多个键具有相同使用频率)时,应该去除 最久未使用 的键。

为了确定最不常使用的键,可以为缓存中的每个键维护一个 使用计数器 。使用计数最小的键是最久未使用的键。

当一个键首次插入到缓存中时,它的使用计数器被设置为 1 (由于 put 操作)。对缓存中的键执行 getput 操作,使用计数器的值将会递增。

函数 getput 必须以 O(1) 的平均时间复杂度运行。

 

示例:

输入:
["LFUCache", "put", "put", "get", "put", "get", "get", "put", "get", "get", "get"]
[[2], [1, 1], [2, 2], [1], [3, 3], [2], [3], [4, 4], [1], [3], [4]]
输出:
[null, null, null, 1, null, -1, 3, null, -1, 3, 4]

解释:
// cnt(x) = 键 x 的使用计数
// cache=[] 将显示最后一次使用的顺序(最左边的元素是最近的)
LFUCache lfu = new LFUCache(2);
lfu.put(1, 1);   // cache=[1,_], cnt(1)=1
lfu.put(2, 2);   // cache=[2,1], cnt(2)=1, cnt(1)=1
lfu.get(1);      // 返回 1
                 // cache=[1,2], cnt(2)=1, cnt(1)=2
lfu.put(3, 3);   // 去除键 2 ,因为 cnt(2)=1 ,使用计数最小
                 // cache=[3,1], cnt(3)=1, cnt(1)=2
lfu.get(2);      // 返回 -1(未找到)
lfu.get(3);      // 返回 3
                 // cache=[3,1], cnt(3)=2, cnt(1)=2
lfu.put(4, 4);   // 去除键 1 ,1 和 3 的 cnt 相同,但 1 最久未使用
                 // cache=[4,3], cnt(4)=1, cnt(3)=2
lfu.get(1);      // 返回 -1(未找到)
lfu.get(3);      // 返回 3
                 // cache=[3,4], cnt(4)=1, cnt(3)=3
lfu.get(4);      // 返回 4
                 // cache=[3,4], cnt(4)=2, cnt(3)=3

 

提示:

  • 1 <= capacity <= 104
  • 0 <= key <= 105
  • 0 <= value <= 109
  • 最多调用 2 * 105getput 方法

设计日志存储系统

设计日志存储系统