这是一道面试题。我们有一个大小为 N 的整数数组,其中包含 0 到 N-1 之间的元素。一个数字可能出现两次以上。目标是找到总和为给定数字 X 的对。
我使用具有主数组元素计数的辅助数组,然后根据辅助数组重新排列主数组,以便对主数组进行排序,然后搜索对。
但是面试官希望空间复杂度不变,所以我告诉他对数组进行排序,但它是 nlogn 时间复杂度解决方案。他想要 O(n) 解决方案。
有没有什么方法可以在 O(n) 中做到这一点而没有任何额外的空间?
不,我不相信。您要么需要额外的空间才能通过分配给存储桶来对 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 的第一个索引。因此,该赋值语句会将 16 添加到索引 7,从而增加 7 的计数。模运算符是为了确保已经增加的值只使用低四位来指定数组索引。
所以数组最终变成:
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
值。
但是,这是相当不正当的,需要如前所述的某些假设。这很可能是面试官正在寻找的那种狡猾程度,或者他们可能只是想看看未来的员工如何处理小林丸的编码:-)
一旦你有了计数,找到总和为给定 的对是一件简单的事情X
,仍然在 O(N) 中。基本方法是获得笛卡尔积。例如,再次考虑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
。加上一点数学知识,结果是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)
而且,如果您检查输入数字,您会发现这些对是正确的。
这可以通过在 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
)以获得所需的对数。
字符串排序是 n log n 但是,如果您可以假设数字是有界的(并且您可以,因为您只对总和为某个值的数字感兴趣),您可以使用基数排序。基数排序需要 O(kN) 时间,其中“k”是键的长度。在你的情况下这是一个常数,所以我认为说 O(N) 是公平的。
但是,通常我会使用哈希来解决这个问题,例如
http://41j.com/blog/2012/04/find-items-in-an-array-that-sum-to-15/
虽然这当然不是线性时间解决方案。