algorithm - 联合查找解决方案的时间复杂度
问题描述
以下问题的解决方案的大约时间复杂度是多少?如果我们假设由于路径压缩,每次调用 self.find() 大致摊销到 ~O(1)
问题陈述:
给定一个帐户列表,每个元素 accounts[i] 是一个字符串列表,其中第一个元素 accounts[i][0] 是名称,其余元素是表示帐户电子邮件的电子邮件。
现在,我们想合并这些帐户。如果两个帐户有一些共同的电子邮件,则两个帐户肯定属于同一个人。请注意,即使两个帐户具有相同的名称,它们也可能属于不同的人,因为人们可能具有相同的名称。一个人最初可以拥有任意数量的帐户,但他们的所有帐户肯定都具有相同的名称。
合并账户后,按以下格式返回账户:每个账户的第一个元素是名称,其余元素是排序后的电子邮件。帐户本身可以按任何顺序退回。
示例:输入:accounts = [["John", "johnsmith@example.com", "john00@example.com"], ["John", "johnnybravo@example.com"], ["John", "johnsmith @example.com", "john_newyork@example.com"], ["Mary", "mary@example.com"]]
输出:[["John", 'john00@example.com', 'john_newyork@example.com', 'johnsmith@example.com'], ["John", "johnnybravo@example.com"], ["Mary ", "mary@example.com"]]
解释:第一个和第三个 John 是同一个人,因为他们有共同的电子邮件“johnsmith@example.com”。第二个 John 和 Mary 是不同的人,因为他们的电子邮件地址都没有被其他帐户使用。我们可以按任何顺序返回这些列表,例如答案 [['Mary', 'mary@example.com'], ['John', 'johnnybravo@example.com'], ['John', 'john00@ example.com', 'john_newyork@example.com', 'johnsmith@example.com']] 仍然会被接受。
class Solution:
def accountsMerge(self, accounts):
"""
:type accounts: List[List[str]]
:rtype: List[List[str]]
"""
owners={}
parents={}
merged=collections.defaultdict(set)
results=[]
for acc in accounts:
for i in range(1,len(acc)):
owners[acc[i]] = acc[0]
parents[acc[i]] = acc[i]
for acc in accounts:
p = self.find(acc[1],parents) #Find parent of the first email in the list.
for i in range(2,len(acc)):
#Perform union find on the rest of the emails across all accounts (regardless of account name, as no common email can exist between different names.)
#Any common emails between any 2 lists will make those 2 lists belong to the same set.
currP = self.find(acc[i],parents)
if p!=currP:
parents[currP] = p
for acc in accounts:
p = self.find(acc[1],parents)
for i in range(1,len(acc)):
merged[p].add(acc[i])
for name,emails in merged.items():
res = [owners[name]] + sorted(emails)
results.append(res)
return results
def find(self,node,parents):
if node!=parents[node]:
parents[node] = self.find(parents[node],parents)
return parents[node]
解决方案
逐部分检查代码:
for acc in accounts:
for i in range(1,len(acc)):
owners[acc[i]] = acc[0]
parents[acc[i]] = acc[i]
这是 O(N),其中 N 是输入文本的大小,因为算法只访问输入的每个部分一次。请注意,每个文本元素可能具有任意大小,但要注意这一点,因为 N 是输入文本的大小。
然后:
for acc in accounts:
p = self.find(acc[1],parents) #Find parent of the first email in the list.
for i in range(2,len(acc)):
currP = self.find(acc[i],parents)
if p!=currP:
parents[currP] = p
这也是 O(N),因为:
self.find(acc[i], parents)
由于路径压缩,被认为是摊销 O(1)。- 和以前一样 - 每个输入元素都被访问一次。
下一个:
for acc in accounts:
p = self.find(acc[1],parents)
for i in range(1,len(acc)):
merged[p].add(acc[i])
循环本身取决于 N - 输入文本的大小。该add()
方法对集合进行操作,在 python 中被认为是 O(1)。总而言之,这个块也需要 O(N)。
最后:
for name,emails in merged.items():
res = [owners[name]] + sorted(emails)
results.append(res)
令人惊讶的是,这里存在瓶颈(至少在 O 表示法方面)。中的元素数量emails
是 O(N),因为很可能系统有一个大用户拥有大部分电子邮件。这意味着sorted(emails)
可以采取 O(N log N)。
排序后创建 res 的代码部分:
res = [owners[name]] + <the sort result>
这仅在排序数据的大小上是线性的,小于排序的 O(N log N)。
尽管处于循环中,但排序的成本加起来总体上不会超过 O(N log N),因为 O(N log N) 的成本假设有一个大用户。不能超过少数几个大用户。
例如,假设系统中有 K 个相等的用户。这使得每个用户的排序成本为 O(N/K log(N/K))。整个系统的总计为 O(N log (N/K))。如果 K 是常数,则变为 O(N log N)。如果 K 是 N 的函数,则 O(N log(N/K)) 小于 O(N log N)。这不是一个严格的证明,但足以了解为什么它是 O(N log N) 而不是更糟的基本概念。
结论:算法以 O(N log N) 复杂度运行,其中 N 是输入文本的大小。
注意:复杂度计算有一个主要假设,即在 Python 中,通过长度为 L 的字符串键访问映射或集合是 O(L) 操作。这通常是正确的,具有完美的散列函数,而 Python 没有。
推荐阅读
- entity-framework - 如何创建和填充 DbSet(Of T)?
- android - 任何可用于对讲阅读完成的回调 - Android
- lazy-loading - 带有惰性大小的 Swiper 5
- c# - 如何从这个 JSON 数组中读取所有值?
- json - 我们可以将 JSON 中的数据返回到除了 ListView 之外的任何东西中吗?
- c++ - D3D12 SetPredicate 性能
- android - 应用程序读取分钟为秒
- r - 使用 geom_freqpoly 时如何删除范围末端的线条?
- kubernetes - 如何仅根据它们是否已完成 kubernetes 中的某个任务来终止某些 pod?
- c# - 批量删除行。如何打开/重用 SQL Server 连接?