设计内存文件系统
难度:
标签:
题目描述
代码结果
运行时间: 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)而不是其他数据结构,如哈希表或平衡树?
▷🦆
在`ls`操作中,当路径指向一个文件时,返回的是文件名而不是文件内容,这种设计的原因是什么?
▷🦆
在执行`mkdir`操作时,如何处理已经存在的目录路径?是否会重新创建或者覆盖,还是简单地忽略?
▷🦆
对于`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
,则应该 逐出 最久未使用的关键字。
函数 get
和 put
必须以 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 * 105
次get
和put
LFU 缓存
请你为 最不经常使用(LFU)缓存算法设计并实现数据结构。
实现 LFUCache
类:
LFUCache(int capacity)
- 用数据结构的容量capacity
初始化对象int get(int key)
- 如果键key
存在于缓存中,则获取键的值,否则返回-1
。void put(int key, int value)
- 如果键key
已存在,则变更其值;如果键不存在,请插入键值对。当缓存达到其容量capacity
时,则应该在插入新项之前,移除最不经常使用的项。在此问题中,当存在平局(即两个或更多个键具有相同使用频率)时,应该去除 最久未使用 的键。
为了确定最不常使用的键,可以为缓存中的每个键维护一个 使用计数器 。使用计数最小的键是最久未使用的键。
当一个键首次插入到缓存中时,它的使用计数器被设置为 1
(由于 put 操作)。对缓存中的键执行 get
或 put
操作,使用计数器的值将会递增。
函数 get
和 put
必须以 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 * 105
次get
和put
方法