繁体   English   中英

在大小为N的整数数组中找到与X相加的对,其中元素的范围为0到N-1

[英]Find pairs that sum to X in an array of integers of size N having element in the range 0 to N-1

这是一个面试问题。 我们有一个大小为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的第一个索引。因此,赋值语句将向索引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的数字计数的乘积。

  • 对于0,您需要添加8(不存在)。
  • 对于1,您需要添加7.计数的乘积为0 x 3,因此不提供任何内容。
  • 对于2,您需要添加6.计数的乘积是1 x 1,因此给出一次(2,6)
  • 对于3,您需要添加5.计数的乘积是2 x 1,因此出现两次(3,5)
  • 对于4,这是一个特例,因为您无法使用该产品。 在这种情况下它没关系,因为没有4,但如果有一个,那就不能成为一对。 如果您配对的数字相同,则公式为(假设有m个) 1 + 2 + 3 + ... + m-1 通过一些数学widardry,结果是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)时间内将输入数组“就地”转换为计数器列表来完成。 当然这假设输入数组不是不可变的。 不需要对每个数组元素中的未使用位进行任何额外的假设。

从以下预处理开始:尝试将每个数组的元素移动到由元素值确定的位置; 将此位置上的元素移动到由其值确定的位置; 继续直到:

  • 将下一个元素移动到此循环开始的位置,
  • next元素无法移动,因为它已经位于与其值对应的位置(在这种情况下,将当前元素放到此循环开始的位置)。

在预处理之后,每个元素都位于其“适当”位置或“指向”其“适当”位置。 如果我们在每个元素中都有一个未使用的位,我们可以将每个正确定位的元素转换为一个计数器,用“1”初始化它,并允许每个“指向”元素增加适当的计数器。 附加位允许区分计数器和值。 可以在没有任何附加位但是具有较少的简单算法的情况下完成相同的操作。

计算数组中的值如何等于0或1.如果有任何此类值,则将它们重置为零并更新位置0和/或1处的计数器。设置k=2 (数组的部分大小值小于而k由计数器代替)。 对k = 2,4,8,......应用以下程序

  1. 在位置k .. 2k-1处找到处于“正确”位置的元素,用计数器替换它们,初始值为“1”。
  2. 对于位置k .. 2k-1任何元素,值为2 .. k-1更新位置2 .. k-1处的相应计数器并将值复位为零。
  3. 对于位置为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/

虽然这当然不是线性时间解决方案。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM