【问题标题】: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 的对
【发布时间】:2013-01-15 06:08:20
【问题描述】:

这是一道面试题。我们有一个大小为 N 的整数数组,其中包含 0 到 N-1 之间的元素。一个数字可能出现两次以上。目标是找到总和为给定数字 X 的对。

我使用具有主数组元素计数的辅助数组进行此操作,然后根据辅助数组重新排列主数组,以便对主数组进行排序,然后搜索对。

但是面试官希望空间复杂度不变,所以我告诉他对数组进行排序,但它是 nlogn 时间复杂度解决方案。他想要 O(n) 的解决方案。

有没有什么方法可以在 O(n) 中做到这一点而不需要任何额外的空间?

【问题讨论】:

    标签: arrays algorithm sorting


    【解决方案1】:

    不,我不这么认为。您要么需要额外的空间才能通过分配给存储桶来对 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 的数字计数的乘积。

    • 对于 0,您需要添加 8(不存在)。
    • 对于 1,您需要加 7。计数的乘积是 0 x 3,因此没有给出任何结果。
    • 对于 2,您需要加 6。计数的乘积是 1 x 1,因此出现了一次 (2,6)
    • 对于 3,您需要添加 5。计数的乘积是 2 x 1,因此会出现两次 (3,5)
    • 对于 4,这是一个特殊情况,因为您不能使用该产品。在这种情况下,这无关紧要,因为没有 4,但如果有 一个, 就不能成为一对。如果您配对的数字相同,则公式为(假设有m1 + 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)
    

    而且,如果您检查输入的数字,您会发现这些对是正确的。

    【讨论】:

    • 你能详细说明一下这个数组[array[idx]] += 16
    • +1 为那令人敬畏的技术。请考虑花一些时间来回答问题的其余部分,以使答案完整。找到总和为给定数字 X 的对。
    • @Sibrajas,如你所愿。我已经介绍了该理论并在 C 中实现了除“中间元素”部分之外的所有内容。希望这会有所帮助。
    • 实际上,我现在已经实现了“中间元素”位。
    • @paxdiablo: is not condition j>=0 in " if ((j >= 0) && (j =0 而言是多余的。
    【解决方案2】:

    这可以通过在 O(N) 时间内将输入数组“就地”转换为计数器列表来完成。当然,这假设输入数组不是不可变的。不需要对每个数组元素中未使用的位进行任何额外的假设。

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

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

    预处理后,每个元素要么位于其“正确”位置,要么“指向”其“正确”位置。如果我们在每个元素中有一个未使用的位,我们可以将每个正确定位的元素转换为一个计数器,用“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)以获得所需的对数。

    【讨论】:

      【解决方案3】:

      字符串排序是 n log n 但是如果你可以假设数字是有界的(你可以因为你只对总和为某个值的数字感兴趣)你可以使用基数排序。基数排序需要 O(kN) 时间,其中“k”是键的长度。在你的情况下这是一个常数,所以我认为说 O(N) 是公平的。

      通常我会使用哈希来解决这个问题,例如

      http://41j.com/blog/2012/04/find-items-in-an-array-that-sum-to-15/

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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2023-03-09
        • 2022-11-17
        • 1970-01-01
        • 1970-01-01
        • 2022-11-18
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多