哪个代码在时间复杂度方面表现更好?

时间:2016-07-06 09:53:10

标签: java algorithm bit-manipulation time-complexity

下面是两个java代码,它们找到整数N中的1位数。

代码1:

int count = 0;

while (N != 0) {
    N = N & (N - 1);
    count++;
}

return count;

代码2:

int i = N;
i = i - ((i >>> 1) & 0x55555555);
i = (i & 0x33333333) + ((i >>> 2) & 0x33333333);
i = (i + (i >>> 4)) & 0x0f0f0f0f;
i = i + (i >>> 8);
i = i + (i >>> 16);
return i & 0x3f;

据我所知,第一种方法的复杂性为O(N),而第二种方法则为O(1)。所以第二个应该更好。

但我不确定。

4 个答案:

答案 0 :(得分:4)

在谈论时间复杂性时,你必须要小心。 Big-O提供渐近运行时的度量,这意味着您需要测量任意大N的运行时间。大多数编程语言都不提供(默认情况下)任意大的int类型,所以通常我们稍微捏造一下这个问题并假装他们这样做。

如果我们假装int可以尽可能大,那么第一个代码片段就可以工作了。它在1的二进制表示中每N运行一个循环,因此在最坏的情况下需要log_2(N)次迭代。所以它是O(log N)。

如果int大于32位,则第二个代码段不起作用(即产生错误的结果)。如果我们想象int越来越大,以支持我们在渐近分析中需要的更大N,那么我们在程序中需要更多行来支持表示N所需的额外位。因为程序必须改变为正确,所以渐近分析是不可能的。程序的任何特定版本都在O(1)时间内运行,但是对于足够大的N,它不能产生正确的结果,因此它与第一个代码片段不是一个公平的比较。

您可以尝试通过使用循环来修复第二个程序以适应N的大小而不是硬编码的移位和添加。然后我认为它在O(log(log(N)))时间内运行,因为每增加一次移位就会使聚合的位数加倍。

要记住的一件重要事情是,我们假设移位和按位运算仍为O(1),无论我们使int越来越大。这是在这些分析中非常常见的假设,但在机器指令仅处理4或8字节数量的实际计算机上并不是这样。

算法2的动态版本

你的问题是Java,但这是Python中算法2的正确O(log(log(N))版本(它有任意大的int)。如果你不知道,你可以将它视为伪代码Python - 但我认为无论如何这都是相对可读的,并且将其转换为使用java bignums将使其更难理解。它计算k,表示N所需的位数,四舍五入到2的幂,然后构造一个计数位所需的移位和掩码列表。这需要O(log(k))时间,并且由于k = log(N),整个算法需要O(log( log(N))时间(这里的时间表示按位或算术运算)。

def generate_ops(k):
    ops = []
    mask = (1 << k) - 1
    while k:
        ops.append((k, mask))
        k >>= 1
        mask = mask ^ (mask << k)
    return reversed(ops)

def bit_count(N):
    k = 1
    while (1 << k) < N:
        k *= 2
    for shift, mask in generate_ops(k):
        N = (N & mask) + ((N >> shift) & mask)
    return N

答案 1 :(得分:2)

由于 <?php //Saving Data to Image Table function saveImageData($data,$imgFile){ $project_id = isset($data['project_id']) ? mysql_real_escape_string($data['project_id']) : ""; $image_id = isset($data['image_id']) ? mysql_real_escape_string($data['image_id']) : ""; //$user_id = isset($data['user_id']) ? mysql_real_escape_string($data['user_id']) : ""; $image_name = isset($data['image_name']) ? mysql_real_escape_string($data['image_name']) : ""; if(isset($imgFile['tmp_name']) && filesize($imgFile['tmp_name'])>0) saveImageFile($imgFile['tmp_name'],$image_id."_".$project_id."_".$category_id."_".$image_name); $category_id = isset($data['category_id']) ? mysql_real_escape_string($data['category_id']) : ""; $houssup_id = isset($data['houssup_id']) ? mysql_real_escape_string($data['houssup_id']) : ""; $location_id = isset($data['location_id']) ? mysql_real_escape_string($data['location_id']) : ""; // Insert data into data base $sql = "INSERT INTO `".DB_NAME."`.`image_table`(`project_id`, `image_id`,`image_name`,`category_id`,`houssup_id`,`location_id`) VALUES(NULL,'$image_id','$image_name','$category_id','$houssup_id','$location_id')"; echo $sql; if(mysql_query($sql)) return true; else return false; } 表示法指的是输入的 size / length ,因此对于值O的输入,代码1实际上是{{ 1}},因为N是位O(log2(N))的长度。

log2(N)中设置的每个执行一次循环,因此对于32位N,最坏情况为N运行时间不超过32次而不是N次。

答案 2 :(得分:0)

第二种算法在恒定时间内运行,为真,但该常数相对于第一算法的常数(5)和运行时间(log2(N)= 32,对于最大N)较大。此外,通过渐近分析来估计时间复杂度,即算法如何针对大输入(N接近无穷大)执行。渐近分析不适用于第二种算法,因为该算法受到N拟合32位的限制,并且对于较大的输入不能产生正确的值,而第一种算法确实如此。

当将第二算法扩展到更大的输入时,可以注意到它的时间复杂度随着f(N)的增加而增长。在您的示例中,f(N)= 4(至少),因为您可以注意到32位整数中的4个相同组件。尽管在N停止拟合平台寄存器大小(例如64位)之后,第一算法的时间复杂度也增加。

答案 3 :(得分:0)

如上所述,这是一个毫无意义的问题。第二段代码不是算法,而是针对特定大小的整数的算法的特化。在某种程度上,第一段代码也是如此,虽然我们原则上可以选择任何大小的int,但必须有一个选择,因此输入的大小不能改变。因此,任何大O都没有n,任何东西都必须是O(1),因为我们看错了东西,所以没有给我们任何信息。

对于这两种情况,都有一种适用于任何大小的位向量的通用算法, 可用于查看。

第一种算法显然最多循环次数与位数一样多。因此,对于一般位向量n次,但请注意,这不会转换为O(n)“传统的基本步骤”,因为它是用n位位向量计算的。但是,如果算上宽字步骤(这是典型的问题),那么显然有O(n)。

第二种算法的一般形式比较棘手,TAOCP第4A卷给出了一些提示,按位技巧和技巧。它实际上并没有在那里定义,但它应该是显而易见的:一个加法树,通过添加相邻的popcnt对,在每一层上我们拥有popcnt的大小加倍,其中每个层以恒定数量的宽字步骤实现(“魔术面具”是μ k 所以我们可以假装存在这些常量)。由于加倍,层数为ceil(log2(n)),因此总算法采用O(log n)宽字步骤。