1166. Design File System π
Problem Description
You need to design a file system that can create new file paths and associate integer values with them.
A valid path follows these rules:
- It starts with
/
followed by one or more lowercase English letters - Multiple directories can be concatenated with
/
as separator - Examples of valid paths:
/leetcode
,/leetcode/problems
- Invalid paths: empty string
""
, just/
Your FileSystem
class should implement two methods:
-
createPath(path, value)
:- Creates a new path and associates it with the given integer value
- Returns
true
if the path was successfully created - Returns
false
if:- The path already exists, OR
- The parent path doesn't exist (e.g., trying to create
/a/b/c
when/a/b
doesn't exist)
-
get(path)
:- Returns the integer value associated with the given path
- Returns
-1
if the path doesn't exist
The solution uses a Trie (prefix tree) data structure where:
- Each node stores a value (
v
) representing the value of that path - Each node has children stored in a hash table, where keys are path segments and values are child nodes
- When inserting a path like
/a/b/c
, it splits by/
and traverses through nodesa
, thenb
, checking that parent paths exist before creatingc
- When searching for a path, it traverses the trie following the path segments and returns the value at the final node
Intuition
When we think about file systems, they naturally form a tree-like hierarchy. Each directory can contain multiple subdirectories, and we navigate through them level by level using path separators (/
). This hierarchical structure immediately suggests using a tree data structure.
The key insight is that we need to efficiently:
- Check if parent paths exist when creating a new path
- Navigate through the path components to store or retrieve values
A Trie (prefix tree) is perfect for this because:
- File paths share common prefixes (e.g.,
/leetcode
and/leetcode/problems
share/leetcode
) - We can traverse the trie level by level, matching each path segment
- Checking parent existence becomes natural - we just verify each node exists as we traverse
For the createPath
operation, we can split the path by /
and traverse the trie until we reach the parent of the target node. If any intermediate node doesn't exist, the parent path is missing, so we return false
. If we successfully reach the parent and the target doesn't already exist, we create it.
For the get
operation, we simply traverse the trie following the path segments. If we can reach the target node, we return its value; otherwise, we return -1
.
The beauty of this approach is that both operations have time complexity proportional to the path length O(|path|)
, and the tree structure naturally enforces the parent-child relationships that the file system requires.
Learn more about Trie patterns.
Solution Approach
The solution implements a Trie data structure to represent the file system hierarchy. Let's walk through the implementation:
Trie Node Structure
Each [Trie](/problems/trie_intro)
node contains:
children
: A dictionary/hash table where keys are path segment names and values are references to child Trie nodesv
: An integer value associated with the current path (default is-1
for non-existent paths)
FileSystem Class Implementation
The FileSystem
class maintains a root Trie node and delegates operations to it.
Creating a Path - insert(path, value)
- Start from the root node
- Split the path by
/
to get individual segments:ps = path.split("/")
- Traverse through parent segments
ps[1:-1]
(excluding empty first element and last segment):- For each segment, check if it exists in current node's children
- If any parent segment doesn't exist, return
false
(parent path missing) - Move to the child node
- Check if the last segment
ps[-1]
already exists in current node's children:- If it exists, return
false
(path already exists) - Otherwise, create a new Trie node with the given value and add it as a child
- If it exists, return
- Return
true
for successful creation
Getting a Value - search(path)
- Start from the root node
- Split the path by
/
and iterate through all segmentspath.split("/")[1:]
:- For each segment, check if it exists in current node's children
- If not found, return
-1
(path doesn't exist) - Move to the child node
- After traversing all segments, return the value
node.v
of the final node
Time and Space Complexity
- Time Complexity: Both
createPath
andget
operations areO(n)
wheren
is the length of the path string, as we need to split the path and traverse through each segment - Space Complexity:
O(m * k)
wherem
is the total number of paths created andk
is the average path length
The Trie structure efficiently handles the hierarchical nature of file paths while ensuring that parent-child relationships are maintained and validated during path creation.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a simple example to illustrate how the Trie-based file system works.
Example Operations:
createPath("/a", 1)
β returnstrue
createPath("/a/b", 2)
β returnstrue
createPath("/a/b/c", 3)
β returnstrue
createPath("/a/d", 4)
β returnstrue
get("/a/b")
β returns2
createPath("/a/b", 5)
β returnsfalse
(already exists)createPath("/x/y", 6)
β returnsfalse
(parent/x
doesn't exist)
Step-by-step visualization:
Initial state: Root node (empty)
root
After createPath("/a", 1)
:
- Split path:
["", "a"]
- Traverse parent segments (none in this case)
- Create child "a" with value 1
root βββ a (v=1)
After createPath("/a/b", 2)
:
- Split path:
["", "a", "b"]
- Traverse to parent "a" (exists β)
- Create child "b" under "a" with value 2
root βββ a (v=1) βββ b (v=2)
After createPath("/a/b/c", 3)
:
- Split path:
["", "a", "b", "c"]
- Traverse: root β a (exists β) β b (exists β)
- Create child "c" under "b" with value 3
root βββ a (v=1) βββ b (v=2) βββ c (v=3)
After createPath("/a/d", 4)
:
- Split path:
["", "a", "d"]
- Traverse to parent "a" (exists β)
- Create child "d" under "a" with value 4
root βββ a (v=1) βββ b (v=2) β βββ c (v=3) βββ d (v=4)
When calling get("/a/b")
:
- Split path:
["", "a", "b"]
- Traverse: root β a β b
- Return value at node "b":
2
When calling createPath("/a/b", 5)
:
- Split path:
["", "a", "b"]
- Traverse to parent "a"
- Check if "b" exists in "a"'s children: YES
- Return
false
(path already exists)
When calling createPath("/x/y", 6)
:
- Split path:
["", "x", "y"]
- Try to traverse to parent "x"
- "x" doesn't exist in root's children
- Return
false
(parent path missing)
This example demonstrates how the Trie structure naturally enforces the file system constraints: paths must be created hierarchically (parents before children), duplicate paths are rejected, and the tree structure allows efficient navigation for both insertion and retrieval operations.
Solution Implementation
1class TrieNode:
2 """
3 A node in the Trie structure representing a directory or file in the file system.
4 Each node can have multiple children (subdirectories/files) and an associated value.
5 """
6 def __init__(self, value: int = -1):
7 # Dictionary to store child nodes, where key is the directory/file name
8 self.children = {}
9 # Value associated with this path (-1 indicates no value set)
10 self.value = value
11
12 def insert(self, path: str, value: int) -> bool:
13 """
14 Insert a new path with its associated value into the Trie.
15
16 Args:
17 path: The file system path (e.g., "/a/b/c")
18 value: The value to associate with this path
19
20 Returns:
21 True if the path was successfully created, False otherwise
22 """
23 current_node = self
24 # Split the path by "/" to get individual components
25 # Example: "/a/b/c" -> ["", "a", "b", "c"]
26 path_components = path.split("/")
27
28 # Check if all parent directories exist (exclude root "" and last component)
29 for component in path_components[1:-1]:
30 if component not in current_node.children:
31 # Parent directory doesn't exist, cannot create path
32 return False
33 current_node = current_node.children[component]
34
35 # Check if the final component already exists
36 last_component = path_components[-1]
37 if last_component in current_node.children:
38 # Path already exists, cannot create duplicate
39 return False
40
41 # Create the new path with its value
42 current_node.children[last_component] = TrieNode(value)
43 return True
44
45 def search(self, path: str) -> int:
46 """
47 Search for a path in the Trie and return its associated value.
48
49 Args:
50 path: The file system path to search for
51
52 Returns:
53 The value associated with the path, or -1 if path doesn't exist
54 """
55 current_node = self
56 # Split path and iterate through each component (skip empty root "")
57 for component in path.split("/")[1:]:
58 if component not in current_node.children:
59 # Path doesn't exist
60 return -1
61 current_node = current_node.children[component]
62
63 return current_node.value
64
65
66class FileSystem:
67 """
68 A file system implementation using a Trie data structure.
69 Supports creating paths with values and retrieving values by path.
70 """
71 def __init__(self):
72 # Initialize the root of the Trie
73 self.trie_root = TrieNode()
74
75 def createPath(self, path: str, value: int) -> bool:
76 """
77 Create a new path in the file system with an associated value.
78 The parent path must already exist.
79
80 Args:
81 path: The path to create (e.g., "/a/b/c")
82 value: The value to associate with the path
83
84 Returns:
85 True if the path was successfully created, False if it already exists
86 or if the parent path doesn't exist
87 """
88 return self.trie_root.insert(path, value)
89
90 def get(self, path: str) -> int:
91 """
92 Retrieve the value associated with a given path.
93
94 Args:
95 path: The path to look up
96
97 Returns:
98 The value associated with the path, or -1 if the path doesn't exist
99 """
100 return self.trie_root.search(path)
101
102
103# Your FileSystem object will be instantiated and called as such:
104# obj = FileSystem()
105# param_1 = obj.createPath(path, value)
106# param_2 = obj.get(path)
107
1/**
2 * Trie node implementation for representing file system paths
3 */
4class Trie {
5 // Map to store child directories/files
6 Map<String, Trie> children = new HashMap<>();
7 // Value associated with this path node
8 int value;
9
10 /**
11 * Constructor to initialize a Trie node with a value
12 * @param value The value to store at this node
13 */
14 Trie(int value) {
15 this.value = value;
16 }
17
18 /**
19 * Inserts a new path into the trie with associated value
20 * @param path The full path to insert (e.g., "/a/b/c")
21 * @param value The value to associate with the path
22 * @return true if insertion successful, false if parent path doesn't exist or path already exists
23 */
24 boolean insert(String path, int value) {
25 Trie currentNode = this;
26 String[] pathSegments = path.split("/");
27
28 // Traverse to the parent directory (stop before last segment)
29 // Start from index 1 to skip empty string before first "/"
30 for (int i = 1; i < pathSegments.length - 1; i++) {
31 String segment = pathSegments[i];
32 // Check if parent path exists
33 if (!currentNode.children.containsKey(segment)) {
34 return false; // Parent path doesn't exist
35 }
36 currentNode = currentNode.children.get(segment);
37 }
38
39 // Check if the final path segment already exists
40 String lastSegment = pathSegments[pathSegments.length - 1];
41 if (currentNode.children.containsKey(lastSegment)) {
42 return false; // Path already exists
43 }
44
45 // Create new node for the final path segment
46 currentNode.children.put(lastSegment, new Trie(value));
47 return true;
48 }
49
50 /**
51 * Searches for a path in the trie and returns its value
52 * @param path The full path to search for
53 * @return The value associated with the path, or -1 if path doesn't exist
54 */
55 int search(String path) {
56 Trie currentNode = this;
57 String[] pathSegments = path.split("/");
58
59 // Traverse the entire path
60 // Start from index 1 to skip empty string before first "/"
61 for (int i = 1; i < pathSegments.length; i++) {
62 String segment = pathSegments[i];
63 // Check if path segment exists
64 if (!currentNode.children.containsKey(segment)) {
65 return -1; // Path doesn't exist
66 }
67 currentNode = currentNode.children.get(segment);
68 }
69
70 // Return the value at the final node
71 return currentNode.value;
72 }
73}
74
75/**
76 * File System implementation using Trie data structure
77 * Supports creating paths and retrieving values associated with paths
78 */
79class FileSystem {
80 // Root node of the trie representing the file system
81 private Trie trie = new Trie(-1);
82
83 /**
84 * Constructor to initialize the file system
85 */
86 public FileSystem() {
87 }
88
89 /**
90 * Creates a new path in the file system with an associated value
91 * @param path The path to create (must start with "/")
92 * @param value The value to associate with the path
93 * @return true if path was created successfully, false otherwise
94 */
95 public boolean createPath(String path, int value) {
96 return trie.insert(path, value);
97 }
98
99 /**
100 * Retrieves the value associated with a given path
101 * @param path The path to query
102 * @return The value associated with the path, or -1 if path doesn't exist
103 */
104 public int get(String path) {
105 return trie.search(path);
106 }
107}
108
109/**
110 * Your FileSystem object will be instantiated and called as such:
111 * FileSystem obj = new FileSystem();
112 * boolean param_1 = obj.createPath(path,value);
113 * int param_2 = obj.get(path);
114 */
115
1class Trie {
2public:
3 // Map from path component to child trie node
4 unordered_map<string, Trie*> children;
5 // Value associated with this path node
6 int value;
7
8 // Constructor to initialize trie node with a value
9 Trie(int val) {
10 this->value = val;
11 }
12
13 // Insert a new path with associated value
14 // Returns true if path was created successfully, false if parent path doesn't exist or path already exists
15 bool insert(string& path, int val) {
16 Trie* currentNode = this;
17
18 // Split path into components (e.g., "/a/b/c" -> ["", "a", "b", "c"])
19 vector<string> pathComponents = split(path, '/');
20
21 // Traverse to the parent directory (skip empty string at index 0, stop before last component)
22 for (int i = 1; i < pathComponents.size() - 1; ++i) {
23 string component = pathComponents[i];
24
25 // Parent path doesn't exist, cannot create the new path
26 if (!currentNode->children.count(component)) {
27 return false;
28 }
29 currentNode = currentNode->children[component];
30 }
31
32 // Check if the path already exists
33 if (currentNode->children.count(pathComponents.back())) {
34 return false;
35 }
36
37 // Create new node for the last path component
38 currentNode->children[pathComponents.back()] = new Trie(val);
39 return true;
40 }
41
42 // Search for a path and return its associated value
43 // Returns -1 if path doesn't exist
44 int search(string& path) {
45 Trie* currentNode = this;
46
47 // Split path into components
48 vector<string> pathComponents = split(path, '/');
49
50 // Traverse the entire path
51 for (int i = 1; i < pathComponents.size(); ++i) {
52 string component = pathComponents[i];
53
54 // Path doesn't exist
55 if (!currentNode->children.count(component)) {
56 return -1;
57 }
58 currentNode = currentNode->children[component];
59 }
60
61 // Return the value at the found node
62 return currentNode->value;
63 }
64
65private:
66 // Helper function to split string by delimiter
67 vector<string> split(string& str, char delimiter) {
68 stringstream stringStream(str);
69 string item;
70 vector<string> result;
71
72 // Extract each component separated by delimiter
73 while (getline(stringStream, item, delimiter)) {
74 result.emplace_back(item);
75 }
76
77 return result;
78 }
79};
80
81class FileSystem {
82public:
83 // Initialize file system with root directory
84 FileSystem() {
85 root = new Trie(-1); // Root has no associated value
86 }
87
88 // Create a new path with associated value
89 // Returns true if successful, false otherwise
90 bool createPath(string path, int value) {
91 return root->insert(path, value);
92 }
93
94 // Get the value associated with a path
95 // Returns -1 if path doesn't exist
96 int get(string path) {
97 return root->search(path);
98 }
99
100private:
101 Trie* root; // Root of the trie structure representing file system
102};
103
104/**
105 * Your FileSystem object will be instantiated and called as such:
106 * FileSystem* obj = new FileSystem();
107 * bool param_1 = obj->createPath(path,value);
108 * int param_2 = obj->get(path);
109 */
110
1// Map structure to store child nodes in the trie
2interface TrieNode {
3 children: Map<string, TrieNode>;
4 value: number;
5}
6
7// Global trie root for the file system
8let fileSystemRoot: TrieNode;
9
10// Initialize the file system with root node
11function initializeFileSystem(): void {
12 fileSystemRoot = {
13 children: new Map<string, TrieNode>(),
14 value: -1
15 };
16}
17
18// Create a new path in the file system with associated value
19function createPath(path: string, value: number): boolean {
20 let currentNode: TrieNode = fileSystemRoot;
21
22 // Split path into segments, removing empty first element from leading slash
23 const pathSegments = path.split('/').slice(1);
24
25 // Traverse to the parent directory of the new path
26 for (let i = 0; i < pathSegments.length - 1; i++) {
27 const segment = pathSegments[i];
28
29 // Parent path doesn't exist, cannot create child
30 if (!currentNode.children.has(segment)) {
31 return false;
32 }
33
34 currentNode = currentNode.children.get(segment)!;
35 }
36
37 // Check if the path already exists
38 const lastSegment = pathSegments[pathSegments.length - 1];
39 if (currentNode.children.has(lastSegment)) {
40 return false;
41 }
42
43 // Create new node for the path
44 currentNode.children.set(lastSegment, {
45 children: new Map<string, TrieNode>(),
46 value: value
47 });
48
49 return true;
50}
51
52// Retrieve the value associated with a given path
53function get(path: string): number {
54 let currentNode: TrieNode = fileSystemRoot;
55
56 // Split path into segments, removing empty first element from leading slash
57 const pathSegments = path.split('/').slice(1);
58
59 // Traverse the trie following the path segments
60 for (const segment of pathSegments) {
61 // Path doesn't exist
62 if (!currentNode.children.has(segment)) {
63 return -1;
64 }
65
66 currentNode = currentNode.children.get(segment)!;
67 }
68
69 // Return the value stored at this path
70 return currentNode.value;
71}
72
73// Initialize the file system on startup
74initializeFileSystem();
75
Time and Space Complexity
Time Complexity:
For the createPath
operation:
- Splitting the path by "/" takes
O(|w|)
where|w|
is the length of the path string - Iterating through path components
ps[1:-1]
takesO(n)
wheren
is the number of components - Each dictionary lookup/insertion is
O(1)
on average - Overall for a single
createPath
:O(|w|)
For the get
operation:
- Splitting the path takes
O(|w|)
- Iterating through components and dictionary lookups takes
O(n)
wheren
is the number of components - Overall for a single
get
:O(|w|)
For all operations across the FileSystem's lifetime with set of paths W
:
- Total time complexity:
O(β_{w β W}|w|)
Space Complexity:
The Trie structure stores:
- Each unique path component as a key in the dictionary
- A Trie node for each path component in all paths
- The total number of characters stored across all path components is bounded by the sum of all path lengths
For the set of all inserted paths W
:
- Total space complexity:
O(β_{w β W}|w|)
This accounts for storing all the path components and the Trie node structure, where the space used is proportional to the total length of all paths inserted into the system.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Handling of Root Path and Empty Segments
One of the most common mistakes is mishandling the split operation on paths. When you split a path like /a/b/c
by /
, you get ["", "a", "b", "c"]
where the first element is an empty string. Many implementations forget to skip this empty string, leading to incorrect traversal.
Problematic Code:
# Wrong - includes empty string path_components = path.split("/") for component in path_components: # This will process "" # Logic here
Correct Approach:
# Correct - skip the empty string path_components = path.split("/") for component in path_components[1:]: # Skip first empty element # Logic here
2. Not Validating Parent Path Existence Before Creating
A critical requirement is that parent paths must exist before creating a child path. For example, you cannot create /a/b/c
if /a/b
doesn't exist. Some implementations only check if the final path exists but forget to verify all parent directories.
Problematic Code:
def insert(self, path: str, value: int) -> bool:
current = self
components = path.split("/")[1:]
# Wrong - directly creates all intermediate nodes
for component in components[:-1]:
if component not in current.children:
current.children[component] = TrieNode() # Creates missing parents!
current = current.children[component]
# Only checks final component
if components[-1] in current.children:
return False
current.children[components[-1]] = TrieNode(value)
return True
Correct Approach:
def insert(self, path: str, value: int) -> bool:
current = self
components = path.split("/")[1:]
# Check all parent paths exist first
for component in components[:-1]:
if component not in current.children:
return False # Parent doesn't exist, cannot create
current = current.children[component]
# Then check and create final component
if components[-1] in current.children:
return False
current.children[components[-1]] = TrieNode(value)
return True
3. Edge Case: Creating Root Path /
The problem states that /
alone is invalid, but some implementations don't handle this edge case properly. When splitting /
by /
, you get ["", ""]
, which could lead to unexpected behavior.
Solution:
def createPath(self, path: str, value: int) -> bool:
# Add validation for edge cases
if not path or path == "/" or not path.startswith("/"):
return False
return self.trie_root.insert(path, value)
4. Confusing Node Value vs Path Existence
Some implementations use the node's value to determine if a path exists (e.g., checking if value == -1
). This is incorrect because -1
could be a valid value that a user wants to store.
Problematic Code:
def search(self, path: str) -> int:
# ... traverse to node ...
if node.value == -1: # Wrong assumption
return -1 # Thinks path doesn't exist
return node.value
Correct Approach:
The existence of a path should be determined by whether the node exists in the parent's children dictionary, not by the node's value. The value -1
should only be returned when the path doesn't exist in the tree structure.
Which of the following problems can be solved with backtracking (select multiple)
Recommended Readings
Trie Introduction Imagine you have a magic dictionary where as soon as you start spelling a word it immediately tells you how many words that start with those letters How cool would that be This is exactly the kind of magic a data structure called trie performs What's a Trie
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Donβt Miss This!