Prerequisites

Before attempting this problem, you should be comfortable with:

  • Tree Data Structures - Modeling hierarchical relationships like directories and subdirectories
  • Hash Maps/Dictionaries - Storing and retrieving children nodes by name efficiently
  • String Parsing - Splitting file paths by delimiters and traversing path components
  • Object-Oriented Design - Creating classes to represent files and directories with their attributes

1. Using separate Directory and File List

Intuition

A file system has a natural tree structure where each directory can contain subdirectories and files. We model this by creating a Dir class that holds two hash maps: one for subdirectories and one for files. The root of the file system is a single Dir object. When we need to navigate to a path, we split it by "/" and traverse through the tree one directory at a time. This separation of directories and files makes it easy to distinguish between them when listing contents or reading file data.

Algorithm

  1. Initialization: Create a root Dir object with empty maps for subdirectories and files.
  2. ls(path): Split the path and traverse to the target. If the last component is a file, return just its name. Otherwise, collect all directory and file names at that location, sort them, and return.
  3. mkdir(path): Split the path and traverse from root. At each level, create a new Dir if it does not exist, then move into it.
  4. addContentToFile(filePath, content): Navigate to the parent directory, then append the content to the file (creating it if needed).
  5. readContentFromFile(filePath): Navigate to the parent directory and return the file's content.
class FileSystem:
    class Dir:
        def __init__(self):
            self.dirs = {}
            self.files = {}
    
    def __init__(self):
        self.root = self.Dir()
    
    def ls(self, path: str) -> List[str]:
        t = self.root
        files = []
        
        if path != "/":
            d = path.split("/")

            for i in range(1, len(d) - 1):
                t = t.dirs[d[i]]
            
            if d[-1] in t.files:
                files.append(d[-1])
                return files
            else:
                t = t.dirs[d[-1]]
        
        files.extend(t.dirs.keys())
        files.extend(t.files.keys())
        files.sort()
        return files
    
    def mkdir(self, path: str) -> None:
        t = self.root
        d = path.split("/")
        
        for i in range(1, len(d)):
            if d[i] not in t.dirs:
                t.dirs[d[i]] = self.Dir()
            t = t.dirs[d[i]]
    
    def addContentToFile(self, filePath: str, content: str) -> None:
        t = self.root
        d = filePath.split("/")
        
        for i in range(1, len(d) - 1):
            t = t.dirs[d[i]]
        
        if d[-1] not in t.files:
            t.files[d[-1]] = ""
        t.files[d[-1]] += content
    
    def readContentFromFile(self, filePath: str) -> str:
        t = self.root
        d = filePath.split("/")
        
        for i in range(1, len(d) - 1):
            t = t.dirs[d[i]]
        
        return t.files[d[-1]]

Time Complexity

  • The time complexity of executing an ls command : O(m+n+klog(k))O(m + n + k\log (k))

    • Here mm is the length of the input string, nn is the depth of the last directory level in the given input for ls, and kk is the number of entries(files + subdirectories) in the last level directory(in the current input)
  • The time complexity of executing a mkdir command : O(m+n)O(m + n)

    • Here mm is the length of the input string and nn is the depth of the last directory level in the given input for mkdir
  • The time complexity of both addContentToFile and readContentFromFile : O(m+n)O(m + n)

    • Here mm is the length of the input string and nn refers to the depth of the file name in the current input

Common Pitfalls

Forgetting to Handle the Root Path

When the path is "/", splitting by "/" produces an empty string or array depending on the language. Failing to handle this case causes index errors or incorrect traversal.

# Wrong: assumes d[1] always exists
d = path.split("/")
t = t.dirs[d[1]]  # IndexError when path is "/"

# Correct: check for root path first
if path != "/":
    d = path.split("/")
    for i in range(1, len(d)):
        t = t.dirs[d[i]]

Confusing Files and Directories in ls()

The ls command must return just the file name (not contents) when the path points to a file, but return sorted directory contents when pointing to a directory. Mixing up these cases leads to wrong output.

# Wrong: always treating path as directory
return sorted(t.dirs.keys()) + sorted(t.files.keys())

# Correct: check if final component is a file
if d[-1] in t.files:
    return [d[-1]]  # Return just the filename
else:
    t = t.dirs[d[-1]]
    return sorted(list(t.dirs.keys()) + list(t.files.keys()))

Replacing File Content Instead of Appending

The addContentToFile method should append to existing content, not replace it. This is a common oversight that causes test failures.

# Wrong: overwrites existing content
t.files[filename] = content

# Correct: append to existing content
t.files[filename] = t.files.get(filename, "") + content

Off-by-One Errors in Path Traversal

When navigating to a file's parent directory, you must stop one level before the final component. Traversing all the way causes you to look for the file as a directory.

# Wrong: traverses to the file itself (crashes if file isn't a directory)
for i in range(1, len(d)):
    t = t.dirs[d[i]]

# Correct: stop before the last component
for i in range(1, len(d) - 1):
    t = t.dirs[d[i]]
# Now access the file: t.files[d[-1]]

Not Sorting ls() Output

The problem requires ls to return entries in lexicographical order. Forgetting to sort causes wrong answer even when the data structure is correct.

# Wrong: returns unsorted keys
return list(t.dirs.keys()) + list(t.files.keys())

# Correct: sort the combined result
files = list(t.dirs.keys()) + list(t.files.keys())
files.sort()
return files

2. Using unified Directory and File List

Intuition

Instead of maintaining separate maps for directories and files, we can use a single unified structure. Each node in our tree is a File object that can act as either a directory or a file. A boolean flag isFile tells us which role it plays. Directories store child nodes in a map, while files store their content in a string. This unified approach simplifies the data structure since we only need one type of node, and path traversal becomes more uniform.

Algorithm

  1. Initialization: Create a root File object with isFile = false and an empty children map.
  2. ls(path): Split the path and traverse through children. If the final node is a file, return its name. Otherwise, return the sorted keys of its children map.
  3. mkdir(path): Split the path and create child File nodes (as directories) along the way if they do not exist.
  4. addContentToFile(filePath, content): Traverse to the parent, create the file node if missing, mark it as a file, and append the content.
  5. readContentFromFile(filePath): Traverse to the file node and return its content.
class FileSystem:
    class File:
        def __init__(self):
            self.isfile = False
            self.files = {}
            self.content = ""

    def __init__(self):
        self.root = self.File()

    def ls(self, path: str) -> List[str]:
        t = self.root
        files = []
        if path != "/":
            d = path.split("/")
            for i in range(1, len(d)):
                t = t.files[d[i]]
            if t.isfile:
                files.append(d[-1])
                return files
        res_files = sorted(t.files.keys())
        return res_files

    def mkdir(self, path: str) -> None:
        t = self.root
        d = path.split("/")
        for i in range(1, len(d)):
            if d[i] not in t.files:
                t.files[d[i]] = self.File()
            t = t.files[d[i]]

    def addContentToFile(self, filePath: str, content: str) -> None:
        t = self.root
        d = filePath.split("/")
        for i in range(1, len(d) - 1):
            t = t.files[d[i]]
        if d[-1] not in t.files:
            t.files[d[-1]] = self.File()
        t = t.files[d[-1]]
        t.isfile = True
        t.content += content

    def readContentFromFile(self, filePath: str) -> str:
        t = self.root
        d = filePath.split("/")
        for i in range(1, len(d) - 1):
            t = t.files[d[i]]
        return t.files[d[-1]].content

Time Complexity

  • The time complexity of executing an ls command : O(m+n+klog(k))O(m + n + k\log (k))

    • Here mm is the length of the input string, nn is the depth of the last directory level in the given input for ls, and kk is the number of entries(files + subdirectories) in the last level directory(in the current input)
  • The time complexity of executing a mkdir command : O(m+n)O(m + n)

    • Here mm is the length of the input string and nn is the depth of the last directory level in the given input for mkdir
  • The time complexity of both addContentToFile and readContentFromFile : O(m+n)O(m + n)

    • Here mm is the length of the input string and nn refers to the depth of the file name in the current input