71. Simplify Path
題目 / Problem
給你一個 Unix 風格檔案系統的絕對路徑(一定以斜線 '/' 開頭),請把它轉換成簡化後的標準路徑(canonical path)。
規則:
- 單個句點 '.' 代表「目前目錄」,要忽略。
- 雙句點 '..' 代表「上一層(父)目錄」,要回到上一層。
- 多個連續斜線('//'、'///')視為一個斜線。
- 任何不符合上述規則的句點序列,都當作合法的目錄/檔名。例如 '...'、'....' 都是合法名字。
輸出的標準路徑必須:以單一 '/' 開頭、目錄之間用恰好一個 '/' 分隔、結尾不能有 '/'(除非是根目錄)、不能再含有 '.' 或 '..'。
You are given an absolute Unix-style path that always begins with '/'. Transform it into its simplified canonical path. A single '.' means the current directory (skip it); a double '..' means the parent directory (go up one level); multiple consecutive slashes collapse into one; any other sequence of periods like '...' is a valid name. The result must start with one '/', separate directories by exactly one '/', and not end with '/' unless it is the root.
Constraints / 限制:
- 1 <= path.length <= 3000
- path 只含英文字母、數字、'.'、'/'、'_'
- path 一定是合法的絕對路徑
Worked example / 範例:
- Input: path = "/home/user/Documents/../Pictures"
- Output: "/home/user/Pictures"
- 解釋:.. 把我們從 Documents 退回到 user,再進入 Pictures。
名詞解釋 / Glossary
- stack / 堆疊:一種「後進先出」(LIFO) 的資料結構,只能在尾端加入(push)或取出(pop)。這題我們用一個陣列當堆疊,存放目前有效的目錄名;遇到
..就 pop 掉最後一個目錄,正好對應「回到上一層」。 - token / 詞元(路徑片段):把路徑用
'/'切開後得到的每一小段,例如"home"、".."、"."、""(空字串,來自連續斜線)。我們逐一檢視每個片段並決定怎麼處理。 - string tokenization / 字串切割:按某個分隔符(這裡是
'/')把長字串拆成多個小段的過程。 - in-place / 原地操作:直接在輸入或固定緩衝區上修改,不額外配置大量記憶體。C 解法用一個字元緩衝區直接拼出答案。
- pointer / 指標 (C):一個變數,裡面存的是「記憶體位址」。
char *s表示 s 指向某個字元的位置;*s取出該位置的字元;s[i]等同於「從 s 往後第 i 個字元」。 malloc/free:C 裡面手動向系統「借」一塊記憶體 (malloc) 與「還」回去 (free)。LeetCode 要你回傳的字串通常需要malloc出來,因為函式結束後區域變數會消失。std::vector/ 動態陣列 (C++):會自動增長的陣列,push_back加到尾端、pop_back移除尾端、back()看尾端元素——正好拿來當堆疊。std::stringstream/getline(C++):把字串當成輸入流,getline(ss, token, '/')可以一次讀出兩個'/'之間的內容,是最方便的切割工具。
思路
最直覺的想法可能是用字串「找到 .. 就把前面的目錄刪掉」,但直接在字串上來回刪除很容易出錯,而且每次刪除都要搬動後面的字元,效率差。關鍵觀察是:路徑其實是由 '/' 分隔的一串「片段」,我們只要從左到右處理每個片段,並維護一個「目前有效目錄的列表」即可。這個列表的行為恰好是堆疊——.. 要移除最近一個目錄(pop 尾端),而新目錄要加到尾端(push)。於是演算法變成:用 '/' 切割路徑,對每個片段分四種情況處理:空字串或 '.' 直接跳過(對應連續斜線與「目前目錄」);'..' 時若堆疊非空就 pop 一個(回到上層,根目錄時無事可做);其他任何字串(包含 '...'、'....' 這種合法名字)就 push 進堆疊。最後把堆疊裡的目錄用單一 '/' 連起來,前面加一個 '/';若堆疊為空就直接回傳 "/"。為什麼這樣對?因為堆疊在任何時刻都精確表示「從根目錄一路走下來目前所在的目錄序列」,這就是一個不變量(invariant),每處理一個片段都維持它成立,所以最後直接輸出就是答案。
The naive idea—scan the string and delete the previous directory whenever you hit ..—is fragile and slow, because deleting in place forces you to shift characters around. The key insight is that the path is just a list of segments separated by '/', so we process them left-to-right while maintaining a list of the currently-valid directories. That list behaves exactly like a stack: .. pops the most recent directory, and a real directory name gets pushed. So the algorithm is: split on '/', and for each segment handle four cases—an empty string or '.' is skipped (this absorbs consecutive slashes and the "current directory"); '..' pops one directory if the stack is non-empty (going up from root does nothing); anything else, including valid names like '...', is pushed. Finally, join the stack with single slashes and prepend a '/'; if the stack is empty, return "/". This is correct because the stack always represents the exact directory chain from the root to where we currently are—an invariant preserved at every step—so emitting it at the end gives the canonical path.
逐步走查 / Walkthrough
以 path = "/home/user/Documents/../Pictures" 為例,用 '/' 切割後得到片段序列:["", "home", "user", "Documents", "..", "Pictures"]。堆疊一開始是空的。
| 步驟 Step | 片段 token | 判斷 Decision | 堆疊 Stack (處理後) |
|---|---|---|---|
| 1 | "" |
空字串,跳過 / empty, skip | [] |
| 2 | "home" |
一般目錄,push / normal name, push | ["home"] |
| 3 | "user" |
一般目錄,push | ["home", "user"] |
| 4 | "Documents" |
一般目錄,push | ["home", "user", "Documents"] |
| 5 | ".." |
上一層,pop 尾端 / parent, pop | ["home", "user"] |
| 6 | "Pictures" |
一般目錄,push | ["home", "user", "Pictures"] |
結束後把堆疊用 '/' 連接並在最前面加 '/':"/" + "home/user/Pictures" = "/home/user/Pictures"。這就是答案。
Solution — C
// 演算法 / Algorithm:
// 用 '/' 切割路徑,逐段處理;空字串與 "." 跳過,".." 退回上一層,其餘 push。
// Split on '/'; skip "" and ".", pop on "..", push everything else.
// 我們用一個指標陣列 stack[] 記錄每段目錄在緩衝區中的位置與長度。
// We keep a stack of (start,length) for each kept directory, then join them.
#include <stdlib.h>
#include <string.h>
char* simplifyPath(char* path) {
int n = (int)strlen(path); // 路徑長度 / length of the input path
// 緩衝區:複製一份 path,方便我們就地切出每個片段
// Buffer: a copy of path so we can carve tokens out of it.
char* buf = (char*)malloc((size_t)n + 1); // +1 給結尾的 '