Facebook Pixel

71. Simplify Path

MediumStackString
Leetcode Link

Problem Description

You are given an absolute path for a Unix-style file system that always starts with a slash '/'. Your task is to convert this path into its simplified canonical form.

The Unix-style file system follows these rules:

  • A single period '.' means the current directory
  • A double period '..' means the parent directory (go up one level)
  • Multiple consecutive slashes like '//' or '///' should be treated as a single slash '/'
  • Any sequence of periods that doesn't match the above rules (like '...' or '....') should be treated as a valid directory or file name

The simplified canonical path must:

  • Start with a single slash '/'
  • Separate directories with exactly one slash '/'
  • Not end with a slash '/' (unless it's the root directory)
  • Not contain any '.' or '..' as directory references

For example:

  • Input: "/home/" → Output: "/home"
  • Input: "/../" → Output: "/" (can't go above root)
  • Input: "/home//foo/" → Output: "/home/foo"
  • Input: "/a/./b/../../c/" → Output: "/c"

The solution uses a stack to process each directory component. When encountering '..', it pops from the stack (going up one directory). When encountering a valid directory name, it pushes to the stack. Finally, it joins all stack elements with '/' to form the canonical path.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

Think of navigating through directories like walking through rooms in a building. When you encounter a directory name, you enter that room (go deeper). When you see '..', you go back to the previous room (go up one level). When you see '.', you stay in the same room (no movement).

The key insight is that we need to keep track of our current path as we process each component. A stack is perfect for this because:

  1. Directory navigation is inherently stack-like: When you enter a directory, you're adding to your path (push). When you go back with '..', you're removing the last directory you entered (pop).

  2. We only care about the final state: We don't need to preserve the original path structure - we just need to know where we end up after all the navigation commands.

  3. Sequential processing: We can process the path from left to right, making decisions about each component independently based on simple rules.

By splitting the path on '/', we get individual components that we can evaluate:

  • Empty strings (from consecutive slashes) and '.' don't change our position
  • '..' means go back (pop from stack if possible)
  • Everything else is a real directory name to enter (push to stack)

After processing all components, the stack contains exactly the directories in our final path, in order from root to destination. Joining them with '/' and adding a leading '/' gives us the canonical path.

This approach naturally handles edge cases like trying to go above root (stack is empty, so pop does nothing) and multiple consecutive slashes (they create empty strings that we skip).

Learn more about Stack patterns.

Solution Approach

The solution uses a stack data structure to build the canonical path by processing each directory component sequentially.

Step-by-step implementation:

  1. Split the path into components: Use path.split('/') to break the path into individual directory names. This automatically handles multiple consecutive slashes by creating empty strings between them.

  2. Initialize an empty stack: stk = [] will store the valid directory names in our final path.

  3. Process each component: Iterate through each substring s from the split operation:

    • Skip empty strings and current directory: If s is empty (from consecutive slashes) or equals '.', continue to the next iteration
    • Handle parent directory: If s == '..':
      • Check if the stack is not empty before popping (we can't go above root)
      • If stk has elements, call stk.pop() to remove the last directory
    • Handle regular directories: For any other string (including '...', '....', etc.):
      • Push it onto the stack with stk.append(s)
  4. Build the final path: After processing all components:

    • Join all elements in the stack with '/' separator: '/'.join(stk)
    • Add a leading '/' to ensure the path starts with root: '/' + '/'.join(stk)
    • This automatically handles the root directory case (empty stack returns '/')

Example walkthrough with path "/a/./b/../../c/":

  • Split: ['', 'a', '.', 'b', '..', '..', 'c', '']
  • Process:
    • '' → skip
    • 'a'stack: ['a']
    • '.' → skip, stack: ['a']
    • 'b' → stack: ['a', 'b']
    • '..' → pop, stack: ['a']
    • '..' → pop, stack: []
    • 'c' → stack: ['c']
    • '' → skip
  • Result: '/' + 'c' = '/c'

The time complexity is O(n) where n is the length of the input path, and space complexity is O(n) for the stack storage.

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 trace through the path "/home/../usr/./bin/" step by step:

Initial Setup:

  • Input path: "/home/../usr/./bin/"
  • Split on /: ["", "home", "..", "usr", ".", "bin", ""]
  • Initialize empty stack: []

Processing Each Component:

  1. Component: "" (empty string)

    • This comes from the leading /
    • Action: Skip
    • Stack: []
  2. Component: "home"

    • Valid directory name
    • Action: Push to stack
    • Stack: ["home"]
  3. Component: ".."

    • Parent directory reference
    • Action: Pop from stack (remove "home")
    • Stack: []
  4. Component: "usr"

    • Valid directory name
    • Action: Push to stack
    • Stack: ["usr"]
  5. Component: "."

    • Current directory reference
    • Action: Skip (stay in same directory)
    • Stack: ["usr"]
  6. Component: "bin"

    • Valid directory name
    • Action: Push to stack
    • Stack: ["usr", "bin"]
  7. Component: "" (empty string)

    • This comes from the trailing /
    • Action: Skip
    • Stack: ["usr", "bin"]

Building Final Path:

  • Join stack elements with /: "usr/bin"
  • Add leading /: "/usr/bin"

Result: "/usr/bin"

The stack perfectly tracks our navigation: we entered "home", went back up with "..", entered "usr", stayed put with ".", then entered "bin". The final canonical path shows where we ended up after all these operations.

Solution Implementation

1class Solution:
2    def simplifyPath(self, path: str) -> str:
3        """
4        Simplifies a Unix-style file path by resolving . and .. references.
5      
6        Args:
7            path: A string representing a Unix-style absolute path
8          
9        Returns:
10            The simplified canonical path
11        """
12        # Use a stack to keep track of valid directory names
13        directory_stack = []
14      
15        # Split the path by '/' to get individual components
16        path_components = path.split('/')
17      
18        # Process each component in the path
19        for component in path_components:
20            # Skip empty components (from consecutive slashes) and current directory references
21            if not component or component == '.':
22                continue
23          
24            # Handle parent directory reference
25            if component == '..':
26                # Pop from stack if not empty (go to parent directory)
27                if directory_stack:
28                    directory_stack.pop()
29            else:
30                # Add valid directory name to the stack
31                directory_stack.append(component)
32      
33        # Construct the simplified path from the stack
34        # Always start with '/' for absolute path
35        simplified_path = '/' + '/'.join(directory_stack)
36      
37        return simplified_path
38
1class Solution {
2    public String simplifyPath(String path) {
3        // Use a deque (double-ended queue) as a stack to store valid directory names
4        Deque<String> stack = new ArrayDeque<>();
5      
6        // Split the path by "/" to get individual components
7        String[] components = path.split("/");
8      
9        // Process each component
10        for (String component : components) {
11            // Skip empty strings (consecutive slashes) and current directory references "."
12            if (component.isEmpty() || component.equals(".")) {
13                continue;
14            }
15          
16            // Handle parent directory reference ".."
17            if (component.equals("..")) {
18                // Go back to parent directory by removing the last directory from stack
19                // pollLast() returns null if stack is empty, so it's safe to call
20                stack.pollLast();
21            } else {
22                // Add valid directory name to the stack
23                stack.offerLast(component);
24            }
25        }
26      
27        // Build the simplified path by joining all directories with "/"
28        // Always start with "/" for absolute path
29        return "/" + String.join("/", stack);
30    }
31}
32
1class Solution {
2public:
3    string simplifyPath(string path) {
4        // Use deque to store valid directory names
5        deque<string> directoryStack;
6      
7        // Create stringstream to split path by '/'
8        stringstream pathStream(path);
9        string token;
10      
11        // Process each component between '/' delimiters
12        while (getline(pathStream, token, '/')) {
13            // Skip empty tokens (consecutive slashes) and current directory references
14            if (token == "" || token == ".") {
15                continue;
16            }
17          
18            // Handle parent directory reference
19            if (token == "..") {
20                // Go up one level if not already at root
21                if (!directoryStack.empty()) {
22                    directoryStack.pop_back();
23                }
24            } else {
25                // Add valid directory name to stack
26                directoryStack.push_back(token);
27            }
28        }
29      
30        // Handle root directory case
31        if (directoryStack.empty()) {
32            return "/";
33        }
34      
35        // Build the simplified path
36        string simplifiedPath;
37        for (const auto& directory : directoryStack) {
38            simplifiedPath += "/" + directory;
39        }
40      
41        return simplifiedPath;
42    }
43};
44
1/**
2 * Simplifies a Unix-style file path by removing redundant elements
3 * @param path - The input path string to simplify
4 * @returns The simplified canonical path
5 */
6function simplifyPath(path: string): string {
7    // Stack to store valid directory names
8    const directoryStack: string[] = [];
9  
10    // Split the path by '/' and process each component
11    const pathComponents: string[] = path.split('/');
12  
13    for (const component of pathComponents) {
14        // Skip empty components (consecutive slashes) and current directory references
15        if (component === '' || component === '.') {
16            continue;
17        }
18      
19        // Handle parent directory reference
20        if (component === '..') {
21            // Pop from stack if not empty (go to parent directory)
22            if (directoryStack.length > 0) {
23                directoryStack.pop();
24            }
25        } else {
26            // Valid directory name, add to stack
27            directoryStack.push(component);
28        }
29    }
30  
31    // Construct the simplified path with leading slash
32    return '/' + directoryStack.join('/');
33}
34

Time and Space Complexity

Time Complexity: O(n), where n is the length of the path string.

The algorithm performs the following operations:

  • path.split('/'): This operation traverses the entire string once to split it by '/', which takes O(n) time.
  • The for loop iterates through each component produced by the split operation. In the worst case, there could be O(n) components (though typically much fewer).
  • Inside the loop, each operation (append, pop, string comparison) takes O(1) time for each component.
  • '/'.join(stk): This operation takes O(m) time where m is the total length of all strings in the stack, which is bounded by O(n) since these strings came from the original path.

Overall, the time complexity is O(n) + O(n) = O(n).

Space Complexity: O(n), where n is the length of the path string.

The space usage includes:

  • The stk list: In the worst case, if the path contains no '..' or '.' and all valid directory names, the stack could store all components from the path, using up to O(n) space.
  • The result of path.split('/'): This creates a list of substrings that together can be at most O(n) characters.
  • The final string created by '/' + '/'.join(stk): This creates a new string of length at most O(n).

Therefore, the total space complexity is O(n).

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Incorrectly handling the root directory case

A common mistake is forgetting that when the stack is empty, the result should be "/" (root directory), not an empty string.

Incorrect approach:

# This would return an empty string for root directory
return '/'.join(directory_stack)  # Wrong: returns "" when stack is empty

Correct solution:

# Always prepend '/' to ensure we have at least the root
return '/' + '/'.join(directory_stack)  # Returns "/" when stack is empty

2. Not properly handling consecutive slashes

Some developers try to manually handle consecutive slashes with complex logic instead of leveraging the split function's behavior.

Incorrect approach:

# Trying to manually remove consecutive slashes
while '//' in path:
    path = path.replace('//', '/')  # Unnecessary preprocessing

Correct solution:

# split('/') naturally handles consecutive slashes by creating empty strings
path_components = path.split('/')
# Then just skip empty components in the loop
if not component or component == '.':
    continue

3. Treating special directory names incorrectly

A critical mistake is assuming that any sequence of dots should be treated specially. Only exactly "." and ".." have special meanings.

Incorrect approach:

# Wrong: treating "..." as going up multiple directories
if component.startswith('..'):
    for _ in range(len(component) - 1):
        if directory_stack:
            directory_stack.pop()

Correct solution:

# Only ".." means parent directory; "..." is a valid directory name
if component == '..':
    if directory_stack:
        directory_stack.pop()
else:
    # "...", "....", etc. are treated as regular directory names
    directory_stack.append(component)

4. Attempting to go above the root directory

When encountering ".." at the root level, some implementations might crash or produce incorrect results.

Incorrect approach:

# This could cause an error if stack is empty
if component == '..':
    directory_stack.pop()  # IndexError when stack is empty!

Correct solution:

# Always check if stack has elements before popping
if component == '..':
    if directory_stack:  # Only pop if there's something to pop
        directory_stack.pop()

5. Adding a trailing slash to the result

The problem specifically states that the result should not end with a slash unless it's the root directory.

Incorrect approach:

# Wrong: always adding a trailing slash
return '/' + '/'.join(directory_stack) + '/'

Correct solution:

# No trailing slash (except for root which is just "/")
return '/' + '/'.join(directory_stack)
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

What's the output of running the following function using input [30, 20, 10, 100, 33, 12]?

1def fun(arr: List[int]) -> List[int]:
2    import heapq
3    heapq.heapify(arr)
4    res = []
5    for i in range(3):
6        res.append(heapq.heappop(arr))
7    return res
8
1public static int[] fun(int[] arr) {
2    int[] res = new int[3];
3    PriorityQueue<Integer> heap = new PriorityQueue<>();
4    for (int i = 0; i < arr.length; i++) {
5        heap.add(arr[i]);
6    }
7    for (int i = 0; i < 3; i++) {
8        res[i] = heap.poll();
9    }
10    return res;
11}
12
1class HeapItem {
2    constructor(item, priority = item) {
3        this.item = item;
4        this.priority = priority;
5    }
6}
7
8class MinHeap {
9    constructor() {
10        this.heap = [];
11    }
12
13    push(node) {
14        // insert the new node at the end of the heap array
15        this.heap.push(node);
16        // find the correct position for the new node
17        this.bubble_up();
18    }
19
20    bubble_up() {
21        let index = this.heap.length - 1;
22
23        while (index > 0) {
24            const element = this.heap[index];
25            const parentIndex = Math.floor((index - 1) / 2);
26            const parent = this.heap[parentIndex];
27
28            if (parent.priority <= element.priority) break;
29            // if the parent is bigger than the child then swap the parent and child
30            this.heap[index] = parent;
31            this.heap[parentIndex] = element;
32            index = parentIndex;
33        }
34    }
35
36    pop() {
37        const min = this.heap[0];
38        this.heap[0] = this.heap[this.size() - 1];
39        this.heap.pop();
40        this.bubble_down();
41        return min;
42    }
43
44    bubble_down() {
45        let index = 0;
46        let min = index;
47        const n = this.heap.length;
48
49        while (index < n) {
50            const left = 2 * index + 1;
51            const right = left + 1;
52
53            if (left < n && this.heap[left].priority < this.heap[min].priority) {
54                min = left;
55            }
56            if (right < n && this.heap[right].priority < this.heap[min].priority) {
57                min = right;
58            }
59            if (min === index) break;
60            [this.heap[min], this.heap[index]] = [this.heap[index], this.heap[min]];
61            index = min;
62        }
63    }
64
65    peek() {
66        return this.heap[0];
67    }
68
69    size() {
70        return this.heap.length;
71    }
72}
73
74function fun(arr) {
75    const heap = new MinHeap();
76    for (const x of arr) {
77        heap.push(new HeapItem(x));
78    }
79    const res = [];
80    for (let i = 0; i < 3; i++) {
81        res.push(heap.pop().item);
82    }
83    return res;
84}
85

Recommended Readings

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

Load More