找到时间为O(n)且空间为O(1)的有符号整数

时间:2011-11-21 07:08:27

标签: c++ c algorithm math

(这是:Finding duplicates in O(n) time and O(1) space

的概括

问题:编写一个C ++或C函数,它们分别具有O(n)和O(1)的时间和空间复杂度,可以在不改变它的情况下找到给定数组中的重复整数。

示例:给定{1,0,-2,4,4,1,3,1,-2}函数必须打印1,-2和4一次(按任意顺序)。

<小时/> 编辑:以下解决方案需要在数组的最小值到最大值范围内的每个整数的二进制位(表示0,1和2)。必要字节数(不管数组大小)从不超过(INT_MAX – INT_MIN)/4 + 1

#include <stdio.h>

void set_min_max(int a[], long long unsigned size,\
                 int* min_addr, int* max_addr)
{
    long long unsigned i;

    if(!size) return;
    *min_addr = *max_addr = a[0];
    for(i = 1; i < size; ++i)
    {
        if(a[i] < *min_addr) *min_addr = a[i];
        if(a[i] > *max_addr) *max_addr = a[i];
    }
}

void print_repeats(int a[], long long unsigned size)
{
    long long unsigned i;
    int min, max = min;
    long long diff, q, r;
    char* duos;

    set_min_max(a, size, &min, &max);
    diff = (long long)max - (long long)min;
    duos = calloc(diff / 4 + 1, 1);
    for(i = 0; i < size; ++i)
    {
        diff = (long long)a[i] - (long long)min; /* index of duo-bit
                                                    corresponding to a[i]
                                                    in sequence of duo-bits */
        q = diff / 4; /* index of byte containing duo-bit in "duos" */
        r = diff % 4; /* offset of duo-bit */
        switch( (duos[q] >> (6 - 2*r )) & 3 )
        {
            case 0: duos[q] += (1 << (6 - 2*r));
                    break;
            case 1: duos[q] += (1 << (6 - 2*r));
                    printf("%d ", a[i]);
        }
    }
    putchar('\n');
    free(duos);
}

void main()
{
    int a[] = {1, 0, -2, 4, 4, 1, 3, 1, -2};
    print_repeats(a, sizeof(a)/sizeof(int));
}

7 个答案:

答案 0 :(得分:7)

big-O表示法的定义是它的参数是一个函数( f(x)),因为函数中的变量( x )倾向于无穷大,存在常数 K ,使得目标成本函数小于 Kf(x)。通常选择 f 作为最小的这种简单函数,以便满足条件。 (如何将上述内容提升为多个变量非常明显。)

这很重要,因为您不需要指定的 K - 允许将大量复杂行为隐藏在视线之外。例如,如果算法的核心是O(n 2 ),它允许各种其他O(1),O(logn),O(n),O(nlogn),O (n 3/2 )等支持要隐藏的位,即使对于真实的输入数据,这些部分实际上也是主导的。这是正确的,它可能完全是误导性的! (一些更高级的bignum算法具有真实的这个属性。用数学说谎是一件很棒的事情。)

那么这是怎么回事?好吧,您可以假设int足够容易(例如,32位)固定大小并使用该信息来避免很多麻烦并分配固定大小标志位数组到保存您真正需要的所有信息。实际上,通过每个潜在值使用两位(一位表示您是否已经看到该值,另一位表示您是否已经打印它),您可以使用1GB大小的固定内存块来处理代码。然后,它将为您提供足够的标记信息,以处理您可能曾希望处理的32位整数。 (哎呀,这在64位机器上实际上是可行的。)是的,它需要一些时间来设置内存块,但它是不变的,所以它正式为O(1),因此退出分析。鉴于此,您可以获得恒定(但非常高)的内存消耗和线性时间(您必须查看每个值以查看它是否是新的,一次看到的等等),这正是所要求的。

但这是一个肮脏的伎俩。您还可以尝试扫描输入列表以计算范围,从而允许在正常情况下使用更少的内存;再次,只增加线性时间,你可以严格限制上面所需的内存,这是常量。更狡猾,但正式合法。


[编辑]示例 C 代码(这不是C ++,但我不擅长C ++;主要区别在于如何分配和管理标志数组):

#include <stdio.h>
#include <stdlib.h>

// Bit fiddling magic
int is(int *ary, unsigned int value) {
    return ary[value>>5] & (1<<(value&31));
}
void set(int *ary, unsigned int value) {
    ary[value>>5] |= 1<<(value&31);
}

// Main loop
void print_repeats(int a[], unsigned size) {
    int *seen, *done;
    unsigned i;

    seen = calloc(134217728, sizeof(int));
    done = calloc(134217728, sizeof(int));

    for (i=0; i<size; i++) {
        if (is(done, (unsigned) a[i]))
            continue;
        if (is(seen, (unsigned) a[i])) {
            set(done, (unsigned) a[i]);
            printf("%d ", a[i]);
        } else
            set(seen, (unsigned) a[i]);
    }

    printf("\n");
    free(done);
    free(seen);
}

void main() {
    int a[] = {1,0,-2,4,4,1,3,1,-2};
    print_repeats(a,sizeof(a)/sizeof(int));
}

答案 1 :(得分:5)

由于你有一个整数数组,你可以使用简单的解决方案来排序数组(你没有说它不能被修改)和打印重复。可以使用Radix sort使用O(n)和O(1)时间和空间复杂度对整数数组进行排序。虽然通常它可能需要O(n)空间,但是使用O(1)空间可以简单地实现就地二进制MSD基数排序(更多细节,请查看here)。

答案 2 :(得分:2)

O(1)空间约束是难以处理的。

根据定义,打印阵列本身的事实需要O(N)存储。

现在,感觉很慷慨,我会告诉你,你可以在你的程序中有一个缓冲区的O(1)存储空间,并认为程序之外的空间不关心你,因此输出是不是问题......

但是,由于输入数组的不变性约束,O(1)空间约束感觉难以处理。它可能不是,但感觉如此。

并且您的解决方案溢出,因为您尝试以有限数据类型记忆O(N)信息。

答案 3 :(得分:1)

这里的定义存在一个棘手的问题。 O(n)是什么意思?

Konstantin的回答声称基数排序时间复杂度为O(n)。实际上它是O(n log M),其中对数的基数是所选择的基数,M是数组元素可以具有的值的范围。因此,例如,二进制基数排序的32位整数将具有log M = 32。

因此,在某种意义上,这仍然是O(n),因为log M是一个独立于n的常数。但是如果我们允许这个,那么有一个更简单的解决方案:对于范围内的每个整数(它们都是4294967296),遍历数组以查看它是否多次出现。从某种意义上说,这也是O(n),因为4294967296也是一个独立于n的常数。

我认为我的简单解决方案不会算作答案。但如果没有,那么我们也不应该允许基数排序。

答案 4 :(得分:1)

我真的不知道如何只有O(1)空间而不修改初始数组。我的猜测是你需要一个额外的数据结构。例如,整数的范围是多少?如果它与您链接的其他问题中的0..N相似,则可以有一个大小为N的附加计数数组。然后在O(N)中遍历原始数组并在当前元素的位置递增计数器。然后遍历另一个数组并打印数字&count; = 2.类似于:

int* counts = new int[N];
for(int i = 0; i < N; i++) {
    counts[input[i]]++;
}

for(int i = 0; i < N; i++) {
    if(counts[i] >= 2) cout << i << " ";
}

delete [] counts;

答案 5 :(得分:1)

我怀疑这是可能的。假设有一个解决方案,让我们看看它是如何工作的。我会尽量做到尽善尽美,并证明它不起作用......那么,它是如何运作的?

在不失一般性的情况下,我们可以说我们处理数组k次,其中k是固定的。当m个重复时,解决方案也应该有效,m>&gt; ķ。因此,在至少一个传递中,我们应该能够输出x个重复,其中x在m增长时增长。为此,在先前的传递中计算了一些有用的信息并存储在O(1)存储器中。 (不能使用数组本身,这会产生O(n)存储。)

问题:我们有O(1)信息,当我们遍历数组时,我们必须识别x数字(输出它们)。我们需要O(1)存储,而不是在O(1)时间内告诉我们,如果元素在其中。或者以不同的方式说,我们需要一个数据结构来存储使用O(1)空间的n个布尔(使用true),并且需要O(1)时间来查询。

这个数据结构是否存在?如果没有,那么我们找不到具有O(n)时间和O(1)空间的数组中的所有重复项(或者有一些奇特的算法以完全不同的方式工作)。

答案 6 :(得分:0)

假设您可以使用您没有使用所有空间的事实。每个可能的值只需要一个位,并且在32位int值中有很多未使用的位。

这有严重的局限性,但在这种情况下有效。数字必须在-n / 2和n / 2之间,如果它们重复m次,它们将被打印m / 2次。

void print_repeats(long a[], unsigned size) {
    long i, val, pos, topbit = 1 << 31, mask = ~topbit;
    for (i = 0; i < size; i++)
        a[i] &= mask;

    for (i = 0; i < size; i++) {
        val = a[i] & mask;
        if (val <= mask/2) {
           pos = val;
        } else {
            val += topbit;
            pos = size + val;
        }
        if (a[pos] < 0) {
            printf("%d\n", val);
            a[pos] &= mask;
        } else {
            a[pos] |= topbit;
        }
    }
}

void main() {
    long a[] = {1, 0, -2, 4, 4, 1, 3, 1, -2};
    print_repeats(a, sizeof (a) / sizeof (long));
}

打印

4
1
-2