这是一个面试问题。我们有一个大小为N的整数数组,包含0到N-1之间的元素。数字可能会出现两次以上。目标是找到总和为给定数字X的对。
我使用具有主数组元素数量的辅助数组,然后根据辅助数组重新排列主数据,以便对主数据进行排序,然后搜索成对数据。
但是面试官希望空间复杂度不变,所以我告诉他对数组进行排序,但它是nlogn时间复杂度解决方案。他想要O(n)解决方案。
有没有任何方法可以在没有任何额外空间的情况下在O(n)中进行此操作?
答案 0 :(得分:6)
不,我不相信。您需要额外的空间才能通过分配给桶来“排序”O(n)中的数据,或者您需要就地排序而不是O(n)。
当然,如果你能做出某些假设,总有一些技巧。例如,如果N < 64K
和您的整数是32位宽,则可以在当前数组的顶部复用计数数组所需的空间。
换句话说,使用低16位来存储数组中的值,然后使用数组的高16位,只需存储与索引匹配的值的数量。
让我们使用N == 8
的简化示例。因此,数组长度为8个元素,每个元素的整数小于8,尽管它们是8位宽。这意味着(最初)每个元素的前四位为零。
0 1 2 3 4 5 6 7 <- index
(0)7 (0)6 (0)2 (0)5 (0)3 (0)3 (0)7 (0)7
用于将计数存储到高四位的O(n)调整的伪代码是:
for idx = 0 to N:
array[array[idx] % 16] += 16 // add 1 to top four bits
举例来说,考虑存储7的第一个索引。因此,赋值语句将向索引7添加16,增加七次计数。模运算符是为了确保已增加的值仅使用低4位来指定数组索引。
所以数组最终变为:
0 1 2 3 4 5 6 7 <- index
(0)7 (0)6 (1)2 (2)5 (0)3 (1)3 (1)7 (3)7
然后你将新数组放在恒定的空间中,你可以使用int (array[X] / 16)
来计算有多少X
个值。
但是,这非常狡猾,需要如前所述的某些假设。很可能是面试官正在寻找的那种狡猾程度,或者他们可能只是想看看未来的员工如何处理编码的Kobayashi Maru: - )
一旦你有了计数,找到总和给定X
的对仍然是O(N)是一件简单的事情。获得cartestian产品的基本方法是。例如,再次考虑N
为8,你想要总和为8的对。忽略上面多路复用数组的下半部分(因为你只对计数感兴趣,你有:
0 1 2 3 4 5 6 7 <- index
(0) (0) (1) (2) (0) (1) (1) (3)
你基本上做的是逐一逐步完成数组,得到总和为8的数字计数的乘积。
(2,6)
一次。(3,5)
。m
个)1 + 2 + 3 + ... + m-1
。通过一些数学widard,结果证明是m(m-1)/2
。除此之外,你要与左边的值配对,你已经这样做了,所以你就停止了。
那么你最终得到的是
a b c d e f g h <- identifiers
7 6 2 5 3 3 7 7
是:
(2,6) (3,5) (3,5)
(c,b) (e,d) (f,d) <- identifiers
没有其他值加起来为8。
以下程序说明了这一点:
#include <stdio.h>
int arr[] = {3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5, 8, 9, 4, 4, 4, 4};
#define SZ (sizeof(arr) / sizeof(*arr))
static void dumpArr (char *desc) {
int i;
printf ("%s:\n Indexes:", desc);
for (i = 0; i < SZ; i++) printf (" %2d", i);
printf ("\n Counts :");
for (i = 0; i < SZ; i++) printf (" %2d", arr[i] / 100);
printf ("\n Values :");
for (i = 0; i < SZ; i++) printf (" %2d", arr[i] % 100);
puts ("\n=====\n");
}
上面的内容仅用于调试。执行存储桶排序的实际代码如下:
int main (void) {
int i, j, find, prod;
dumpArr ("Initial");
// Sort array in O(1) - bucket sort.
for (i = 0; i < SZ; i++) {
arr[arr[i] % 100] += 100;
}
我们完成了配对的代码:
dumpArr ("After bucket sort");
// Now do pairings.
find = 8;
for (i = 0, j = find - i; i <= j; i++, j--) {
if (i == j) {
prod = (arr[i]/100) * (arr[i]/100-1) / 2;
if (prod > 0) {
printf ("(%d,%d) %d time(s)\n", i, j, prod);
}
} else {
if ((j >= 0) && (j < SZ)) {
prod = (arr[i]/100) * (arr[j]/100);
if (prod > 0) {
printf ("(%d,%d) %d time(s)\n", i, j, prod);
}
}
}
}
return 0;
}
输出结果为:
Initial:
Indexes: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
Counts : 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
Values : 3 1 4 1 5 9 2 6 5 3 5 8 9 4 4 4 4
=====
After bucket sort:
Indexes: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
Counts : 0 2 1 2 5 3 1 0 1 2 0 0 0 0 0 0 0
Values : 3 1 4 1 5 9 2 6 5 3 5 8 9 4 4 4 4
=====
(2,6) 1 time(s)
(3,5) 6 time(s)
(4,4) 10 time(s)
并且,如果检查输入数字,您会发现对是正确的。
答案 1 :(得分:1)
这可以通过在O(N)时间内将输入数组“就地”转换为计数器列表来完成。当然这假设输入数组不是不可变的。不需要对每个数组元素中未使用的位进行任何其他假设。
从以下预处理开始:尝试将每个数组的元素移动到由元素值确定的位置;将此位置上的元素移动到由其值确定的位置;继续直到:
在预处理之后,每个元素都位于其“正确”位置或“指向”其“正确”位置。如果我们在每个元素中都有一个未使用的位,我们可以将每个正确定位的元素转换为一个计数器,用“1”初始化它,并允许每个“指向”元素增加适当的计数器。附加位允许区分计数器和值。可以在没有任何额外位的情况下完成相同的操作,但使用较少的简单算法。
计算数组中的值如何等于0或1.如果有任何此类值,则将它们重置为零并更新位置0和/或1处的计数器。设置k=2
(数组的大小)值小于k
的部分由计数器替换。对k = 2,4,8,...
k .. 2k-1
找到处于“正确”位置的元素,将其替换为计数器,初始值为“1”。k .. 2k-1
且值2 .. k-1
的任何元素,更新位置2 .. k-1
处的相应计数器并将值重置为零。0 .. 2k-1
且值k .. 2k-1
的任何元素,更新位置k .. 2k-1
处的相应计数器并将值重置为零。此过程的所有迭代一起具有O(N)时间复杂度。最后,输入数组完全转换为计数器数组。这里唯一的困难是位置0 .. 2k-1
最多两个计数器的值可能大于k-1
。但这可以通过为每个索引存储两个附加索引并将这些索引处理元素作为计数器而不是值来缓解。
在生成一组计数器之后,我们可以将成对的计数器(其中对应的索引对总和为X
)相乘以获得所需的对数。
答案 2 :(得分:0)
字符串排序是n log n但是如果您可以假设数字是有界的(并且您可以因为您只对总和为某个值的数字感兴趣),则可以使用基数排序。基数排序需要O(kN)时间,其中“k”是密钥的长度。在你的情况下,这是一个常数,所以我认为说O(N)是公平的。
一般情况下,我会使用哈希来解决这个问题,例如
http://41j.com/blog/2012/04/find-items-in-an-array-that-sum-to-15/
虽然这当然不是线性时间解决方案。