Skip to content

Commit 9c8c113

Browse files
committed
feat(algorithms, stack): simplify path
1 parent bf48799 commit 9c8c113

3 files changed

Lines changed: 215 additions & 0 deletions

File tree

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
# Simplify Path
2+
3+
You are given an absolute path for a Unix-style file system, which always begins with a slash '/'. Your task is to
4+
transform this absolute path into its simplified canonical path.
5+
6+
The rules of a Unix-style file system are as follows:
7+
8+
- A single period '.' represents the current directory.
9+
- A double period '..' represents the previous/parent directory.
10+
- Multiple consecutive slashes such as '//' and '///' are treated as a single slash '/'.
11+
- Any sequence of periods that does not match the rules above should be treated as a valid directory or file name. For
12+
example, '...' and '....' are valid directory or file names.
13+
14+
The simplified canonical path should follow these rules:
15+
16+
- The path must start with a single slash '/'.
17+
- Directories within the path must be separated by exactly one slash '/'.
18+
- The path must not end with a slash '/', unless it is the root directory.
19+
- The path must not have any single or double periods ('.' and '..') used to denote current or parent directories.
20+
21+
Return the simplified canonical path.
22+
23+
## Examples
24+
25+
Example 1:
26+
27+
```text
28+
Input: path = "/home/"
29+
30+
Output: "/home"
31+
32+
Explanation:
33+
34+
The trailing slash should be removed.
35+
```
36+
37+
Example 2:
38+
```text
39+
Input: path = "/home//foo/"
40+
41+
Output: "/home/foo"
42+
43+
Explanation:
44+
45+
Multiple consecutive slashes are replaced by a single one.
46+
```
47+
48+
Example 3:
49+
50+
```text
51+
Input: path = "/home/user/Documents/../Pictures"
52+
53+
Output: "/home/user/Pictures"
54+
55+
Explanation:
56+
57+
A double period ".." refers to the directory up a level (the parent directory).
58+
```
59+
60+
Example 4:
61+
62+
```text
63+
Input: path = "/../"
64+
65+
Output: "/"
66+
67+
Explanation:
68+
69+
Going one level up from the root directory is not possible.
70+
```
71+
72+
Example 5:
73+
```text
74+
Input: path = "/.../a/../b/c/../d/./"
75+
76+
Output: "/.../b/d"
77+
78+
Explanation:
79+
80+
"..." is a valid name for a directory in this problem.
81+
```
82+
83+
## Constraints
84+
85+
- 1 <= path.length <= 3000
86+
- path consists of English letters, digits, period '.', slash '/' or '_'.
87+
- path is a valid absolute Unix path.
88+
89+
## Topics
90+
91+
- String
92+
- Stack
93+
94+
## Solution
95+
96+
Think of navigating through directories like walking through rooms in a building. When you encounter a directory name,
97+
you enter that room (go deeper). When you see '..', you go back to the previous room (go up one level). When you see '.',
98+
you stay in the same room (no movement).
99+
100+
The key insight is that we need to keep track of our current path as we process each component. A stack is perfect for
101+
this because:
102+
103+
- Directory navigation is inherently stack-like: When you enter a directory, you're adding to your path (push). When you
104+
go back with '..', you're removing the last directory you entered (pop).
105+
- We only care about the final state: We don't need to preserve the original path structure - we just need to know where
106+
we end up after all the navigation commands.
107+
- Sequential processing: We can process the path from left to right, making decisions about each component independently
108+
based on simple rules.
109+
110+
By splitting the path on '/', we get individual components that we can evaluate:
111+
112+
- Empty strings (from consecutive slashes) and '.' don't change our position
113+
- '..' means go back (pop from stack if possible)
114+
- Everything else is a real directory name to enter (push to stack)
115+
116+
After processing all components, the stack contains exactly the directories in our final path, in order from root to
117+
destination. Joining them with '/' and adding a leading '/' gives us the canonical path.
118+
119+
This approach naturally handles edge cases like trying to go above root (stack is empty, so pop does nothing) and
120+
multiple consecutive slashes (they create empty strings that we skip).
121+
122+
Step-by-step implementation:
123+
124+
- Split the path into components: Use path.split('/') to break the path into individual directory names. This
125+
automatically handles multiple consecutive slashes by creating empty strings between them.
126+
127+
- Initialize an empty stack: stk = [] will store the valid directory names in our final path.
128+
- Process each component: Iterate through each substring s from the split operation:
129+
- Skip empty strings and current directory: If s is empty (from consecutive slashes) or equals '.', continue to the
130+
next iteration
131+
- Handle parent directory: If s == '..':
132+
- Check if the stack is not empty before popping (we can't go above root)
133+
- If stk has elements, call stk.pop() to remove the last directory
134+
- Handle regular directories: For any other string (including '...', '....', etc.):
135+
- Push it onto the stack with stk.append(s)
136+
- Build the final path: After processing all components:
137+
- Join all elements in the stack with '/' separator: '/'.join(stk)
138+
- Add a leading '/' to ensure the path starts with root: '/' + '/'.join(stk)
139+
- This automatically handles the root directory case (empty stack returns '/')
140+
141+
### Time and Space Complexity
142+
143+
#### Time Complexity: O(n), where n is the length of the path string.
144+
145+
The algorithm performs the following operations:
146+
147+
- path.split('/'): This operation traverses the entire string once to split it by '/', which takes O(n) time.
148+
- The for loop iterates through each component produced by the split operation. In the worst case, there could be O(n)
149+
components (though typically much fewer).
150+
- Inside the loop, each operation (append, pop, string comparison) takes O(1) time for each component.
151+
- '/'.join(stk): This operation takes O(m) time where m is the total length of all strings in the stack, which is
152+
bounded by O(n) since these strings came from the original path.
153+
154+
Overall, the time complexity is O(n) + O(n) = O(n).
155+
156+
#### Space Complexity: O(n), where n is the length of the path string.
157+
158+
The space usage includes:
159+
160+
- The stk list: In the worst case, if the path contains no '..' or '.' and all valid directory names, the stack could
161+
store all components from the path, using up to O(n) space.
162+
- The result of path.split('/'): This creates a list of substrings that together can be at most O(n) characters.
163+
- The final string created by '/' + '/'.join(stk): This creates a new string of length at most O(n).
164+
165+
Therefore, the total space complexity is O(n).
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
def simplify_path(path: str) -> str:
2+
# Use a stack to keep track of valid directory names
3+
directory_stack = []
4+
5+
# Split the path by '/' to get individual components
6+
path_components = path.split("/")
7+
8+
# Process each component in the path
9+
for component in path_components:
10+
# Skip empty components (from consecutive slashes) and current directory references
11+
if not component or component == ".":
12+
continue
13+
14+
# Handle parent directory reference
15+
if component == "..":
16+
# Pop from stack if not empty (go to parent directory)
17+
if directory_stack:
18+
directory_stack.pop()
19+
else:
20+
# Add valid directory name to the stack
21+
directory_stack.append(component)
22+
23+
# Construct the simplified path from the stack
24+
# Always start with '/' for absolute path
25+
simplified_path = "/" + "/".join(directory_stack)
26+
27+
return simplified_path
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import unittest
2+
from parameterized import parameterized
3+
from utils.test_utils import custom_test_name_func
4+
from algorithms.stack.simplify_path import simplify_path
5+
6+
SIMPLIFY_PATH_TEST_CASES = [
7+
("/home/", "/home"),
8+
("/home//foo/", "/home/foo"),
9+
("/home/user/Documents/../Pictures", "/home/user/Pictures"),
10+
("/../", "/"),
11+
("/.../a/../b/c/../d/./", "/.../b/d"),
12+
]
13+
14+
15+
class SimplifyPathTestCase(unittest.TestCase):
16+
@parameterized.expand(SIMPLIFY_PATH_TEST_CASES, name_func=custom_test_name_func)
17+
def test_simplify_path(self, path: str, expected: str):
18+
actual = simplify_path(path)
19+
self.assertEqual(expected, actual)
20+
21+
22+
if __name__ == "__main__":
23+
unittest.main()

0 commit comments

Comments
 (0)