一、题目概述
虽然是中档题,但是这个题目对我来说太难了。。。我研究了挺长时间,本来不想写什么了,但是既然flag都立起来了,那我总要完成。
这个题应该是有两个做法,一套是并查集和哈希表,另一套是BFS,DFS和并查集。深度广度我倒是会一些,但现在也有点模糊了,并查集和哈希表,我现在基本忘干净,当时学数据结构的时候就没学好。。。
由此可以看出,我的编码能力还是挺差的,我也知道这个能力是时间活,没办法,一步一步来吧。
二、个人思路
我的思路,比较暴力,我用样例来给大家解释:
比如第一个和第三个John,我们遍历他的每一个邮箱,发现有一个一致的,那就证明,这两个是同一个人的两份数据,然后我们再把第三个John中其他不一样的邮箱尾接到第一个John里,再进行排序就好了。这样的话,编写起来很麻烦不说,时间复杂度预计是O(n²),极有可能超时,我就没有实现。我觉得我这样应该是可行的,但是空口无凭。
这个办法确实很笨,但是是我目前能想到的唯一的办法。
顺便说一句,这个题是我和一个同学一起研究的,时间复杂度的估计也是他估计的,我感觉他编码能力很强,至少比我强,在学院能排前20%,但是他总是装弱,说自己是废物这种,他可能确实考试成绩没有我好,但是成绩只说明一部分问题,可能我考试技巧比他好,但是他写代码确实很厉害。
三、大佬思路
我看的是并查集+哈希表的方法,题解里面也有用图论的方式求解的,但是我想跳出我的舒适区,要学新的,就干脆从头开始,都了解一下。
我所借鉴的是这篇回答,先上代码:
↓这个是官方题解的答案
class UnionFind {
public:
vector<int> parent;
UnionFind(int n) {
parent.resize(n);
for (int i = 0; i < n; i++) {
parent[i] = i;
}
}
void unionSet(int index1, int index2) {
parent[find(index2)] = find(index1);
}
int find(int index) {
if (parent[index] != index) {
parent[index] = find(parent[index]);
}
return parent[index];
}
};
class Solution {
public:
vector<vector<string>> accountsMerge(vector<vector<string>>& accounts) {
map<string, int> emailToIndex;
map<string, string> emailToName;
int emailsCount = 0;
for (auto& account : accounts) {
string& name = account[0];
int size = account.size();
for (int i = 1; i < size; i++) {
string& email = account[i];
if (!emailToIndex.count(email)) {
emailToIndex[email] = emailsCount++;
emailToName[email] = name;
}
}
}
UnionFind uf(emailsCount);
for (auto& account : accounts) {
string& firstEmail = account[1];
int firstIndex = emailToIndex[firstEmail];
int size = account.size();
for (int i = 2; i < size; i++) {
string& nextEmail = account[i];
int nextIndex = emailToIndex[nextEmail];
uf.unionSet(firstIndex, nextIndex);
}
}
map<int, vector<string>> indexToEmails;
for (auto& [email, _] : emailToIndex) {
int index = uf.find(emailToIndex[email]);
vector<string>& account = indexToEmails[index];
account.emplace_back(email);
indexToEmails[index] = account;
}
vector<vector<string>> merged;
for (auto& [_, emails] : indexToEmails) {
sort(emails.begin(), emails.end());
string& name = emailToName[emails[0]];
vector<string> account;
account.emplace_back(name);
for (auto& email : emails) {
account.emplace_back(email);
}
merged.emplace_back(account);
}
return merged;
}
};
时间复杂度:O(nlogn),其中 n 是不同邮箱地址的数量。
需要遍历所有邮箱地址,在并查集内进行查找和合并操作,对于两个不同的邮箱地址,如果它们的祖先不同则需要进行合并,需要进行 22 次查找和最多 11 次合并。一共需要进行 2n 次查找和最多 n 次合并,因此时间复杂度是 O(2nlogn)=O(nlogn)。这里的并查集使用了路径压缩,但是没有使用按秩合并,最坏情况下的时间复杂度是 O(nlogn),平均情况下的时间复杂度依然是 O(nα(n)),其中 α 为阿克曼函数的反函数,α(n) 可以认为是一个很小的常数。
整理出题目要求的返回账户的格式时需要对邮箱地址排序,时间复杂度是 O(nlogn)。
其余操作包括遍历所有邮箱地址,在哈希表中记录相应的信息,时间复杂度是 O(n),在渐进意义下 O(n)O(n) 小于O(nlogn)。
因此总时间复杂度是 O(nlogn)。
空间复杂度:O(n),其中 n 是不同邮箱地址的数量。空间复杂度主要取决于哈希表和并查集,每个哈希表存储的邮箱地址的数量为 n,并查集的大小为 n。
作者:LeetCode-Solution
链接:https://leetcode.cn/problems/accounts-merge/solution
/zhang-hu-he-bing-by-leetcode-solution-3dyq/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
想要看懂这篇题解,首先要知道两个(或者三个)东西,第一是并查集,第二是哈希表,(第三个是提到的阿克曼函数)。
那么什么是并查集?
以下一些内容,来自这篇文章:【算法与数据结构】—— 并查集
并查集,顾名思义,可以一起被查找的集合。我们用一个元素,作为代表元代表着一整个集合。剩下的元素组织成一棵树,根节点为代表元。如果我们要进行查找元素所在的集合,我们就一级一级往上找,先找这个元素的父亲节点,再找祖父,一直找到代表元即可。
基于这样的特性,并查集的主要用途有以下两点:
1、维护无向图的连通性(判断两个点是否在同一连通块内,或增加一条边后是否会产生环);
2、用在求解最小生成树的Kruskal算法里。
我的理解是这样的:,我们把整个二维数组,看成一张图,一张无向图。代表元是每一个人的名字(如果有好几个叫一个名字的,那我们在心里给他分个类,张三A,张三B这种),然后让其邮箱地址是人名结点的子节点,这样就构建出几个连通分量了。然后我们再合并这几个连通分量,合并的方法就是遍历每个连通分量中的邮箱节点,要是有一样的,那他们就是一家人,可以合并到一起。
但是怎么代码实现,我还是不会。。。只能说明白了他的意思,我把我借鉴的代码给大家粘在下面:
class Djset {
public:
vector<int> parent; // 记录节点的根
vector<int> rank; // 记录根节点的深度(用于优化)
Djset(int n): parent(vector<int>(n)), rank(vector<int>(n)) {
for (int i = 0; i < n; i++) {
parent[i] = i;
}
}
int find(int x) {
// 压缩方式:直接指向根节点
if (x != parent[x]) {
parent[x] = find(parent[x]);
}
return parent[x];
}
void merge(int x, int y) {
int rootx = find(x);
int rooty = find(y);
if (rootx != rooty) {
if (rank[rootx] < rank[rooty]) {
swap(rootx, rooty);
}
parent[rooty] = rootx;
if (rank[rootx] == rank[rooty]) rank[rootx] += 1;
}
}
};
class Solution {
public:
vector<vector<string>> accountsMerge(vector<vector<string>>& acc) {
vector<vector<string> > res;
// 作用:存储每个邮箱属于哪个账户 ,同时 在遍历邮箱时,判断邮箱是否出现过
// 格式:<邮箱,账户id>
unordered_map<string, int> um;
int n = acc.size();
Djset ds(n);
for (int i = 0; i < n; i++) {
int m = acc[i].size();
for (int j = 1; j < m; j++) {
string s = acc[i][j];
if (um.find(s) == um.end()) {
um[s] = i;
} else {
ds.merge(i, um[s]);
}
}
}
// 作用: 存储每个账户下的邮箱
// 格式: <账户id, 邮箱列表> >
// 注意:这里的key必须是账户id,不能是账户名称,名称可能相同,会造成覆盖
unordered_map<int, vector<string> > umv;
for (auto& [k, v] : um) umv[ds.find(v)].emplace_back(k);
for (auto& [k, v] : umv){
sort(v.begin(), v.end());
vector<string> tmp(1, acc[k][0]);
tmp.insert(tmp.end(), v.begin(), v.end());
res.emplace_back(tmp);
}
return res;
}
};
作者:yexiso
链接:https://leetcode.cn/problems/accounts-merge/solution
/tu-jie-yi-ran-shi-bing-cha-ji-by-yexiso-5ncf/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
第二个就是哈希表,哈希表比并查集好理解一点。以下的一些内容参考了这篇文章:来吧!一文彻底搞定哈希表!
用我自己的话讲,就是:我们举个例子,现在有十个人点了外卖,他们手机尾号分别是0~9,如果外卖员接的单是尾号为9的外卖,但是这十单的饭放在一堆了,拿外卖员就需要自己翻一翻找一找才找得到9号,这中间会耽误时间,给差评。哈希表在外卖上的应用,就是把放外卖的地方分区,0号一块,1号一块这样,这样外卖员直接去9号区找外卖就可以送了,节约了时间,还获得了五星好评。
这是最理想的哈希表,按照手机尾号分类就是哈希函数。
但是哈希表也有不理想的时候,如果十个人尾号都是9怎么办?我们这时候称之为哈希冲突,那么如何处理哈希冲突?关于哈希冲突的解决办法有好几种,我主要分享我学懂了的两种主要的方法,一个是开放寻址法,一个是拉链法。
开放寻址法,就是有人占据了9的位置,那我不跟你挤,虽然我也是9号区的,但是我去你后面的0号区,我也有地方,你也有地方,我还没浪费空的位置。
拉链法,结合了链表的知识。我也是9号,我也不跟你争抢,但是你不能把我挤到别人家去,我把我电话号码给你,要是有人来9号区找我,看见你了没看见我,你就把我电话给他,他顺着电话号就找到我了。我们也可以称之为链表法,逻辑上,和有向图的链表形式很像。
哈希表中,哈希函数是最重要的一环,哈希函数的合理性决定这个哈希表的合理性。
本题中,哈希表是用于存放姓名的数据结构,但是大家知道我要说什么,我不知道怎么代码实现,我是赛博赵括(裂开)。
第三点是阿克曼函数,这个更好理解了,大家应该见过,但不知道他是叫这个名字。
阿克曼函数(Ackermann)是非原始递归函数的例子。它需要两个自然数作为输入值,输出一个自然数。它的输出值增长速度非常高,仅是对于(4,3)的输出已大得不能准确计算。
Ackermann函数定义如下:
现有Ackermann(m,n),
若m=0,返回n+1。
若m>0且n=0,返回Ackermann(m-1,1)。
若m>0且n>0,返回Ackermann(m-1,Ackermann(m,n-1))。
四、总结
这个题差不多就到这了,对我来说,这个题真的很难。首先,我从没写过并查集的代码,只能说知道是怎么回事,其次,哈希表的代码也没实现过,只知道逻辑上是这样子的。对于数据结构而言,动手能力真的非常重要。我感觉我现在有点过分追求理论,毕竟上课的时候,基本都是讲理论,很少带着我们写代码。
还有就是,我现在在复习数学和期末考试,但是这个每日一题吧,确实是耽误了我一点时间,有时候我在想,要不要把这个放一放,或者糊弄着写,心理斗争了一下决定正常写,好好写,把平时的玩手机什么时间减少一点,就有时间复习了。
今天的新知识主要是并查集和哈希表,大家一定要记住这两个的概念和逻辑含义。给这道题打分的话,我打三分,虽然他让我学到很多新知识,但是耽误了太长时间,有点影响我的其他的计划,希望明天的每日一题简单一些。