Given a list of folders folder
, return the folders after removing all sub-folders in those folders. You may return the answer in any order.
If a folder[i]
is located within another folder[j]
, it is called a sub-folder of it.
The format of a path is one or more concatenated strings of the form: '/'
followed by one or more lowercase English letters.
- For example,
"/leetcode"
and"/leetcode/problems"
are valid paths while an empty string and"/"
are not.
Example 1:
Input: folder = ["/a","/a/b","/c/d","/c/d/e","/c/f"]
Output: ["/a","/c/d","/c/f"]
Explanation: Folders "/a/b/" is a subfolder of "/a" and "/c/d/e" is inside of folder "/c/d" in our filesystem.
Example 2:
Input: folder = ["/a","/a/b/c","/a/b/d"]
Output: ["/a"]
Explanation: Folders "/a/b/c" and "/a/b/d/" will be removed because they are subfolders of "/a".
Example 3:
Input: folder = ["/a/b/c","/a/b/ca","/a/b/d"]
Output: ["/a/b/c","/a/b/ca","/a/b/d"]
Constraints:
1 <= folder.length <= 4 * 104
2 <= folder[i].length <= 100
folder[i]
contains only lowercase letters and'/'
.folder[i]
always starts with the character'/'
.- Each folder name is unique.
這道題給了一堆文件目錄的地址,讓去除其中所有的子目錄,所謂的子目錄,就是在父文件夾下的文件,比如 /home/user
這個文件夾下有個叫 grandyang 的文件夾,則 /home/user/grandyang
就是其的一個子目錄。既然是子目錄,那么路徑中肯定有一部分是和父目錄相同的,所以可以把所有的父目錄都放到一個 HashSet 中,然后遍歷某個要檢驗的路徑的所有前綴,若其在 HashSet 中存在,說明當前是個子目錄。由於路徑越長,其是子目錄的可能性就越大,所以可以按照長度先來排個序,先處理那些長度短的路徑,同時排序也保證了父目錄一定比其子目錄先處理。可以從第三個字符開始遍歷,因為限定了路徑長度至少為2,若遍歷到的字符是 '/',說明前面的部分已經是一個合法的路徑,而且可能已經存在了,需要到 HashSet 中檢測一下,若存在,后面的部分就不用檢測了,直接 break 掉循環。若所有字符成功遍歷完,說明當前路徑沒有父目錄,可以加到 HashSet 中。最后循環結束后,把 HashSet 中所有的內容放到數組中返回即可,參見代碼如下:
解法一:
class Solution {
public:
vector<string> removeSubfolders(vector<string>& folder) {
sort(folder.begin(), folder.end(), [](string& a, string& b) {return a.size() < b.size();});
unordered_set<string> seen;
for (string name : folder) {
int i = 2, n = name.size();
for (; i < n; ++i) {
if (name[i] == '/' && seen.count(name.substr(0, i))) {
break;
}
}
if (i == n) seen.insert(name);
}
return vector<string>(seen.begin(), seen.end());
}
};
再來看一種解法,這里不是按路徑長度排序,而是按照字母順序排序,這樣相同的父路徑的目錄就會被放到一起,而且長度短的在前面。此時遍歷所有目錄,若結果 res 為空,則可以將當前目錄加入結果中,因為按照排序后的順序,肯定不存在當前目錄的父目錄。若 res 不為空,則將其最后一個目錄取出來,並且加上 '/',看是否是當前路徑的前綴,若不是的話就可以將當前路徑加入結果 res。想一下為何要在后面加上 '/' 呢,因為只有加上了才能保證是父目錄,比如結果 res 的最后一項為 /a
,而當前的路徑為 /ab
,不加 '/' 的話,則 /a
也是前綴,就出錯了,參見代碼如下:
解法二:
class Solution {
public:
vector<string> removeSubfolders(vector<string>& folder) {
vector<string> res;
sort(folder.begin(), folder.end());
for (string name : folder) {
if (res.empty() || name.rfind(res.back() + "/", 0) != 0) {
res.push_back(name);
}
}
return res;
}
};
這道題的標簽中還有前綴樹 Trie,表示還可以用這種方法來做。當然首先是要定義前綴樹的結構體,這里由於還存在 '/' 字符,所以很明顯 26 個字符是不夠用的,則 child 數組可以定義為 27 個,將最后一個位置留給 '/' 字符,同時這里還需要一個變量 index,表示當前路徑在給定路徑中的位置,不存在的用 -1 表示,這里相當於一般的前綴樹的中的 isWord 標識。接下來先構建前綴樹,遍歷所有的路徑,並且遍歷路徑上的每一個字符,若是 '/' 字符,則 idx 為 26,否則是其跟小寫字母a之間的距離。若 idx 位置為空,則在該位置上新建 Trie,接下來將指針移動 child[idx] 結點上繼續建樹。當樹建好了之后,就可以開始查找所有的父目錄了。這里用 BFS 來遍歷,將根結點 root 放到 queue 中,然后開始遍歷,取出隊首元素,查看若其 index 大於等於0,則將其加入結果 res 中,由於是 BFS 的順序,則第一個遇到的目錄肯定是父目錄,然后遍歷其 child 結點,若子結點存在,且不是遇到了 '/' 字符或者 index 小於0,則將當前結點放入 queue 繼續遍歷,因為遇到 '/' 字符且 index 大於等於0時,表示是子目錄,這種情況應該跳過,參見代碼如下:
解法三:
class Solution {
public:
struct Trie {
Trie *child[27];
int index = -1;
};
vector<string> removeSubfolders(vector<string>& folder) {
Trie* root = new Trie();
for (int i = 0; i < folder.size(); ++i) {
Trie *t = root;
for (char c : folder[i]) {
int idx = (c == '/') ? 26 : c - 'a';
if (!t->child[idx]) t->child[idx] = new Trie();
t = t->child[idx];
}
t->index = i;
}
return bfs(folder, root);
}
vector<string> bfs(vector<string>& folder, Trie* root) {
vector<string> res;
queue<Trie*> q{{root}};
while (!q.empty()) {
Trie *t = q.front(); q.pop();
if (t->index >= 0) res.push_back(folder[t->index]);
for (int i = 0; i < 27; ++i) {
if (t->child[i] && !(i == 26 && t->index >= 0)) {
q.push(t->child[i]);
}
}
}
return res;
}
};
Github 同步地址:
https://github.com/grandyang/leetcode/issues/1233
參考資料:
https://leetcode.com/problems/remove-sub-folders-from-the-filesystem/