Simplify Path
Treating this as string cleanup is how you lose the problem. Removing every dot breaks valid names such as ..., while careless parent handling can let /../…

Simplify Path
Given an absolute Unix-style path, return its simplified canonical path by resolving current-directory markers '.', parent-directory markers '..', and redundant slashes while preserving valid names such as '...' or '....'.
Constraints
- 1 <= path.length <= 3000
- path consists of English letters, digits, '.', '/', or '_'
- path is a valid absolute Unix path
Important details
- The result starts with exactly one slash and uses exactly one slash between directory names.
- The result has no trailing slash unless it is the root path.
- A single period denotes the current directory and a double period denotes the parent directory; moving above the root leaves the path at the root.
- Other sequences of periods are ordinary directory or file names.
- Return the canonical path.
Key topics
Treating this as string cleanup is how you lose the problem. Removing every dot breaks valid names such as ..., while careless parent handling can let /../ escape the root.
The reliable Simplify Path solution is a left-to-right simulation:
- Split the path into slash-separated components.
- Ignore empty components and the exact token
".". - Pop the most recent component for
".."when possible. - Push every other component.
- Reconstruct the answer from the stack.
The stack represents the current canonical path. Once that meaning is correct, the code becomes small.
Read the Path Contract First
The input is an absolute Unix-style path. It begins with /, has length at most 3000, and uses letters, digits, periods, slashes, and underscores.
The output must:
- Begin with exactly one
/. - Use exactly one
/between components. - Have no trailing slash unless the result is the root path
/. - Remove the control tokens
"."and".."from the final representation. - Treat repeated slashes as a single separator.
- Never move above the root.
The crucial detail is exact token matching:
| Component | Meaning |
|---|---|
"" | Empty component caused by repeated, leading, or trailing slashes |
"." | Current directory; no state change |
".." | Parent directory; remove the latest component if one exists |
| Anything else | An ordinary directory or file-name component |
So "..." and "...." are ordinary names. They are not variations of "." or "..".
This is a canonical path parser under the rules supplied by the problem. It is lexical processing: we are resolving the characters and components in the input, not asking the operating system to inspect a real filesystem or resolve symbolic links.
Make the Stack the Current Path
The recognition clue is simple:
Components are processed in order, and
".."cancels the most recently entered component.
That is last-added-first-removed behavior. A stack fits the state transition exactly.
Define the stack precisely:
Each stack entry is an ordinary component currently present in the canonical path, stored from root toward the current location.
For example, after processing /home/user, the stack is:
["home", "user"]
The stack does not store slashes, ".", or "..". It stores only the names that survive canonicalization.
Each component produces one of four transitions:
- Empty component: ignore it.
".": ignore it because the current location does not change."..": pop the stack if it is nonempty.- Ordinary name: append it to the stack.
An empty stack represents the root /. That gives root clamping for free: if ".." appears at the root, there is nothing to pop, so the path stays at /.
This is related to the broader stack pattern used for nested structures, but the meaning is different. Here, the stack is not matching opening and closing symbols. It is modeling reversible navigation state.
Derive the One-Pass Algorithm
A tempting baseline is repeated string replacement:
- Remove
"/./". - Collapse
"//". - Remove the component before
"/..". - Repeat until nothing changes.
The problem is that string rewriting loses the clean boundary between components. A replacement can create a new pattern that needs another scan, and the interactions between slashes, dots, and parent moves become difficult to reason about.
The stack preserves those boundaries. We classify each component once, update the current state once, and reconstruct only after the scan.
The pseudocode is:
stack = []
for component in path split by "/":
if component is empty or component == ".":
continue
if component == "..":
if stack is not empty:
pop stack
else:
push component
return "/" + join(stack, "/")
The exact equality checks matter. Do not use a test such as “the component contains only periods” or “the component starts with dots.” The contract gives special meaning only to "." and "..".
For example:
"...": ordinary name
"....": ordinary name
".hidden": ordinary name
"a..b": ordinary name
Only the two exact control tokens change the stack structure.
Prove the Stack Invariant
The implementation is short enough to memorize, but memorization is not the useful skill. The useful skill is knowing why every branch is safe.
Use this invariant:
After processing any prefix of the components, the stack contains exactly the ordinary components remaining in that prefix’s canonical path, in root-to-current order.
Now check each case.
Empty components and "."
An empty component comes from repeated or boundary slashes. Under the problem rules, those slashes do not add a directory.
The component "." means the current directory. Staying where you are also adds nothing.
In both cases, leaving the stack unchanged preserves the canonical path.
An ordinary component
An ordinary component names a new location below the current one. It must appear at the end of the canonical path, so pushing it extends the stack correctly.
For example:
["home", "user"] + "docs"
→ ["home", "user", "docs"]
The ".." component
".." moves to the parent directory. The current component is therefore removed, which is exactly pop().
If the stack is empty, the current location is already root. There is no parent above root, so doing nothing is correct.
This handles both:
["home", "user"] + ".."
→ ["home"]
and:
[] + ".."
→ []
Because every branch preserves the invariant, it holds after the entire path has been processed. At that point, the stack contains precisely the components of the canonical result.
Joining the stack with / removes redundant separators. Adding one leading slash satisfies the output format:
[]becomes"/".["home"]becomes"/home".["home", "user"]becomes"/home/user".
No separate trailing-slash cleanup is necessary.
Trace the Boundaries Before Coding
A dry run is most useful when it attacks the assumptions likely to be wrong.
Ordinary navigation and backtracking
For:
/home/user/Documents/../Pictures
| Component | Action | Stack after action | Reason |
|---|---|---|---|
"" | Ignore | [] | Leading slash |
home | Push | ["home"] | Ordinary name |
user | Push | ["home", "user"] | Ordinary name |
Documents | Push | ["home", "user", "Documents"] | Ordinary name |
.. | Pop | ["home", "user"] | Return to parent |
Pictures | Push | ["home", "user", "Pictures"] | Ordinary name |
The result is:
/home/user/Pictures
Ellipses are names
Now consider:
/.../a/../b/c/../d/./
The component "..." must be pushed. It is not "." and it is not "..".
The important state changes are:
"..." → ["..."]
"a" → ["...", "a"]
".." → ["..."]
"b" → ["...", "b"]
"c" → ["...", "b", "c"]
".." → ["...", "b"]
"d" → ["...", "b", "d"]
"." → ["...", "b", "d"]
The canonical result is:
/.../b/d
A broad “all dots are special” check fails this example immediately.
Parent moves at root
For:
/../../x
the first ".." sees an empty stack and does nothing. The second does the same. Then "x" is pushed:
[] → [] → [] → ["x"]
The result is:
/x
Root is a boundary, not a component that should be pushed. Never store ".." in the stack when there is nothing to remove; that would turn an absolute path into an invalid relative-looking result.
Repeated and trailing slashes
For:
/home//foo/
splitting produces empty components around the repeated and trailing slashes. Ignoring those components leaves:
["home", "foo"]
Joining once produces:
/home/foo
The parser handles both duplicate separators and trailing separators through the same empty-component rule.
Implement the Python Solution
Here is a self-contained Python implementation:
class Solution:
def simplifyPath(self, path: str) -> str:
stack = []
for component in path.split("/"):
if component == "" or component == ".":
continue
if component == "..":
if stack:
stack.pop()
else:
stack.append(component)
return "/" + "/".join(stack)
Each variable has a narrow job:
path.split("/")exposes the component boundaries.stackstores the ordinary components that remain in the canonical path.- The first branch removes components that do not change location.
- The second branch applies a parent move while clamping at root.
- The final branch preserves every ordinary name, including
"...". "/" + "/".join(stack)reconstructs the required absolute format.
The final expression deserves attention. When stack is empty:
"/" + "/".join([])
becomes:
"/"
When the stack is nonempty, join inserts exactly one separator between components. This is better than building a string with repeated conditional trimming because the output format follows directly from the data structure.
A standard filesystem helper would be the wrong abstraction here. The problem gives a small, explicit lexical contract, and the direct parser makes every rule visible during an interview.
Complexity and Submission Checks
Let n be the path length.
- Time:
O(n). Splitting, scanning the components, and joining the surviving components each take time proportional to the input size. - Auxiliary space:
O(n)in the worst case. The split result and the stack can both contain information proportional to the path length.
The stack entries are each pushed at most once and popped at most once. There is no repeated search for the previous slash or repeated rewriting of the path.
Before submitting, test the boundaries that expose incorrect assumptions:
- Root-only input:
/ - A trailing slash:
/home/ - Repeated slashes:
/home//foo/ - Current-directory markers:
/a/./b - Parent moves:
/a/b/../c - Several parent moves:
/a/b/../../c - Moving above root:
/../or/../../x - A valid ellipsis name:
/.../a - Longer period names:
/..../x - Backtracking through multiple components:
/a/b/c/../../d
The reusable recognition rule is this:
When tokens arrive left to right, some tokens add nested state, and a later token cancels the most recent active state, define stack entries semantically, process each token once, and prove a prefix invariant before coding.
For this problem, the stack is the current canonical path. Classify exact tokens. Clamp parent moves at root. Reconstruct once. That is the whole mechanism—and it is much stronger than memorizing a string-cleanup trick.
References
Research updated Sep 7, 2026
