Facebook Pixel

1166. Design File System πŸ”’

MediumDesignTrieHash TableString
Leetcode Link

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:

  1. 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)
  2. 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 nodes a, then b, checking that parent paths exist before creating c
  • When searching for a path, it traverses the trie following the path segments and returns the value at the final node
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. Check if parent paths exist when creating a new path
  2. 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 nodes
  • v: 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)

  1. Start from the root node
  2. Split the path by / to get individual segments: ps = path.split("/")
  3. 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
  4. 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
  5. Return true for successful creation

Getting a Value - search(path)

  1. Start from the root node
  2. Split the path by / and iterate through all segments path.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
  3. After traversing all segments, return the value node.v of the final node

Time and Space Complexity

  • Time Complexity: Both createPath and get operations are O(n) where n is the length of the path string, as we need to split the path and traverse through each segment
  • Space Complexity: O(m * k) where m is the total number of paths created and k 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 Evaluator

Example Walkthrough

Let's walk through a simple example to illustrate how the Trie-based file system works.

Example Operations:

  1. createPath("/a", 1) β†’ returns true
  2. createPath("/a/b", 2) β†’ returns true
  3. createPath("/a/b/c", 3) β†’ returns true
  4. createPath("/a/d", 4) β†’ returns true
  5. get("/a/b") β†’ returns 2
  6. createPath("/a/b", 5) β†’ returns false (already exists)
  7. createPath("/x/y", 6) β†’ returns false (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] takes O(n) where n 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) where n 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.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which of the following problems can be solved with backtracking (select multiple)


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More