Solving tips
- Tokenize first: split on '/', then each component is one decision, with '..' meaning 'undo the most recent directory' which is a stack pop.
- Skip empty strings (from // and the leading /) and '.'; push everything else, including tricky names like '...' and 'a..b' which are ordinary directories.
- Guard the pop: '..' at the root (empty stack) must be a silent no-op, not an error.
- Build the answer as '/' + '/'.join(stack), which naturally yields '/' for an empty stack; target O(n) time and O(n) space.
Problem
Given an absolute Unix-style file path (a string starting with /), reduce it to its canonical form:
- A single dot
. means “current directory” — it disappears.
- A double dot
.. means “go up one directory” — it removes the previous directory name (at the root it does nothing).
- Multiple consecutive slashes collapse into one.
- Any other run of characters between slashes — including names like
... or a..b — is a legitimate directory name and must be kept verbatim.
The canonical path must start with exactly one /, use single slashes between names, not end with a trailing slash (unless the whole path is just /), and contain no . or .. components.
Examples
Example 1
Input: path = "/home//foo/./"
Output: "/home/foo"
Explanation: the double slash collapses, . is dropped, and the trailing slash is removed.
Example 2
Input: path = "/a/b/../../c/"
Output: "/c"
Explanation: the first .. cancels b, the second cancels a, leaving only c.
Example 3
Input: path = "/../.../up"
Output: "/.../up"
Explanation: .. at the root is a no-op, and ... is an ordinary directory name, not a “go up twice.”
Constraints
1 <= path.length <= 3000
path consists of letters, digits, ., /, and _, and begins with /.
- A single linear pass is expected — O(n) time.
Think about it first
Hint 1
Split the path on `/`. What do the empty pieces produced by `//` and by the leading slash correspond to?
Hint 2
`..` undoes the most recently entered directory. Which data structure gives you "undo the most recent thing" for free?
Hint 3
Walk the split components with a stack: skip `""` and `"."`; pop (if non-empty) on `".."`; push anything else. The answer is `"/" + "/".join(stack)`.
TL;DR
Split on / and replay the components onto a stack (.. pops, names push) — O(n) time, O(n) space.
Approach 1 — Brute force (repeated textual rewriting)
Keep applying the rules directly to the string until nothing changes: collapse // to /, delete /./, and resolve the leftmost /<name>/../ by scanning back for the previous slash and cutting the segment out.
class Solution:
def simplifyPath(self, path: str) -> str:
path += "/"
changed = True
while changed:
changed = False
if "//" in path:
path = path.replace("//", "/")
changed = True
elif "/./" in path:
path = path.replace("/./", "/")
changed = True
else:
i = path.find("/../")
if i != -1:
j = path.rfind("/", 0, i) if i > 0 else 0
path = path[:j] + path[i + 3 :]
changed = True
if len(path) > 1 and path.endswith("/"):
path = path[:-1]
return path if path else "/"
Complexity: each rewrite copies the whole string and there can be O(n) rewrites, so O(n²) time, O(n) space.
At n = 3000 this still runs, but it is fiddly, easy to get wrong (e.g. distinguishing /../ from /.../), and quadratic — the constraint is really telling you to tokenize instead of doing string surgery.
Approach 2 — Split + stack
The insight: slashes are just separators — after path.split("/") every rule becomes a decision about one whole component, and .. means “undo the most recent kept component,” which is exactly a stack pop. Empty strings (from // and the leading /) and "." are skipped; everything else, including "...", is a name to push.
class Solution:
def simplifyPath(self, path: str) -> str:
stack: list[str] = []
for comp in path.split("/"):
if comp == "" or comp == ".":
continue
if comp == "..":
if stack:
stack.pop()
else:
stack.append(comp)
return "/" + "/".join(stack)
Walkthrough of Example 2 — path = "/a/b/../../c/":
split("/") → ["", "a", "b", "..", "..", "c", ""]
| comp | action | stack |
|---|
"" | skip | — |
a | push | a |
b | push | a b |
.. | pop b | a |
.. | pop a | (empty) |
c | push | c |
"" | skip | c |
Result: "/" + "c" → "/c". Matches. (In Example 3, .. arrives while the stack is empty — the if stack guard makes it a no-op, and ... falls through to the push branch as a plain name.)
Complexity: O(n) time (split, one pass, join), O(n) space for the components and stack.
Variant worth knowing — manual tokenizer, no split
Interviewers sometimes ask for the same thing without str.split, to see the two-pointer scan. Same algorithm, hand-rolled tokenization:
class Solution:
def simplifyPath(self, path: str) -> str:
stack: list[str] = []
i, n = 0, len(path)
while i < n:
while i < n and path[i] == "/":
i += 1
j = i
while j < n and path[j] != "/":
j += 1
comp = path[i:j]
if comp == "..":
if stack:
stack.pop()
elif comp and comp != ".":
stack.append(comp)
i = j
return "/" + "/".join(stack)
Complexity: O(n) time, O(n) space — identical; it only trades library splitting for explicit pointers.
Common pitfalls
- Treating anything starting with dots as special:
"..." and "a..b" are valid directory names — only the exact strings "." and ".." have meaning.
- Popping on
".." without checking the stack is non-empty — going above root must be a silent no-op, not an exception.
- Building the result as
"/".join(stack) without the leading "/", or leaving a trailing slash; the empty-stack case must return exactly "/" (the join formula handles it: "/" + "").
- Doing
replace passes on the raw string and matching ".." inside "..." — tokenize first, then compare whole components.
Pattern takeaway
When input rules include an “undo the most recent item” operation (.., backspace, canceling pair), tokenize the input and replay the tokens onto a stack: keeps push, undos pop, no-ops skip. The stack’s final contents are the canonical answer — the same skeleton handles path canonicalization, backspace-editing, and tag matching.