为啥这个例子的时间复杂度来自“Cracking the Coding Interview” O(k c^k)?

Posted

技术标签:

【中文标题】为啥这个例子的时间复杂度来自“Cracking the Coding Interview” O(k c^k)?【英文标题】:Why is the time complexity of this example from "Cracking the Coding Interview" O(k c^k)?为什么这个例子的时间复杂度来自“Cracking the Coding Interview” O(k c^k)? 【发布时间】:2017-10-24 01:35:16 【问题描述】:

这个问题来自Cracking the Coding Interview 6th Edition,问题V1.11。

以下代码打印所有长度为 k 的字符串,其中字符 是按顺序排列的。它通过生成所有长度的字符串来做到这一点 k 然后检查每个是否已排序。什么是运行时?

package QVI_11_Print_Sorted_Strings;


public class Question 

    public static int numChars = 26;

    public static void printSortedStrings(int remaining) 
        printSortedStrings(remaining, "");
    

    public static void printSortedStrings(int remaining, String prefix) 
        if (remaining == 0) 
            if (isInOrder(prefix)) 
                System.out.println(prefix);
            
         else 
            for (int i = 0; i < numChars; i++) 
                char c = ithLetter(i);
                printSortedStrings(remaining - 1, prefix + c);
            
        
    

    public static boolean isInOrder(String s) 
        for (int i = 1; i < s.length(); i++) 
            int prev = ithLetter(s.charAt(i - 1));
            int curr = ithLetter(s.charAt(i));
            if (prev > curr) 
                return false;
            
        
        return true;
    

    public static char ithLetter(int i) 
        return (char) (((int) 'a') + i);
    

    public static void main(String[] args) 
        printSortedStrings(5);
    


答案是 O(k c^k),其中 k 是字符串的长度,c 是 字母表中的字符数。需要 O(c^k) 时间 生成每个字符串。然后,我们需要检查这些中的每一个是否 已排序,需要 O(k) 时间。

现在,我了解 O(k) 的来源,但我不明白 O(c^k) 的来源。

【问题讨论】:

【参考方案1】:

上述算法通过使用一组 c 个字符选择递归生成所有可能的长度为 k 的字符串来工作。您可以从 c 个字母中选择的长度为 k 的字符串的数量等于 ck。例如,如果我有两个字母要从(a 和 b)中挑选,并且我有长度为 3 的字符串,那么我可以制作 23 = 8 个可能的字符串:

aaa aab aba abb 咩 bab bba bbb

为了更好地了解它的来源,请注意,每次在字符串末尾添加一个新字母时,您有 c 个选项可以选择该字母的含义,因此可能的字符串数为

c·c····c(k次)=

ck

这意味着通过生成这些字符串中的每一个来工作的上述代码必须至少 Ω(ck) 工作,因为这是最小数量的要检查的字符串。

那么它对每个字符串做了多少工作?这就是事情变得棘手的地方。这些字符串是通过不断地从可能的字符列表中附加一个新字符来一次构建一个字符的。在 Java 中,附加到字符串会生成字符串的完整副本,因此附加第一个字符的成本是(大约)1,第二个是(大约)2,然后是 3,然后是 4,依此类推。这意味着成本建立一个长度为 k 的完整字符串将是

1 + 2 + 3 + ... + k

= Θ(k2)

所以实际上这里的运行时间似乎是 O(ck k2) 而不是 O(kck),因为构建所有这些字符串的成本加起来很快。

但是,这不是一个严格的限制。例如,为形成字符串aaa 所做的一些工作也用于形成字符串aab,因为这两个字符串都是通过以aa 开头并连接另一个字符而形成的。

为了获得更好的分析,我们可以总结在树中每个级别执行连接的总工作量。树的第 0 层有一个大小为 0 的字符串,因此不进行连接。树的第一层有 c 个大小为 1 的字符串,需要 c 个工作来处理连接。树的第二层有 c2 个大小为 2 的字符串,需要 2c2 个工作才能形成。三级中的第三级有 c3 个大小为 3 的字符串,需要 3c3 个工作才能形成。更一般地说,第 i 级需要 ici 工作才能形成。这意味着我们要确定

0c0 + 1c1 + 2c2 + ... + kck

此求和结果为 Θ(kck),其中 k 项的指数较低。

总结一下:

ck 项来自需要检查的字符串数。 k 项来自每个字符串完成的工作。 仔细分析表明,生成所有这些字符串的时间不会影响 O(k ck) 的整体运行时间。

【讨论】:

字符串共享共同的前缀,因此共享一些已完成的工作。这会改变复杂性吗? 有趣的地方!我相信它可能会,但是考虑到这个通过在 Java 中进行大量字符串连接来工作的特定实现,它可能不适用于这里。在没有限制的情况下重新审视这个想法可能会带来一些改进。【参考方案2】:

printSortedStrings 的递归调用形成一个递归树。因为节点的总数大约是最低层节点数的一个常数因子,并且上层不比最低层做更多的工作,所以只有最低层的成本是显着的。

例如,c 为 2,k 为 3:

查看生成的树的第一层:

2(或2^1)字符串,"a""b"

第二级产生:

4(或2^2)字符串,"aa""ab""ba""bb"

第三级产生:

8(或2^3)字符串,"aaa""aab""aba""abb""baa""bab""bba""bbb"

按顺序生成下一个字符串的成本是线性的。旧字符串中的字符加上新字符被复制到新字符串中。

这取决于字符串的长度,因此对于第一级成本为 1,第二级成本为 2,第三级成本为 3。乘以每个级别的项目数:

(2^1)*1 + (2^2)*2 + (2^3)*3 = 34

如果k4,这种模式将继续存在,那么它将是:

(2^1)*1 + (2^2)*2 + (2^3)*3 + (2^4)*4 = 98

这样的总和是最后一项大于前面所有项的总和。所以只有最后一项是重要的:

(2^1)*1 + (2^2)*2 + (2^3)*3 < (2^4)*4

因为:

(2^1)*1 + (2^2)*2 + (2^3)*3
< (2^1)*3 + (2^2)*3 + (2^3)*3
= (2^1 + 2^2 + 2^3) * 3
= (2^4 - 2) * 3
< (2^4 - 2) * 4
< (2^4) * 4

所以总和小于2*(2^4)*4,或2*(c^k)*k,或O(k c^k)

在递归结束时,有更多的线性时间工作。有c^k 节点与k 工作给另一个O(k c^k) 成本。所以总成本还是O(k c^k)

另一件事是 for 循环也需要每个字符串的线性时间,但由于与上述类似的原因,这总共需要 O(k c^k)

【讨论】:

出色的分析。我要修改我的答案。【参考方案3】:

我认为他们没有考虑书中的 k^2,因为实际书中的代码不是用 Java 编写的。因此可以安全地假设字符串连接是 O(1)。另外,书中的答案解释也不是最好的。生成每个字符串只需要 O(k) 时间,但有 O(c^k) 个可能的字符串。并且需要 O(k) 来验证结果,因此它应该是 O(k^2 c^k) 而不是 O(kc^k) (具有 O(1) 字符串连接时间)。但是,您应该与面试官讨论,只需询问打印、连接等是否需要 O(1) 时间或更长的时间。

【讨论】:

生成每个字符串的成本不会与处理每个字符串的成本相乘,而是相加。

以上是关于为啥这个例子的时间复杂度来自“Cracking the Coding Interview” O(k c^k)?的主要内容,如果未能解决你的问题,请参考以下文章

为啥这个例子不离开

为啥这个循环的时间复杂度是非线性的?

为啥在这个例子中 LINQ 更快

为啥这个例子只适用于断点

为啥在这个例子中需要后期绑定? [复制]

为啥在这个例子中可以返回临时对象?