寻找在时间上为O(n),在空间上为O(1)的重复有符号整数
Finding repeating signed integers with O(n) in time and O(1) in space
(这是在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
- 在C++中,将大的无符号浮点数四舍五入为整数的最佳方法是什么
- 检查TCHAR数组输入是否为带符号整数C++
- 如何打印boost多精度128位无符号整数
- C++模板函数,用于比较任何无符号整数和有符号整数
- 在数字之间插入 + 或 - 符号以使其等于整数
- 为什么:不同符号的整数比较只是偶尔发生?
- 将超出范围的整数分配给有符号字符类型
- 为什么乘以常量有符号整数分数没有优化?
- 如何解决隐式转换丢失整数精度:'size_t'(又名"无符号长")到'int'警告?
- 在线程中读取无符号整数时,c++ 位是否以原子方式切换?
- FlatBuffers/Protobuf 中是否有支持任意 24 位有符号整数定义的可移植二进制序列化架构?
- C++11 标准是否保证零值有符号整数的一元减号为零?
- 整数类型应该显式转换(例如"int"到"无符号")还是只会增加混乱?
- Constexpr 可变参数模板,用于对无符号整数进行重新排序
- AVX2 整数乘以有符号 8 位元素,产生有符号 16 位结果?
- 为什么 Clang 和 GCC 中两个无符号整数之和的结果类型不同
- 使用192/256位整数求和无符号64位整数向量的点积的最快方法
- 为什么对无符号字符进行算术运算会将它们提升为有符号整数
- 从 std::string 转换为 const 无符号整数
- 矢量浮点到整数/符号转换