【字典树(前缀树) 哈希映射 后序序列化】1948. 删除系统中的重复文件夹

本文涉及知识点

字典树(前缀树) 哈希映射 后序序列化

LeetCode 1948. 删除系统中的重复文件夹

由于一个漏洞,文件系统中存在许多重复文件夹。给你一个二维数组 paths,其中 paths[i] 是一个表示文件系统中第 i 个文件夹的绝对路径的数组。

例如,["one", "two", "three"] 表示路径 "/one/two/three" 。

如果两个文件夹(不需要在同一层级)包含 非空且相同的 子文件夹 集合 并具有相同的子文件夹结构,则认为这两个文件夹是相同文件夹。相同文件夹的根层级 不 需要相同。如果存在两个(或两个以上)相同 文件夹,则需要将这些文件夹和所有它们的子文件夹 标记 为待删除。

例如,下面文件结构中的文件夹 "/a" 和 "/b" 相同。它们(以及它们的子文件夹)应该被 全部 标记为待删除:

/a

/a/x

/a/x/y

/a/z

/b

/b/x

/b/x/y

/b/z

然而,如果文件结构中还包含路径 "/b/w" ,那么文件夹 "/a" 和 "/b" 就不相同。注意,即便添加了新的文件夹 "/b/w" ,仍然认为 "/a/x" 和 "/b/x" 相同。

一旦所有的相同文件夹和它们的子文件夹都被标记为待删除,文件系统将会 删除 所有上述文件夹。文件系统只会执行一次删除操作。执行完这一次删除操作后,不会删除新出现的相同文件夹。

返回二维数组 ans ,该数组包含删除所有标记文件夹之后剩余文件夹的路径。路径可以按 任意顺序 返回。

示例 1:

输入:paths = [["a"],["c"],["d"],["a","b"],["c","b"],["d","a"]]

输出:[["d"],["d","a"]]

解释:文件结构如上所示。

文件夹 "/a" 和 "/c"(以及它们的子文件夹)都会被标记为待删除,因为它们都包含名为 "b" 的空文件夹。

示例 2:

输入:paths = [["a"],["c"],["a","b"],["c","b"],["a","b","x"],["a","b","x","y"],["w"],["w","y"]]

输出:[["c"],["c","b"],["a"],["a","b"]]

解释:文件结构如上所示。

文件夹 "/a/b/x" 和 "/w"(以及它们的子文件夹)都会被标记为待删除,因为它们都包含名为 "y" 的空文件夹。

注意,文件夹 "/a" 和 "/c" 在删除后变为相同文件夹,但这两个文件夹不会被删除,因为删除只会进行一次,且它们没有在删除前被标记。

示例 3:

输入:paths = [["a","b"],["c","d"],["c"],["a"]]

输出:[["c"],["c","d"],["a"],["a","b"]]

解释:文件系统中所有文件夹互不相同。

注意,返回的数组可以按不同顺序返回文件夹路径,因为题目对顺序没有要求。

示例 4:

输入:paths = [["a"],["a","x"],["a","x","y"],["a","z"],["b"],["b","x"],["b","x","y"],["b","z"]]

输出:[]

解释:文件结构如上所示。

文件夹 "/a/x" 和 "/b/x"(以及它们的子文件夹)都会被标记为待删除,因为它们都包含名为 "y" 的空文件夹。

文件夹 "/a" 和 "/b"(以及它们的子文件夹)都会被标记为待删除,因为它们都包含一个名为 "z" 的空文件夹以及上面提到的文件夹 "x" 。

示例 5:

输入:paths = [["a"],["a","x"],["a","x","y"],["a","z"],["b"],["b","x"],["b","x","y"],["b","z"],["b","w"]]

输出:[["b"],["b","w"],["b","z"],["a"],["a","z"]]

解释:本例与上例的结构基本相同,除了新增 "/b/w" 文件夹。

文件夹 "/a/x" 和 "/b/x" 仍然会被标记,但 "/a" 和 "/b" 不再被标记,因为 "/b" 中有名为 "w" 的空文件夹而 "/a" 没有。

注意,"/a/z" 和 "/b/z" 不会被标记,因为相同子文件夹的集合必须是非空集合,但这两个文件夹都是空的。

提示:

1 <= paths.length <= 2 * 10^4^

1 <= paths[i].length <= 500

1 <= paths[i][j].length <= 10

1 <= sum(paths[i][j].length) <= 2 * 10^5^

path[i][j] 由小写英文字母组成

不会存在两个路径都指向同一个文件夹的情况

对于不在根层级的任意文件夹,其父文件夹也会包含在输入中

字典树

如何判断两个文件夹是否相等? 除子树根节点外的序列化是否相等。后序序列化更简单。序列化完子孙后就比较。

分三步:

一,将所有path放到字典树中。字典树的节点包括path[i][j],i。

二,DFS字典树,将各路径不包括当前节点的后序序列化放到哈希映射m_mSer中,并记录数量。注意 :序列时,要排序。不如:直接用有序映射。

三,DFS字典树,看各节点是否有相等的哈希,标记此节点需要删除。

四,复制不需要删除的节点到vRet中。

复杂度

主要复杂度在序列化。

令 ∀ i ∀ j \forall i\forall j ∀i∀j path[i][i]的层次是leve(从0开始),则它序列化leve+1次,它会被它及它的祖先序列化。

它及它的祖先分别被序列化: leve+1 ,leve ⋯ \cdots ⋯ 1 次

它及它的祖先分别在path出现的次数: 1 ,2 ⋯ \cdots ⋯ leve+1 次。

次数相同,等于 path[i][j] 元素的个数,我们假设最极端情况下: ∀ i ∀ j \forall i\forall j ∀i∀j path[i][j].length==1。令o=sum(path[i][j].lenght()) ,则 ∑ \sum ∑path[i][j]序列的次数 <= o。再次假设极端情况下: ∀ i ∀ j \forall i\forall j ∀i∀j path[i][j] == 10。 则序列化的空间复制度和时间复杂度等于O(o × \times × 10) ≈ \approx ≈ 2 × \times × 10^6^

代码

核心代码

cpp 复制代码
class CStrToIndex
{
public:
	CStrToIndex() {

	}
	CStrToIndex(const vector<string>& wordList) {
		for (const auto& str : wordList)
		{
			Add(str);
		}
	}
	int Add(const string& str)
	{
		if (m_mIndexs.count(str)) { return m_mIndexs[str]; }
		m_mIndexs[str] = m_strs.size();
		m_strs.push_back(str);
		return  m_strs.size()-1;
	}
	vector<string> m_strs;
	int GetIndex(const string& str)
	{
		if (m_mIndexs.count(str)) { return m_mIndexs[str]; }
		return -1;
	}
protected:
	unordered_map<string, int> m_mIndexs;
};

class CPathTrieNode
{
public:
	CPathTrieNode* Add(string str) {
		if (!m_mChild.count(str)) {
			m_mChild[str] = new CPathTrieNode();
		}
		return m_mChild[str];
	}
	int m_iSerIndex =-1;
	map<string, CPathTrieNode*> m_mChild;
};

class CPathTrie {
public:
	void Add(const vector<string>& path) {
		auto ptr = &m_root;
		for (auto& s : path) {
			ptr = ptr->Add(s);
		}
	}
	CPathTrieNode m_root;
};
class Solution {
public:
	vector<vector<string>> deleteDuplicateFolder(vector<vector<string>>& paths) {
		CPathTrie trie;
		for (const auto& path : paths) {
			trie.Add(path);
		}
		DFSForSer("", &trie.m_root);
		vector<string> stack;
		DFS(stack, &trie.m_root);
		return m_vRet;
	}
	string DFSForSer (string strName,CPathTrieNode* cur) {
		string str;
		for (const auto& [tmp, child] : cur->m_mChild) {
			str += "(" + DFSForSer(tmp,child) + ")";
		}
		if ("" != str)	{
			cur->m_iSerIndex = m_strIndex.Add(str);	
			m_mIndexCount[cur->m_iSerIndex]++;
		}
		str += strName;
		return str ;
	};
	void DFS(vector<string>& stack, CPathTrieNode* cur)
	{
		for (const auto& [tmp, child] : cur->m_mChild) {
			if ((-1 != child->m_iSerIndex) && (m_mIndexCount[child->m_iSerIndex] > 1)) {
				continue;
			}
			stack.emplace_back(tmp);
			m_vRet.emplace_back(stack);
			DFS(stack, child);
			stack.pop_back();
		}
	}
	vector<vector<string>> m_vRet;
	unordered_map<int, int> m_mIndexCount;
	CStrToIndex m_strIndex;
};

VS自带的单元测试

cpp 复制代码
template<class T1,class T2>
void AssertEx(const T1& t1, const T2& t2)
{
	Assert::AreEqual(t1 , t2);
}

template<class T>
void AssertEx(const vector<T>& v1, const vector<T>& v2)
{
	Assert::AreEqual(v1.size(), v2.size());	
	for (int i = 0; i < v1.size(); i++)
	{
		Assert::AreEqual(v1[i], v2[i]);
	}
}

template<class T>
void AssertV2(vector<vector<T>> vv1, vector<vector<T>> vv2)
{
	sort(vv1.begin(), vv1.end());
	sort(vv2.begin(), vv2.end());
	Assert::AreEqual(vv1.size(), vv2.size());
	for (int i = 0; i < vv1.size(); i++)
	{
		AssertEx(vv1[i], vv2[i]);
	}
}

namespace UnitTest
{
	vector<vector<string>> paths;
	TEST_CLASS(UnitTest)
	{
	public:
		
		TEST_METHOD(TestMethod1)
		{
			paths = { {"a"},{"c"},{"d"},{"a","b"},{"c","b"},{"d","a"} };
			auto res = Solution().deleteDuplicateFolder(paths);			
			AssertV2(res,{ {"d"},{"d","a"} });
		}
		TEST_METHOD(TestMethod2)
		{
			paths = { {"a"},{"c"},{"a","b"},{"c","b"},{"a","b","x"},{"a","b","x","y"},{"w"},{"w","y"} };
			auto res = Solution().deleteDuplicateFolder(paths);
			AssertV2(res, { {"c"},{"c","b"},{"a"},{"a","b"} });
		}
		TEST_METHOD(TestMethod3)
		{
			paths = { {"a","b"},{"c","d"},{"c"},{"a"} };
			auto res = Solution().deleteDuplicateFolder(paths);
			AssertV2(res, { {"c"},{"c","d"},{"a"},{"a","b"} });
		}
		TEST_METHOD(TestMethod4)
		{
			paths = { {"a"},{"a","x"},{"a","x","y"},{"a","z"},{"b"},{"b","x"},{"b","x","y"},{"b","z"} };
			auto res = Solution().deleteDuplicateFolder(paths);
			AssertV2(res, {  });
		}
		TEST_METHOD(TestMethod5)
		{
			paths = { {"a"},{"a","x"},{"a","x","y"},{"a","z"},{"b"},{"b","x"},{"b","x","y"},{"b","z"},{"b","w"} };
			auto res = Solution().deleteDuplicateFolder(paths);
			AssertV2(res, { {"b"},{"b","w"},{"b","z"},{"a"},{"a","z"} });
		}
	};
}

扩展阅读

视频课程

有效学习:明确的目标 及时的反馈 拉伸区(难度合适),可以先学简单的课程,请移步CSDN学院,听白银讲师(也就是鄙人)的讲解。
https://edu.csdn.net/course/detail/38771

如何你想快速形成战斗了,为老板分忧,请学习C#入职培训、C++入职培训等课程
https://edu.csdn.net/lecturer/6176

相关下载

想高屋建瓴的学习算法,请下载《喜缺全书算法册》doc版
https://download.csdn.net/download/he_zhidan/88348653

我想对大家说的话
《喜缺全书算法册》以原理、正确性证明、总结为主。
闻缺陷则喜是一个美好的愿望,早发现问题,早修改问题,给老板节约钱。
子墨子言之:事无终始,无务多业。也就是我们常说的专业的人做专业的事。
如果程序是一条龙,那算法就是他的是睛

测试环境

操作系统:win7 开发环境: VS2019 C++17

或者 操作系统:win10 开发环境: VS2022 C++17

如无特殊说明,本算法用**C++**实现。

相关推荐
我言秋日胜春朝★7 分钟前
【Linux】进程地址空间
linux·运维·服务器
爪哇学长11 分钟前
双指针算法详解:原理、应用场景及代码示例
java·数据结构·算法
爱摸鱼的孔乙己13 分钟前
【数据结构】链表(leetcode)
c语言·数据结构·c++·链表·csdn
Dola_Pan15 分钟前
C语言:数组转换指针的时机
c语言·开发语言·算法
繁依Fanyi27 分钟前
简易安卓句分器实现
java·服务器·开发语言·算法·eclipse
C-cat.28 分钟前
Linux|环境变量
linux·运维·服务器
yunfanleo42 分钟前
docker run m3e 配置网络,自动重启,GPU等 配置渠道要点
linux·运维·docker
烦躁的大鼻嘎44 分钟前
模拟算法实例讲解:从理论到实践的编程之旅
数据结构·c++·算法·leetcode
IU宝1 小时前
C/C++内存管理
java·c语言·c++
fhvyxyci1 小时前
【C++之STL】摸清 string 的模拟实现(下)
开发语言·c++·string