寻找在时间上为O(n),在空间上为O(1)的重复有符号整数

Finding repeating signed integers with O(n) in time and O(1) in space

本文关键字:整数 符号 时间上 寻找 空间      更新时间:2023-10-16

(这是在O(n)时间和O(1)空间中查找重复项的推广)

问题:编写一个时间复杂度为O(n)、空间复杂度为O(1)的c++或C函数,在不改变给定数组的情况下找到数组中的重复整数

示例:给定{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));
}

大o符号的定义是它的参数是一个函数(f(x)),当函数(x)中的变量趋于无穷时,存在一个常数K使得目标代价函数小于Kf(x)。通常选择f作为满足条件的最小的简单函数。(很明显,如何将上述内容提升为多个变量。)

这很重要,因为K——你不需要指定它——允许大量复杂的行为隐藏在视线之外。例如,如果算法的核心是O(n2),它允许隐藏所有其他类型的O(1), O(logn), O(n), O(nlogn), O(n3/2)等支持位,即使对于实际输入数据这些部分实际上占主导地位。这是正确的,它可以完全误导!(一些更花哨的大格数算法确实具有这种性质。与数学共处是一件美妙的事情。

这是怎么回事?好吧,您可以假设int是一个固定大小(例如,32位),并使用该信息来跳过许多麻烦,并分配固定大小的标志位数组来保存您真正需要的所有信息。实际上,通过对每个潜在值使用两位(一位表示是否看到了该值,另一位表示是否打印了该值),您就可以使用1GB大小的固定内存块来处理代码。然后,这将为您提供足够的标志信息,以处理您可能希望处理的尽可能多的32位整数。(见鬼,这在64位机器上也很实用。)是的,要花一些时间来设置内存块,但它是常数,所以形式上是0(1)所以从分析中退出。考虑到这一点,你有恒定的(但巨大的)内存消耗和线性时间(你必须查看每个值,看看它是否是新的,见过一次,等等),这正是所要求的。

这是一个肮脏的伎俩。您也可以尝试扫描输入列表,以计算出允许在正常情况下使用较少内存的范围;同样,这只增加了线性时间,你可以像上面那样严格限制所需的内存,所以这是常数。更狡猾,但形式上是合法的。


[EDIT]示例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));
}

既然您有一个整数数组,您可以使用直接的解决方案对数组进行排序(您没有说它不能被修改)并打印重复项。整数数组可以使用基数排序以O(n)和O(1)的时间和空间复杂度排序。虽然通常它可能需要O(n)空间,但是就地二进制MSD基数排序可以使用O(1)空间轻松实现(请参阅此处了解更多详细信息)。

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

根据定义,打印数组本身需要O(N)存储空间。

现在,我宽宏大量地告诉您,您可以在程序中为缓冲区提供O(1)存储空间,并认为程序外部占用的空间与您无关,因此输出不是问题…

但是,由于输入数组的不变性约束,O(1)空间约束感觉很棘手。也许不是,但感觉是。

并且你的解决方案溢出,因为你试图在有限数据类型中记住O(N)个信息

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

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

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

我不认为我的简单的解决办法算一个答案。但如果不是,那么我们也不应该允许基数排序。
我怀疑这是可能的。假设有一个解决方案,让我们看看它是如何工作的。我将尽可能概括地说明这是行不通的……那么,它是如何工作的呢?

在不失去一般性的前提下,我们可以说我们处理这个数组k次,其中k是固定的。当有m个重复项且m>> k时,解也应该有效。因此,在至少一次传递中,我们应该能够输出x个重复项,其中x随着m的增长而增长。为了做到这一点,一些有用的信息已经在前一轮中被计算出来并存储在0(1)存储器中。(数组本身不能使用,这将提供O(n)存储空间。)

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

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

我真的不明白你怎么能只有0(1)空间而不修改初始数组。我猜您需要一个额外的数据结构。例如,整数的范围是多少?如果是0…就像你链接的另一个问题一样,你可以有一个额外的大小为N的计数数组,然后在0 (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;

说你可以利用你没有使用所有空间的事实。每个可能的值只需要一个比特,并且在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("%dn", 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