71. Simplify Path
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.
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:
-
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). -
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.
-
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:
-
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. -
Initialize an empty stack:
stk = []
will store the valid directory names in our final path. -
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, callstk.pop()
to remove the last directory
- Handle regular directories: For any other string (including
'...'
,'....'
, etc.):- Push it onto the stack with
stk.append(s)
- Push it onto the stack with
- Skip empty strings and current directory: If
-
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
'/'
)
- Join all elements in the stack with
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 EvaluatorExample 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:
-
Component:
""
(empty string)- This comes from the leading
/
- Action: Skip
- Stack:
[]
- This comes from the leading
-
Component:
"home"
- Valid directory name
- Action: Push to stack
- Stack:
["home"]
-
Component:
".."
- Parent directory reference
- Action: Pop from stack (remove "home")
- Stack:
[]
-
Component:
"usr"
- Valid directory name
- Action: Push to stack
- Stack:
["usr"]
-
Component:
"."
- Current directory reference
- Action: Skip (stay in same directory)
- Stack:
["usr"]
-
Component:
"bin"
- Valid directory name
- Action: Push to stack
- Stack:
["usr", "bin"]
-
Component:
""
(empty string)- This comes from the trailing
/
- Action: Skip
- Stack:
["usr", "bin"]
- This comes from the trailing
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 takesO(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) takesO(1)
time for each component. '/'.join(stk)
: This operation takesO(m)
time wherem
is the total length of all strings in the stack, which is bounded byO(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 toO(n)
space. - The result of
path.split('/')
: This creates a list of substrings that together can be at mostO(n)
characters. - The final string created by
'/' + '/'.join(stk)
: This creates a new string of length at mostO(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)
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
Stack Intro Imagine you have a pile of books on your desk If you want to add a new book you place it on top If you want to read a book you take it from the top And if you simply want to see which book is on the top you
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!