来源:力扣(LeetCode)
描述:
你是一位系统管理员,手里有一份文件夹列表 folder
,你的任务是要删除该列表中的所有 子文件夹,并以 任意顺序 返回剩下的文件夹。
如果文件夹 folder[i]
位于另一个文件夹 folder[j]
下,那么 folder[i]
就是 folder[j]
的 子文件夹 。
文件夹的「路径」是由一个或多个按以下格式串联形成的字符串:'/'
后跟一个或者多个小写英文字母。
- 例如,
"/leetcode"
和"/leetcode/problems"
都是有效的路径,而空字符串和"/"
不是。
示例 1:
输入:folder = ["/a","/a/b","/c/d","/c/d/e","/c/f"]
输出:["/a","/c/d","/c/f"]
解释:"/a/b" 是 "/a" 的子文件夹,而 "/c/d/e" 是 "/c/d" 的子文件夹。
示例 2:
输入:folder = ["/a","/a/b/c","/a/b/d"]
输出:["/a"]
解释:文件夹 "/a/b/c" 和 "/a/b/d" 都会被删除,因为它们都是 "/a" 的子文件夹。
示例 3:
输入: folder = ["/a/b/c","/a/b/ca","/a/b/d"]
输出: ["/a/b/c","/a/b/ca","/a/b/d"]
提示:
- 1 <= folder.length <= 4 * 104
- 2 <= folder[i].length <= 100
- folder[i] 只包含小写字母和 ‘/’
- folder[i] 总是以字符 ‘/’ 起始
- 每个文件夹名都是 唯一 的
方法一:排序
思路与算法
我们可以将字符串数组 folder 按照字典序进行排序。在排序完成后,对于每一个 folder[i],如果 folder[i−1] 恰好是它的前缀,并且 folder[i] 第一个多出的字符是 /,那么我们就可以把 folder[i] 删除。
注意当 folder[i] 被删除后,后续的所有字符串都需要向前移动一个位置。例如 [“/a”,“/a/b”,“/a/c”] 中,“/a/b” 被删除后,数组会变为 [“/a”,“/a/c”],“/a/c” 也会被删除。
这样做的必要性是显然的,因为如果上述条件满足,就说明 folder[i] 是 folder[i−1] 的子文件夹。对于充分性,我们可以使用反证法:
假设 folder[i] 是某个 folder[j] (j ≠ i − 1) 的子文件夹但不是 folder[i − 1] 的子文件夹,那么在排序后,folder[j] 一定出现在 folder[i] 的前面,也就是有 j < i。如果有多个满足要求的 j,我们选择最早出现的那个。这样就保证了 folder[j] 本身不会是其它文件夹的子文件夹。
由于 “/” 的字典序小于所有的小写字母,并且 folder[i] 是由 folder[j] 加上 “/” 再加上后续字符组成,因此在 folder[i] 和 folder[j] 之间的所有字符串也都一定是由 folder[j] 加上 “/” 再加上后续字符组成。这些字符串都是 folder[i] 的子文件夹,它们会依次被删除。当遍历到 folder[i] 时,它的上一个元素恰好是 folder[j],因此它一定会被删除。
代码:
class Solution {
public:
vector<string> removeSubfolders(vector<string>& folder) {
sort(folder.begin(), folder.end());
vector<string> ans = {folder[0]};
for (int i = 1; i < folder.size(); ++i) {
if (int pre = ans.end()[-1].size(); !(pre < folder[i].size() && ans.end()[-1] == folder[i].substr(0, pre) && folder[i][pre] == '/')) {
ans.push_back(folder[i]);
}
}
return ans;
}
};
执行用时:132 ms, 在所有 C++ 提交中击败了88.46%的用户
内存消耗:40 MB, 在所有 C++ 提交中击败了82.31%的用户
复杂度分析
时间复杂度:O(nl⋅logn),其中 n 和 l 分别是数组 folder 的长度和文件夹的平均长度。O(nl⋅logn) 为排序需要的时间,后续构造答案需要的时间为 O(nl),在渐进意义下小于前者。
空间复杂度:O(l)。在构造答案比较前缀时,我们使用了字符串的截取子串操作,因此需要 O(l) 的临时空间。我们也可以使用一个递增的指针依次对两个字符串的每个相同位置进行比较,省去这一部分的空间,使得空间复杂度降低至排序需要的栈空间 O(logn)。但空间优化并不是本题的重点,因此上述的代码中仍然采用空间复杂度为 O(l) 的写法。注意这里不计入返回值占用的空间。
方法二:字典树
思路与算法
我们也可以使用字典树来解决本题。文件夹的拓扑结构正好是树形结构,即字典树上的每一个节点就是一个文件夹。
对于字典树中的每一个节点,我们仅需要存储一个变量 ref,如果 ref ≥ 0,说明该节点对应着 folder[ref ],否则(ref = −1)说明该节点只是一个中间节点。
我们首先将每一个文件夹按照 “/” 进行分割,作为一条路径加入字典树中。随后我们对字典树进行一次深度优先搜索,搜索的过程中,如果我们走到了一个 ref ≥ 0 的节点,就将其加入答案,并且可以直接回溯,因为后续(更深的)所有节点都是该节点的子文件夹。
代码:
struct Trie {
Trie(): ref(-1) {}
unordered_map<string, Trie*> children;
int ref;
};
class Solution {
public:
vector<string> removeSubfolders(vector<string>& folder) {
auto split = [](const string& s) -> vector<string> {
vector<string> ret;
string cur;
for (char ch: s) {
if (ch == '/') {
ret.push_back(move(cur));
cur.clear();
}
else {
cur.push_back(ch);
}
}
ret.push_back(move(cur));
return ret;
};
Trie* root = new Trie();
for (int i = 0; i < folder.size(); ++i) {
vector<string> path = split(folder[i]);
Trie* cur = root;
for (const string& name: path) {
if (!cur->children.count(name)) {
cur->children[name] = new Trie();
}
cur = cur->children[name];
}
cur->ref = i;
}
vector<string> ans;
function<void(Trie*)> dfs = [&](Trie* cur) {
if (cur->ref != -1) {
ans.push_back(folder[cur->ref]);
return;
}
for (auto&& [_, child]: cur->children) {
dfs(child);
}
};
dfs(root);
return ans;
}
};
执行用时:356 ms, 在所有 C++ 提交中击败了17.69%的用户
内存消耗:174.3 MB, 在所有 C++ 提交中击败了11.54%的用户
复杂度分析
时间复杂度:O(nl),其中 n 和 l 分别是数组 folder 的长度和文件夹的平均长度。即为构造字典树和答案需要的时间。
空间复杂度:O(nl),即为字典树需要使用的空间。注意这里不计入返回值占用的空间。
author:LeetCode-Solution