联合查找解决方案的时间复杂度

时间:2018-09-02 03:37:53

标签: algorithm time-complexity union-find

以下问题的解决方案的大致时间复杂度是多少?如果我们假设由于路径压缩,则对self.find()的每次调用将大致摊销到〜O(1)

问题陈述:

  

给出一个列表帐户,每个元素帐户[i]是一个字符串列表,   其中第一个元素account [i] [0]是一个名称,其余的   元素是代表帐户电子邮件的电子邮件。

     

现在,我们希望合并这些帐户。绝对有两个帐户   如果有一些共同的电子邮件属于同一个人   两个帐户。请注意,即使两个帐户具有相同的名称,它们   可能属于不同的人,因为人们可能具有相同的名字。一种   一个人最初可以拥有任意数量的帐户,但他们的所有帐户   帐户肯定具有相同的名称。

     

合并帐户后,返回以下帐户   格式:每个帐户的第一个元素是名称,其余的   元素是按排序顺序的电子邮件。帐户本身可以   可以以任何顺序退回。

     

示例:输入:accounts = [[“ John”,“ johnsmith@example.com”,   “ john00@example.com”],[“ John”,“ johnnybravo@example.com”],[“ John”,   “ johnsmith@example.com”,“ john_newyork@example.com”],[“玛丽”,   “ mary@example.com”]]

     

输出:[[“ John”,'john00@example.com','john_newyork@example.com',   'johnsmith@example.com'],[“ John”,“ johnnybravo@example.com”],[“ Mary”,   “ mary@example.com”]]

     

说明:第一个和第三个约翰与他们是同一个人   拥有通用电子邮件“ johnsmith@example.com”。第二约翰和玛丽   是不同的人,因为他们的电子邮件地址都没有被使用   其他帐户。我们可以以任何顺序返回这些列表,例如   答案[['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]

1 个答案:

答案 0 :(得分:0)

逐段检查代码:

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),因为:

  1. self.find(acc[i], parents)由于路径而被视为摊销O(1) 压实。
  2. 与以前相同-每个输入元素都会被访问一次。


下一个:

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则没有。